Skip to content

Commit

Permalink
Fix classloader and parser handling for libraries, add tests
Browse files Browse the repository at this point in the history
- resolves a ClassNotFoundException when libraries contain multiple classes
- resolves a ParserException when using libraries in the same file that imports them
- adds automated tests for compiling and using libraries
  • Loading branch information
BlazingTwist committed Jan 24, 2025
1 parent 1edb277 commit 0117f50
Show file tree
Hide file tree
Showing 9 changed files with 185 additions and 44 deletions.
16 changes: 11 additions & 5 deletions build-scripts/run-aya-tests-build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@
<mkdir dir="${work.dir}"/>
<mkdir dir="${work.dir}/fs_test"/>

<path id="cp.aya">
<fileset dir="${build-dir}/libs" includes="**/*.jar"/>
<file file="${test.dir}"/>
</path>

<!-- running the lib test requires the example library to be built -->
<ant antfile="${test.dir}/test/lib/example/build.xml" usenativebasedir="true" inheritall="false" inheritrefs="true">
<property name="aya.classpath" value="cp.aya"/>
</ant>

<run_aya work.dir="${work.dir}/fs_test" test.dir="${test.dir}" test.aya="filesystem.aya"/>
<run_aya work.dir="${work.dir}" test.dir="${test.dir}" test.aya="test.aya"/>
</target>
Expand All @@ -60,12 +70,8 @@
classname="ui.AyaIDE"
failonerror="true"
errorproperty="error.log.str"
classpathref="cp.aya"
>
<classpath>
<fileset dir="${build-dir}/libs" includes="**/*.jar"/>
<file file="@{test.dir}"/>
</classpath>

<arg value="@{work.dir}"/>
<arg value="@{test.dir}/test/@{test.aya}"/>
</java>
Expand Down
26 changes: 12 additions & 14 deletions src/aya/StaticData.java
Original file line number Diff line number Diff line change
Expand Up @@ -165,20 +165,18 @@ public ArrayList<NamedInstructionStore> loadLibrary(File path) {

try {
URL[] urls = {path.toURI().toURL()};

try (URLClassLoader libClassLoader = new URLClassLoader(urls)) {
StreamSupport.stream(
ServiceLoader.load(NamedInstructionStore.class, libClassLoader).spliterator(),
false
).forEach(store -> {
//IO.out().println("found store: " + store.getClass().getName());
addNamedInstructionStore(store);
loaded.add(store);
});
} catch (IOException e) {
throw new IOError("library.load", path.getPath(), e);
}


// Do not release the classloader, otherwise the library will fail to access its own classes later.
URLClassLoader libClassLoader = new URLClassLoader(urls);
StreamSupport.stream(
ServiceLoader.load(NamedInstructionStore.class, libClassLoader).spliterator(),
false
).forEach(store -> {
//IO.out().println("found store: " + store.getClass().getName());
addNamedInstructionStore(store);
loaded.add(store);
});

} catch (MalformedURLException e) {
throw new IOError("library.load", path.getPath(), e);
}
Expand Down
51 changes: 34 additions & 17 deletions src/aya/instruction/named/NamedOperatorInstruction.java
Original file line number Diff line number Diff line change
@@ -1,27 +1,44 @@
package aya.instruction.named;

import aya.ReprStream;
import aya.StaticData;
import aya.eval.BlockEvaluator;
import aya.exceptions.runtime.InternalAyaRuntimeException;
import aya.instruction.Instruction;
import aya.obj.symbol.SymbolConstants;
import aya.parser.SourceStringRef;

public class NamedOperatorInstruction extends Instruction {

private NamedOperator op;

public NamedOperatorInstruction(SourceStringRef source, NamedOperator op) {
super(source);
this.op = op;
}

@Override
public void execute(BlockEvaluator blockEvaluator) {
this.op.execute(blockEvaluator);

}

@Override
public ReprStream repr(ReprStream stream) {
return this.op.repr(stream);
}
private final String opName;
private NamedOperator op = null;

public NamedOperatorInstruction(SourceStringRef source, String opName) {
super(source);
this.opName = opName;
}

private void loadOp() {
if (op != null) {
return;
}

op = StaticData.getInstance().getNamedInstruction(opName);
if (op == null) {
throw new InternalAyaRuntimeException(SymbolConstants.NOT_AN_OP_ERROR, "Named instruction :(" + opName + ") does not exist");
}
}

@Override
public void execute(BlockEvaluator blockEvaluator) {
this.loadOp();
op.execute(blockEvaluator);
}

@Override
public ReprStream repr(ReprStream stream) {
// repr doesn't need to load the OP-instance, we already know the opName.
stream.print(":(" + opName + ")");
return stream;
}
}
9 changes: 1 addition & 8 deletions src/aya/parser/tokens/NamedOpToken.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
package aya.parser.tokens;

import aya.StaticData;
import aya.exceptions.parser.ParserException;
import aya.instruction.Instruction;
import aya.instruction.named.NamedOperator;
import aya.instruction.named.NamedOperatorInstruction;
import aya.parser.SourceStringRef;

Expand All @@ -15,12 +13,7 @@ public NamedOpToken(String data, SourceStringRef source) {

@Override
public Instruction getInstruction() throws ParserException {
NamedOperator instruction = StaticData.getInstance().getNamedInstruction(data);
if (instruction != null) {
return new NamedOperatorInstruction(this.getSourceStringRef(), instruction);
} else {
throw new ParserException("Named instruction :(" + data + ") does not exist", source);
}
return new NamedOperatorInstruction(this.getSourceStringRef(), data);
}

@Override
Expand Down
54 changes: 54 additions & 0 deletions test/lib/example/ExampleStore.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package example;

import aya.eval.BlockEvaluator;
import aya.instruction.named.NamedInstructionStore;
import aya.instruction.named.NamedOperator;
import aya.obj.list.Str;

import java.util.Collection;
import java.util.List;

public class ExampleStore implements NamedInstructionStore {
private static DataStorage data;

@Override
public Collection<NamedOperator> getNamedInstructions() {
return List.of(
new OpPut(),
new OpGet()
);
}

private static class OpPut extends NamedOperator {
public OpPut() {
super("example.put");
}

@Override
public void execute(BlockEvaluator blockEvaluator) {
data = new DataStorage(blockEvaluator.pop().str());
}
}

private static class OpGet extends NamedOperator {
public OpGet() {
super("example.get");
}

@Override
public void execute(BlockEvaluator blockEvaluator) {
blockEvaluator.push(aya.obj.list.List.fromStr(new Str(data.value)));
}
}

/**
* Use an extra class for this to verify that the class (/classloader) is not unloaded
*/
private static class DataStorage {
String value;

public DataStorage(String value) {
this.value = value;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
example.ExampleStore
43 changes: 43 additions & 0 deletions test/lib/example/build.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<project name="example-lib" default="jar" basedir=".">
<!-- verify that all parameters were passed -->
<fail unless="aya.classpath"/>

<!-- re-define the parameters, so that they can be used with autocompletion -->
<property name="aya.classpath" value="ALREADY_DEFINED"/>

<property name="target.dir" location="${basedir}/target/"/>
<property name="target.manifest.file" location="${target.dir}/manifest.mf"/>
<property name="target.jar.file" location="${target.dir}/example.jar"/>

<target name="check_modified">
<!-- if the source files were not modified after the jar file, set 'is_uptodate' -->
<uptodate targetfile="${target.jar.file}" property="is_uptodate">
<srcfiles dir="${basedir}">
<include name="aya.instruction.named.NamedInstructionStore"/>
<include name="build.xml"/>
<include name="ExampleStore.java"/>
</srcfiles>
</uptodate>
</target>

<target name="jar" depends="check_modified" unless="is_uptodate">
<!-- reset the target directory -->
<delete failonerror="false" dir="${target.dir}"/>
<mkdir dir="${target.dir}"/>

<!-- compile and jar the example library -->
<javac destdir="${target.dir}" debug="true" target="11" source="11"
srcdir="${basedir}" includeantruntime="false" includes="ExampleStore.java" classpathref="${aya.classpath}">
</javac>

<manifest file="${target.manifest.file}"/>
<copy file="aya.instruction.named.NamedInstructionStore" todir="${target.dir}/META-INF/services/"/>
<jar jarfile="${target.jar.file}" manifest="${target.manifest.file}">
<fileset dir="${target.dir}">
<include name="**/*.class"/>
<include name="META-INF/**"/>
</fileset>
</jar>
</target>

</project>
27 changes: 27 additions & 0 deletions test/lib/lib.aya
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[

.# check loaded namedOps match expected
{
"$(:(sys.ad))/test/lib/example/target/example.jar" :lib_path;
"load example.jar from $(lib_path)" :P
lib_path :(library.load) :ops;
"example.jar ops: $(ops)" :P
ops [":(example.put)" ":(example.get)"]
}

.# verify that accessing namedOp that does not exist still causes an error
{
{ :(does.not.exist) "ok" } {"failed"} .K :call_result;
"calling undefined namedOp: $(call_result)" :P
call_result "failed"
}

.# verify that basic library usage works
{
"my-data" :(example.put)
:(example.get) :lib_output;
"obtained data from lib: $(lib_output)":P
lib_output "my-data"
}

] :# { test.test }
2 changes: 2 additions & 0 deletions test/test.aya
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,5 @@

.# Also load and auto-run many examples
"test/examples" load_test

"test/lib/lib" load_test

0 comments on commit 0117f50

Please sign in to comment.