55 * LICENSE file in the root directory of this source tree.
66 */
77
8+ import { CompilerError } from '..' ;
89import {
910 convertHoistedLValueKind ,
11+ Environment ,
1012 IdentifierId ,
13+ InstructionId ,
1114 InstructionKind ,
15+ Place ,
1216 ReactiveFunction ,
1317 ReactiveInstruction ,
18+ ReactiveScope ,
1419 ReactiveScopeBlock ,
1520 ReactiveStatement ,
1621} from '../HIR' ;
@@ -24,31 +29,78 @@ import {
2429/*
2530 * Prunes DeclareContexts lowered for HoistedConsts, and transforms any references back to its
2631 * original instruction kind.
32+ *
33+ * Detects and bails out on context variables which are:
34+ * - function declarations, which are hoisted by JS engines to the nearest block scope
35+ * - referenced before they are defined (i.e. having a `DeclareContext HoistedConst`)
36+ * - declared
2737 */
2838export function pruneHoistedContexts ( fn : ReactiveFunction ) : void {
2939 visitReactiveFunction ( fn , new Visitor ( ) , {
3040 activeScopes : empty ( ) ,
41+ exceptions : new Set ( ) ,
42+ uninitialized : new Map ( ) ,
3143 } ) ;
3244}
3345
3446type VisitorState = {
3547 activeScopes : Stack < Set < IdentifierId > > ;
48+ exceptions : Set < Place > ;
49+ uninitialized : Map <
50+ IdentifierId ,
51+ | {
52+ kind : 'maybefunc' ;
53+ }
54+ | {
55+ kind : 'func' ;
56+ definition : Place | null ;
57+ }
58+ > ;
3659} ;
3760
61+ /**
62+ * Oh man what about declarations in nested scopes?? t.t
63+ * We *might* encounter the following.
64+ * scope @0
65+ */
3866class Visitor extends ReactiveFunctionTransform < VisitorState > {
3967 override visitScope ( scope : ReactiveScopeBlock , state : VisitorState ) : void {
4068 state . activeScopes = state . activeScopes . push (
4169 new Set ( scope . scope . declarations . keys ( ) ) ,
4270 ) ;
71+ for ( const decl of scope . scope . declarations . values ( ) ) {
72+ state . uninitialized . set ( decl . identifier . id , { kind : 'maybefunc' } ) ;
73+ }
4374 this . traverseScope ( scope , state ) ;
4475 state . activeScopes . pop ( ) ;
76+
77+ /**
78+ * References to hoisted functions are now "safe" as it has been assigned
79+ */
80+ for ( const id of scope . scope . declarations . keys ( ) ) {
81+ state . uninitialized . delete ( id ) ;
82+ }
83+ }
84+ override visitPlace (
85+ _id : InstructionId ,
86+ place : Place ,
87+ state : VisitorState ,
88+ ) : void {
89+ const maybeHoistedFn = state . uninitialized . get ( place . identifier . id ) ;
90+ if (
91+ maybeHoistedFn ?. kind === 'func' &&
92+ maybeHoistedFn . definition !== place
93+ ) {
94+ CompilerError . throwTodo ( {
95+ reason : '[PruneHoistedContexts] Rewrite hoisted function references' ,
96+ loc : place . loc ,
97+ } ) ;
98+ }
4599 }
46100 override transformInstruction (
47101 instruction : ReactiveInstruction ,
48102 state : VisitorState ,
49103 ) : Transformed < ReactiveStatement > {
50- this . visitInstruction ( instruction , state ) ;
51-
52104 /**
53105 * Remove hoisted declarations to preserve TDZ
54106 */
@@ -57,6 +109,18 @@ class Visitor extends ReactiveFunctionTransform<VisitorState> {
57109 instruction . value . lvalue . kind ,
58110 ) ;
59111 if ( maybeNonHoisted != null ) {
112+ if (
113+ maybeNonHoisted === InstructionKind . Function &&
114+ state . uninitialized . has ( instruction . value . lvalue . place . identifier . id )
115+ ) {
116+ state . uninitialized . set (
117+ instruction . value . lvalue . place . identifier . id ,
118+ {
119+ kind : 'func' ,
120+ definition : null ,
121+ } ,
122+ ) ;
123+ }
60124 return { kind : 'remove' } ;
61125 }
62126 }
@@ -65,18 +129,44 @@ class Visitor extends ReactiveFunctionTransform<VisitorState> {
65129 instruction . value . lvalue . kind !== InstructionKind . Reassign
66130 ) {
67131 /**
68- * Rewrite StoreContexts let/const/functions that will be pre-declared in
132+ * Rewrite StoreContexts let/const that will be pre-declared in
69133 * codegen to reassignments.
70134 */
71135 const lvalueId = instruction . value . lvalue . place . identifier . id ;
72136 const isDeclaredByScope = state . activeScopes . find ( scope =>
73137 scope . has ( lvalueId ) ,
74138 ) ;
75139 if ( isDeclaredByScope ) {
76- instruction . value . lvalue . kind = InstructionKind . Reassign ;
140+ if (
141+ instruction . value . lvalue . kind === InstructionKind . Let ||
142+ instruction . value . lvalue . kind === InstructionKind . Const
143+ ) {
144+ instruction . value . lvalue . kind = InstructionKind . Reassign ;
145+ } else if ( instruction . value . lvalue . kind === InstructionKind . Function ) {
146+ state . exceptions . add ( instruction . value . lvalue . place ) ;
147+ const maybeHoistedFn = state . uninitialized . get ( lvalueId ) ;
148+ if ( maybeHoistedFn != null ) {
149+ CompilerError . invariant ( maybeHoistedFn . kind === 'func' , {
150+ reason : '[PruneHoistedContexts] Unexpected hoisted function' ,
151+ loc : instruction . loc ,
152+ } ) ;
153+ maybeHoistedFn . definition = instruction . value . lvalue . place ;
154+ }
155+ } else {
156+ CompilerError . throwTodo ( {
157+ reason : '[PruneHoistedContexts] Unexpected kind ' ,
158+ description : `(${ instruction . value . lvalue . kind } )` ,
159+ loc : instruction . loc ,
160+ } ) ;
161+ }
77162 }
78163 }
79164
165+ this . visitInstruction ( instruction , state ) ;
80166 return { kind : 'keep' } ;
81167 }
82168}
169+
170+ /**
171+ * For functions whose declarations span block boundaries,
172+ */
0 commit comments