5
5
* LICENSE file in the root directory of this source tree.
6
6
*/
7
7
8
- import { CompilerError , CompilerErrorDetailOptions , ErrorSeverity } from '..' ;
8
+ import {
9
+ CompilerError ,
10
+ CompilerErrorDetailOptions ,
11
+ ErrorSeverity ,
12
+ SourceLocation ,
13
+ } from '..' ;
9
14
import {
10
15
CallExpression ,
11
16
Effect ,
@@ -28,14 +33,11 @@ import {
28
33
import { createTemporaryPlace , markInstructionIds } from '../HIR/HIRBuilder' ;
29
34
import { getOrInsertWith } from '../Utils/utils' ;
30
35
import { BuiltInFireId , DefaultNonmutatingHook } from '../HIR/ObjectShape' ;
36
+ import { eachInstructionOperand } from '../HIR/visitors' ;
37
+ import { printSourceLocationLine } from '../HIR/PrintHIR' ;
31
38
32
39
/*
33
40
* TODO(jmbrown):
34
- * In this stack:
35
- * - Assert no lingering fire calls
36
- * - Ensure a fired function is not called regularly elsewhere in the same effect
37
- *
38
- * Future:
39
41
* - rewrite dep arrays
40
42
* - traverse object methods
41
43
* - method calls
@@ -187,6 +189,7 @@ type FireCalleesToFireFunctionBinding = Map<
187
189
{
188
190
fireFunctionBinding : Place ;
189
191
capturedCalleeIdentifier : Identifier ;
192
+ fireLoc : SourceLocation ;
190
193
}
191
194
> ;
192
195
@@ -295,7 +298,11 @@ class Context {
295
298
return this . #loadLocals. get ( id ) ;
296
299
}
297
300
298
- getOrGenerateFireFunctionBinding ( callee : Place , env : Environment ) : Place {
301
+ getOrGenerateFireFunctionBinding (
302
+ callee : Place ,
303
+ env : Environment ,
304
+ fireLoc : SourceLocation ,
305
+ ) : Place {
299
306
const fireFunctionBinding = getOrInsertWith (
300
307
this . #fireCalleesToFireFunctions,
301
308
callee . identifier . id ,
@@ -305,6 +312,7 @@ class Context {
305
312
this . #capturedCalleeIdentifierIds. set ( callee . identifier . id , {
306
313
fireFunctionBinding,
307
314
capturedCalleeIdentifier : callee . identifier ,
315
+ fireLoc,
308
316
} ) ;
309
317
310
318
return fireFunctionBinding ;
@@ -346,8 +354,12 @@ class Context {
346
354
return this . #loadGlobalInstructionIds. get ( id ) ;
347
355
}
348
356
357
+ hasErrors ( ) : boolean {
358
+ return this . #errors. hasErrors ( ) ;
359
+ }
360
+
349
361
throwIfErrorsFound ( ) : void {
350
- if ( this . #errors . hasErrors ( ) ) throw this . #errors;
362
+ if ( this . hasErrors ( ) ) throw this . #errors;
351
363
}
352
364
}
353
365
@@ -510,6 +522,11 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
510
522
rewriteInstrs . set ( loadUseEffectInstrId , newInstrs ) ;
511
523
}
512
524
}
525
+ ensureNoRemainingCalleeCaptures (
526
+ lambda . loweredFunc . func ,
527
+ context ,
528
+ capturedCallees ,
529
+ ) ;
513
530
}
514
531
} else if (
515
532
value . kind === 'CallExpression' &&
@@ -551,6 +568,7 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
551
568
context . getOrGenerateFireFunctionBinding (
552
569
{ ...loadLocal . place } ,
553
570
fn . env ,
571
+ value . loc ,
554
572
) ;
555
573
556
574
loadLocal . place = { ...fireFunctionBinding } ;
@@ -626,8 +644,74 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
626
644
}
627
645
}
628
646
647
+ /*
648
+ * eachInstructionOperand is not sufficient for our cases because:
649
+ * 1. fire is a global, which will not appear
650
+ * 2. The HIR may be malformed, so can't rely on function deps and must
651
+ * traverse the whole function.
652
+ */
653
+ function * eachReachablePlace ( fn : HIRFunction ) : Iterable < Place > {
654
+ for ( const [ , block ] of fn . body . blocks ) {
655
+ for ( const instr of block . instructions ) {
656
+ if (
657
+ instr . value . kind === 'FunctionExpression' ||
658
+ instr . value . kind === 'ObjectMethod'
659
+ ) {
660
+ yield * eachReachablePlace ( instr . value . loweredFunc . func ) ;
661
+ } else {
662
+ yield * eachInstructionOperand ( instr ) ;
663
+ }
664
+ }
665
+ }
666
+ }
667
+
668
+ function ensureNoRemainingCalleeCaptures (
669
+ fn : HIRFunction ,
670
+ context : Context ,
671
+ capturedCallees : FireCalleesToFireFunctionBinding ,
672
+ ) : void {
673
+ for ( const place of eachReachablePlace ( fn ) ) {
674
+ const calleeInfo = capturedCallees . get ( place . identifier . id ) ;
675
+ if ( calleeInfo != null ) {
676
+ const calleeName =
677
+ calleeInfo . capturedCalleeIdentifier . name ?. kind === 'named'
678
+ ? calleeInfo . capturedCalleeIdentifier . name . value
679
+ : '<unknown>' ;
680
+ context . pushError ( {
681
+ loc : place . loc ,
682
+ description : `All uses of ${ calleeName } must be either used with a fire() call in \
683
+ this effect or not used with a fire() call at all. ${ calleeName } was used with fire() on line \
684
+ ${ printSourceLocationLine ( calleeInfo . fireLoc ) } in this effect`,
685
+ severity : ErrorSeverity . InvalidReact ,
686
+ reason : CANNOT_COMPILE_FIRE ,
687
+ suggestions : null ,
688
+ } ) ;
689
+ }
690
+ }
691
+ }
692
+
693
+ function ensureNoMoreFireUses ( fn : HIRFunction , context : Context ) : void {
694
+ for ( const place of eachReachablePlace ( fn ) ) {
695
+ if (
696
+ place . identifier . type . kind === 'Function' &&
697
+ place . identifier . type . shapeId === BuiltInFireId
698
+ ) {
699
+ context . pushError ( {
700
+ loc : place . identifier . loc ,
701
+ description : 'Cannot use `fire` outside of a useEffect function' ,
702
+ severity : ErrorSeverity . Invariant ,
703
+ reason : CANNOT_COMPILE_FIRE ,
704
+ suggestions : null ,
705
+ } ) ;
706
+ }
707
+ }
708
+ }
709
+
629
710
export function transformFire ( fn : HIRFunction ) : void {
630
711
const context = new Context ( ) ;
631
712
replaceFireFunctions ( fn , context ) ;
713
+ if ( ! context . hasErrors ( ) ) {
714
+ ensureNoMoreFireUses ( fn , context ) ;
715
+ }
632
716
context . throwIfErrorsFound ( ) ;
633
717
}
0 commit comments