Declarations and scope

Two namespaces

The language has two disjoint namespaces with no shadowing between them:

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:

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:

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).