Status: ✅ COMPLETE
Step 2 adds the ability to detect macro calls and capture their arguments as Closure objects instead of following normal bundling rules. When code like closure(add) is encountered where closure is a macro, the argument add is captured with its AST and out-of-scope references.
File: src/execution_request/closure.rs
- Added
Closurestruct that holds:expression: Expr- The captured AST nodereferences: HashMap<String, FuneeIdentifier>- Map of variable names to their canonical definitions
File: src/execution_request/declaration.rs
- Added
Declaration::ClosureValue(Closure)variant - Represents a captured closure that will be emitted as a Closure object at runtime
- Added handling in
into_module_item()to emit closures (currently as plain expressions, TODO: emit proper Closure construction)
File: src/execution_request/detect_macro_calls.rs
- New module for finding macro calls in expressions
MacroCallstruct captures:macro_name: String- The name of the macro functionarguments: Vec<Expr>- The argument expressions passed to the macro
MacroCallFindervisitor walks the AST looking forCallExprnodes where the callee is a known macrofind_macro_calls()function returns all macro calls found in an expression
File: src/execution_request/capture_closure.rs
capture_closure()function that:- Takes an expression (the macro argument)
- Analyzes it to find all variable references
- Filters to only out-of-scope references (defined in parent scope)
- Returns a
Closurewith the expression and its reference map
File: src/execution_request/source_graph.rs
- Modified
SourceGraph::load()to do two passes:- Pass 1 (existing): Build the dependency graph normally, track macro definitions
- Pass 2 (new):
process_macro_calls()method that:- Iterates through all nodes
- Checks if expressions contain macro calls
- For each macro call argument, captures it as a Closure
- Adds Closure nodes to the graph
File: src/execution_request/get_references_from_declaration.rs
- Added
Declaration::ClosureValuecase toget_references_from_declaration()- Returns the reference names from the closure's reference map
- Added
Declaration::ClosureValuecase torename_references_in_declaration()- Renames references in the closure expression AST
- Made
get_references_from_ast()public socapture_closurecan use it
import { closure } from "./macro-lib.ts";
const add = (a: number, b: number) => a + b;
const addClosure = closure(add);-
Graph Construction (Pass 1)
- Load
entry.ts, find declarations:add,addClosure,default - Follow import to
macro-lib.ts, findclosuredeclaration - Detect that
closureis created viacreateMacro()→ mark as macro - Add nodes and edges to graph
- Load
-
Macro Processing (Pass 2)
- Iterate through all nodes looking for expressions
- Find
addClosure = closure(add)node - Detect
closure(...)is a macro call (callee resolves to a macro) - Extract argument:
add(identifier expression) - Build scope references map:
{ "closure": ..., "add": ... } - Call
capture_closure(add, scope_refs):- Get references from
addexpression → finds"add" - Filter to out-of-scope:
"add"is in scope_refs → include it - Return
Closure { expression: Ident("add"), references: {"add": FuneeIdentifier{...}} }
- Get references from
- Create new graph node:
Declaration::ClosureValue(closure) - Add edge from
addClosurenode to the new Closure node
-
Result
- Graph now has a
ClosureValuenode containing:- The expression
add(as written) - A reference map showing
addpoints to the arrow function
- The expression
- Graph now has a
The captured Closure stores the expression as written (add) plus a map of what add refers to. This preserves the original code structure while providing the information needed to resolve references. Later, when the macro is executed, it can:
- Access the expression AST directly
- Resolve references through the reference map
- Transform or inline the expression as needed
File: src/execution_request/tests.rs::test_macro_detection
- Verifies that
createMacro()calls are detected - Checks that the macro function is stored as
Declaration::Macro
File: src/execution_request/tests.rs::test_macro_functions_tracked_in_source_graph
- Verifies that macros are tracked in
SourceGraph.macro_functions - Ensures the set is populated during graph construction
File: src/execution_request/tests.rs::test_macro_call_argument_captured_as_closure
- Input:
const addClosure = closure(add)whereclosureis a macro - Verifies:
- A
ClosureValuenode is created in the graph - The captured expression is the identifier
add - The reference map contains
{"add": FuneeIdentifier{...}}
- A
- Output: ✅ Closure correctly captures the argument
File: src/execution_request/capture_closure.rs::test_capture_closure_with_no_references
- Tests capturing a literal (no external references)
File: src/execution_request/capture_closure.rs::test_capture_closure_with_references
- Tests capturing an identifier with external references
File: src/execution_request/detect_macro_calls.rs::test_find_macro_call
- Tests that
closure(add)is detected as a macro call
File: src/execution_request/detect_macro_calls.rs::test_no_macro_calls
- Tests that regular function calls are not mistaken for macro calls
-
Closure Emission
- Currently,
Declaration::ClosureValueemits just the expression - TODO: Emit proper Closure construction:
Closure({ expression: ..., references: {...} })
- Currently,
-
Macro Execution
- Closures are captured but not yet passed to macro functions
- TODO: Step 3 will execute the macro function with the captured Closures
-
Multiple Arguments
- Code supports multiple arguments
- Test only covers single argument
- TODO: Add test for
macro(arg1, arg2, arg3)
-
Nested Macro Calls
- What if a macro call argument contains another macro call?
- Currently untested
- TODO: Add test for
macro1(macro2(arg))
-
Reference Filtering
- Currently captures all out-of-scope references
- May need refinement based on macro semantics
When implementing macro execution, you'll need to:
- Get the macro function from
Declaration::Macro - Collect the
ClosureValuearguments for a macro call - Execute the macro function (in JS runtime) with the Closures
- Get back a transformed Closure result
- Replace the macro call with the result expression
When emitting the final bundle, Declaration::ClosureValue needs to:
- Serialize the expression AST to a string
- Serialize the references map to a JavaScript object
- Emit:
Closure({ expression: <ast>, references: <map> })
# Run all tests
cargo test --lib
# Run just the macro tests
cargo test test_macro
# Run with output
cargo test test_macro_call_argument_captured_as_closure -- --nocaptureStep 2 successfully implements macro call detection and argument capture. When a macro call like closure(add) is encountered:
- The call is detected via AST traversal
- The argument
addis captured as aClosureobject - The Closure contains both the expression and its out-of-scope references
- A new graph node is created to represent the captured Closure
This provides the foundation for Step 3 (macro execution), which will take these captured Closures, pass them to the macro function, and transform the code accordingly.