Semantics & Scoping
Bindings & Environments
A binding attaches a name to a value or storage slot; the environment is the chain of scopes that answers “what does this name mean here?”.
Names to Storage
Write a DSL spec in terms of bindings, not variables: each name occurrence resolves to a binding, and evaluating that name yields the bound value (or storage address). Getting this layer right makes scoping, typing, and later compilers fall into place.
Environment Chains
Nested blocks create nested environments; resolution walks outward. The order of that walk is the scoping rule — and it is specified, not chosen by the implementation.
Scoping Rules
Figure 1 — The same text means different things under each rule; a spec must pick one.
Lexical (Static) Scope
Resolution follows the text of the program: a name means whatever an enclosing block declared, regardless of who calls the function. Predictable, toolable (editors and AI assistants can follow it), and the modern default.
Dynamic Scope
Resolution follows the call stack at runtime: a function reads the binding of its caller. Historically AutoLISP and some shells behave this way; it is convenient for debugging magic but surprises readers, which is why new languages avoid it.
Which to Specify
For a DSL: lexical scope, always, unless the domain genuinely needs stack-like context. Then write “lexical scope with these block rules” into the one-page design document before Phase 2.
Evaluation
Semantics is incomplete without saying when and in what order things happen.
Strict vs. Lazy
Strict (eager) evaluation computes arguments before calling; lazy evaluation defers until needed (Haskell). DSLs and configuration grammars are overwhelmingly strict — laziness pays only in streaming or infinite-data domains.
Order of Evaluation
Specify left-to-right for operands and short-circuit rules for and/or; leave nothing to the implementation. Also declare what happens on the first runtime failure: stop, or continue and report all errors (batch DSLs often prefer the latter).
Name Resolution as a Service
Resolution Rules
Write them as an ordered list: innermost block first, then enclosing blocks, then the module/global scope, then built-ins, then an explicit error. Shadowing (a block redeclaring a name) is a feature — document that the inner binding hides the outer one until the block ends.
Resolution Errors
Undefined-name at runtime is the worst kind for a DSL because it appears late. When feasible, resolve names before execution (Phase 2 analysis) so the error is a compile-time diagnostic instead.
Example: Scope in Action
Two tiny programs, one difference: where box resolves.
Lexical (Python)
# scope.py — lexical scope: text nesting wins
box = "global" # outer binding
def inside():
print(box) # resolves to the enclosing block: "global"
inside() # reads the def-site text, not the caller
Dynamic (AutoLISP note)
; scope.lsp — dynamic scope: the caller's binding wins
(setq box "global")
(defun inside () (princ box)) ; box resolved against the call stack
(defun caller () (setq box "caller") (inside))
(caller) ; prints "caller", not "global"
Both are valid specifications — but only one of them is predictable for your users. Pick lexical scope unless the domain demands stack context, and say so in the design document.
Next Steps
Continue Phase 1
Meaning needs a vocabulary: Types & Values classifies that vocabulary, and Errors & Diagnostics decides how failed programs speak. Then Phase 2 turns meaning into execution.