@@ -16,30 +16,10 @@ import type {
1616 Terminal ,
1717} from "./types.js" ;
1818
19- // HACK: lower ESTree → HIR. Mirrors the structure of React Compiler's
20- // `BuildHIR.ts::lower` but with several deliberate simplifications:
21- //
22- // - single block per function (no control flow modeled in v1)
23- // - no SSA: a mutable name→Place table tracks current bindings
24- // - no aliasing / mutation effect inference (every Place gets
25- // `effect: 'Read'` for v1; validators don't depend on this yet)
26- // - JSX folded into a single `JSXExpression` placeholder per JSX
27- // node — we don't need to model individual elements/attrs to
28- // detect setState calls in a return position
29- //
30- // What carries over from the compiler:
31- // - lvalue / value-place / operand-place shape so validators can
32- // `switch (instr.value.kind)` exactly like the upstream code
33- // - identifier IDs are stable per-binding, so propagation analyses
34- // (e.g. setState flowing through a const) work the same way
35- // - source locations threaded through every Place
36-
3719interface LoweringEnvironment {
38- // HACK: id allocators are SHARED across all nested envs of the same
39- // lowering. A child env references the same allocator object so a
40- // captured outer binding keeps its IdentifierId when seen by the
41- // inner function — this is how the compiler's `loweredFunc.func.context`
42- // ends up referencing identifiers shared with the outer function.
20+ // HACK: id allocators are shared across all nested envs of one
21+ // lowering so a captured outer binding keeps its IdentifierId when
22+ // seen by an inner function.
4323 ids : { nextIdentifierId : number ; nextInstructionId : number ; nextSyntheticName : number } ;
4424 bindings : Map < string , Place > ;
4525 parent : LoweringEnvironment | null ;
@@ -143,11 +123,6 @@ const setBinding = (env: LoweringEnvironment, name: string, place: Place): void
143123 env . bindings . set ( name , place ) ;
144124} ;
145125
146- // HACK: mirrors the destructured-prop scaffolding in noPropCallbackInEffect.
147- // For `function Foo({ value, onChange }) {}`, we walk the ObjectPattern
148- // and create one Identifier per shorthand entry, tagging callbacks
149- // (`/^on[A-Z]/`) so prop-callback rules don't have to re-derive that
150- // every time.
151126const PROP_CALLBACK_NAME_PATTERN = / ^ o n [ A - Z ] / ;
152127
153128const isPropCallbackName = ( name : string ) : boolean => PROP_CALLBACK_NAME_PATTERN . test ( name ) ;
@@ -194,13 +169,8 @@ const collectFunctionParams = (
194169 return places ;
195170} ;
196171
197- // HACK: `f(...args)` in ESTree wraps the spread in `SpreadElement`,
198- // which `lowerExpression` doesn't recognize and would otherwise drop
199- // silently. Lower the spread's `argument` directly so the spread
200- // source still appears as a Place in the call's args list — the
201- // "spread"-ness is lost (we have no SpreadPlace shape in v1) but
202- // the operand identity is preserved, which is what validators that
203- // trace setState propagation through arguments care about.
172+ // HACK: SpreadElement (`f(...args)`) isn't a real expression node in
173+ // ESTree, so unwrap to its `argument` to keep operand identity.
204174const lowerCallArguments = (
205175 env : LoweringEnvironment ,
206176 argumentNodes : Array < EsTreeNode > | undefined ,
@@ -333,10 +303,8 @@ const lowerExpression = (env: LoweringEnvironment, node: EsTreeNode | null | und
333303 loc ,
334304 node ,
335305 ) ;
336- // HACK: a nested FunctionDeclaration introduces a binding in its
337- // enclosing scope (`function helper() {}` ≈ `var helper = ...`).
338- // Tie the name to the FunctionExpression's lvalue so subsequent
339- // `helper()` calls resolve to it, not LoadGlobal.
306+ // HACK: nested `function helper() {}` declares `helper` in the
307+ // enclosing scope; bind the name so call sites resolve to it.
340308 if ( node . type === "FunctionDeclaration" && node . id ?. type === "Identifier" ) {
341309 setBinding ( env , node . id . name , place ) ;
342310 }
@@ -435,14 +403,10 @@ const lowerExpression = (env: LoweringEnvironment, node: EsTreeNode | null | und
435403 return emitTemporary ( env , { kind : "Unsupported" , reason : node . type } , loc , node ) ;
436404} ;
437405
438- // HACK: collects which Places the lowered inner function reads from
439- // any enclosing scope. Because env IDs are shared (root env's
440- // allocator is reused by every child), we just look at every
441- // LoadLocal whose source identifier was bound OUTSIDE the inner
442- // function's own params / destructured props / lvalues — that's a
443- // capture. The outer env isn't needed here because the captured
444- // Place is the SAME Place object (identifier id and originNode)
445- // that exists in the outer scope.
406+ // HACK: a "capture" is any LoadLocal whose source Identifier wasn't
407+ // declared inside the inner function (params, destructured props,
408+ // instruction lvalues). Shared id allocator means the captured Place
409+ // is already === the outer Place.
446410const collectCapturedPlaces = ( innerFn : HIRFunction ) : Array < Place > => {
447411 const captured : Array < Place > = [ ] ;
448412 const seenIds = new Set < IdentifierId > ( ) ;
@@ -546,10 +510,8 @@ const lowerStatement = (env: LoweringEnvironment, node: EsTreeNode | null | unde
546510 }
547511
548512 // HACK: control-flow statements collapse into the surrounding block
549- // for v1 (no real CFG terminals yet). We still recurse into their
550- // bodies so a `useEffect` inside `try { ... }` or `for (...) { ... }`
551- // gets lowered — silently dropping these statements would have
552- // missed real validator findings.
513+ // for v1 (no CFG terminals modeled). We recurse into their bodies
514+ // so hooks/effects inside them still get lowered.
553515 if ( node . type === "ForStatement" || node . type === "WhileStatement" ) {
554516 if ( node . test ) lowerExpression ( env , node . test ) ;
555517 if ( node . update ) lowerExpression ( env , node . update ) ;
@@ -593,13 +555,8 @@ const lowerStatement = (env: LoweringEnvironment, node: EsTreeNode | null | unde
593555 if ( node . type === "TryStatement" ) {
594556 lowerStatement ( env , node . block ) ;
595557 if ( node . handler ) {
596- // HACK: bind the catch param (` } catch (error) {`) so references
597- // to it inside the handler body resolve as a LoadLocal instead
598- // of falling through to LoadGlobal "error". The param's scope
599- // technically ends with the handler block; we keep it visible
600- // in the surrounding env (no real block scoping in v1) — that's
601- // a minor over-scope which doesn't affect the validators we
602- // currently run.
558+ // HACK: bind catch param so references inside the handler body
559+ // resolve as LoadLocal, not LoadGlobal.
603560 if ( node . handler . param ?. type === "Identifier" ) {
604561 const paramName = node . handler . param . name ;
605562 const paramIdentifier = createIdentifier ( env , paramName , "local" ) ;
@@ -632,7 +589,6 @@ const lowerStatement = (env: LoweringEnvironment, node: EsTreeNode | null | unde
632589 node . type === "ArrowFunctionExpression" ||
633590 node . type === "FunctionExpression"
634591 ) {
635- // Treat as an expression so it gets a FunctionExpression instruction.
636592 lowerExpression ( env , node ) ;
637593 return ;
638594 }
0 commit comments