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:
c, sys, math, etc.).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.
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. |
<dependency>
<groupId>io.github.rsv-code</groupId>
<artifactId>aussom.base</artifactId>
<version>1.2.3</version>
</dependency>
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.
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().
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)
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.
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.
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.
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();
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.
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.
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.
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.
| 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. |
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));
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).
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.
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.
Use the parse / run pipeline (Section 3) for:
.aus files with main().run() returns the int result of
main, or 1 on exception).Use script mode for:
class and
main.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.
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:
__script_main class's main body.Environment whose Members persists across calls.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.
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.
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.
Script mode and the classical run pipeline coexist on one engine without interference:
__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.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
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.
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.
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.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.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:
extern class declaration that binds method
names to the Java class.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.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();
}
}
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.
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.)
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.
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.
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.
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 |
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.
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;
As covered in Section 6.4:
return env.getClassInstance();
externObject slotAussomObject 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"));
The Aussom runtime distinguishes two failure shapes:
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.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:
try { ... } catch (Exception e) { return new AussomException(...) } so nothing
leaks out as a Java throwable.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.
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.
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.
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.
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.
parseFile / parseString / addInclude mutate engine-level
collections (fileNames, includes, classes) without locking.
Either:
synchronized (eng) { ... } block.Once parsing is complete, concurrent reads of the class registry
are safe (the underlying classes and staticClasses maps are
ConcurrentHashMap).
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.
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.
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.
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.
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.
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());
}
}
}
/* 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);
}
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
}
}
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));
}
}
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.
A condensed reference for things that bite host developers most often.
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.
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.
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.
Return env.getClassInstance() for chainable methods. Any
method that doesn't have a useful value should return the
receiver so script callers can chain.
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.
Parse before fanning out. All parseFile / parseString /
addInclude calls must finish before you start running scripts
on multiple threads.
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.
console.get() is ThreadLocal. Register your logger on
each thread that runs Aussom code. Registering on the main
thread does not cover worker threads.
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.
AussomInt is 64-bit. Use (long)((AussomInt)v).getValue()
or (int)((AussomInt)v).getValue(); never assume int.
AussomMap.getValue() is a ConcurrentHashMap -- key
lookup is safe but compound read-modify-write across threads
needs your own coordination.
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.
hasParseErrors is sticky. After a failed parse,
eng.run() will refuse to execute until you call
eng.clearParseError().
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.
design/usage-docs/aussom-lang-jsr223-usage.md -- shorter, more
language-neutral, fits when you don't need direct Engine control.design/aussom-jsr-223.md -- the design doc behind the JSR 223
layer; covers the threading model in depth.src/main/java/com/aussom/stdlib/ -- the
implementations of c, sys, math, etc. are the canonical
reference for "how to write an extern class."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.design/script-mode-design.md -- the design behind
Engine.setScriptMode / evalLine / getScriptClass.