This guide is for Java developers who want to add Aussom scripting to
a host JVM application. It walks through the standard javax.script
API end-to-end as the Aussom engine implements it: discovery, eval,
bindings, compilation, invocation, I/O routing, threading, and the
gotchas that come from Aussom's syntax differing from JavaScript or
Groovy.
You do not need to know the Aussom interpreter's internals to use it through JSR 223. Everything below is API-level.
JSR 223 is the standard Java specification for "scripting for the
Java platform." It defines a small set of interfaces in
javax.script:
| Interface / Class | Role |
|---|---|
ScriptEngineManager |
Entry point. Discovers engines on the classpath. |
ScriptEngineFactory |
Engine metadata, mints engine instances. |
ScriptEngine |
Per-instance script runner. eval, put, get. |
Bindings |
A Map<String,Object> representing variable scope. |
ScriptContext |
Holds engine-scope and global-scope bindings + I/O. |
Compilable (optional) |
Pre-parse a script for repeated execution. |
Invocable (optional) |
Call named functions and methods after eval. |
ScriptException |
Checked exception for any script-side failure. |
Aussom's engine implements ScriptEngine, Compilable, and
Invocable, so the surface area you have to learn is small.
The big advantage of going through JSR 223 (rather than calling
Aussom's Engine class directly) is that your host code stays
language-neutral. The same code path that runs an Aussom script can
run a Groovy or Kotlin script if you swap which jar is on the
classpath.
aussom-base is published to Maven Central. Add it to your pom.xml:
<dependency>
<groupId>io.github.rsv-code</groupId>
<artifactId>aussom.base</artifactId>
<version>1.2.3</version>
</dependency>
Or in Gradle:
implementation 'io.github.rsv-code:aussom.base:1.2.3'
The jar contains a
META-INF/services/javax.script.ScriptEngineFactory descriptor, so
the standard Java service loader will discover the engine
automatically. You do not have to register anything by hand.
The engine targets Java 8 and uses only the javax.script API that
ships with the JDK. There are no extra runtime dependencies you need
to add.
import javax.script.*;
public class HelloAussom {
public static void main(String[] args) throws ScriptException {
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine eng = mgr.getEngineByName("aussom");
Object out = eng.eval("return \"hello \" + \"world\";");
System.out.println(out); // -> hello world
}
}
That's the whole loop. Anything below this section is detail.
The standard discovery methods all work:
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine byName = mgr.getEngineByName("aussom");
// also: "aus", "Aussom"
ScriptEngine byExt = mgr.getEngineByExtension("aus");
ScriptEngine byMime = mgr.getEngineByMimeType("application/x-aussom");
// also: "text/x-aussom"
Each call to getEngineByName returns a new ScriptEngine
instance. Engines are cheap to construct, so this is fine for most
hosts. Reuse one engine across many evals when you can — see the
threading section for the rules.
To enumerate all engines available on the classpath (for example to check that Aussom is actually loaded):
for (ScriptEngineFactory f : mgr.getEngineFactories()) {
System.out.println(f.getEngineName() + " " + f.getEngineVersion());
}
The ScriptEngine interface gives you six eval overloads. They all
ultimately delegate to eval(String, ScriptContext) or
eval(Reader, ScriptContext):
eng.eval("return 1 + 2;"); // String, default context
eng.eval(new FileReader("script.aus")); // Reader, default context
eng.eval("return x + 1;", aBindings); // String + ad-hoc bindings
eng.eval(reader, aBindings); // Reader + ad-hoc bindings
eng.eval("return 1;", aScriptContext); // String + custom context
eng.eval(reader, aScriptContext); // Reader + custom context
The Aussom engine returns the value from the script's return
statement, marshalled into a Java type (see Section 6). If the script
does not return anything, the engine returns null.
Object n = eng.eval("return 42;"); // -> Long(42)
Object s = eng.eval("return \"hi\";"); // -> String("hi")
Object xs = eng.eval("return [1, 2, 3];"); // -> ArrayList<Object>
Object u = eng.eval("x = 1;"); // -> null (no return)
Aussom is a class-based language: a normal program declares a class
with a main(args) method. The engine is friendly to both shapes.
Snippets. If your source does not declare a top-level class,
the engine wraps it in a synthetic class with a main for you:
eng.eval("return 7 * 6;"); // wrapped automatically
eng.eval("c.log(\"hello\");"); // also wrapped
Full programs. If your source already declares one or more
classes, the engine treats it as a full program. If any of those
classes has a main, the engine runs it. If none does, the engine
just registers the class definitions for later use:
// No main -> just registers Calc; the eval returns null.
eng.eval("class Calc { public add(a, b) { return a + b; } }");
// Has main -> runs main, returns its value.
Object r = eng.eval("class App { public main(args) { return 99; } }");
// r == 99L
This pattern (define classes once with eval, then call methods on
them with Invocable) is the recommended way to expose Aussom-side
business logic to your host. See Section 8.
Inside a snippet, a value flows back to your host via an explicit
return statement. Aussom does not have JavaScript's "last
expression wins" rule, so this:
eng.eval("1 + 2;"); // returns null, NOT 3
eng.eval("return 1 + 2;"); // returns Long(3)
If you need an Aussom expression's value, write return ...;.
A Bindings is just a Map<String,Object>. The engine has two
scopes per the JSR 223 spec:
engine.put(k, v)
or engine.getContext().setBindings(b, ScriptContext.ENGINE_SCOPE).engine.getContext().setBindings(b, ScriptContext.GLOBAL_SCOPE).Engine scope shadows global scope; that is, if both define the same key, the script sees the engine-scope value.
The engine exposes the merged bindings to the script as a single
class member named bindings, of Aussom type map. Read it from
inside main (or any method on the synthetic class) with the
idiomatic this. prefix that Aussom requires for instance members:
eng.put("name", "Ada");
Object r = eng.eval("return \"hello \" + this.bindings[\"name\"] + \"!\";");
// r == "hello Ada!"
This is the most common gotcha to be aware of: it is
this.bindings["name"], not bindings["name"] and not just
name. Aussom requires this. to access an instance member from
inside a method body, and we expose bindings as one such member.
Anything the script writes to this.bindings["..."] is reflected
back into the host's engine scope after the eval completes:
eng.put("counter", 10L);
eng.eval("this.bindings[\"counter\"] = this.bindings[\"counter\"] + 5;");
System.out.println(eng.get("counter")); // -> 15
The script can also create new bindings:
eng.eval("this.bindings[\"created\"] = \"yes\";");
System.out.println(eng.get("created")); // -> yes
If you want bindings that only apply to one eval (without touching
the engine's default state), use the two-argument eval:
Bindings b = new SimpleBindings();
b.put("x", 5L);
b.put("y", 7L);
Object r = eng.eval(
"return this.bindings[\"x\"] + this.bindings[\"y\"];", b);
// r == 12L
The engine converts between Java host types and Aussom runtime types in both directions. You usually do not have to think about it -- pass plain Java values in, get plain Java values back -- but here are the rules.
| Host type | Aussom type |
|---|---|
null |
cnull |
Boolean |
bool |
Byte, Short, Integer, Long |
int |
Float, Double |
double |
Character, String, CharSequence |
string |
java.util.List<?> |
list |
java.util.Map<String,?> |
map |
Java array (e.g. Object[], long[]) |
list |
Any other Object |
opaque object wrap |
Maps with non-String keys have their keys stringified.
| Aussom type | Host type |
|---|---|
cnull |
null |
bool |
Boolean |
int |
Long |
double |
Double |
string |
String |
list |
java.util.ArrayList<Object> |
map |
java.util.LinkedHashMap<String,Object> (preserves insertion order) |
object |
the original externObject if set, else the AussomObject itself |
exception |
translated to a thrown ScriptException |
Note that Aussom integers are 64-bit, so int always comes back as
Long -- never Integer. Cast accordingly:
long sum = (Long) eng.eval("return 2 + 3;"); // 5L
double d = (Double) eng.eval("return 1.5 + 2.5;"); // 4.0
com.aussom.stdlib.console (c) is Aussom's standard output sink.
By default, c.log("..."), c.info(...), c.warn(...), and friends
write to the JVM's System.out. c.err(...) writes to System.err.
You can redirect both per-engine, per-eval, by setting writers on
the ScriptContext:
StringWriter out = new StringWriter();
StringWriter err = new StringWriter();
eng.getContext().setWriter(out);
eng.getContext().setErrorWriter(err);
eng.eval("c.log(\"normal message\"); c.err(\"problem\");");
System.out.println(out); // -> normal message
System.out.println(err); // -> problem
Replacing the writer between calls takes effect immediately on the next eval -- there is no need to rebuild the engine.
The routing is per-thread thanks to a ThreadLocal inside console,
so if two threads call eval on the same engine with different
contexts, neither sees the other's output. See the threading section
for details.
Implementing Compilable lets you save the cost of parsing on
repeated invocations. Cast the engine to Compilable and call
compile:
Compilable comp = (Compilable) eng;
CompiledScript cs = comp.compile(
"return this.bindings[\"x\"] * this.bindings[\"x\"];");
eng.put("x", 7L);
System.out.println(cs.eval()); // 49
eng.put("x", 9L);
System.out.println(cs.eval()); // 81
CompiledScript.eval accepts the same overloads as
ScriptEngine.eval: with a default context, with a Bindings, or
with a custom ScriptContext.
The compiled form is tied to its owning engine. If the host needs
several CompiledScript objects that share state, build them all
from the same ScriptEngine instance.
Invocable is the bridge that lets a host call into Aussom-defined
code by name after a script has been loaded.
eng.eval("class Calc { public add(int a, int b) { return a + b; } }");
Invocable inv = (Invocable) eng;
Object sum = inv.invokeFunction("add", 2L, 3L); // -> 5L
invokeFunction looks for a method named add with a matching
arity on any class registered in the engine. The first matching
class wins. If no class has such a method, NoSuchMethodException
is thrown (per JSR 223, this is not a ScriptException).
If you have a handle to a particular Aussom object, dispatch on it directly:
eng.eval("class Greeter { public hi(string name) { return \"hi \" + name; } }");
Object g = eng.eval("return new Greeter();");
Invocable inv = (Invocable) eng;
String s = (String) inv.invokeMethod(g, "hi", "alice"); // -> "hi alice"
The receiver must be an AussomObject produced by an earlier eval
or invokeFunction. Passing a plain Java object will throw
IllegalArgumentException.
If your host already has an interface like java.lang.Runnable or
java.util.function.Function, you can ask the engine for an
implementation backed by Aussom code:
eng.eval("class Worker { public run() { c.log(\"working\"); return null; } }");
Invocable inv = (Invocable) eng;
Runnable r = inv.getInterface(Runnable.class);
new Thread(r).start();
getInterface(Class) walks every interface method and checks that
some Aussom class declares a method with the same name. If even one
is missing it returns null. The proxy it returns dispatches each
Java call through invokeFunction, so the same arity / name rules
apply.
There is also getInterface(Object thiz, Class) which proxies onto
a specific Aussom receiver (analogous to invokeMethod).
This is convenient for callbacks, but a Java -> Proxy -> Aussom hop is not free; do not put one inside a tight inner loop.
All script-side failures arrive as javax.script.ScriptException (a
checked exception). The engine maps the underlying cause as follows:
| Source | Resulting exception |
|---|---|
| Parse failure | ScriptException with file/line |
| Runtime error (bad call, type mismatch, ...) | ScriptException with message |
Invocable lookup miss |
NoSuchMethodException (per JSR 223) |
getInterface non-interface argument |
IllegalArgumentException |
The engine internally subclasses ScriptException as
com.aussom.script.AussomScriptException when the failure came from
the Aussom runtime. If you need to introspect the Aussom-side stack,
cast and call getAussomException(). For most hosts, the message
text is enough.
A failed parse leaves the engine in a clean state. The next eval
on the same engine starts fresh -- you do not need to rebuild it.
ScriptEngineFactory.getParameter("THREADING") returns
"MULTITHREADED". In JSR 223 terms that means: a single
ScriptEngine is safe to call from multiple threads
concurrently, as long as the host does not modify shared scopes
without synchronizing.
The engine's threading guarantees in detail:
compile or into eval(source-string, ...) (which compiles
under the hood) take a per-engine lock automatically. You do not
need to wrap them.eval, invokeFunction,
or invokeMethod allocates a fresh stack frame and locals, so
running scripts on different threads do not see each other's
variables.console instance via a ThreadLocal. A ScriptContext writer
set up on thread A is not visible to thread B.static classes
(including stdlib singletons like c, sys, math) keep one
instance per JVM. If a script mutates a field on a static class
from multiple threads, it is the script's job to coordinate.
The engine does not synchronize individual field writes.What we do not advertise:
THREAD-ISOLATED: would require per-thread Bindings views.
Hosts that need thread-isolated bindings should pass a fresh
ScriptContext to each eval (see Section 5.3).STATELESS: incompatible with Aussom's static-class model.Two patterns hosts commonly use safely:
// Pattern A: single engine, many threads, per-call bindings.
ScriptEngine eng = new ScriptEngineManager().getEngineByName("aussom");
Runnable task = () -> {
SimpleScriptContext ctx = new SimpleScriptContext();
Bindings b = new SimpleBindings();
b.put("n", Thread.currentThread().getId());
ctx.setBindings(b, ScriptContext.ENGINE_SCOPE);
Object r = eng.eval("return this.bindings[\"n\"] * 2;", ctx);
// ...
};
// Pattern B: one engine per thread (cheaper alternative if you
// already use a thread pool with a fixed number of workers).
ThreadLocal<ScriptEngine> per = ThreadLocal.withInitial(
() -> new ScriptEngineManager().getEngineByName("aussom"));
Both work. Pattern A is more memory-efficient because it shares the parsed stdlib AST. Pattern B avoids any chance of contention on the parse lock when you compile a lot.
A list of things that bite host developers most often. Read this once and keep it as a reference.
this.bindings["x"], not bindings["x"].
Aussom requires the this. prefix to access an instance member
from inside a method body. The engine exposes host bindings as
one such member.
Local variables in Aussom are untyped.
int x = 1; is not valid as a local declaration. Write
x = 1; -- the type is inferred from the value. (Typed
declarations are allowed for class members and method parameters,
not for locals.)
for (item : list), not foreach.
Aussom's iteration syntax uses a colon:
for (v : this.bindings["xs"]) { sum = sum + v; }
There is no foreach keyword.
You must return an expression to get its value back.
Aussom does not auto-return the last expression. Without an
explicit return ...;, eval gives you null.
Aussom integers are 64-bit.
They marshal to Long, never Integer. Cast accordingly.
Aussom maps are keyed on String.
When you pass a Map<Integer,X> from Java, the keys are
stringified.
Strings are double-quoted. Single quotes are for character literals.
c.log is the print function.
print, println, info, dbg, warn, and err are all
available on c (the console static class). err is the only
one that goes to the error writer; the rest go to the regular
writer.
Class definitions persist across evals on the same engine.
eng.eval("class Calc { public add(a,b) { return a + b; } }");
// Calc is now available for any future eval / invokeFunction
// call on this engine. To start clean, build a new engine.
Synthetic eval classes accumulate per engine.
Every eval of a snippet adds one synthetic class to the engine's
class registry. Long-lived hosts that evaluate many distinct
snippets per second should compile via Compilable.compile
(which reuses one class per compiled script) or recreate the
engine periodically. The total memory footprint is small per
class, but it does grow.
Cold-path class instantiation.
The very first instantiation of a class with inheritance
rebuilds parent-link state in non-thread-safe ways. The engine
automatically pre-warms classes that contain a main (which
covers eval of full programs and snippets). For other user
classes you intend to call concurrently via invokeFunction,
make at least one warm-up call on a single thread before fanning
out -- typically by invoking any method on the class once at
startup.
Compilable does not freeze the engine's class list.
A CompiledScript holds a reference to a synthetic class name
in the underlying engine. If you call
Compilable.compile(badSource) after compiling a good one, the
good one keeps working -- but a new bad parse on the same
engine still throws. Errors in one compile do not invalidate
the others.
A practical pattern: load a set of Aussom rules at startup, then
call into them from a hot path with invokeFunction. This is what
JSR 223 was designed for.
import javax.script.*;
import java.io.*;
import java.util.*;
public class RulesDemo {
public static void main(String[] args) throws Exception {
ScriptEngineManager mgr = new ScriptEngineManager();
ScriptEngine eng = mgr.getEngineByName("aussom");
// Load the rules definitions once at startup.
eng.eval(new FileReader("rules.aus"));
Invocable inv = (Invocable) eng;
// Apply the rules to a stream of events.
List<Map<String,Object>> events = new ArrayList<>();
events.add(Map.of("kind", "login", "user", "alice"));
events.add(Map.of("kind", "purchase", "amount", 99L));
for (Map<String,Object> ev : events) {
Object verdict = inv.invokeFunction("evaluate", ev);
System.out.println(ev + " -> " + verdict);
}
}
}
rules.aus might look like:
class RuleSet {
public evaluate(map event) {
if (event["kind"] == "purchase" && event["amount"] > 100) {
return "review";
}
return "allow";
}
}
The host code knows nothing about Aussom internals. It just calls the standard JSR 223 API, and the engine takes care of parse, dispatch, and marshalling.
design/aussom-jsr-223.md in this repository.