55 * LICENSE file in the root directory of this source tree.
66 */
77
8- import { CompilerError , CompilerErrorDetailOptions , ErrorSeverity } from '..' ;
8+ import {
9+ CompilerError ,
10+ CompilerErrorDetailOptions ,
11+ ErrorSeverity ,
12+ SourceLocation ,
13+ } from '..' ;
914import {
1015 CallExpression ,
1116 Effect ,
@@ -28,14 +33,11 @@ import {
2833import { createTemporaryPlace , markInstructionIds } from '../HIR/HIRBuilder' ;
2934import { getOrInsertWith } from '../Utils/utils' ;
3035import { BuiltInFireId , DefaultNonmutatingHook } from '../HIR/ObjectShape' ;
36+ import { eachInstructionOperand } from '../HIR/visitors' ;
37+ import { printSourceLocationLine } from '../HIR/PrintHIR' ;
3138
3239/*
3340 * 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:
3941 * - rewrite dep arrays
4042 * - traverse object methods
4143 * - method calls
@@ -47,6 +49,9 @@ const CANNOT_COMPILE_FIRE = 'Cannot compile `fire`';
4749export function transformFire ( fn : HIRFunction ) : void {
4850 const context = new Context ( fn . env ) ;
4951 replaceFireFunctions ( fn , context ) ;
52+ if ( ! context . hasErrors ( ) ) {
53+ ensureNoMoreFireUses ( fn , context ) ;
54+ }
5055 context . throwIfErrorsFound ( ) ;
5156}
5257
@@ -120,6 +125,11 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
120125 }
121126 rewriteInstrs . set ( loadUseEffectInstrId , newInstrs ) ;
122127 }
128+ ensureNoRemainingCalleeCaptures (
129+ lambda . loweredFunc . func ,
130+ context ,
131+ capturedCallees ,
132+ ) ;
123133 }
124134 }
125135 } else if (
@@ -159,7 +169,10 @@ function replaceFireFunctions(fn: HIRFunction, context: Context): void {
159169 }
160170
161171 const fireFunctionBinding =
162- context . getOrGenerateFireFunctionBinding ( loadLocal . place ) ;
172+ context . getOrGenerateFireFunctionBinding (
173+ loadLocal . place ,
174+ value . loc ,
175+ ) ;
163176
164177 loadLocal . place = { ...fireFunctionBinding } ;
165178
@@ -320,6 +333,69 @@ function visitFunctionExpressionAndPropagateFireDependencies(
320333 return calleesCapturedByFnExpression ;
321334}
322335
336+ /*
337+ * eachInstructionOperand is not sufficient for our cases because:
338+ * 1. fire is a global, which will not appear
339+ * 2. The HIR may be malformed, so can't rely on function deps and must
340+ * traverse the whole function.
341+ */
342+ function * eachReachablePlace ( fn : HIRFunction ) : Iterable < Place > {
343+ for ( const [ , block ] of fn . body . blocks ) {
344+ for ( const instr of block . instructions ) {
345+ if (
346+ instr . value . kind === 'FunctionExpression' ||
347+ instr . value . kind === 'ObjectMethod'
348+ ) {
349+ yield * eachReachablePlace ( instr . value . loweredFunc . func ) ;
350+ } else {
351+ yield * eachInstructionOperand ( instr ) ;
352+ }
353+ }
354+ }
355+ }
356+
357+ function ensureNoRemainingCalleeCaptures (
358+ fn : HIRFunction ,
359+ context : Context ,
360+ capturedCallees : FireCalleesToFireFunctionBinding ,
361+ ) : void {
362+ for ( const place of eachReachablePlace ( fn ) ) {
363+ const calleeInfo = capturedCallees . get ( place . identifier . id ) ;
364+ if ( calleeInfo != null ) {
365+ const calleeName =
366+ calleeInfo . capturedCalleeIdentifier . name ?. kind === 'named'
367+ ? calleeInfo . capturedCalleeIdentifier . name . value
368+ : '<unknown>' ;
369+ context . pushError ( {
370+ loc : place . loc ,
371+ description : `All uses of ${ calleeName } must be either used with a fire() call in \
372+ this effect or not used with a fire() call at all. ${ calleeName } was used with fire() on line \
373+ ${ printSourceLocationLine ( calleeInfo . fireLoc ) } in this effect`,
374+ severity : ErrorSeverity . InvalidReact ,
375+ reason : CANNOT_COMPILE_FIRE ,
376+ suggestions : null ,
377+ } ) ;
378+ }
379+ }
380+ }
381+
382+ function ensureNoMoreFireUses ( fn : HIRFunction , context : Context ) : void {
383+ for ( const place of eachReachablePlace ( fn ) ) {
384+ if (
385+ place . identifier . type . kind === 'Function' &&
386+ place . identifier . type . shapeId === BuiltInFireId
387+ ) {
388+ context . pushError ( {
389+ loc : place . identifier . loc ,
390+ description : 'Cannot use `fire` outside of a useEffect function' ,
391+ severity : ErrorSeverity . Invariant ,
392+ reason : CANNOT_COMPILE_FIRE ,
393+ suggestions : null ,
394+ } ) ;
395+ }
396+ }
397+ }
398+
323399function makeLoadUseFireInstruction ( env : Environment ) : Instruction {
324400 const useFirePlace = createTemporaryPlace ( env , GeneratedSource ) ;
325401 useFirePlace . effect = Effect . Read ;
@@ -422,6 +498,7 @@ type FireCalleesToFireFunctionBinding = Map<
422498 {
423499 fireFunctionBinding : Place ;
424500 capturedCalleeIdentifier : Identifier ;
501+ fireLoc : SourceLocation ;
425502 }
426503> ;
427504
@@ -523,8 +600,10 @@ class Context {
523600 getLoadLocalInstr ( id : IdentifierId ) : LoadLocal | undefined {
524601 return this . #loadLocals. get ( id ) ;
525602 }
526-
527- getOrGenerateFireFunctionBinding ( callee : Place ) : Place {
603+ getOrGenerateFireFunctionBinding (
604+ callee : Place ,
605+ fireLoc : SourceLocation ,
606+ ) : Place {
528607 const fireFunctionBinding = getOrInsertWith (
529608 this . #fireCalleesToFireFunctions,
530609 callee . identifier . id ,
@@ -534,6 +613,7 @@ class Context {
534613 this . #capturedCalleeIdentifierIds. set ( callee . identifier . id , {
535614 fireFunctionBinding,
536615 capturedCalleeIdentifier : callee . identifier ,
616+ fireLoc,
537617 } ) ;
538618
539619 return fireFunctionBinding ;
@@ -575,8 +655,12 @@ class Context {
575655 return this . #loadGlobalInstructionIds. get ( id ) ;
576656 }
577657
658+ hasErrors ( ) : boolean {
659+ return this . #errors. hasErrors ( ) ;
660+ }
661+
578662 throwIfErrorsFound ( ) : void {
579- if ( this . #errors . hasErrors ( ) ) throw this . #errors;
663+ if ( this . hasErrors ( ) ) throw this . #errors;
580664 }
581665}
582666
0 commit comments