// Goto/label resolution and validation for function bodies import type { ASTCtx, LuaBlock, LuaForInStatement, LuaForStatement, LuaGotoStatement, LuaIfStatement, LuaLabelStatement, LuaLocalStatement, LuaRepeatStatement, LuaWhileStatement, } from "./ast.ts"; import { LuaAttribute } from "./ast.ts"; import { asBlock } from "./ast_narrow.ts"; type BlockGotoMeta = { labels: Map; // in this block }; type FunctionMeta = { // Stores per-block data and drops entries automatically when a block // is no longer used. blockMeta: WeakMap; funcHasGotos: boolean; }; class LabelResolveError extends Error { constructor( msg: string, public astCtx: ASTCtx, ) { super(msg); } } // Cache const functionMetaByRoot = new WeakMap(); const functionMetaByAnyBlock = new WeakMap(); export function getBlockGotoMeta( block: LuaBlock, ): (BlockGotoMeta & { funcHasGotos: boolean }) | undefined { let fm = functionMetaByAnyBlock.get(block); if (!fm) { fm = resolveFunction(block); } const bm = fm.blockMeta.get(block); if (!bm) { return undefined; } return { ...bm, funcHasGotos: fm.funcHasGotos }; } type LocalID = number; type GotoInfo = { node: LuaGotoStatement; active: Set; block: LuaBlock; }; type ValidationCtx = { labelActiveByBlock: WeakMap>>; labelLocByBlock: WeakMap< LuaBlock, Map >; gotos: GotoInfo[]; hasGoto: boolean; nextLocalId: number; closeLocals: Set; }; type BlockRole = "Root" | "Do" | "If" | "While" | "Repeat" | "For" | "ForIn"; function resolveFunction(root: LuaBlock): FunctionMeta { const existing = functionMetaByRoot.get(root); if (existing) { return existing; } const blockMeta = new WeakMap(); const vctx: ValidationCtx = { labelActiveByBlock: new WeakMap(), labelLocByBlock: new WeakMap(), gotos: [], hasGoto: false, nextLocalId: 1, closeLocals: new Set(), }; const seenBlocks = new Set(); const parentByBlock = new WeakMap(); const roleByBlock = new WeakMap(); processBlock( root, undefined, "Root", new Set(), blockMeta, vctx, seenBlocks, parentByBlock, roleByBlock, ); // Validate gotos for (const g of vctx.gotos) { const target = g.node.name; // Search current block for the label, then ancestors let searchBlock: LuaBlock | undefined = g.block; let labelIndex: number | undefined; let labelDefBlock: LuaBlock | undefined; while (searchBlock) { const meta = blockMeta.get(searchBlock); if (meta?.labels.has(target)) { labelIndex = meta.labels.get(target); labelDefBlock = searchBlock; break; } searchBlock = parentByBlock.get(searchBlock); } if (labelIndex === undefined || !labelDefBlock) { throw new LabelResolveError( `no visible label '${target}' for goto`, g.node.ctx, ); } const activeMap = vctx.labelActiveByBlock.get(labelDefBlock); const locMap = vctx.labelLocByBlock.get(labelDefBlock); if (!activeMap || !locMap) { throw new LabelResolveError( `no visible label '${target}' for goto`, g.node.ctx, ); } const lset = activeMap.get(target); const lloc = locMap.get(target); if (!lset || !lloc) { throw new LabelResolveError( `no visible label '${target}' for goto`, g.node.ctx, ); } // Local scope forward jump check let entersLocalScope = false; let entersCloseScope = false; for (const id of lset) { if (!g.active.has(id)) { entersLocalScope = true; if (vctx.closeLocals.has(id)) { entersCloseScope = true; break; } } } if (entersCloseScope) { throw new LabelResolveError( `goto '${target}' jumps into the scope of a local variable`, g.node.ctx, ); } if (entersLocalScope) { const safeEnd = isSafeEndLabel(labelDefBlock, lloc.index, roleByBlock); if (!safeEnd) { throw new LabelResolveError( `goto '${target}' jumps into the scope of a local variable`, g.node.ctx, ); } } } const fm: FunctionMeta = { blockMeta, funcHasGotos: vctx.hasGoto, }; functionMetaByRoot.set(root, fm); for (const block of seenBlocks) { functionMetaByAnyBlock.set(block, fm); } return fm; } function isSafeEndLabel( block: LuaBlock, labelIndex: number, roleByBlock: WeakMap, ): boolean { const role = roleByBlock.get(block); if (role === "Repeat") { return false; } for (let i = labelIndex + 1; i < block.statements.length; i++) { const t = block.statements[i].type; if (t !== "Label" && t !== "Semicolon") { return false; } } return true; } function cloneSet(s: Set): Set { const c = new Set(); for (const v of s) { c.add(v); } return c; } function processBlock( block: LuaBlock, parent: LuaBlock | undefined, role: BlockRole, active: Set, blockMeta: WeakMap, vctx: ValidationCtx, seen: Set, parentByBlock: WeakMap, roleByBlock: WeakMap, ): void { const labels = new Map(); blockMeta.set(block, { labels }); seen.add(block); parentByBlock.set(block, parent); roleByBlock.set(block, role); const labelActiveMap = new Map>(); const labelLocMap = new Map(); vctx.labelActiveByBlock.set(block, labelActiveMap); vctx.labelLocByBlock.set(block, labelLocMap); const curActive = cloneSet(active); const stmts = block.statements; for (let i = 0; i < stmts.length; i++) { const s = stmts[i]; switch (s.type) { case "Label": { const lab = s as LuaLabelStatement; if (labels.has(lab.name)) { throw new LabelResolveError( `label '${lab.name}' already defined`, lab.ctx, ); } labels.set(lab.name, i); const actSet = cloneSet(curActive); labelActiveMap.set(lab.name, actSet); labelLocMap.set(lab.name, { index: i, ctx: lab.ctx }); break; } case "Goto": { const g = s as LuaGotoStatement; vctx.hasGoto = true; vctx.gotos.push({ node: g, active: cloneSet(curActive), block }); break; } case "Local": { const l = s as LuaLocalStatement; for (let j = 0; j < l.names.length; j++) { const id = vctx.nextLocalId++; curActive.add(id); const isClose = l.names[j].attributes?.includes(LuaAttribute.Close) === true; if (isClose) { vctx.closeLocals.add(id); } } break; } case "LocalFunction": { curActive.add(vctx.nextLocalId++); break; } case "Function": { break; } case "For": { const fr = s as LuaForStatement; const childActive = cloneSet(curActive); childActive.add(vctx.nextLocalId++); processBlock( fr.block, block, "For", childActive, blockMeta, vctx, seen, parentByBlock, roleByBlock, ); break; } case "ForIn": { const fi = s as LuaForInStatement; const childActive = cloneSet(curActive); for (let j = 0; j < fi.names.length; j++) { childActive.add(vctx.nextLocalId++); } processBlock( fi.block, block, "ForIn", childActive, blockMeta, vctx, seen, parentByBlock, roleByBlock, ); break; } case "While": { const w = s as LuaWhileStatement; processBlock( w.block, block, "While", cloneSet(curActive), blockMeta, vctx, seen, parentByBlock, roleByBlock, ); break; } case "Repeat": { const r = s as LuaRepeatStatement; processBlock( r.block, block, "Repeat", cloneSet(curActive), blockMeta, vctx, seen, parentByBlock, roleByBlock, ); break; } case "If": { const iff = s as LuaIfStatement; for (let k = 0; k < iff.conditions.length; k++) { processBlock( iff.conditions[k].block, block, "If", cloneSet(curActive), blockMeta, vctx, seen, parentByBlock, roleByBlock, ); } if (iff.elseBlock) { processBlock( iff.elseBlock, block, "If", cloneSet(curActive), blockMeta, vctx, seen, parentByBlock, roleByBlock, ); } break; } case "Block": { const child = asBlock(s); processBlock( child, block, "Do", cloneSet(curActive), blockMeta, vctx, seen, parentByBlock, roleByBlock, ); break; } default: { break; } } } }