* Space Lua: Add AST block type label/goto and duplicated label bools * Space Lua: Add label/goto resolution and evaluation Add per block map of labels metadata, per function block metadata cache and `processBlock` to track AST blocks (`If`, `While`, `Repeat`, `For` and `ForIn`, and `Do`) while recording labels and their positions. Align with Lua scoping rules (_e.g._, a `goto` can not jump into the scope of a local). Also allow the super-special "safe" end of block cases (except inside `Repeat`). Duplicated labels are detected (error `label 'foo' is already defined` etc.). The AST context for error reporting is preserved. The `WeakMap` caches map any block to its function's metadata. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: The `goto` parse flags and eval path At parser mark each block with `hasLabel`/`hasGoto` and record the first duplicated label. Flags then propagate through blocks and control flow. If no `goto`, run the block in a simple loop and raise duplicate label error if any is present. If `goto` exists, jump via a label map. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Add labels and `goto` statement test suite Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Minor `goto`/labels improvement and test hotfix - The `getBlockGotoMeta` now called only when a block has either `hasGoto` or `hasLabel`. - Add `BlocksFlags` type to get rid of some `(... as any)`s by view. - Add `blockMetaOrThrow` helper. - Fix test to reflect standard Lua semantics. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Goto: Add missing Lua semantics checks Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Goto: Add Lua semantics checks also covered in Lua tests Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Goto: Fix semantic divergence revealed by fixing unfinished test Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix typo Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Optimize block env creation and cache goto metadata Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Avoid using the venomous snake case Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Add comment for WeakMap use Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Block Label Fast-path Optimization Add block flag `hasLabelHere` during parsing and use it in eval to skip expensive metadata generation for blocks and entire functions without label or goto. Refactor eval logic: - early fast path when a function is known to have no label/goto, - later fast path for blocks in functions with goto. Fetch metadata only when needed. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Remove `BlockFlags` indirection from eval - Add `hasLabelHere` flag to `LuaBlock`. - Remove `BlockFlags` indirection from eval. - Set `hasLabelHere` directly. - Add some "guards". - Improve consistency of child filtering. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Remove redundant return statement Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> --------- Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
404 lines
9.3 KiB
TypeScript
404 lines
9.3 KiB
TypeScript
// Goto/label resolution and validation for function bodies
|
|
|
|
import type {
|
|
ASTCtx,
|
|
LuaBlock,
|
|
LuaForInStatement,
|
|
LuaForStatement,
|
|
LuaGotoStatement,
|
|
LuaIfStatement,
|
|
LuaLabelStatement,
|
|
LuaLocalStatement,
|
|
LuaRepeatStatement,
|
|
LuaWhileStatement,
|
|
} from "./ast.ts";
|
|
import { asBlock } from "./ast_narrow.ts";
|
|
|
|
type BlockGotoMeta = {
|
|
labels: Map<string, number>; // in this block
|
|
};
|
|
|
|
type FunctionMeta = {
|
|
// Stores per-block data and drops entries automatically when a block
|
|
// is no longer used.
|
|
blockMeta: WeakMap<LuaBlock, BlockGotoMeta>;
|
|
funcHasGotos: boolean;
|
|
};
|
|
|
|
class LabelResolveError extends Error {
|
|
constructor(msg: string, public astCtx: ASTCtx) {
|
|
super(msg);
|
|
}
|
|
}
|
|
|
|
// Cache
|
|
const functionMetaByRoot = new WeakMap<LuaBlock, FunctionMeta>();
|
|
const functionMetaByAnyBlock = new WeakMap<LuaBlock, FunctionMeta>();
|
|
|
|
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<LocalID>;
|
|
block: LuaBlock;
|
|
};
|
|
|
|
type ValidationCtx = {
|
|
labelActiveByBlock: WeakMap<LuaBlock, Map<string, Set<LocalID>>>;
|
|
labelLocByBlock: WeakMap<
|
|
LuaBlock,
|
|
Map<string, { index: number; ctx: ASTCtx }>
|
|
>;
|
|
gotos: GotoInfo[];
|
|
hasGoto: boolean;
|
|
nextLocalId: number;
|
|
};
|
|
|
|
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<LuaBlock, BlockGotoMeta>();
|
|
const vctx: ValidationCtx = {
|
|
labelActiveByBlock: new WeakMap(),
|
|
labelLocByBlock: new WeakMap(),
|
|
gotos: [],
|
|
hasGoto: false,
|
|
nextLocalId: 1,
|
|
};
|
|
|
|
const seenBlocks = new Set<LuaBlock>();
|
|
const parentByBlock = new WeakMap<LuaBlock, LuaBlock | undefined>();
|
|
const roleByBlock = new WeakMap<LuaBlock, BlockRole>();
|
|
|
|
processBlock(
|
|
root,
|
|
undefined,
|
|
"Root",
|
|
new Set<LocalID>(),
|
|
new Set<string>(),
|
|
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 && 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;
|
|
for (const id of lset) {
|
|
if (!g.active.has(id)) {
|
|
entersLocalScope = true;
|
|
break;
|
|
}
|
|
}
|
|
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<LuaBlock, BlockRole>,
|
|
): 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<T>(s: Set<T>): Set<T> {
|
|
const c = new Set<T>();
|
|
for (const v of s) {
|
|
c.add(v);
|
|
}
|
|
return c;
|
|
}
|
|
|
|
function processBlock(
|
|
block: LuaBlock,
|
|
parent: LuaBlock | undefined,
|
|
role: BlockRole,
|
|
active: Set<LocalID>,
|
|
visibleLabels: Set<string>,
|
|
blockMeta: WeakMap<LuaBlock, BlockGotoMeta>,
|
|
vctx: ValidationCtx,
|
|
seen: Set<LuaBlock>,
|
|
parentByBlock: WeakMap<LuaBlock, LuaBlock | undefined>,
|
|
roleByBlock: WeakMap<LuaBlock, BlockRole>,
|
|
): void {
|
|
const labels = new Map<string, number>();
|
|
blockMeta.set(block, { labels });
|
|
|
|
seen.add(block);
|
|
parentByBlock.set(block, parent);
|
|
roleByBlock.set(block, role);
|
|
|
|
const labelActiveMap = new Map<string, Set<LocalID>>();
|
|
const labelLocMap = new Map<string, { index: number; ctx: ASTCtx }>();
|
|
vctx.labelActiveByBlock.set(block, labelActiveMap);
|
|
vctx.labelLocByBlock.set(block, labelLocMap);
|
|
|
|
const curActive = cloneSet(active);
|
|
const vis = new Set<string>(visibleLabels);
|
|
|
|
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 });
|
|
vis.add(lab.name);
|
|
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++) {
|
|
curActive.add(vctx.nextLocalId++);
|
|
}
|
|
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,
|
|
new Set<string>(vis),
|
|
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,
|
|
new Set<string>(vis),
|
|
blockMeta,
|
|
vctx,
|
|
seen,
|
|
parentByBlock,
|
|
roleByBlock,
|
|
);
|
|
break;
|
|
}
|
|
case "While": {
|
|
const w = s as LuaWhileStatement;
|
|
processBlock(
|
|
w.block,
|
|
block,
|
|
"While",
|
|
cloneSet(curActive),
|
|
new Set<string>(vis),
|
|
blockMeta,
|
|
vctx,
|
|
seen,
|
|
parentByBlock,
|
|
roleByBlock,
|
|
);
|
|
break;
|
|
}
|
|
case "Repeat": {
|
|
const r = s as LuaRepeatStatement;
|
|
processBlock(
|
|
r.block,
|
|
block,
|
|
"Repeat",
|
|
cloneSet(curActive),
|
|
new Set<string>(vis),
|
|
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),
|
|
new Set<string>(vis),
|
|
blockMeta,
|
|
vctx,
|
|
seen,
|
|
parentByBlock,
|
|
roleByBlock,
|
|
);
|
|
}
|
|
if (iff.elseBlock) {
|
|
processBlock(
|
|
iff.elseBlock,
|
|
block,
|
|
"If",
|
|
cloneSet(curActive),
|
|
new Set<string>(vis),
|
|
blockMeta,
|
|
vctx,
|
|
seen,
|
|
parentByBlock,
|
|
roleByBlock,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case "Block": {
|
|
const child = asBlock(s);
|
|
processBlock(
|
|
child,
|
|
block,
|
|
"Do",
|
|
cloneSet(curActive),
|
|
new Set<string>(vis),
|
|
blockMeta,
|
|
vctx,
|
|
seen,
|
|
parentByBlock,
|
|
roleByBlock,
|
|
);
|
|
break;
|
|
}
|
|
default: {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|