Declarations and scope
Two namespaces
The language has two disjoint namespaces with no shadowing between them:
- Command namespace. A bareword in command position resolves here.
It holds the built-in commands and the per-script commands
introduced by
def. Builtins take precedence; adefmay not shadow a builtin or a reserved command name. Adef'd command can also be taken as a first-class block value with the&sigil. - Variable namespace. A
$namereference resolves here. It holds block parameters,let/constlocals, handler-injected names ($self/$actor/…), and top-levelconst. All are lexical: each resolves to the nearest enclosing binding of that name (see Name resolution).
A let count 0 does not affect the count builtin, and a
def make { … } does not put $make in scope. The two namespaces never
interact except through the & sigil, which yields a def function as
a value usable in the variable namespace.
def greet { <c> do "say hello [name $c]" }
greet $alice # bareword → command namespace → invoke greet
let g &greet # & fetches greet as a value
[$g $alice] # $g → variable namespace; [...] invokes it
Top-level declarations
A script's top level admits only three declaration forms (Program initialization and execution covers handlers):
TopLevelForm = Def | ConstTop | Handler .
Def = 'def' word Block .
ConstTop = 'const' word Argument .
def — named commands
def name { … } binds name in the command namespace. The value must
syntactically be a block; anything else is a compile-time error. defd
names are then invoked like builtins — a bareword in command position — or
passed as a value with &name.
def greet { <c> do "say hello [name $c]" }
def farewell { <c> do "say bye [name $c]" }
def is top-level only. A local callable is a let f { … } invoked
through the variable namespace as [$f …].
const — top-level named values
const name value binds name in the variable namespace as an
immutable value of any type, read as $name (Constants).
Hoisting and evaluation order
Before any code runs, every top-level def and const name is hoisted:
visible throughout the script but not yet initialized. Then the top-level
forms execute in source order, each evaluating its initializer.
Two consequences:
- A block body may reference any top-level name regardless of source
order — mutual recursion needs no forward declaration, because all
defnames exist before any body runs. - A top-level initializer that reads a name whose own initializer has not yet run finds it still unbound, which is a run-time error.
def greet { <c> farewell $c } # OK: farewell resolved when greet runs
def farewell { <c> do "say bye [name $c]" }
const a [foo $b] # error at run time: $b not yet initialized
const b 5
Duplicate names within a namespace are compile-time errors, as is a def
that collides with a builtin or a reserved command name (the reserved set
includes the comparison/logic names, if, do, unless, let, halt,
return, nuke, pause, each, randomly, require, select,
slice, first, empty, count, ismember, hasabbrev, choose,
def).
The & sigil
&name yields a callable value: it looks name up among the top-level
def bindings, and failing that, among the builtins, producing the bound
function. & is normally used in argument position, to pass or capture a
command as a value.
&name is also accepted in command position — [&name args] and
&name args are legal — but there it is redundant: the bare name
already resolves and invokes the same callable, so [&isdark $room]
behaves exactly like [isdark $room]. Prefer the bare form.
& on a builtin is legal but pointless for the same reason. The one
exception is the special forms — if, each, select, the operator
builtins, and so on — which are not first-class callable values; &if,
&concat, … are compile-time errors.
select $rooms &is_dark # pass the is_dark function as a predicate
let p &is_dark ; [$p $room] # capture, then invoke
[&is_dark $room] # legal but redundant; same as [is_dark $room]
Lexical scope
Blocks are lexically scoped: a free variable in a block resolves up the chain of blocks that defined it, not the chain that called it. Each block invocation has its own local bindings; its enclosing scope is the scope where the block was defined. That chain of enclosing scopes is the lexical environment.
each $rooms { <room>
each [people $room] { <person>
do "say [name $person] is in [name $room]"
}
}
The inner block sees $room because its enclosing-scope chain reaches the
outer each's scope.
Name resolution
Every $name is lexical and resolves at compile time to a single
binding. The search walks outward through the scopes that defined the
reference, not the scopes that called it. The kinds of binding are:
- Block parameters declared with
<a b>— bound by the call. let/constlocals.- Top-level
const— bound in the outermost scope. - Handler-injected names —
$self,$actor, and others per the event (Program initialization and execution). A nested block inside the handler sees them lexically through its enclosing scopes. A top-leveldef, whose enclosing scope is the outermost scope (not a handler body), does not see them — pass what it needs as arguments.
A $name read searches the innermost scope outward — the current scope's
parameters, injected names, and let/const, then each enclosing scope,
then the outermost scope's top-level const. An unresolved name is a
compile-time error.
Within a single scope, names form one flat namespace: a let or nested
const that reintroduces a name already bound in that scope — a parameter,
an injected name, or an earlier let/const — is a compile-time
error.
Top-level def bindings are in the command namespace and are never
consulted for a $name read.
Mutation and the closure-counter idiom
Because set crosses scope boundaries
(Variables), an inner block can mutate a binding owned
by an enclosing scope. A returned block retains access to its enclosing
bindings, so it can carry mutable state:
def make_counter { let n 0 ; { set n [+ $n 1] ; $n } }
let bump [make_counter]
[$bump] ; [$bump] ; [$bump] # 1, 2, 3
The returned block can still read and mutate n because the enclosing
bindings remain accessible through it.
Scope lifetime
A block's local bindings are fresh for each invocation and are no longer
accessible after it returns — unless a nested block references them, in
which case they remain accessible through that block. Locals therefore do
not persist between separate handler invocations; cross-invocation state
belongs in persistent entity state (store/recall,
Variables).