diff --git a/CLAUDE.md b/CLAUDE.md index 214ac30f..c050a11c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -201,6 +201,8 @@ Pattern: 2. Add build step in `scripts/build-vendor.sh` (compile to `.o`) 3. Declare extern functions in LLVM IR and call them from codegen 4. Add conditional linking in `src/compiler.ts` and `src/native-compiler-lib.ts` +5. Add to `scripts/build-target-sdk.sh` bridge list (cross-compile SDK packaging) +6. Add to `ci.yml` in all 4 places: Linux verify loop, Linux release copy, macOS verify loop, macOS release copy Existing bridges: `regex-bridge.c`, `yyjson-bridge.c`, `os-bridge.c`, `child-process-bridge.c`, `child-process-spawn.c`, `lws-bridge.c`, `treesitter-bridge.c`. diff --git a/src/codegen/infrastructure/generator-context.ts b/src/codegen/infrastructure/generator-context.ts index 8f379535..9c5dd923 100644 --- a/src/codegen/infrastructure/generator-context.ts +++ b/src/codegen/infrastructure/generator-context.ts @@ -942,6 +942,12 @@ export interface IGeneratorContext { lastTypeAssertionSourceVar: string | null; getLastTypeAssertionSourceVar(): string | null; setLastTypeAssertionSourceVar(name: string | null): void; + + setStackEligibleVars(vars: string[]): void; + isStackEligibleKey(key: string): boolean; + currentVarDeclKey: string | null; + setCurrentVarDeclKey(key: string | null): void; + getCurrentVarDeclKey(): string | null; } /** @@ -1002,6 +1008,9 @@ export class MockGeneratorContext implements IGeneratorContext { public lastInlineLambdaEnvPtr: string | null = null; public lastTypeAssertionSourceVar: string | null = null; + private stackEligibleVars: string[] = []; + public currentVarDeclKey: string | null = null; + constructor() { this.typeContext = new TypeContext(); this.symbolTable = new SymbolTable(this.typeContext); @@ -2190,4 +2199,23 @@ export class MockGeneratorContext implements IGeneratorContext { if (!ta || !ta.unionMembers) return null; return ta.unionMembers; } + + setStackEligibleVars(vars: string[]): void { + this.stackEligibleVars = vars; + } + + isStackEligibleKey(key: string): boolean { + for (let i = 0; i < this.stackEligibleVars.length; i++) { + if (this.stackEligibleVars[i] === key) return true; + } + return false; + } + + setCurrentVarDeclKey(key: string | null): void { + this.currentVarDeclKey = key; + } + + getCurrentVarDeclKey(): string | null { + return this.currentVarDeclKey; + } } diff --git a/src/codegen/infrastructure/variable-allocator.ts b/src/codegen/infrastructure/variable-allocator.ts index ae05ad0a..a7781feb 100644 --- a/src/codegen/infrastructure/variable-allocator.ts +++ b/src/codegen/infrastructure/variable-allocator.ts @@ -232,6 +232,8 @@ export interface VariableAllocatorContext { getWantsBinaryReturn(): boolean; getLastTypeAssertionSourceVar(): string | null; setLastTypeAssertionSourceVar(name: string | null): void; + setCurrentVarDeclKey(key: string | null): void; + isStackEligibleKey(key: string): boolean; } export class VariableAllocator { @@ -877,6 +879,10 @@ export class VariableAllocator { arrayMethodReturnType, ); + const declLine = stmt.loc ? stmt.loc.line : (stmt.line ?? -1); + const declCol = stmt.loc ? stmt.loc.column : -1; + const declKey = stmt.name + ":" + declLine + ":" + declCol; + this.ctx.setCurrentVarDeclKey(declKey); switch (classification.kind) { case VarKind.DeclaredInterface: this.allocateDeclaredInterface(stmt, params, classification.declaredInterfaceType!); @@ -976,6 +982,8 @@ export class VariableAllocator { break; } + this.ctx.setCurrentVarDeclKey(null); + if (resolved && !stmt.declaredType && !isNull) { this.ctx.symbolTable.setResolvedType(stmt.name, resolved); } diff --git a/src/codegen/llvm-generator.ts b/src/codegen/llvm-generator.ts index e95be98b..2bc9618e 100644 --- a/src/codegen/llvm-generator.ts +++ b/src/codegen/llvm-generator.ts @@ -121,6 +121,7 @@ import type { TargetInfo } from "../target-types.js"; import { checkClosureMutations } from "../semantic/closure-mutation-checker.js"; import { checkUnionTypes } from "../semantic/union-type-checker.js"; import { checkTypeAssertions } from "../semantic/type-assertion-checker.js"; +import { analyzeEscapes } from "../semantic/escape-analysis.js"; export interface SemaSymbolData { names: string[]; @@ -270,6 +271,10 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { private semaSymbolSchemaTypes: (string[] | undefined)[]; private semaSymbolCount: number; + // Escape analysis result — string keys "name:line:col" for stack-eligible var decls + private stackEligibleVars: string[] = []; + public currentVarDeclKey: string | null = null; + // Debug info emitter (null when debug info is disabled) private debugInfoEmitter: number = 0; private currentSubprogramId: number = -1; @@ -1109,6 +1114,21 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { public setLastTypeAssertionSourceVar(name: string | null): void { this.lastTypeAssertionSourceVar = name; } + public setStackEligibleVars(vars: string[]): void { + this.stackEligibleVars = vars; + } + public isStackEligibleKey(key: string): boolean { + for (let i = 0; i < this.stackEligibleVars.length; i++) { + if (this.stackEligibleVars[i] === key) return true; + } + return false; + } + public setCurrentVarDeclKey(key: string | null): void { + this.currentVarDeclKey = key; + } + public getCurrentVarDeclKey(): string | null { + return this.currentVarDeclKey; + } public setIsAsyncFunction(value: boolean): void { this.isAsyncFunction = value; } @@ -1898,6 +1918,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { let isClassInstance = false; let isUint8Array = false; let isBoolean = false; + let isNumber = false; if (resolved) { const base = resolved.base; const depth = resolved.arrayDepth; @@ -1910,6 +1931,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { isRegex = base === "RegExp"; isObject = base === "object" && depth === 0; isBoolean = base === "boolean" && depth === 0; + isNumber = base === "number" && depth === 0; isUint8Array = base === "Uint8Array" && depth === 0; isClassInstance = !isRegex && @@ -2146,6 +2168,10 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { llvmType = "double"; kind = SymbolKind.Boolean; defaultValue = "0.0"; + } else if (isNumber) { + llvmType = "double"; + kind = SymbolKind.Number; + defaultValue = "0.0"; } else if (isJSONParse) { const interfaceName = this.typeInference.getJSONParseInterface( stmt.value as MethodCallNode, @@ -2446,6 +2472,7 @@ export class LLVMGenerator extends BaseGenerator implements IGeneratorContext { checkClosureMutations(this.ast); checkUnionTypes(this.ast); checkTypeAssertions(this.ast); + this.stackEligibleVars = analyzeEscapes(this.ast); const irParts: string[] = []; diff --git a/src/codegen/types/objects/object.ts b/src/codegen/types/objects/object.ts index dfc094ef..b1e4987b 100644 --- a/src/codegen/types/objects/object.ts +++ b/src/codegen/types/objects/object.ts @@ -15,6 +15,20 @@ export class ObjectGenerator { this.ctx.emit(instruction); } + private allocateStruct(structType: string, structSizeBytes: number): string { + const key = this.ctx.getCurrentVarDeclKey(); + if (key && this.ctx.isStackEligibleKey(key)) { + const allocaReg = this.ctx.nextAllocaReg("_obj"); + this.emit(`${allocaReg} = alloca ${structType}`); + return allocaReg; + } + const objMem = this.nextTemp(); + this.emit(`${objMem} = call i8* @GC_malloc(i64 ${structSizeBytes})`); + const objPtr = this.nextTemp(); + this.emit(`${objPtr} = bitcast i8* ${objMem} to ${structType}*`); + return objPtr; + } + generateObjectLiteral(expr: Expression, params: string[]): string { if (expr.type !== "object") { throw new Error("Expected object literal"); @@ -134,13 +148,9 @@ export class ObjectGenerator { } const structFields = llvmTypes.join(", "); const structSizeBytes = orderedFields.length * 8; - - const objMem = this.nextTemp(); - this.emit(`${objMem} = call i8* @GC_malloc(i64 ${structSizeBytes})`); - const structType = `{ ${structFields} }`; - const objPtr = this.nextTemp(); - this.emit(`${objPtr} = bitcast i8* ${objMem} to ${structType}*`); + + const objPtr = this.allocateStruct(structType, structSizeBytes); for (let i = 0; i < orderedFields.length; i++) { const field = orderedFields[i] as { key: string; llvmType: string; value: string }; @@ -258,13 +268,9 @@ export class ObjectGenerator { } const structType = `%${interfaceName}`; - const structSize = this.ctx.interfaceStructGen?.getStructSize(interfaceName); - - const objMem = this.nextTemp(); - this.emit(`${objMem} = call i8* @GC_malloc(i64 ${structSize})`); + const structSize = this.ctx.interfaceStructGen?.getStructSize(interfaceName) ?? 0; - const objPtr = this.nextTemp(); - this.emit(`${objPtr} = bitcast i8* ${objMem} to ${structType}*`); + const objPtr = this.allocateStruct(structType, structSize); for (let i = 0; i < orderedFields.length; i++) { const field = orderedFields[i] as { key: string; llvmType: string; value: string }; @@ -356,13 +362,9 @@ export class ObjectGenerator { } const structFields = llvmTypes.join(", "); const structSizeBytes = fieldTypes.length * 8; - - const objMem = this.nextTemp(); - this.emit(`${objMem} = call i8* @GC_malloc(i64 ${structSizeBytes})`); - const structType = `{ ${structFields} }`; - const objPtr = this.nextTemp(); - this.emit(`${objPtr} = bitcast i8* ${objMem} to ${structType}*`); + + const objPtr = this.allocateStruct(structType, structSizeBytes); for (let i = 0; i < fieldTypes.length; i++) { const field = fieldTypes[i] as { key: string; llvmType: string; value: string }; diff --git a/src/semantic/escape-analysis.ts b/src/semantic/escape-analysis.ts new file mode 100644 index 00000000..03168c57 --- /dev/null +++ b/src/semantic/escape-analysis.ts @@ -0,0 +1,672 @@ +// Escape analysis semantic pass — runs before IR generation. +// Identifies local object-literal variables that do not escape their function scope. +// These can be stack-allocated (alloca) instead of heap-allocated (GC_malloc), +// eliminating allocation cost and GC pressure for short-lived objects. +// +// Conservative: a variable is stack-eligible only when we can PROVE it doesn't escape. +// False "escape" is always safe; false "no-escape" would produce dangling pointers. + +import type { + AST, + Statement, + Expression, + BlockStatement, + VariableDeclaration, + AssignmentStatement, + ReturnStatement, + IfStatement, + WhileStatement, + ForStatement, + ForOfStatement, + ThrowStatement, + TryStatement, + SwitchStatement, + FunctionNode, + ClassNode, + ClassMethod, + CallNode, + MethodCallNode, + BinaryNode, + UnaryNode, + MemberAccessNode, + IndexAccessNode, + ArrayNode, + ObjectNode, + ArrowFunctionNode, + TemplateLiteralNode, + ConditionalExpressionNode, + AwaitExpressionNode, + MemberAccessAssignmentNode, + IndexAccessAssignmentNode, + TypeAssertionNode, + NewNode, + SpreadElementNode, +} from "../ast/types.js"; + +export function varDeclKey(decl: VariableDeclaration): string { + const line = + (decl as unknown as VariableDeclaration).loc?.line ?? + (decl as unknown as VariableDeclaration).line ?? + -1; + const col = (decl as unknown as VariableDeclaration).loc?.column ?? -1; + return decl.name + ":" + line + ":" + col; +} + +export function analyzeEscapes(ast: AST): string[] { + const result: string[] = []; + const analyzer = new EscapeAnalyzer(); + + for (let i = 0; i < ast.functions.length; i++) { + const func = ast.functions[i] as FunctionNode; + if (func.declare || !func.body) continue; + analyzer.analyzeBlock(func.body, result); + } + + for (let i = 0; i < ast.classes.length; i++) { + const cls = ast.classes[i] as ClassNode; + for (let j = 0; j < cls.methods.length; j++) { + const method = cls.methods[j] as ClassMethod; + analyzer.analyzeBlock(method.body, result); + } + } + + return result; +} + +interface StmtBase { + type: string; +} + +interface ExprBase { + type: string; +} + +class EscapeAnalyzer { + private candidateDecls: VariableDeclaration[] = []; + private candidateNames: string[] = []; + private escapedNames: string[] = []; + + analyzeBlock(body: BlockStatement, result: string[]): void { + this.candidateDecls = []; + this.candidateNames = []; + this.escapedNames = []; + + this.collectCandidates(body.statements); + if (this.candidateDecls.length > 0) this.scanBlockForEscapes(body); + + for (let i = 0; i < this.candidateDecls.length; i++) { + const decl = this.candidateDecls[i] as VariableDeclaration; + const name = this.candidateNames[i] as string; + if (!this.isEscaped(name)) { + result.push(varDeclKey(decl)); + } + } + } + + private blockContainsArrow(stmts: Statement[]): boolean { + for (let i = 0; i < stmts.length; i++) { + if (this.stmtContainsArrow(stmts[i])) return true; + } + return false; + } + + private stmtContainsArrow(stmt: Statement): boolean { + const s = stmt as StmtBase; + if (s.type === "variable_declaration") { + const d = stmt as unknown as VariableDeclaration; + return d.value ? this.exprContainsArrow(d.value) : false; + } + if (s.type === "assignment") { + const a = stmt as unknown as AssignmentStatement; + return this.exprContainsArrow(a.value); + } + if (s.type === "return") { + const r = stmt as unknown as ReturnStatement; + return r.value ? this.exprContainsArrow(r.value) : false; + } + if (s.type === "if") { + const i = stmt as unknown as IfStatement; + return ( + this.exprContainsArrow(i.condition) || + this.blockContainsArrow(i.thenBlock.statements) || + (i.elseBlock ? this.blockContainsArrow(i.elseBlock.statements) : false) + ); + } + if (s.type === "while" || s.type === "do_while") { + const w = stmt as unknown as WhileStatement; + return this.exprContainsArrow(w.condition) || this.blockContainsArrow(w.body.statements); + } + if (s.type === "for") { + const f = stmt as unknown as ForStatement; + return this.blockContainsArrow(f.body.statements); + } + if (s.type === "for_of") { + const f = stmt as unknown as ForOfStatement; + return this.exprContainsArrow(f.iterable) || this.blockContainsArrow(f.body.statements); + } + if (s.type === "try") { + const t = stmt as unknown as TryStatement; + return ( + this.blockContainsArrow(t.tryBlock.statements) || + (t.catchBody ? this.blockContainsArrow(t.catchBody.statements) : false) || + (t.finallyBlock ? this.blockContainsArrow(t.finallyBlock.statements) : false) + ); + } + if (s.type === "switch") { + const sw = stmt as unknown as SwitchStatement; + for (let i = 0; i < sw.cases.length; i++) { + const c = sw.cases[i] as { test: Expression | null; consequent: Statement[] }; + if (this.blockContainsArrow(c.consequent)) return true; + } + return false; + } + if (s.type === "block") { + const b = stmt as unknown as BlockStatement; + return this.blockContainsArrow(b.statements); + } + return this.exprContainsArrow(stmt as unknown as Expression); + } + + private exprContainsArrow(expr: Expression): boolean { + const e = expr as ExprBase; + if (e.type === "arrow_function") return true; + if ( + e.type === "number" || + e.type === "string" || + e.type === "boolean" || + e.type === "null" || + e.type === "undefined" || + e.type === "regex" || + e.type === "variable" || + e.type === "this" || + e.type === "super" + ) { + return false; + } + if (e.type === "call") { + const c = expr as unknown as CallNode; + for (let i = 0; i < c.args.length; i++) { + if (this.exprContainsArrow(c.args[i])) return true; + } + return false; + } + if (e.type === "method_call") { + const m = expr as unknown as MethodCallNode; + if (this.exprContainsArrow(m.object)) return true; + for (let i = 0; i < m.args.length; i++) { + if (this.exprContainsArrow(m.args[i])) return true; + } + return false; + } + if (e.type === "new") { + const n = expr as unknown as NewNode; + for (let i = 0; i < n.args.length; i++) { + if (this.exprContainsArrow(n.args[i])) return true; + } + return false; + } + if (e.type === "binary") { + const b = expr as unknown as BinaryNode; + return this.exprContainsArrow(b.left) || this.exprContainsArrow(b.right); + } + if (e.type === "unary") { + const u = expr as unknown as UnaryNode; + return this.exprContainsArrow(u.operand); + } + if (e.type === "member_access") { + const m = expr as unknown as MemberAccessNode; + return this.exprContainsArrow(m.object); + } + if (e.type === "index_access") { + const i = expr as unknown as IndexAccessNode; + return this.exprContainsArrow(i.object) || this.exprContainsArrow(i.index); + } + if (e.type === "array") { + const a = expr as unknown as ArrayNode; + for (let i = 0; i < a.elements.length; i++) { + if (this.exprContainsArrow(a.elements[i])) return true; + } + return false; + } + if (e.type === "object") { + const o = expr as unknown as ObjectNode; + for (let i = 0; i < o.properties.length; i++) { + const prop = o.properties[i] as { key: string; value: Expression }; + if (this.exprContainsArrow(prop.value)) return true; + } + return false; + } + if (e.type === "conditional") { + const c = expr as unknown as ConditionalExpressionNode; + return ( + this.exprContainsArrow(c.condition) || + this.exprContainsArrow(c.consequent) || + this.exprContainsArrow(c.alternate) + ); + } + if (e.type === "template_literal") { + const t = expr as unknown as TemplateLiteralNode; + for (let i = 0; i < t.parts.length; i++) { + const part = t.parts[i]; + if (typeof part !== "string" && this.exprContainsArrow(part as Expression)) return true; + } + return false; + } + if (e.type === "await") { + const a = expr as unknown as AwaitExpressionNode; + return this.exprContainsArrow(a.argument); + } + if (e.type === "member_access_assignment") { + const m = expr as unknown as MemberAccessAssignmentNode; + return this.exprContainsArrow(m.object) || this.exprContainsArrow(m.value); + } + if (e.type === "index_access_assignment") { + const i = expr as unknown as IndexAccessAssignmentNode; + return this.exprContainsArrow(i.object) || this.exprContainsArrow(i.value); + } + if (e.type === "type_assertion") { + const t = expr as unknown as TypeAssertionNode; + return this.exprContainsArrow(t.expression); + } + return false; + } + + private isEscaped(name: string): boolean { + for (let i = 0; i < this.escapedNames.length; i++) { + if (this.escapedNames[i] === name) return true; + } + return false; + } + + private isObjectLiteralInit(value: Expression | null): boolean { + if (!value) return false; + const v = value as ExprBase; + return v.type === "object"; + } + + private collectCandidates(stmts: Statement[]): void { + for (let i = 0; i < stmts.length; i++) { + const stmt = stmts[i] as StmtBase; + if (stmt.type === "variable_declaration") { + const decl = stmt as VariableDeclaration; + if (decl.kind === "const" && this.isObjectLiteralInit(decl.value)) { + this.candidateDecls.push(decl); + this.candidateNames.push(decl.name); + } + } + this.collectCandidatesFromStmt(stmts[i]); + } + } + + private collectCandidatesFromStmt(stmt: Statement): void { + const s = stmt as StmtBase; + if (s.type === "if") { + const ifStmt = stmt as unknown as IfStatement; + this.collectCandidates(ifStmt.thenBlock.statements); + if (ifStmt.elseBlock) { + this.collectCandidates(ifStmt.elseBlock.statements); + } + } else if (s.type === "while" || s.type === "do_while") { + const w = stmt as unknown as WhileStatement; + this.collectCandidates(w.body.statements); + } else if (s.type === "for") { + const f = stmt as unknown as ForStatement; + this.collectCandidates(f.body.statements); + } else if (s.type === "for_of") { + const f = stmt as unknown as ForOfStatement; + this.collectCandidates(f.body.statements); + } else if (s.type === "try") { + const t = stmt as unknown as TryStatement; + this.collectCandidates(t.tryBlock.statements); + if (t.catchBody) this.collectCandidates(t.catchBody.statements); + if (t.finallyBlock) this.collectCandidates(t.finallyBlock.statements); + } else if (s.type === "switch") { + const sw = stmt as unknown as SwitchStatement; + for (let i = 0; i < sw.cases.length; i++) { + const c = sw.cases[i] as { test: Expression | null; consequent: Statement[] }; + this.collectCandidates(c.consequent); + } + } else if (s.type === "block") { + const b = stmt as unknown as BlockStatement; + this.collectCandidates(b.statements); + } + } + + private scanBlockForEscapes(body: BlockStatement): void { + for (let i = 0; i < body.statements.length; i++) { + this.scanStmtForEscapes(body.statements[i]); + } + } + + private scanStmtForEscapes(stmt: Statement): void { + const s = stmt as StmtBase; + if (s.type === "variable_declaration") { + const decl = stmt as unknown as VariableDeclaration; + if (decl.value) { + const v = decl.value as ExprBase; + if (v.type === "variable") { + const varRef = decl.value as unknown as { type: string; name: string }; + this.escapedNames.push(varRef.name); + } else { + this.walkExprEscaping(decl.value); + } + } + } else if (s.type === "assignment") { + const a = stmt as unknown as AssignmentStatement; + const v = a.value as ExprBase; + if (v.type === "variable") { + const varRef = a.value as unknown as { type: string; name: string }; + this.escapedNames.push(varRef.name); + } else { + this.walkExprEscaping(a.value); + } + } else if (s.type === "return") { + const r = stmt as unknown as ReturnStatement; + if (r.value) this.walkExprEscaping(r.value); + } else if (s.type === "throw") { + const t = stmt as unknown as ThrowStatement; + this.walkExprEscaping(t.argument); + } else if (s.type === "if") { + const ifStmt = stmt as unknown as IfStatement; + this.walkExprNonEscaping(ifStmt.condition); + this.scanBlockForEscapes(ifStmt.thenBlock); + if (ifStmt.elseBlock) this.scanBlockForEscapes(ifStmt.elseBlock); + } else if (s.type === "while" || s.type === "do_while") { + const w = stmt as unknown as WhileStatement; + this.walkExprNonEscaping(w.condition); + this.scanBlockForEscapes(w.body); + } else if (s.type === "for") { + const f = stmt as unknown as ForStatement; + if (f.init) this.scanStmtForEscapes(f.init as Statement); + if (f.condition) this.walkExprNonEscaping(f.condition); + if (f.update) { + const upd = f.update as ExprBase; + if (upd.type === "assignment") { + this.scanStmtForEscapes(f.update as Statement); + } else { + this.walkExprNonEscaping(f.update as Expression); + } + } + this.scanBlockForEscapes(f.body); + } else if (s.type === "for_of") { + const f = stmt as unknown as ForOfStatement; + this.walkExprNonEscaping(f.iterable); + this.scanBlockForEscapes(f.body); + } else if (s.type === "try") { + const t = stmt as unknown as TryStatement; + this.scanBlockForEscapes(t.tryBlock); + if (t.catchBody) this.scanBlockForEscapes(t.catchBody); + if (t.finallyBlock) this.scanBlockForEscapes(t.finallyBlock); + } else if (s.type === "switch") { + const sw = stmt as unknown as SwitchStatement; + this.walkExprNonEscaping(sw.discriminant); + for (let i = 0; i < sw.cases.length; i++) { + const c = sw.cases[i] as { test: Expression | null; consequent: Statement[] }; + if (c.test) this.walkExprNonEscaping(c.test); + for (let j = 0; j < c.consequent.length; j++) { + this.scanStmtForEscapes(c.consequent[j]); + } + } + } else if (s.type === "block") { + const b = stmt as unknown as BlockStatement; + this.scanBlockForEscapes(b); + } else { + this.walkExprNonEscaping(stmt as unknown as Expression); + } + } + + private walkExprEscaping(expr: Expression): void { + const e = expr as ExprBase; + if (e.type === "variable") { + const v = expr as unknown as { type: string; name: string }; + this.escapedNames.push(v.name); + return; + } + this.walkExprInner(expr, true); + } + + private walkExprNonEscaping(expr: Expression): void { + const e = expr as ExprBase; + if (e.type === "variable") return; + this.walkExprInner(expr, false); + } + + private walkExprInner(expr: Expression, isEscaping: boolean): void { + const e = expr as ExprBase; + if ( + e.type === "number" || + e.type === "string" || + e.type === "boolean" || + e.type === "null" || + e.type === "undefined" || + e.type === "regex" || + e.type === "this" || + e.type === "super" + ) { + return; + } + if (e.type === "variable") { + if (isEscaping) { + const v = expr as unknown as { type: string; name: string }; + this.escapedNames.push(v.name); + } + return; + } + if (e.type === "call") { + const c = expr as unknown as CallNode; + for (let i = 0; i < c.args.length; i++) { + this.walkExprEscaping(c.args[i]); + } + return; + } + if (e.type === "method_call") { + const m = expr as unknown as MethodCallNode; + if (isEscaping) { + this.walkExprEscaping(m.object); + } else { + this.walkExprNonEscaping(m.object); + } + for (let i = 0; i < m.args.length; i++) { + this.walkExprEscaping(m.args[i]); + } + return; + } + if (e.type === "new") { + const n = expr as unknown as NewNode; + for (let i = 0; i < n.args.length; i++) { + this.walkExprEscaping(n.args[i]); + } + return; + } + if (e.type === "binary") { + const b = expr as unknown as BinaryNode; + this.walkExprNonEscaping(b.left); + this.walkExprNonEscaping(b.right); + return; + } + if (e.type === "unary") { + const u = expr as unknown as UnaryNode; + this.walkExprNonEscaping(u.operand); + return; + } + if (e.type === "member_access") { + const m = expr as unknown as MemberAccessNode; + if (isEscaping) { + this.walkExprEscaping(m.object); + } else { + this.walkExprNonEscaping(m.object); + } + return; + } + if (e.type === "index_access") { + const i = expr as unknown as IndexAccessNode; + if (isEscaping) { + this.walkExprEscaping(i.object); + } else { + this.walkExprNonEscaping(i.object); + } + this.walkExprNonEscaping(i.index); + return; + } + if (e.type === "array") { + const a = expr as unknown as ArrayNode; + for (let i = 0; i < a.elements.length; i++) { + this.walkExprEscaping(a.elements[i]); + } + return; + } + if (e.type === "object") { + const o = expr as unknown as ObjectNode; + for (let i = 0; i < o.properties.length; i++) { + const prop = o.properties[i] as { key: string; value: Expression }; + this.walkExprEscaping(prop.value); + } + return; + } + if (e.type === "arrow_function") { + const a = expr as unknown as ArrowFunctionNode; + if ((a.body as ExprBase).type === "block") { + const block = a.body as unknown as BlockStatement; + this.scanBlockForArrowEscapes(block); + } else { + this.scanArrowExprForEscapes(a.body as Expression); + } + return; + } + if (e.type === "template_literal") { + const t = expr as unknown as TemplateLiteralNode; + for (let i = 0; i < t.parts.length; i++) { + const part = t.parts[i]; + if (typeof part !== "string") { + this.walkExprNonEscaping(part as Expression); + } + } + return; + } + if (e.type === "conditional") { + const c = expr as unknown as ConditionalExpressionNode; + this.walkExprNonEscaping(c.condition); + if (isEscaping) { + this.walkExprEscaping(c.consequent); + this.walkExprEscaping(c.alternate); + } else { + this.walkExprNonEscaping(c.consequent); + this.walkExprNonEscaping(c.alternate); + } + return; + } + if (e.type === "await") { + const a = expr as unknown as AwaitExpressionNode; + this.walkExprEscaping(a.argument); + return; + } + if (e.type === "member_access_assignment") { + const m = expr as unknown as MemberAccessAssignmentNode; + if (isEscaping) { + this.walkExprEscaping(m.object); + } else { + this.walkExprNonEscaping(m.object); + } + this.walkExprEscaping(m.value); + return; + } + if (e.type === "index_access_assignment") { + const i = expr as unknown as IndexAccessAssignmentNode; + if (isEscaping) { + this.walkExprEscaping(i.object); + } else { + this.walkExprNonEscaping(i.object); + } + this.walkExprNonEscaping(i.index); + this.walkExprEscaping(i.value); + return; + } + if (e.type === "type_assertion") { + const t = expr as unknown as TypeAssertionNode; + if (isEscaping) { + this.walkExprEscaping(t.expression); + } else { + this.walkExprNonEscaping(t.expression); + } + return; + } + if (e.type === "spread_element") { + const s = expr as unknown as { type: string; argument: Expression }; + this.walkExprEscaping(s.argument); + return; + } + } + + private scanBlockForArrowEscapes(block: BlockStatement): void { + for (let i = 0; i < block.statements.length; i++) { + this.scanArrowStmtForEscapes(block.statements[i]); + } + } + + private scanArrowStmtForEscapes(stmt: Statement): void { + const s = stmt as StmtBase; + if (s.type === "return") { + const r = stmt as unknown as ReturnStatement; + if (r.value) this.walkExprEscaping(r.value); + } else if (s.type === "variable_declaration") { + const d = stmt as unknown as VariableDeclaration; + if (d.value) this.walkExprEscaping(d.value); + } else if (s.type === "assignment") { + const a = stmt as unknown as AssignmentStatement; + this.walkExprEscaping(a.value); + } else if (s.type === "if") { + const i = stmt as unknown as IfStatement; + this.walkExprEscaping(i.condition); + this.scanBlockForArrowEscapes(i.thenBlock); + if (i.elseBlock) this.scanBlockForArrowEscapes(i.elseBlock); + } else if (s.type === "while" || s.type === "do_while") { + const w = stmt as unknown as WhileStatement; + this.walkExprEscaping(w.condition); + this.scanBlockForArrowEscapes(w.body); + } else if (s.type === "for") { + const f = stmt as unknown as ForStatement; + if (f.init) this.scanArrowStmtForEscapes(f.init as Statement); + if (f.condition) this.walkExprEscaping(f.condition); + if (f.update) { + const upd = f.update as ExprBase; + if (upd.type === "assignment") { + this.scanArrowStmtForEscapes(f.update as Statement); + } else { + this.walkExprEscaping(f.update as Expression); + } + } + this.scanBlockForArrowEscapes(f.body); + } else if (s.type === "for_of") { + const f = stmt as unknown as ForOfStatement; + this.walkExprEscaping(f.iterable); + this.scanBlockForArrowEscapes(f.body); + } else if (s.type === "try") { + const t = stmt as unknown as TryStatement; + this.scanBlockForArrowEscapes(t.tryBlock); + if (t.catchBody) this.scanBlockForArrowEscapes(t.catchBody); + if (t.finallyBlock) this.scanBlockForArrowEscapes(t.finallyBlock); + } else if (s.type === "switch") { + const sw = stmt as unknown as SwitchStatement; + this.walkExprEscaping(sw.discriminant); + for (let i = 0; i < sw.cases.length; i++) { + const c = sw.cases[i] as { test: Expression | null; consequent: Statement[] }; + if (c.test) this.walkExprEscaping(c.test); + for (let j = 0; j < c.consequent.length; j++) { + this.scanArrowStmtForEscapes(c.consequent[j]); + } + } + } else if (s.type === "throw") { + const t = stmt as unknown as ThrowStatement; + this.walkExprEscaping(t.argument); + } else if (s.type === "block") { + const b = stmt as unknown as BlockStatement; + this.scanBlockForArrowEscapes(b); + } else { + this.scanArrowExprForEscapes(stmt as unknown as Expression); + } + } + + private scanArrowExprForEscapes(expr: Expression): void { + this.walkExprEscaping(expr); + } +}