Menu

Embedding Aussom Directly via the Engine API

This guide is for Java developers who want to embed Aussom in a host JVM application using the direct com.aussom.Engine API rather than going through javax.script (JSR 223). The direct path gives you finer control over security policy, extern class loading, the parse/run lifecycle, and threading.

If you only need to run scripts and pass a few values back and forth, prefer the JSR 223 guide (design/usage-docs/aussom-lang-jsr223-usage.md). It is shorter and more language-neutral. Use this guide when you need to:

  • Customize Aussom's security policy at the Java level.
  • Wrap your own Java classes as Aussom extern classes (the way the stdlib does for c, sys, math, etc.).
  • Drive the parse / run lifecycle directly (multiple includes, search paths, doc-mode parsing, mid-script reflection).
  • Build a non-standard runner like aussom-script (browser-side) or the aussom CLI on top of aussom-base.

You should be comfortable with Java 8+ and have a basic feel for Aussom syntax before starting.


1. The big picture

aussom-base ships three layers:

+---------------------------------------------------+
|  Aussom source code (.aus files / strings)        |
|  - your scripts, your stdlib, the host's hooks    |
+---------------------------------------------------+
|  com.aussom.Engine     parse, run, instantiate    |
|  com.aussom.Universe   global class registry      |
|  com.aussom.Environment per-call scope + locals   |
|  com.aussom.CallStack  per-call stack trace       |
+---------------------------------------------------+
|  Java extern classes (com.aussom.stdlib.*, yours) |
|  - implement Aussom-callable methods              |
+---------------------------------------------------+

The interpreter walks an AST built by the CUP/JFlex parser stack. Most of the moving parts you interact with are on Engine and the com.aussom.types.* value classes. You will rarely need to touch the AST directly.

Concrete pieces you'll work with:

Class What it is
Engine One interpreter instance.
Universe (singleton) Process-wide registry of stdlib class definitions.
Environment Per-call wrapper holding class instance, locals, callstack, current object.
CallStack Linked stack-frame chain for tracebacks.
Members ConcurrentHashMap of an object's instance members.
SecurityManagerInt Property-based access control.
LoggingInt + console Output sink, ThreadLocal per thread.
AussomType (+ subclasses) Runtime value types -- everything is one of these.

2. Quick start

2.1 Add the dependency

<dependency>
    <groupId>io.github.rsv-code</groupId>
    <artifactId>aussom.base</artifactId>
    <version>1.2.3</version>
</dependency>

2.2 Run a file

import com.aussom.DefaultSecurityManagerImpl;
import com.aussom.Engine;
import com.aussom.ast.aussomException;
import com.aussom.stdlib.console;
import com.aussom.DefaultLoggingImpl;

public class RunFile {
    public static void main(String[] args) throws Exception {
        // Optional: register a logger so c.log lands somewhere visible.
        DefaultLoggingImpl logger = new DefaultLoggingImpl();
        logger.setLevel(DefaultLoggingImpl.INFO);
        console.get().register(logger);

        // 1. Construct the engine with a security policy.
        Engine eng = new Engine(new DefaultSecurityManagerImpl());

        // 2. Tell the engine where to find stdlib resources packaged
        //    inside the aussom-base jar.
        eng.addResourceIncludePath("/com/aussom/stdlib/aus/");

        // 3. Parse a file.
        eng.parseFile("script.aus");

        // 4. Run. The engine looks for any class with a main() and
        //    calls it. The return value is the exit code.
        int rc = eng.run();
        System.exit(rc);
    }
}

The CLI entry point (com.aussom.Main) is essentially the snippet above plus argument parsing. If you only need to "run a script," this is the whole story.

2.3 Run a string

Engine eng = new Engine(new DefaultSecurityManagerImpl());
eng.addResourceIncludePath("/com/aussom/stdlib/aus/");

eng.parseString("inline.aus",
    "include sys;\n" +
    "class App {\n" +
    "    public main(args) {\n" +
    "        c.log(\"hello from inline\");\n" +
    "        return 0;\n" +
    "    }\n" +
    "}\n");

int rc = eng.run();

parseString takes a virtual filename (used in stack traces) plus the source text. You can call it multiple times to layer multiple files into one engine before calling run().


3. The Engine lifecycle

A typical Engine usage cycle is:

new Engine(SecurityManagerInt)
   |
   v
addIncludePath / addResourceIncludePath / addExcludePath
   |
   v
parseFile / parseString / addInclude     (any number of times)
   |
   v
run()                                    OR   instantiateObject(...) + call methods
   |
   v
(engine state preserved -- you can call again)

3.1 Construction

Engine eng = new Engine();                              // uses SecurityManagerImpl()
Engine eng = new Engine(new DefaultSecurityManagerImpl());
Engine eng = new Engine(new MyCustomSecurityManager());

The first Engine you construct in the JVM parses lang.aus once and copies its class definitions into the process-wide Universe singleton. Subsequent engines reuse that snapshot. So the cold construction is a bit slower than warm ones; for performance- sensitive hosts, build the engine eagerly during startup.

3.2 Includes and search paths

addResourceIncludePath(String) registers a JAR-internal resource path. The default stdlib is at /com/aussom/stdlib/aus/. Add the path before any include statement in user code can resolve.

addIncludePath(String) registers a filesystem search path for include statements in user code.

addExcludePath(String) blocks a filesystem path. If user code tries to include from an excluded path, the engine throws.

addInclude(String) programmatically pulls in a stdlib module by name -- for example eng.addInclude("sys");. User scripts normally do this themselves with the include keyword.

3.3 Parsing

eng.parseFile("path/to/script.aus");
eng.parseString("name.aus", "<source>");

These add class definitions to the engine. They do not run anything. You can parse many sources into the same engine; the order matters only for extends references between user classes.

If parsing fails, eng.hasParseErrors() returns true. Engine.run() will refuse to execute when this flag is set. You can clear it with eng.clearParseError() if you want to reparse.

3.4 Running

int rc = eng.run();

run() searches every registered class for a main function, picks the first one it finds, instantiates that class, and calls main(args) (or main() if no overload accepts args). The integer return value of main is what run() returns. If main throws, run() returns 1.

Pass arguments to main(args) before calling run():

eng.addMainArg("--verbose");
eng.addMainArg("input.txt");
int rc = eng.run();

3.5 Calling without main

You don't have to use main at all. If your scripts are libraries, instantiate classes directly and call methods on them:

import com.aussom.types.AussomList;
import com.aussom.types.AussomObject;
import com.aussom.types.AussomString;
import com.aussom.types.AussomType;

eng.parseString("tools.aus",
    "class Greeter {\n" +
    "    public hi(string name) { return \"hi \" + name; }\n" +
    "}\n");

AussomList ctorArgs = new AussomList();
AussomObject g = eng.instantiateObject("Greeter", ctorArgs);

AussomList callArgs = new AussomList();
callArgs.add(new AussomString("alice"));
// You'd typically dispatch via the class's call() through Environment;
// see Section 8 for the full pattern in concurrent contexts.

For long-running hosts that call the same script repeatedly, this pattern is much faster than rebuilding the engine each time.

3.6 Static classes

Static classes (static class Foo {...}) are auto-instantiated when their definition is added. Retrieve their singleton instance with:

AussomType foo = eng.getStaticClass("Foo");

Use this to read script-side configuration set up at startup, or to hand a Java caller a stable reference to a script-defined service.


4. The security model

Engine always carries a SecurityManagerInt. Every privileged action in the stdlib (filesystem reads, network calls, reflection, documentation generation, mock injection) goes through it as a property check.

4.1 SecurityManagerInt at a glance

public interface SecurityManagerInt {
    Object getProperty(String PropName);             // Java side
    AussomType getProp(Environment, ArrayList<AussomType>);
    AussomType keySet(Environment, ArrayList<AussomType>);
    AussomType getMap(Environment, ArrayList<AussomType>);
    AussomType setProp(Environment, ArrayList<AussomType>);
    AussomType setMap(Environment, ArrayList<AussomType>);
}

SecurityManagerImpl is the concrete base class. It stores properties in a ConcurrentHashMap<String,Object> named props. Stdlib code reads them through getProperty and refuses operations whose property is false. Aussom scripts can read or mutate them through the secman static class via getProp / setProp -- both checked against securitymanager.property.get and securitymanager.property.set.

4.2 Built-in implementations

Class Use case
SecurityManagerImpl Locked-down baseline. Most actions disallowed.
DefaultSecurityManagerImpl Baseline + aussomdoc.* allowed. CLI default.
TestSecurityManagerImpl Adds test.mock.inject, test.mock.spy, aussom.script.mode.enable.

4.3 Building a custom security manager

Subclass SecurityManagerImpl and override property defaults in your constructor:

package com.example;

import com.aussom.SecurityManagerImpl;

public class HostSecurityManager extends SecurityManagerImpl {
    public HostSecurityManager(boolean trustedScript) {
        super();   // installs every default first

        if (trustedScript) {
            // Allow filesystem + reflection only for trusted scripts.
            this.props.put("file.read",            true);
            this.props.put("file.write",           true);
            this.props.put("reflect.eval.string",  true);
            this.props.put("reflect.include.module", true);
        }

        // Always permit reading our internal config map keyset.
        this.props.put("config.list", true);

        // Add custom properties that your own extern classes can
        // check. The key namespace is up to you.
        this.props.put("myhost.allowMutation", false);
        this.props.put("myhost.netCallsPerMinute", 60L);
    }
}

Pass it to the engine:

Engine eng = new Engine(new HostSecurityManager(/*trusted=*/true));

4.4 Reading properties from your own extern classes

Inside any extern class method, the security manager is available through the engine handle on Environment:

public AussomType saveSettings(Environment env, ArrayList<AussomType> args) {
    Object allowed = env.getEngine().getSecurityManager()
        .getProperty("myhost.allowMutation");
    if (!(Boolean) allowed) {
        return new AussomException(
            "saveSettings: action 'myhost.allowMutation' not permitted.");
    }
    // ... do the work ...
    return env.getClassInstance();
}

The check pattern -- "look up a boolean property, return an AussomException if it's false" -- is the convention every stdlib class follows. Sticking to it keeps your security policy auditable in one place (HostSecurityManager's constructor).

4.5 Locking a security manager at runtime

You can flip properties on or off after construction:

HostSecurityManager sm = new HostSecurityManager(false);
Engine eng = new Engine(sm);
eng.parseFile("untrusted.aus");

sm.props.put("file.read", true);   // open up just before run
int rc = eng.run();
sm.props.put("file.read", false);  // and close again afterwards

This is occasionally useful for setup/teardown code that needs elevated permissions while user code does not. Be careful though -- the props map is a public-ish field on SecurityManagerImpl, and script code with securitymanager.property.set = true could mutate it from inside Aussom too. Lock that property down for untrusted code.


5. Script mode

Script mode is an alternative entry point on Engine for embedders that want to evaluate top-level Aussom statements incrementally -- the shape of a REPL prompt, an "evaluate selection" command in an editor, or a server endpoint that runs ad-hoc expressions against a long-lived state.

It is independent of the parse / run pipeline covered in Section 3. The synthetic class script mode builds is not registered in the engine's class registry, so Engine.run() continues to look for a user-declared main exactly as it does today. You can enable script mode and call evalLine on the same engine that you also parseFile and run() -- the two sides do not see each other.

5.1 When to use it

Use the parse / run pipeline (Section 3) for:

  • Complete .aus files with main().
  • Today's exit-code behavior (run() returns the int result of main, or 1 on exception).

Use script mode for:

  • Building a REPL or interactive editor command.
  • Hosts that accept source from a user prompt or a network request and want each fragment to share state with earlier fragments.
  • Evaluating top-level statements without a wrapping class and main.

5.2 The security property

Script mode is gated by a security property, aussom.script.mode.enable. Defaults across the shipped managers:

Manager aussom.script.mode.enable
SecurityManagerImpl (base) false
DefaultSecurityManagerImpl false (inherits)
TestSecurityManagerImpl true

For a host that wants script mode in production, enable the property in your custom SecurityManagerImpl subclass:

public class HostSecurityManager extends SecurityManagerImpl {
    public HostSecurityManager() {
        super();
        this.props.put("aussom.script.mode.enable", true);
    }
}

Without it, setScriptMode(true) throws an aussomException:

Engine.setScriptMode: Security exception, action
'aussom.script.mode.enable' not permitted.

The check fires on setScriptMode(true) and on every evalLine call (matching the per-call check pattern other stdlib actions use). A script that flips the property off via secman.setProp (with securitymanager.property.set enabled) will block subsequent evalLine calls too.

5.3 Enabling and using evalLine

import com.aussom.Engine;
import com.aussom.types.AussomType;

Engine eng = new Engine(new HostSecurityManager());
eng.addResourceIncludePath("/com/aussom/stdlib/aus/");
eng.setScriptMode(true);

eng.evalLine("x = 5;");
eng.evalLine("y = 7;");
AussomType last = eng.evalLine("c.log(\"sum = \" + (x + y));");

Each evalLine call:

  1. Parses the supplied source as a script-mode fragment.
  2. Appends every parsed top-level statement to the synthetic __script_main class's main body.
  3. Walks just the newly-appended slice against a long-lived Environment whose Members persists across calls.
  4. Returns the AussomType produced by the last evaluated statement (or AussomNull if the source produced no executable statements).

include and class declarations in the source still flow through the existing addInclude / addClass paths on the engine -- only bare top-level statements end up in the synthetic main's body.

A whole file can be passed in one call:

String src = new String(Files.readAllBytes(Paths.get("script.aus")));
eng.evalLine(src);

Two overloads exist:

public AussomType evalLine(String source) throws Exception;
public AussomType evalLine(String source, int lineNumber) throws Exception;

The single-argument overload delegates to the two-argument one with lineNumber = 1. See Section 5.4 for what lineNumber does.

5.4 File name and line numbers for error attribution

By default every node parsed by evalLine is tagged with file name "<script>". Set a meaningful name once after enabling script mode:

eng.setScriptFileName("session.aus");

The two-argument evalLine(String, int) tells the lexer which line the first source line should report as. Use it when feeding a snippet from the middle of a larger file:

// Snippet that begins on line 42 of session.aus.
eng.evalLine("a = 1;\nb = 2;\n1/0;", 42);

The third snippet line (1/0;) reports as session.aus line 44. The offset propagates through the lexer into every AST node, so attributions are correct for top-level statements, sub-expressions, and any class or include declarations parsed in the same call.

5.5 Persistent locals across calls

The Environment script mode uses lives until script mode is disabled (which discards it; re-enabling builds a fresh one). Its Members is the persistent locals store. Anything an earlier evalLine call bound -- through assignment, for iteration, a try body -- stays visible to later calls until overwritten or shadowed.

This is the "REPL feel" the API is built around. There is no rollback on failure: a runtime error from an earlier statement in the same evalLine call leaves any locals it bound up to that point intact for the next call.

5.6 Independence from Engine.run

Script mode and the classical run pipeline coexist on one engine without interference:

  • The synthetic __script_main class is not in the engine's classes map. setMainClassAndFunct and Engine.run() do not see it.
  • evalLine does not invoke setMainClassAndFunct, callMain, or astClass.call against the synthetic class.
  • Static classes (Section 3.6) are still resolved normally from top-level statements, so c.log(...), sys.getSysInfo(), and any host-provided static externs work the same as in main.
eng.parseFile("user.aus");      // user file with class Foo { main(args) {...} }
eng.setScriptMode(true);
eng.evalLine("x = 99;");        // touches script-mode state only
int rc = eng.run();             // calls Foo.main, ignoring x = 99
eng.evalLine("c.log(x);");      // x is still 99

5.7 Errors

Parse errors throw aussomException. evalLine rolls back any statements the parser appended before the syntax error so the synthetic main never accumulates half-parsed nodes:

try {
    eng.evalLine("x = ;");        // syntax error
} catch (aussomException pe) {
    System.err.println("parse error: " + pe.getMessage());
}
eng.evalLine("x = 5;");           // recovers, parses cleanly

Runtime errors do not throw. A statement that returns or throws an exception during eval is caught by evalLine and returned as an AussomException value:

AussomType ret = eng.evalLine("z = 1/0;");
if (ret.isEx()) {
    AussomException ex = (AussomException) ret;
    System.err.println("runtime: " + ex.getText()
        + " at line " + ex.getLineNumber());
}

The line on the exception comes from the failing AST node's parserInfo, which carries the file name and the caller-supplied line offset (Section 5.4). So an error on a top-level statement attributes to the original source location straight from the AST.

evalLine itself only throws for parse errors and security- property denials. Statement-level failures are returned as values.

5.8 Inspecting the synthetic class

Tooling (an LSP analyzer, a debugger, a session "show history" command) can introspect the accumulated script via Engine.getScriptClass():

import com.aussom.ast.astClass;
import com.aussom.ast.astFunctDef;
import com.aussom.ast.astNode;

astClass synth = eng.getScriptClass();      // null if script mode is off
if (synth != null) {
    // The synthetic main(args) has an untyped wildcard arg, so
    // it lives in the class's wildcardOverloads list rather
    // than the flat dispatchMap. Reach it via getFunctionsByName.
    astFunctDef mainFn = synth.getFunctionsByName("main").get(0);
    List<astNode> stmts = mainFn.getInstructionList().getStatements();
    // Walk stmts as needed.
}

The accessor is read-only and side-effect-free. The synthetic class's name is Engine.SCRIPT_CLASS_NAME (currently "__script_main") -- avoid declaring your own class with that name in script source.

5.9 What script mode does not do

  • No top-level function definitions. A public foo(x) { ... } at the top level is rejected with a parse error. Scripts that need reusable functions declare a class -- which then registers normally and is available to subsequent evalLine calls.
  • Single-threaded per engine. The script-mode Environment is shared across calls on one engine. Concurrent evalLine calls on the same engine race on its Members. Use one engine per script-mode session.

6. Wrapping a Java class as an Aussom extern class

This is the main extension point of aussom-base. It is how the stdlib exposes c.log, sys.getSysInfo, math.sqrt, and so on.

The pattern has three parts:

  1. A Java class with one method per Aussom-callable function.
  2. An Aussom-side extern class declaration that binds method names to the Java class.
  3. Registering the Aussom-side declaration with the engine (load it like any other source file, or include it).

6.1 The Java side

Every Aussom-callable method has the same signature:

public AussomType methodName(Environment env, ArrayList<AussomType> args)
  • env carries the engine, the current class instance, the locals map, the callstack, and a "current object" pointer.
  • args is the list of arguments the script passed, in order, all marshalled into AussomType instances.
  • Return value is also AussomType. Never throw a checked Java exception out of one of these methods; either return an AussomException (script-visible) or let an uncaught aussomException propagate.

Concrete example -- a "counter" extern class:

package com.example;

import java.util.ArrayList;

import com.aussom.Environment;
import com.aussom.types.AussomException;
import com.aussom.types.AussomInt;
import com.aussom.types.AussomType;

public class ACounter {
    private long value = 0L;

    public AussomType inc(Environment env, ArrayList<AussomType> args) {
        this.value++;
        return env.getClassInstance();   // see Section 6.4 on chaining
    }

    public AussomType incBy(Environment env, ArrayList<AussomType> args) {
        try {
            long n = ((AussomInt) args.get(0)).getValue();
            this.value += n;
        } catch (Exception e) {
            return new AussomException(
                "counter.incBy: expected one int arg: " + e.getMessage());
        }
        return env.getClassInstance();
    }

    public AussomType get(Environment env, ArrayList<AussomType> args) {
        return new AussomInt(this.value);
    }

    public AussomType reset(Environment env, ArrayList<AussomType> args) {
        this.value = 0L;
        return env.getClassInstance();
    }
}

6.2 The Aussom side

A matching extern class declaration tells the engine which Aussom methods route to which Java class:

/*
 * counter.aus
 * A simple monotonically-increasing counter.
 */
extern class counter : com.example.ACounter {
    /**
     * Increments the counter by one.
     * @r The counter object for chaining.
     */
    public extern inc();

    /**
     * Increments the counter by the given amount.
     * @p amount is an int with how much to add.
     * @r The counter object for chaining.
     */
    public extern incBy(int amount);

    /**
     * Returns the current value.
     * @r An int with the count.
     */
    public extern get();

    /**
     * Resets the counter to zero.
     * @r The counter object for chaining.
     */
    public extern reset();
}

The colon syntax extern class counter : com.example.ACounter binds the Aussom name on the left to the fully qualified Java class on the right. The public extern foo(); lines list the methods -- note that there is no body; the body lives on the Java side.

6.3 Registering with the engine

If your extern class lives outside the stdlib, ship the .aus file on the JVM resource path or on the filesystem and tell the engine where to look:

// JAR resource:
eng.addResourceIncludePath("/com/example/aus/");
// or filesystem:
eng.addIncludePath("/opt/myapp/aus/");

eng.addInclude("counter");

Now scripts can use it:

include counter;

class App {
    public main(args) {
        c = new counter();
        c.inc().inc().incBy(5).inc();
        c.log("count = " + c.get());
        return 0;
    }
}

(Note: c here shadows the c console singleton inside this scope -- that's fine, but in real code you'd pick a different name.)

6.4 Method chaining: returning env.getClassInstance()

A method that doesn't have a useful value to return should still return something that lets script-side callers chain. The convention is return env.getClassInstance(), which gives the caller back the receiver of the call. That makes this work in Aussom:

counter.inc().inc().incBy(5);

Without that, every chained call would have to be split onto its own line:

counter.inc();
counter.inc();
counter.incBy(5);

env.getClassInstance() is the exact receiver -- it's the AussomObject whose method is being called, not just any instance of that class. So returning it is safe even when multiple instances of the class exist.

If you do have a useful value, return that instead. The script caller will see whatever AussomType you return.

6.5 Static extern classes

For singletons (the equivalent of Math or console), declare the class static:

static extern class chrono : com.example.AChrono {
    public extern now();
    public extern sleep(int ms);
}

The Aussom engine instantiates the Java class once at engine construction time, and scripts reference it directly without new:

chrono.sleep(100);
t = chrono.now();

Static classes have all the same threading caveats as static state in Java -- if your script mutates fields on a static class from multiple threads, you must coordinate that yourself. The engine will not synchronize individual field writes for you.

6.6 Inheritance: extending an existing extern class

To add behavior to an existing Aussom type (or another extern class), have your Java class extend the appropriate base. For example, the stdlib int type's Java side is AussomInt; an extension would extend AussomInt. For ordinary objects, extend AussomObject. The Aussom side then declares the extension's parent the usual way (extern class myInt extends int : ...).

In practice, most host extern classes do not need this -- a plain Java class with AussomType methods is enough.


7. Type marshalling and AussomType helpers

Every Aussom value at runtime is an AussomType. Subclasses live in com.aussom.types:

Aussom type Java class Underlying value
cnull AussomNull --
bool AussomBool boolean
int AussomInt long
double AussomDouble double
string AussomString String
list AussomList ArrayList<AussomType>
map AussomMap ConcurrentHashMap<String,AussomType>
object AussomObject external object + members
callback AussomCallback parsed lambda / function
exception AussomException message, line, stack

7.1 Reading args inside an extern method

The args ArrayList<AussomType> mirrors the Aussom call site positionally. Cast each entry to its expected type:

public AussomType setName(Environment env, ArrayList<AussomType> args) {
    String name = ((AussomString) args.get(0)).getValueString();
    int    age  = (int) ((AussomInt) args.get(1)).getValue();
    // ...
    return env.getClassInstance();
}

The engine's overload dispatcher already enforces argument types at the Aussom level if you typed them in the extern declaration (public extern setName(string name, int age);), so the cast at the top of the method is normally safe. If you used untyped args in the extern declaration, defend with instanceof first.

7.2 Producing a return value

Constructors on the type classes are simple:

return new AussomNull();
return new AussomBool(true);
return new AussomInt(42);
return new AussomDouble(3.14);
return new AussomString("hello");
return new AussomList();          // empty list, then .add(...)
return new AussomMap();           // empty map, then .put(key, ...)

For a list:

AussomList xs = new AussomList();
xs.add(new AussomInt(1));
xs.add(new AussomInt(2));
xs.add(new AussomInt(3));
return xs;

For a map:

AussomMap m = new AussomMap();
m.put("name", new AussomString("Ada"));
m.put("year", new AussomInt(1815));
return m;

7.3 Returning the receiver for chaining

As covered in Section 6.4:

return env.getClassInstance();

7.4 The externObject slot

AussomObject has a slot named externObject that holds the backing Java object. The runtime sets it automatically for the primitive types. For your own extern class, the linkage is also automatic: when an Aussom script does new mything(), the engine calls Class.newInstance() on the linked Java class, then assigns the result as the externObject of a fresh AussomObject. Inside your method, env.getClassInstance().getExternObject() gives you back the same Java instance you're currently executing on -- which is just this, so you rarely need it.

You only need to think about externObject if you are wrapping an existing Java object (e.g. one your host already constructed and wants to hand to a script). In that case build the AussomObject explicitly:

AussomObject ao = new AussomObject();
ao.setExternObject(myExistingJavaObject);
ao.setClassDef(eng.getClassByName("myJavaWrapperType"));

8. Exception handling

The Aussom runtime distinguishes two failure shapes:

  1. com.aussom.ast.aussomException -- a Java Exception subclass, thrown by the parser, by instantiateObject, and from inside extern code that wants to halt execution. Catch it in your host. The engine catches it during run() and returns 1.
  2. com.aussom.types.AussomException -- an AussomType (a value), returned from an extern method to signal a script-level failure. The runtime checks .isEx() after every dispatch and propagates the exception up the call stack as if it were thrown.

Inside an extern class method, always handle Java exceptions locally and convert them. Never let a RuntimeException or IOException escape -- it bypasses the Aussom call stack and surfaces as a raw Java stack trace through eng.run():

public AussomType readFile(Environment env, ArrayList<AussomType> args) {
    String path;
    try {
        path = ((AussomString) args.get(0)).getValueString();
    } catch (ClassCastException cce) {
        return new AussomException("readFile: arg 0 must be string");
    }

    Object allowed = env.getEngine().getSecurityManager()
        .getProperty("file.read");
    if (!(Boolean) allowed) {
        return new AussomException(
            "readFile: action 'file.read' not permitted.");
    }

    try {
        String contents = new String(
            java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(path)),
            java.nio.charset.StandardCharsets.UTF_8);
        return new AussomString(contents);
    } catch (java.io.IOException ioe) {
        return new AussomException(
            "readFile: " + ioe.getClass().getSimpleName()
            + ": " + ioe.getMessage());
    } catch (Exception e) {
        return new AussomException(
            "readFile: unexpected error: " + e.getMessage());
    }
}

Three rules of thumb:

  1. Validate, then act. Cast args at the top, security-check next, do the work last.
  2. Catch broadly. Wrap the body in a try { ... } catch (Exception e) { return new AussomException(...) } so nothing leaks out as a Java throwable.
  3. Use clear exception messages. They appear verbatim in the Aussom stack trace the user sees.

If an extern method does have a legitimate reason to abort the whole engine (a corrupt configuration, a security breach detected mid-run), throw aussomException directly:

import com.aussom.ast.aussomException;

throw new aussomException("internal: panic, bailing out.");

This is for fatal-only situations; the regular pattern is to return AussomException.

8.1 Reading the Aussom stack on the host side

After eng.run() throws aussomException:

try {
    eng.run();
} catch (aussomException ae) {
    // Pretty Aussom-style stack trace.
    System.err.println(ae.getAussomStackTrace());
}

getAussomStackTrace() produces the same indented multi-line trace that the CLI prints. Use it -- the default Throwable.toString() output is much less readable.


9. Threading

Aussom is already used in multi-threaded environments (the downstream AussomThread stdlib, JavaFX, GTK4). The runtime supports concurrency, but you have to follow the rules.

9.1 The model: one Engine, fresh Environment per thread

Environment carries everything that is per-call: the class instance, locals, callstack, current object. Engine and the parsed AST are shared.

The pattern, taken straight from AussomThread.run():

import com.aussom.CallStack;
import com.aussom.Environment;
import com.aussom.types.Members;

void runOnNewThread(Engine eng, AussomCallback cb) {
    new Thread(() -> {
        // CRUCIAL: clone the captured environment so this thread
        // does not share the locals/callstack with the spawner.
        Environment env = cb.getEnv().clone(cb.getEnv().getCurObj());
        AussomList args = new AussomList();
        AussomType ret = cb.call(env, args);
        // ... handle ret.isEx() etc.
    }).start();
}

The Environment.clone(curObj) call returns a new Environment sharing the same engine but with a fresh curObj. Without the clone, a callback running on a worker thread would mutate the spawner's locals during overload dispatch -- subtle, hard-to-debug data corruption.

For host code that calls Aussom methods directly from multiple threads (rather than through an AussomCallback), build a fresh Environment per call:

import com.aussom.CallStack;
import com.aussom.Environment;
import com.aussom.types.AussomList;
import com.aussom.types.Members;

AussomType callMethod(Engine eng, AussomObject receiver,
                       String name, AussomList args) {
    Environment env = new Environment(eng);
    env.setEnvironment(receiver, new Members(), new CallStack());
    env.setCurObj(receiver);
    return receiver.getClassDef().call(env, false, name, args);
}

This callMethod is safe to call from many threads on the same Engine because every invocation gets its own Environment, locals (Members), and CallStack. The runtime's hot path (astClass.call, astFunctDef.call) reads only AST state set up at parse time.

9.2 What Engine.run() is NOT safe for

Engine.run() mutates engine-level fields (mainCallStack, mainClassDef, mainClassInstance, etc.). It is not safe to call concurrently. If you need parallel execution, run main in one thread and dispatch into your scripts via per-thread Environments after that, as shown above. The JSR 223 layer is built exactly this way -- look at AussomScriptEngine.runCompiled for a worked example.

9.3 Parsing is not thread-safe

parseFile / parseString / addInclude mutate engine-level collections (fileNames, includes, classes) without locking. Either:

  • Do all parsing on a single thread before any worker threads start, or
  • Wrap parse calls in your own synchronized (eng) { ... } block.

Once parsing is complete, concurrent reads of the class registry are safe (the underlying classes and staticClasses maps are ConcurrentHashMap).

9.4 First-time class instantiation

The first instantiate() of a class with inheritance rebuilds parent-link state into plain ArrayLists. Subsequent instantiations are read-only and concurrent-safe, but the very first concurrent first-instantiation can race.

If your scripts define classes that will be hit hard from multiple threads, "warm" them on a single thread first:

AussomType warm = eng.instantiateObject("MyClass");
// throw it away; next instantiations are safe to do concurrently.

For classes containing main, the JSR 223 layer pre-warms them automatically. For direct embedding, do the warm-up yourself if it matters for your workload.

9.5 Output routing is per-thread

com.aussom.stdlib.console is held in a ThreadLocal. Each thread has its own registered LoggingInt. So if thread A does console.get().register(myLogger), thread B is unaffected -- which is usually what you want. To plumb output the same way across worker threads, register your logger on each thread before it starts running scripts.

9.6 Static-class fields are shared

A static class instance lives once per engine. If a script writes to a field on a static class from many threads, the writes race just like they would in plain Java. The engine does not synchronize individual field writes -- the script writer (or the host through the security manager) is responsible.


10. The console / LoggingInt

com.aussom.stdlib.console is the script-visible static c object plus a host-visible singleton sink. To capture script output:

import com.aussom.LoggingInt;
import com.aussom.stdlib.console;

class CapturingLogger implements LoggingInt {
    private final StringBuilder out = new StringBuilder();
    private final StringBuilder err = new StringBuilder();

    @Override public void log(String s)   { out.append(s).append('\n'); }
    @Override public void trc(String s)   { out.append(s).append('\n'); }
    @Override public void dbg(String s)   { out.append(s).append('\n'); }
    @Override public void info(String s)  { out.append(s).append('\n'); }
    @Override public void warn(String s)  { out.append(s).append('\n'); }
    @Override public void err(String s)   { err.append(s).append('\n'); }
    @Override public void print(String s)   { out.append(s); }
    @Override public void println(String s) { out.append(s).append('\n'); }

    public String getOut() { return out.toString(); }
    public String getErr() { return err.toString(); }
}

CapturingLogger cap = new CapturingLogger();
console.get().register(cap);

// ... run scripts ...

console.get().register(null);  // unregister
String captured = cap.getOut();

Remember: registering happens on the current thread only. To capture output from worker threads, register inside each worker before it runs Aussom code.

DefaultLoggingImpl is provided for the common case of writing to System.out / System.err with severity filtering. The CLI uses it.


11. End-to-end example: a host service exposing an API to scripts

Pulling everything together. The host is a small "key-value store" service. We want users to write .aus rules that read and write the store, with the host enforcing access control.

11.1 The host service

package com.example.kv;

import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;

import com.aussom.Environment;
import com.aussom.types.AussomException;
import com.aussom.types.AussomNull;
import com.aussom.types.AussomString;
import com.aussom.types.AussomType;

public class AKvStore {
    /*
     * Backing store. ConcurrentHashMap makes single-key reads/writes
     * thread-safe; compound check-and-set ops still need user-side
     * coordination if they matter.
     */
    private final ConcurrentHashMap<String, String> data =
        new ConcurrentHashMap<>();

    public AussomType get(Environment env, ArrayList<AussomType> args) {
        try {
            if (!(Boolean) env.getEngine().getSecurityManager()
                    .getProperty("kv.read")) {
                return new AussomException(
                    "kv.get: action 'kv.read' not permitted.");
            }
            String key = ((AussomString) args.get(0)).getValueString();
            String val = this.data.get(key);
            if (val == null) return new AussomNull();
            return new AussomString(val);
        } catch (Exception e) {
            return new AussomException("kv.get: " + e.getMessage());
        }
    }

    public AussomType put(Environment env, ArrayList<AussomType> args) {
        try {
            if (!(Boolean) env.getEngine().getSecurityManager()
                    .getProperty("kv.write")) {
                return new AussomException(
                    "kv.put: action 'kv.write' not permitted.");
            }
            String key = ((AussomString) args.get(0)).getValueString();
            String val = ((AussomString) args.get(1)).getValueString();
            this.data.put(key, val);
            return env.getClassInstance();   // chainable
        } catch (Exception e) {
            return new AussomException("kv.put: " + e.getMessage());
        }
    }

    public AussomType remove(Environment env, ArrayList<AussomType> args) {
        try {
            if (!(Boolean) env.getEngine().getSecurityManager()
                    .getProperty("kv.write")) {
                return new AussomException(
                    "kv.remove: action 'kv.write' not permitted.");
            }
            String key = ((AussomString) args.get(0)).getValueString();
            this.data.remove(key);
            return env.getClassInstance();
        } catch (Exception e) {
            return new AussomException("kv.remove: " + e.getMessage());
        }
    }
}

11.2 The Aussom binding

/* kv.aus -- ships at /com/example/kv/aus/kv.aus inside the host jar */

/**
 * Host-provided key-value store. Backed by an in-memory
 * ConcurrentHashMap on the Java side.
 */
static extern class kv : com.example.kv.AKvStore {
    /**
     * Looks up the value for the given key.
     * @p key is a string with the key to read.
     * @r The value as a string, or null when missing.
     */
    public extern get(string key);

    /**
     * Stores the value under the given key.
     * @p key is a string with the key.
     * @p val is a string with the value.
     * @r The kv store for chaining.
     */
    public extern put(string key, string val);

    /**
     * Removes the entry for the given key.
     * @p key is a string with the key.
     * @r The kv store for chaining.
     */
    public extern remove(string key);
}

11.3 The host security policy

package com.example.kv;

import com.aussom.SecurityManagerImpl;

public class KvSecurityManager extends SecurityManagerImpl {
    public KvSecurityManager(boolean readWrite) {
        super();
        this.props.put("kv.read",  true);          // always readable
        this.props.put("kv.write", readWrite);     // mutation gated
    }
}

11.4 The host bootstrap

package com.example.kv;

import com.aussom.Engine;
import com.aussom.ast.aussomException;
import com.aussom.stdlib.console;
import com.aussom.DefaultLoggingImpl;

public class KvHost {
    public static int run(String script, boolean writable) throws Exception {
        DefaultLoggingImpl log = new DefaultLoggingImpl();
        log.setLevel(DefaultLoggingImpl.INFO);
        console.get().register(log);

        Engine eng = new Engine(new KvSecurityManager(writable));

        // stdlib + host-provided extern classes
        eng.addResourceIncludePath("/com/aussom/stdlib/aus/");
        eng.addResourceIncludePath("/com/example/kv/aus/");

        eng.addInclude("kv");
        eng.parseFile(script);

        try {
            return eng.run();
        } catch (aussomException ae) {
            console.get().err(ae.getAussomStackTrace());
            return 1;
        }
    }

    public static void main(String[] args) throws Exception {
        System.exit(run(args[0], /*writable=*/true));
    }
}

11.5 A user script

include kv;

class Job {
    public main(args) {
        kv.put("greeting", "hello").put("name", "world");
        c.log(kv.get("greeting") + " " + kv.get("name"));
        return 0;
    }
}

Run it:

java -cp myhost.jar com.example.kv.KvHost greet.aus
# -> hello world

Run it with the policy locked down:

KvHost.run("greet.aus", /*writable=*/false);
// kv.put returns an AussomException; the script halts and the
// trace shows: "kv.put: action 'kv.write' not permitted."

That's the whole pattern. Every host-provided extern class follows the same shape: declare it, write Java methods that take (Environment, ArrayList<AussomType>) and return AussomType, gate sensitive ops on the security manager, return env.getClassInstance() for chaining, and never let a Java exception escape.


12. Common gotchas

A condensed reference for things that bite host developers most often.

  1. Always register your security manager. The default SecurityManagerImpl() (no constructor arg) locks almost everything out. Use DefaultSecurityManagerImpl if you want stdlib defaults plus aussomdoc, or write your own subclass.

  2. addResourceIncludePath("/com/aussom/stdlib/aus/") is mandatory if your scripts use include sys;, include math;, or any other stdlib include. Without it the parser refuses to resolve the include.

  3. Never let a Java exception escape an extern method. Wrap the body in try { ... } catch (Exception e) { return new AussomException(...) }. Anything that escapes shows up as a raw Java stack trace from eng.run(), breaking the user's mental model.

  4. Return env.getClassInstance() for chainable methods. Any method that doesn't have a useful value should return the receiver so script callers can chain.

  5. Engine.run() is single-threaded. Concurrent call into the same Engine.run() will corrupt engine state. For multi- threaded execution, build per-call Environment + Members + CallStack and dispatch through astClass.call directly.

  6. Parse before fanning out. All parseFile / parseString / addInclude calls must finish before you start running scripts on multiple threads.

  7. Pre-warm classes you'll hit hard concurrently. A single instantiateObject(name) on the main thread before workers start avoids the cold-path race in inheritance link rebuild.

  8. console.get() is ThreadLocal. Register your logger on each thread that runs Aussom code. Registering on the main thread does not cover worker threads.

  9. Environment.clone() is shallow. It shares locals and callstack with the original. If you need true isolation, build a fresh Environment + Members + CallStack instead of cloning.

  10. AussomInt is 64-bit. Use (long)((AussomInt)v).getValue() or (int)((AussomInt)v).getValue(); never assume int.

  11. AussomMap.getValue() is a ConcurrentHashMap -- key lookup is safe but compound read-modify-write across threads needs your own coordination.

  12. The Universe singleton holds only the stdlib. User class defs go into the per-Engine classes map, not into Universe. Two engines in the same JVM share the parsed lang.aus AST but otherwise cannot see each other's classes.

  13. hasParseErrors is sticky. After a failed parse, eng.run() will refuse to execute until you call eng.clearParseError().

  14. Static extern classes are instantiated once at engine construction. Their <init> runs in the engine builder's thread before any Aussom code runs. Make those constructors cheap and side-effect free.


13. Where to go next

  • JSR 223 alternative: design/usage-docs/aussom-lang-jsr223-usage.md -- shorter, more language-neutral, fits when you don't need direct Engine control.
  • Engine internals + threading audit: design/aussom-jsr-223.md -- the design doc behind the JSR 223 layer; covers the threading model in depth.
  • 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
  • Stdlib source: src/main/java/com/aussom/stdlib/ -- the implementations of c, sys, math, etc. are the canonical reference for "how to write an extern class."
  • Reference embedding: src/main/java/com/aussom/Main.java is about thirty lines of CLI plumbing on top of Engine. Read it if you want a minimal end-to-end Engine consumer.
  • Script mode design: design/script-mode-design.md -- the design behind Engine.setScriptMode / evalLine / getScriptClass.