Menu

Embedding Aussom in a Java Application via JSR 223

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.


1. What is JSR 223?

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.


2. Quick start

2.1 Add the dependency

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.

2.2 Hello world

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.


3. Discovering the engine

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());
}

4. Running scripts

4.1 The eval overloads

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)

4.2 Snippets vs. full programs

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.

4.3 Returning values

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 ...;.


5. Bindings (passing values to and from a script)

A Bindings is just a Map<String,Object>. The engine has two scopes per the JSR 223 spec:

  • Engine scope -- per-engine variables. Set with engine.put(k, v) or engine.getContext().setBindings(b, ScriptContext.ENGINE_SCOPE).
  • Global scope -- shared bindings. Set with 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.

5.1 Reading bindings inside Aussom

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.

5.2 Writing bindings from inside Aussom

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

5.3 Per-eval bindings

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

6. Type marshalling

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.

6.1 Java -> Aussom (input)

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.

6.2 Aussom -> Java (output)

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

7. Routing script output

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.


8. Compilable: parse once, run many

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.


9. Invocable: calling Aussom functions and methods from Java

Invocable is the bridge that lets a host call into Aussom-defined code by name after a script has been loaded.

9.1 invokeFunction: call a method on any class

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).

9.2 invokeMethod: call on a specific receiver

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.

9.3 getInterface: bind a Java interface to Aussom code

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.


10. Errors

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.


11. Threading

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:

  1. Parsing is serialized internally. Concurrent calls into compile or into eval(source-string, ...) (which compiles under the hood) take a per-engine lock automatically. You do not need to wrap them.
  2. Run paths use per-call state. Each eval, invokeFunction, or invokeMethod allocates a fresh stack frame and locals, so running scripts on different threads do not see each other's variables.
  3. Output routing is per-thread. Each thread has its own console instance via a ThreadLocal. A ScriptContext writer set up on thread A is not visible to thread B.
  4. Static class fields are shared. Aussom's 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.


12. Common gotchas

A list of things that bite host developers most often. Read this once and keep it as a reference.

  1. 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.

  2. 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.)

  3. 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.

  4. 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.

  5. Aussom integers are 64-bit. They marshal to Long, never Integer. Cast accordingly.

  6. Aussom maps are keyed on String. When you pass a Map<Integer,X> from Java, the keys are stringified.

  7. Strings are double-quoted. Single quotes are for character literals.

  8. 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.

  9. 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.
    
  10. 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.

  11. 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.

  12. 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.


13. End-to-end example: an Aussom-driven rules engine

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.


14. Where to go next

  • JSR 223 reference: https://docs.oracle.com/javase/8/docs/technotes/guides/scripting/prog_guide/api.html
  • Aussom language overview: https://aussom-lang.com/docPage?page=written/aussom.md&title=Aussom%20Language%20Overview
  • Aussom style guide: https://aussom-lang.com/docPage?page=written/aussom-style-guide.md&title=Aussom%20Style%20Guide
  • Engine design notes (for anyone curious about the internals, threading audit, cold-path mitigations): see design/aussom-jsr-223.md in this repository.