Anima is the custom (Scheme-inspired) language used in settings v2 in antiraid for dynamic branching + complex client-side validation etc.
The host API changed. Host functions now go through intrinsics only: there are no host callbacks anymore (BuiltinFunction is gone), and a JS function placed in scope is not a procedure Anima code can call.
- Creating an instance:
new Anima(implRvm)is nowcreateScheme(implRvm)(orimplRvmAot).new Anima(options)still exists, but makes a bare instance with no language: no reader, builtins or prelude. - Registering host functions: the global
registerHostIntrinsic(name, fn, options)is nowanima.registerIntrinsic(name, fn, options). Each instance has its own intrinsics. Names start with%, and a name only works in code compiled after it is registered.anima.freeze()stops further registrations. - The function signature is
fn(regs, start, nargs): the arguments areregs[start]toregs[start + nargs - 1].registerHostIntrinsicfunctions took(...args)and need porting;BuiltinFunctioncallbacks already had this signature. Do not keepregsafter the call returns. - Options:
{ args: [min, max], leaf, inline, deps }. The argument count is checked when code compiles. Setleaf: truefor a function that only computes a value; it is cheaper to call and can have an inline template for AOT code. - Calling back into Anima: an intrinsic that is not a leaf calls an Anima procedure by returning
hostTail(proc, ...args)(orhostTailFrom(proc, regs, from, count)) instead of calling it itself. The call then runs in the VM, so the procedure can yield, capture continuations and raise. - Host functions as values: an intrinsic is not a value. Wrap it in a procedure, e.g.
(define (clamp . args) (%apply %clamp args))for a leaf, or a fixed-arity(lambda (f x) (%with-double f x))for any intrinsic. - Serialized code records the intrinsics it uses by name: load it with
readFull(bytes, anima.intrinsics)into an instance that has registered them.
import { createScheme, implRvm, hostTail } from "animalang";
const anima = createScheme(implRvm); // was: new Anima(implRvm)
// a leaf: computes a value from its arguments, never calls back into Anima
anima.registerIntrinsic("%clamp", (regs, start) => Math.min(Math.max(regs[start], regs[start + 1]), regs[start + 2]), { args: [3, 3], leaf: true });
// not a leaf: calls an Anima procedure by returning a tail request
anima.registerIntrinsic("%with-double", (regs, start) => hostTail(regs[start], regs[start + 1] * 2), { args: [2, 2] });
anima.evaluateRaw(anima.compileRaw(`
(define (clamp . args) (%apply %clamp args))
(define (with-double f x) (%with-double f x))
(list (map (lambda (x) (clamp x 0 10)) '(-5 5 50)) (with-double (lambda (y) (+ y 1)) 5))`)); // ((0 5 10) 11)See ts/bytecode-rvm/README.md (intrinsics) and ts/scheme/README.md (the Scheme front end) for the details.
Anima uses a (simplified) Scheme-like grammar based on 's-expressions'
<program> ::= <expr>*
<expr> ::= <primitive> | <list> | <table-lit> | <quoted>
<primitive> ::= <null> | <boolean> | <number> | <string> | <symbol>
<null> ::= null
<boolean> ::= true | false
<number> ::= [0-9]+ ("." [0-9]+)?
<string> ::= "[json/lisp escaped string]"
<special> ::= ( | ) | [ | ] | { | } | ; | " | '
<symbol> ::= [character (excluding <special> and whitespace)]+
<list> ::= (<expr>*) | [<expr>*]
<table-lit> ::= { (<expr> <expr>)* }
<quoted> ::= '<expr>
Comments begin with ; and continue to the end of the line.
- Strings and symbols are distinct data types. String literals (e.g., "hello") evaluate to themselves as string primitives
- Unquoted symbols (e.g. my-var) are evaluated as dynamic variable lookups in the lexical scope.
- A quoted expression like
'<expr>should have the same effect as(quote <expr>). Quoting a symbol (e.g.,'my-var) returns an interned symbol rather than performing a variable lookup. - Multiple top-level expressions should be evaluated sequentially with the result being the result of the last expression (one way to achieve this is to parse multiple top-level expressions expr1 expr2... in a begin block like (begin expr1 expr2 ...))
-
Like Scheme, Anima makes use of lexical scoping. Nested scopes inherit parent variables and can 'shadow' parent variables of the same name.
definestrictly mutates or initializes within the local execution scope and never the parent scope and variables in the outermost scope cannot be reassigned or mutated whatsoever for sandboxing purposes. -
Truthiness: false (
#f) is falsy. All other values are truthy (including#<void>) -
Tail-Call Optimization (TCO): The runtime must execute the final expression in begin, if, and, or, and custom procedure calls without allocating a new frame on the host call stack.
-
Anima does not support macros/custom syntax currently. Although compliant implementations may choose to additionally support this for future use, code written in Anima must not assume support for macros/custom syntax.
-
It is not allowed for user-code to override a builtin using define. Compliant implementations of Anima should error if an attempt to do so is detected
-
Like Scheme, all procedures in Anima (including builtin procedures that are not special forms) must be first class. Furthermore, both builtin and user-defined procedures must return
procedureif type? is called on it.
(define varname value): Evaluatesvalueand binds it tovarnameglobally. A define inside alambda(internal lambda) is hoisted/treated as aletrec(define (varname [args]) exprs...): Shorthand for(define varname (lambda [args] exprs...))(set! varname value): Evaluatesvalueand binds it tovarnamein the current scope (mutatesvarnamein the current scope)(quote expr): Returns the expression without evaluating it. Any raw identifiers within the quoted expression (or deeply nested within quoted lists) are converted into symbols(lambda [args] exprs...): Returns a closure capturing the current lexical scope. If multipleexprs...are provided, they are evaluated sequentially with the last result returned.[args]can either be of form(args...)(arity ofargs...length),args(variadic with all args to the lambda collapsed into a list and placed inargs) or(args... . rem-params)(minimum arity ofargs...length with all variadic arguments collapsed into a list and placed inrem-params)(if cond true-expr false-expr): Evaluatescond. If truthy, evaluates totrye-expr, elsefalse-expr(cond clauses...): Each clause must be a list of exactly two elements:(condition expr). Each condition must be executed in order. Upon encountering the first truthy clause condition (or the exact symbolelse), expr is evaluated and returned. If there are no clauses or if no conditions match, returns#<void>. Throws an error if any clause is malformed.(and expr...): Short-circuits on the first falsy evaluation or returns the value of the last expression inexpr.... Returns true if 0 arguments.(or expr...): Short-circuits on the first truthy evaluation or returns the value of the last expression inexpr.... Returns false if 0 arguments.(begin expr...): Evaluates arguments sequentially. Returns the result of the last expression.let/let*/letrec: Same as Schemelet/let*/letrec(TODO: Write docs here for this as well)
- list (expr...): Evaluates arguments and returns them as a native array. Arity: >= 0.
- cons (a d): Returns a new list with a as head and d as tail. Arity: 2
- car (list): Returns the first element of the list. Throws if the list is empty. Arity: 1.
- cdr (list): Returns a new list excluding the first element. Throws if the list is empty. Arity: 1.
- last (list): Returns the final element of the list. Throws if the list is empty. Arity: 1.
- length (list | string): Returns the integer length. Returns 0 if the argument is neither a list nor string. Arity: 1.
- contains (list, item): Returns a boolean indicating strict inclusion of item within list. Arity: 2.
Tables are first-class associative maps with freezing support for safe FFI boundaries. Like Lua tables, keys 1..n are stored densely in an array part and every other key in a hash part (a JavaScript Map). A table never holds <#void>: storing <#void> under a key removes it. Keys cannot be <#void> or NaN; 1 and 1.0 are the same key, and so are 0 and -0. Iteration visits 1..n in order, then the other keys in insertion order.
{key1 val1 key2 val2 ...}: Literal syntax for tables. Desugars at read time to(table key1 val1 key2 val2 ...). Empty table literal{}desugars to(table). Keys and values evaluate dynamically at runtime.(table? val): Returns#tifvalis an instance ofTable,#fotherwise. Arity: 1.(table [k1 v1 k2 v2 ...]): Constructs a new mutableTable. Accepts 0 or an even number of arguments (alternating keys and values). Arity: 0 or even.(table-ref tbl key [default]): Looks upkeyintbl. Ifkeyis not found, returnsdefaultif provided; otherwise throws an error. Arity: 2 or 3.(table-set! tbl key val): Associateskeywithvalintbl, or removeskeywhenvalis<#void>. Throws an error iftblis frozen. Returns#<void>. Arity: 3.(table-has? tbl key): Returns#tifkeyexists intbl,#fotherwise. Arity: 2.(table-delete! tbl key): Deleteskeyand its associated value fromtbl. Throws an error iftblis frozen. Returns#tif the key was present and removed,#fotherwise. Arity: 2.(table-clear! tbl): Removes all entries fromtbl. Throws an error iftblis frozen. Returns#<void>. Arity: 1.(table-size tbl): Returns the number of entries stored intbl. Arity: 1.(table-border tbl): Returns a border oftbl, like Lua's#t: thensuch that keys1..nare all set andn + 1is not (0if key1is not set). O(1). Arity: 1.(table-empty? tbl): Returns#tiftblis empty (size === 0),#fotherwise. Arity: 1.(empty? val): Generic empty predicate also returns#tfor empty tables.(table-keys tbl): Returns a vector (native JavaScript array) containing all keys intbl. Arity: 1.(table-values tbl): Returns a vector (native JavaScript array) containing all values intbl. Arity: 1.(table-copy tbl): Creates a new unfrozen shallow copy oftbl. Arity: 1.(table-freeze! tbl): Freezestblin place to prevent any future mutations (table-set!,table-delete!,table-clear!), and returnstbl. Arity: 1.(table-frozen? tbl): Returns#tiftblis frozen,#fotherwise. Arity: 1.(table-merge! target source): Copies all key-value pairs fromsourceintotarget. Throws an error iftargetis frozen. Returnstarget. Arity: 2.(equal? a b): Performs deep recursive equality comparison across tables, vectors, lists, and primitives.
The Table class exported from animalang provides clean integration with host TypeScript / JavaScript environments:
- Constructor:
new Table(frozen = false) - Methods:
.get(key)(undefinedwhen missing),.lookup(key, missing),.set(key, val)(undefinedremoves the key),.has(key),.delete(key),.clear(),.copy() - Properties:
.size,.frozen(getter & setter:tbl.frozen = true), and.border()(seetable-border) - Iteration: Implements
Iterable<[any, any]>(for (const [k, v] of tbl)),.entries(),.keys(),.values()
Global environments are a separate class, Env (anima.scope), with .get, .set, .has and .lookup; an Env chains to its parent environment (user globals over the builtins).
- =: Checks if
nnumber expressions are equal. Errors if any expression is not a number. Arity: >= 2. - eq? + eqv?: Similar to Scheme's eqv?. If number/string, checks equality of num/string even if in different memory locations, eqv? should be
Object.is-style (like Scheme) but eq? can do just===otheriwse checks pointer equality. Arity: 2 - equal?: Similar to Scheme's equal? but does a deep recursive comparison for lists etc.
- not: Returns true if the expression is falsy, otherwise returns false
- <, >, <=, >= (expr1, expr2, ... exprN): Strict comparison between expr1 to exprN. Arity: >= 2.
- type? (expr): Returns one of the following strings: "list", "string", "number", "boolean", "null", "symbol", "procedure", "list", "error", "exposed-prop". Arity: 1.
- error? (expr): Returns if
expris anErrorObjector not - error-message (expr): Returns the underlying error caught within
exprifexpris anErrorObject. Throws an error ifexpris not aErrorObject
- +, -, *, / (expr...): Evaluates sequentially from left to right. Arity: >= 2.
(modulo expr1 expr2): Returns the mathematical modulo ofexpr1withexpr2(remainder expr1 expr2): Returns the mathematical remainder ofexpr1withexpr2
(apply proc args... rem-lst): Same as Scheme specification on apply. Callsprocwith the packedargs... rem-lstas arguments forproc(try proc args... rem-lst): Callsprocwith the packedargs... rem-lstas arguments forproc. Ifprocerrors,tryevaluates to anresof typeErrorObject(whosetype? resiserror,error? resyielding#tanderror-message rescontaining the error caught bytrywhile evaluatingproc)- map: Same as Scheme specification on map (TODO: Write docs here for this as well)
In order to keep Anima simple to implement (and debug!), Anima does not support full first-class continuations yet (such as call-with-current-continuation or call/cc) (although support for this may be implemented later at some point in the future). This also enables for potential future optimizations.