Language Reference
Complete reference for ilusm syntax, semantics, and built-in operations.
Lexical structure
Source files are UTF-8 text with .ilu extension. Lines ending in \n or \r\n. Indentation is significant for block bodies.
- Comments:
# text- from#to end of line. - Identifiers:
[a-zA-Z_][a-zA-Z0-9_]*- any length, convention is ≤5 chars for public names. - String literals:
"text"- double quotes,\\n\\t\\\\\\"escapes. - Number literals: integer
42, float3.14, negative-7.
Keywords (all ≤5 chars)
| Keyword | Meaning |
|---|---|
if | Conditional expression / statement |
whl | While loop |
fn | Function value (anonymous) |
use | Import a stdlib module |
ret | Early return from a function |
try | Catch errors, return nil on failure |
err | Raise an error |
mat | Pattern match |
tru | Boolean true |
fls | Boolean false |
nil | Null / no value |
and | Logical AND (short-circuit) |
or | Logical OR (short-circuit) |
not | Logical NOT |
Types
| Type | typ() result | Example |
|---|---|---|
| Integer | "num" | 42 |
| Float | "num" | 3.14 |
| String | "str" | "hello" |
| Boolean | "bl" | tru, fls |
| Nil | "nil" | nil |
| List | "lst" | [1, 2, 3] |
| Object | "obj" | {name: "alice"} |
| Function | "fn" | \(x) x + 1 |
Truthiness: only fls and nil are falsy. 0, "", and [] are all truthy.
Operators and precedence
| Precedence | Operators | Notes |
|---|---|---|
| Highest | ! | Logical NOT |
* / % | Multiply, divide, modulo | |
+ - | Add/concat, subtract | |
== != < > <= >= | Comparisons → tru/fls | |
and | Short-circuit AND | |
| Lowest | or | Short-circuit OR |
+ on strings concatenates. + on lists appends. Mixed string/number with + coerces the number to string.
Control flow
if - conditional
# Statement (no else required)
if cond:
body
# One-liner with else
if cond: then_expr | else_expr
# Expression form (assigns result)
result = if cond: a | b
# Chained
if x < 0: prn("neg")
| if x == 0: prn("zero")
| prn("pos")
whl - while loop
n = 0 whl n < 10: prn(n) n = n + 1
mat - pattern match
mat x: 0: "zero" 1: "one" _: "other"
Functions
One-line function
add(a, b) = a + b sqr(x) = x * x
Multi-line function
greet(name) = msg = "hello, " + name prn(msg) msg # last expression is return value
Early return with ret
find_first(xs, pred) =
i = 0
whl i < len(xs):
if pred(xs[i]): ret xs[i]
i = i + 1
nil
Lambda (anonymous function)
double = \(x) x * 2 add = \(a, b) a + b prn(double(5)) # 10 prn(add(3, 4)) # 7
Functions are first-class
apply(f, x) = f(x) prn(apply(double, 5)) # 10 make_adder(n) = \(x) x + n add10 = make_adder(10) prn(add10(5)) # 15
Modules
use txt # imports lib/stdlib/txt.ilu
use trl
use json
prn(txt.upr("hello")) # HELLO
Module names are ≤5 characters. The use statement resolves relative to ILUSM_HOME/lib/stdlib/.
Lists
xs = [1, 2, 3] prn(xs[0]) # 1 (zero-indexed) prn(len(xs)) # 3 xs = xs + [4, 5] # append prn(xs[-1]) # 5 (negative index from end)
Objects
p = {name: "alice", age: 30}
prn(p.name) # alice
p.age = 31 # update field
prn(p.age) # 31
# Nested
cfg = {db: {host: "localhost", port: 5432}}
prn(cfg.db.host) # localhost
Error handling
# try - returns nil on error
result = try risky_operation()
if result == nil: prn("failed")
# err - raise an error
validate(x) =
if x < 0: err("negative value")
x
# eru module - structured errors
use eru
safe = eru.wrap(\() validate(-1))
if safe.err != nil: prn(safe.err.msg)
Built-in functions
| Function | Description |
|---|---|
prn(v) | Print value + newline |
len(v) | Length of string or list |
str(v) | Convert to string |
int(v) | Convert to integer |
flt(v) | Convert to float |
typ(v) | Type name as string |
keys(obj) | List of object keys |
vals(obj) | List of object values |
has(obj, k) | True if object has key |
del(obj, k) | Delete key from object |
rng(n) | List [0..n-1] |
rng(a, b) | List [a..b-1] |
Sandbox & capability model
Capability-based restrictions via sbx, pol, and the host syscall contract.
Why capability-based security
Traditional Unix security is identity-based: a process runs as a user and can do anything that user can do. Capability-based security is permission-based: a program explicitly declares what it needs, and is denied everything else.
This is the principle of least privilege applied at the language level. A program that only reads files should not be able to open network connections. A program that only does math should not be able to touch the filesystem.
ilusm's capability model is inspired by:
- OpenBSD pledge/unveil - declare syscall categories and visible paths
- Linux seccomp - syscall filter
- OPA (Open Policy Agent) - declarative policy rules
- Deno - explicit permission flags per capability
Capability taxonomy
| Capability | Allows | Syscalls |
|---|---|---|
fs.read | Read files and directories | open(O_RDONLY), read, stat, ls |
fs.write | Write and create files | open(O_WRONLY), write, mkdir, unlink |
net.connect | Outbound TCP/UDP connections | connect, send, recv |
net.listen | Bind and accept connections | bind, listen, accept |
net.dns | DNS resolution | getaddrinfo |
proc.spawn | Spawn child processes | fork, exec |
proc.signal | Send signals to processes | kill |
env.read | Read environment variables | getenv |
env.write | Set environment variables | setenv |
time | Read system time | clock_gettime |
rand | Cryptographic random bytes | getrandom |
tty | Terminal I/O | ioctl(TIOCGWINSZ), tcsetattr |
ffi | Call native libraries | dlopen, dlsym |
The sbx module
sbx is the sandbox module. It installs a capability filter at the process level using seccomp (Linux) or pledge (OpenBSD).
use sbx
# Declare the capabilities this program needs
sbx.pledge([
"fs.read", # we read config files
"net.connect",# we make HTTP requests
"net.dns", # we resolve hostnames
"time", # we log timestamps
"rand" # we generate request IDs
])
# After pledge, any syscall not in the list causes SIGKILL
# This is enforced by the kernel - not bypassable from ilusm
# Now run the program
use net
use json
resp = net.get("https://api.example.com/data", {})
prn(resp.body)
Path restrictions with unveil
use sbx
# Restrict filesystem access to specific paths
sbx.unveil("/etc/myapp", "r") # read-only
sbx.unveil("/var/myapp", "rwc") # read, write, create
sbx.unveil("/tmp", "rwc") # temp files
# Lock down - no other paths accessible
sbx.unveil_lock()
# Now file operations outside the unveiled paths will fail
use fs
data = fs.read("/etc/myapp/config.json") # ok
# fs.read("/etc/passwd") # would fail with ENOENT
The pol module
pol is the policy engine. It evaluates declarative rules against requests, returning allow/deny decisions. Useful for authorization logic, API access control, and audit logging.
use pol
# Define a policy
policy = pol.new({
rules: [
{
name: "admin-only-delete",
effect: "deny",
when: \(ctx) ctx.method == "DELETE" and ctx.user.role != "admin"
},
{
name: "rate-limit",
effect: "deny",
when: \(ctx) ctx.user.requests_per_min > 100
},
{
name: "allow-read",
effect: "allow",
when: \(ctx) ctx.method == "GET"
}
],
default: "deny"
})
# Evaluate a request
ctx1 = {method: "DELETE", user: {role: "user", requests_per_min: 5}}
ctx2 = {method: "DELETE", user: {role: "admin", requests_per_min: 5}}
ctx3 = {method: "GET", user: {role: "user", requests_per_min: 5}}
prn(pol.eval(policy, ctx1)) # deny (not admin)
prn(pol.eval(policy, ctx2)) # allow (admin)
prn(pol.eval(policy, ctx3)) # allow (GET)
Declaring capabilities in a module
Stdlib modules declare their capability requirements at the top of the file. The runtime checks these before loading the module:
# lib/stdlib/net.ilu # @requires net.connect net.dns use sbx # Module code follows... get(url, headers) = # ...
When a program does use net, the runtime checks whether net.connect and net.dns are in the active capability set. If not, the module fails to load with a clear error.
Enforcement mechanisms
ilusm uses a layered enforcement approach:
Layer 1: Module-level checks
When a module is loaded, its declared capabilities are checked against the active set. This is a fast, early check that prevents accidental use of restricted modules.
Layer 2: Syscall contract
The __sys_* host contract functions check capabilities before executing. A call to __sys_connect without net.connect in the active set raises an error.
Layer 3: Kernel enforcement (seccomp/pledge)
After sbx.pledge(), the kernel enforces the syscall whitelist. This is the strongest layer - it cannot be bypassed from ilusm code, even if the runtime has a bug.
Examples
A read-only data processor
use sbx
# This program only reads files and does computation
sbx.pledge(["fs.read", "time"])
sbx.unveil("/data", "r")
sbx.unveil_lock()
use fs
use json
use ana
data = json.dec(fs.read("/data/input.json"))
result = ana.aggregate(data.records, "sum", "value")
prn(json.enc(result))
A network service with restricted FS
use sbx
sbx.pledge(["net.listen", "net.connect", "net.dns", "fs.read", "time", "rand"])
sbx.unveil("/etc/myapp", "r")
sbx.unveil("/var/myapp/logs", "rwc")
sbx.unveil_lock()
use srv
use log
srv.route("GET", "/health", \(req, res)
res.json({ok: tru})
)
srv.listen(8080)
Policy-gated API
use pol
use auth
use srv
# Load policy from file
policy = pol.load("/etc/myapp/policy.json")
srv.use(\(req, res, next)
token = req.header("Authorization")
user = auth.verify_jwt(token)
ctx = {method: req.method, path: req.path, user: user}
decision = pol.eval(policy, ctx)
if decision.effect == "deny":
res.status(403).json({error: "forbidden", reason: decision.rule})
| next()
)
srv.route("GET", "/api/data", handle_get)
srv.route("POST", "/api/data", handle_post)
srv.route("DELETE", "/api/data", handle_delete)
srv.listen(8080)
Browser sandbox
The Loveshack runner uses a separate, always-on sandbox: the browser itself. JavaScript's security model prevents any filesystem, network, or process access. The teaching shell in js/ilusm-host.js runs entirely in memory with no host syscalls.
This means:
- All Loveshack examples are safe to run without any capability declarations
- Examples that use
fs,net, orprocare simulated in the browser - The browser sandbox is not configurable - it is always maximally restrictive