ilusm.dev

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, float 3.14, negative -7.

Keywords (all ≤5 chars)

KeywordMeaning
ifConditional expression / statement
whlWhile loop
fnFunction value (anonymous)
useImport a stdlib module
retEarly return from a function
tryCatch errors, return nil on failure
errRaise an error
matPattern match
truBoolean true
flsBoolean false
nilNull / no value
andLogical AND (short-circuit)
orLogical OR (short-circuit)
notLogical NOT

Types

Typetyp() resultExample
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

PrecedenceOperatorsNotes
Highest!Logical NOT
* / %Multiply, divide, modulo
+ -Add/concat, subtract
== != < > <= >=Comparisons → tru/fls
andShort-circuit AND
LowestorShort-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

FunctionDescription
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

CapabilityAllowsSyscalls
fs.readRead files and directoriesopen(O_RDONLY), read, stat, ls
fs.writeWrite and create filesopen(O_WRONLY), write, mkdir, unlink
net.connectOutbound TCP/UDP connectionsconnect, send, recv
net.listenBind and accept connectionsbind, listen, accept
net.dnsDNS resolutiongetaddrinfo
proc.spawnSpawn child processesfork, exec
proc.signalSend signals to processeskill
env.readRead environment variablesgetenv
env.writeSet environment variablessetenv
timeRead system timeclock_gettime
randCryptographic random bytesgetrandom
ttyTerminal I/Oioctl(TIOCGWINSZ), tcsetattr
ffiCall native librariesdlopen, 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, or proc are simulated in the browser
  • The browser sandbox is not configurable - it is always maximally restrictive