Scope

The scope of a name determines where it is visible, and how the language finds the binding the name refers to.

Two namespaces

Names live in two separate namespaces that never collide:

Because they are separate, let count 0 does not shadow the count builtin, and def make { … } does not create a variable $make. The bridge between them is the & sigil, which takes a command as a value usable in the variable namespace:

def greet { <c> do "say hello [name $c]" }

greet $alice          # bareword -> command namespace -> run greet
let g &greet          # & takes greet as a value
[$g $alice]           # $g -> variable namespace; [...] invokes it

Top-level declarations

On the top level, a script may contain only three things: handlers, a function definition usingdef, and a top-level assignment using const.

def — named commands

def name { … } binds name in the command namespace. The value must be a block. Afterwards you call name like a builtin, or take it as a value with &name.

def classify { <c>
  if [ge [level $c] 20] { return [upper "elite"] }
  return [upper "common"]
}

after command (say) {
  do "say you are [classify $actor]"
}

Against a level-26 actor it reports "you are ELITE." def is top-level only. For a callable held in a variable, use let f { … } and invoke it as [$f …].

A def may not reuse a builtin or reserved command name, and two defs may not share a name.

const — top-level named values

const name value binds an immutable value in the variable namespace, read as $name from anywhere in the script.

const HOME 3001

Hoisting and order

Before any code runs, every top-level def and const name is made visible to the whole script, then the top-level forms run in source order to fill in their values. Two consequences:

Lexical scope and lookup

Blocks are lexically scoped: a free variable resolves up the chain of scopes where the block was defined, not where it was called. Each block invocation has its own locals; its parent is the scope it sits inside in the source.

each $rooms { <room>
  each [people $room] { <person>
    do "say [name $person] is in [name $room]"
  }
}

The inner block sees $room because its enclosing chain reaches the outer each's scope.

A $name read searches the current scope first (its parameters, injected names, and let/const), then each enclosing scope outward, and finally the top-level consts. The search is resolved when the script compiles; an unresolved name fails to load.

One important consequence: a top-level def sits at the outermost scope, not inside any handler, so it does not see a handler's injected names like $actor. A helper that needs the actor must take it as a parameter:

def greet { <c> do "say hello [name $c]" }

after command (say) {
  greet $actor          # pass the actor in
}

Within one scope all names share a flat space: a let that reuses a parameter or an earlier local's name is a compile-time error.

The counter idiom

Because set crosses scope boundaries, a returned block keeps and can mutate the locals of the scope that made it:

def make_counter { let n 0 ; { set n [+ $n 1] ; return $n } }

after command (say) {
  let bump [make_counter]
  do "say [$bump] [$bump] [$bump]"     # says: 1 2 3
}

The returned block still reaches n through its enclosing scope, so each call advances the same n. Produces 1 2 3.

Lifetime

A block's locals are fresh for each call and gone once it returns — unless a nested block still references them, as above. Locals therefore do not persist between separate handler invocations; for that, use persistent state.

See also