Syntactic Closures Expansion and Binding
This document records the consolidated macro and binding design that Feersum now implements. It supersedes the earlier split documents that separately described work-stack expansion and binder-context restructuring.
Background
Feersum originally expanded macros by transcribing CST nodes and re-binding them in the caller environment. That model was not hygienic for macro-introduced identifiers and required round-tripping through temporary syntax trees.
The current implementation uses syntactic closures and a fused expansion +
binding pass over Stx, implemented in Binding/Stx.fs and Binding/Binder.fs.
This keeps hygiene and storage resolution in one traversal.
Design
Final architecture
- Parse produces CST
Expressionnodes. Stx.ofExprconverts CST intoStx:Stx.Id,Stx.Datum,Stx.List,Stx.Vec,Stx.ErrorStx.Closure(inner, env)for explicit environment override during macro expansion.
- Binding walks
Stxand simultaneously:- resolves syntax names through
StxEnvironment, - resolves variable identities through frame resolution scopes,
- emits
BoundExpr.
- resolves syntax names through
Hygiene model
StxEnvironment = Map<string, StxBinding>maps names to:Specialbuilt-ins,Macrotransformer ids,Variablebinding ids.
Ident.fresh()creates globally unique binding ids.- Resolution is two-phase:
- Name lookup in
StxEnvironmentto get binding identity/kind. FrameCtx.resolvemapsIdenttoStorageRef, traversing parent frames and registering captures for lambda frames.
- Name lookup in
Stx.Closureensures macro-transcribed fragments resolve in the captured environment, not always the ambient caller environment.
Binding context and scope
BinderCtxholds compilation-wide mutable state:- diagnostics,
- library signatures,
- macro registry.
FrameCtxrepresents one binding frame (Global,Library,Lambda) with:ResolutionEnvstack (Map<Ident, StorageRef>scopes),- local counter,
- lambda capture state.
- Lexical forms (
let,let*,letrec,letrec*) push/pop resolution scopes. - Top-level/library definitions mint globals in root scope, otherwise locals.
Macro forms
define-syntaxreserves macro ids before parsing transformers.let-syntaxandletrec-syntaxpre-seed macro names, withletrec-syntaxusing the recursive binding environment for transformer creation.- Macro transformers are stored in
BinderCtx.Macrosand invoked frombindFormwhen a head resolves toStxBinding.Macro.
Alternatives considered (from earlier drafts)
-
Explicit work-stack expander
Rejected for the mainline implementation because continuation management and result assembly were more complex than needed for Feersum’s current shape. -
Larger context split (
CompilationUnit+ standalone expander module)
Partially adopted conceptually (frame kinds + explicit resolution scopes), but not as a separateExpand.fsarchitecture. The implemented design keeps fused expansion/binding inBinder.fswhile still separating syntax environment and resolution environment concerns.
Affected Files
| File | Purpose |
|---|---|
src/Feersum.CompilerServices/Binding/Stx.fs | Stx DU, closure peeling, CST-to-Stx conversion, syntax binding definitions. |
src/Feersum.CompilerServices/Binding/Binder.fs | Fused macro expansion and binding, frame resolution/capture logic, special form handling, macro registration and invocation. |
src/Feersum.CompilerServices/Binding/Macros.fs | Syntax-rules transformer construction used by binder macro forms. |
Open Questions
- Should define-introduction behavior under closure-wrapped names move from the current compatibility behavior to stricter syntax-set semantics?
- Should the
letrecuninitialized-variable check become a depth-aware dataflow analysis to reduce conservative false positives?
Last updated: 2026-04-19