Space Lua: Add <const> attribute (#1715)
* Space Lua: Implement <const> attribute Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Space Lua: Implement <const> attribute - Add Lua attributes with parse time validation - Add `<const>` attribute semantic tests Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> --------- Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz>
This commit is contained in:
@@ -139,10 +139,15 @@ export type LuaLocalStatement = {
|
||||
expressions?: LuaExpression[];
|
||||
} & ASTContext;
|
||||
|
||||
export enum LuaAttribute {
|
||||
Const = "const",
|
||||
}
|
||||
|
||||
export type LuaAttName = {
|
||||
type: "AttName";
|
||||
name: string;
|
||||
attribute?: string;
|
||||
attributes?: LuaAttribute[];
|
||||
} & ASTContext;
|
||||
|
||||
export type LuaFunctionCallStatement = {
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
import {
|
||||
assertEquals,
|
||||
assertInstanceOf,
|
||||
assertStringIncludes,
|
||||
} from "@std/assert";
|
||||
import { parse } from "./parse.ts";
|
||||
import { evalStatement } from "./eval.ts";
|
||||
import { LuaEnv, LuaRuntimeError, LuaStackFrame } from "./runtime.ts";
|
||||
import { luaBuildStandardEnv } from "./stdlib.ts";
|
||||
|
||||
async function evalBlock(code: string, env?: LuaEnv): Promise<LuaEnv> {
|
||||
const ast = parse(code);
|
||||
const G = luaBuildStandardEnv();
|
||||
const base = env ?? new LuaEnv(G);
|
||||
const sf = LuaStackFrame.createWithGlobalEnv(G, ast.ctx);
|
||||
const r = evalStatement(ast, base, sf, false);
|
||||
|
||||
if (r instanceof Promise) {
|
||||
await r;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
||||
async function runAndCatch(code: string, ref = "const_attribute.lua") {
|
||||
const ast = parse(code, { ref });
|
||||
const G = luaBuildStandardEnv();
|
||||
const env = new LuaEnv(G);
|
||||
const sf = LuaStackFrame.createWithGlobalEnv(G, ast.ctx);
|
||||
|
||||
try {
|
||||
const r = evalStatement(ast, env, sf, false);
|
||||
if (r instanceof Promise) {
|
||||
await r;
|
||||
}
|
||||
throw new Error("Expected LuaRuntimeError but evaluation succeeded");
|
||||
} catch (e: unknown) {
|
||||
return { e, code, ref };
|
||||
}
|
||||
}
|
||||
|
||||
Deno.test("const: Unknown attribute (parse-time)", () => {
|
||||
let threw = false;
|
||||
try {
|
||||
parse(`local x<close> = 1`, { ref: "unknown_attribute.lua" });
|
||||
} catch (e: any) {
|
||||
threw = true;
|
||||
assertStringIncludes(String(e?.message ?? e), "unknown attribute 'close'");
|
||||
}
|
||||
if (!threw) {
|
||||
throw new Error("Expected parse error for unknown attribute");
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("const: Case-sensitive attribute (parse-time)", () => {
|
||||
let threw = false;
|
||||
try {
|
||||
parse(`local x<Const> = 1`, { ref: "unknown_attribute.lua" });
|
||||
} catch (e: any) {
|
||||
threw = true;
|
||||
assertStringIncludes(String(e?.message ?? e), "unknown attribute 'Const'");
|
||||
}
|
||||
if (!threw) {
|
||||
throw new Error("Expected parse error for unknown attribute");
|
||||
}
|
||||
});
|
||||
Deno.test("const: Requires initializer", async () => {
|
||||
const { e } = await runAndCatch(`
|
||||
local x <const>
|
||||
`);
|
||||
assertInstanceOf(e, LuaRuntimeError);
|
||||
assertStringIncludes((e as LuaRuntimeError).message, "must be initialized");
|
||||
});
|
||||
|
||||
Deno.test("const: Initialization ok, reassignment error", async () => {
|
||||
const env = await evalBlock(`
|
||||
local x<const> = 1
|
||||
y = x
|
||||
`);
|
||||
assertEquals(env.get("y"), 1);
|
||||
|
||||
const { e } = await runAndCatch(`
|
||||
local x<const> = 1
|
||||
x = 2
|
||||
`);
|
||||
assertInstanceOf(e, LuaRuntimeError);
|
||||
assertStringIncludes(
|
||||
(e as LuaRuntimeError).message,
|
||||
"attempt to assign to const variable 'x'",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("const: Initialization to nil ok, reassignment error", async () => {
|
||||
const { e } = await runAndCatch(`
|
||||
local x <const> = nil
|
||||
x = 2
|
||||
`);
|
||||
assertInstanceOf(e, LuaRuntimeError);
|
||||
assertStringIncludes(
|
||||
(e as LuaRuntimeError).message,
|
||||
"attempt to assign to const variable 'x'",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("const: Multi-declaration and missing RHS", async () => {
|
||||
{
|
||||
const env = await evalBlock(`
|
||||
local a <const>, b = 1
|
||||
y1 = a
|
||||
y2 = b
|
||||
`);
|
||||
assertEquals(env.get("y1"), 1);
|
||||
assertEquals(env.get("y2"), null);
|
||||
}
|
||||
|
||||
{
|
||||
const { e } = await runAndCatch(`
|
||||
local a<const>, b <const>; -- missing assignment
|
||||
`);
|
||||
assertInstanceOf(e, LuaRuntimeError);
|
||||
assertStringIncludes((e as LuaRuntimeError).message, "must be initialized");
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("const: Multi-assignment after declaration fails", async () => {
|
||||
const { e } = await runAndCatch(`
|
||||
local a<const>, b = 1, 2
|
||||
a, b = 3, 4
|
||||
`);
|
||||
assertInstanceOf(e, LuaRuntimeError);
|
||||
assertStringIncludes(
|
||||
(e as LuaRuntimeError).message,
|
||||
"attempt to assign to const variable 'a'",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("const: Second position", async () => {
|
||||
const { e } = await runAndCatch(`
|
||||
local a, b<const> = 1, 2
|
||||
b = 3
|
||||
`);
|
||||
assertInstanceOf(e, LuaRuntimeError);
|
||||
assertStringIncludes(
|
||||
(e as LuaRuntimeError).message,
|
||||
"attempt to assign to const variable 'b'",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("const: Initialization from function returns", async () => {
|
||||
const env = await evalBlock(`
|
||||
local function f()
|
||||
return 7
|
||||
end
|
||||
|
||||
local a <const>, b <const> = f()
|
||||
|
||||
y1, y2 = a, b
|
||||
`);
|
||||
assertEquals(env.get("y1"), 7);
|
||||
assertEquals(env.get("y2"), null);
|
||||
|
||||
const { e } = await runAndCatch(`
|
||||
local function f()
|
||||
return 7
|
||||
end
|
||||
|
||||
local a<const>, b<const> = f()
|
||||
|
||||
a = 2
|
||||
`);
|
||||
assertInstanceOf(e, LuaRuntimeError);
|
||||
assertStringIncludes(
|
||||
(e as LuaRuntimeError).message,
|
||||
"attempt to assign to const variable 'a'",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("const: Shadowing", async () => {
|
||||
{
|
||||
const { e } = await runAndCatch(`
|
||||
local a<const> = 1
|
||||
|
||||
do
|
||||
local a<const> = 2
|
||||
a = 3
|
||||
end
|
||||
`);
|
||||
assertInstanceOf(e, LuaRuntimeError);
|
||||
assertStringIncludes(
|
||||
(e as LuaRuntimeError).message,
|
||||
"attempt to assign to const variable 'a'",
|
||||
);
|
||||
}
|
||||
|
||||
{
|
||||
const env = await evalBlock(`
|
||||
local a<const> = 1
|
||||
|
||||
do
|
||||
local a = 2
|
||||
a = 3
|
||||
end
|
||||
|
||||
y = a
|
||||
`);
|
||||
assertEquals(env.get("y"), 1);
|
||||
}
|
||||
});
|
||||
|
||||
Deno.test("const: Globals and fields not affected", async () => {
|
||||
const env = await evalBlock(`
|
||||
local a<const> = 1
|
||||
G = a
|
||||
|
||||
local T = {}
|
||||
T.x = a
|
||||
|
||||
y1, y2 = G, T.x
|
||||
`);
|
||||
assertEquals(env.get("y1"), 1);
|
||||
assertEquals(env.get("y2"), 1);
|
||||
});
|
||||
|
||||
Deno.test("const: Table binding vs table contents", async () => {
|
||||
const env = await evalBlock(`
|
||||
local t<const> = { a = 1, 2, 3 }
|
||||
|
||||
t.a = 10 -- mutate property
|
||||
t[2] = 20 -- mutate array part
|
||||
|
||||
y1, y2, y3 = t.a, t[2], #t
|
||||
`);
|
||||
assertEquals(env.get("y1"), 10);
|
||||
assertEquals(env.get("y2"), 20);
|
||||
assertEquals(env.get("y3"), 2);
|
||||
});
|
||||
|
||||
Deno.test("const: Rebinding table fails", async () => {
|
||||
const { e } = await runAndCatch(
|
||||
`
|
||||
local t<const> = {
|
||||
a = 1
|
||||
}
|
||||
|
||||
t = {
|
||||
b = 2
|
||||
}
|
||||
`,
|
||||
"const_table_rebind.lua",
|
||||
);
|
||||
assertInstanceOf(e, LuaRuntimeError);
|
||||
assertStringIncludes(
|
||||
(e as LuaRuntimeError).message,
|
||||
"attempt to assign to const variable 't'",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("const: Inner block reassignment", async () => {
|
||||
const { e } = await runAndCatch(`
|
||||
local a<const> = 1
|
||||
do
|
||||
a = 2 -- must fail
|
||||
end
|
||||
`);
|
||||
assertInstanceOf(e, LuaRuntimeError);
|
||||
assertStringIncludes(
|
||||
(e as LuaRuntimeError).message,
|
||||
"attempt to assign to const variable 'a'",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("const: Closure reassignment", async () => {
|
||||
const { e } = await runAndCatch(`
|
||||
local a<const> = 1
|
||||
|
||||
local function f()
|
||||
a = 2 -- must error
|
||||
end
|
||||
|
||||
f()
|
||||
`);
|
||||
assertInstanceOf(e, LuaRuntimeError);
|
||||
assertStringIncludes(
|
||||
(e as LuaRuntimeError).message,
|
||||
"attempt to assign to const variable 'a'",
|
||||
);
|
||||
});
|
||||
|
||||
Deno.test("const: Closure mutates table", async () => {
|
||||
const env = await evalBlock(`
|
||||
local t<const> = { x = 1 }
|
||||
|
||||
local function bump()
|
||||
t.x = t.x + 1
|
||||
end
|
||||
|
||||
bump()
|
||||
|
||||
y = t.x
|
||||
`);
|
||||
assertEquals(env.get("y"), 2);
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
LuaStatement,
|
||||
NumericType,
|
||||
} from "./ast.ts";
|
||||
import { LuaAttribute } from "./ast.ts";
|
||||
import { evalPromiseValues } from "./util.ts";
|
||||
import {
|
||||
getMetatable,
|
||||
@@ -1182,11 +1183,32 @@ export function evalStatement(
|
||||
}
|
||||
case "Local": {
|
||||
const l = asLocal(s);
|
||||
if (l.expressions) {
|
||||
const valuesRP = evalExpressions(l.expressions, env, sf);
|
||||
|
||||
const hasInit = Array.isArray(l.expressions) && l.expressions.length > 0;
|
||||
|
||||
for (const att of l.names) {
|
||||
const isConst = att.attributes?.includes(LuaAttribute.Const) === true;
|
||||
if (isConst && !hasInit) {
|
||||
throw new LuaRuntimeError(
|
||||
`const variable '${att.name}' must be initialized`,
|
||||
sf.withCtx(att.ctx),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (hasInit) {
|
||||
const valuesRP = evalExpressions(l.expressions!, env, sf);
|
||||
const setAll = (values: LuaValue[]) => {
|
||||
for (let i = 0; i < l.names.length; i++) {
|
||||
env.setLocal(l.names[i].name, values[i]);
|
||||
const name = l.names[i];
|
||||
const v = values[i];
|
||||
const isConst =
|
||||
name.attributes?.includes(LuaAttribute.Const) === true;
|
||||
if (isConst) {
|
||||
env.setLocalConst(name.name, v);
|
||||
} else {
|
||||
env.setLocal(name.name, v);
|
||||
}
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
LuaStatement,
|
||||
LuaTableField,
|
||||
} from "./ast.ts";
|
||||
import { LuaAttribute } from "./ast.ts";
|
||||
|
||||
const luaStyleTags = styleTags({
|
||||
Name: t.variableName,
|
||||
@@ -362,12 +363,17 @@ function parseAttName(t: ParseTree, ctx: ASTCtx): LuaAttName {
|
||||
if (t.type !== "AttName") {
|
||||
throw new Error(`Expected AttName, got ${t.type}`);
|
||||
}
|
||||
const attribute = t.children![1].children![1]
|
||||
? t.children![1].children![1].children![0].text!
|
||||
: undefined;
|
||||
if (attribute && attribute !== LuaAttribute.Const) {
|
||||
throw new Error(`unknown attribute '${attribute}'`);
|
||||
}
|
||||
return {
|
||||
type: "AttName",
|
||||
name: t.children![0].children![0].text!,
|
||||
attribute: t.children![1].children![1]
|
||||
? t.children![1].children![1].children![0].text!
|
||||
: undefined,
|
||||
attribute,
|
||||
attributes: attribute ? [LuaAttribute.Const] : undefined,
|
||||
ctx: context(t, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ const EMPTY_CTX = {} as ASTCtx;
|
||||
|
||||
export class LuaEnv implements ILuaSettable, ILuaGettable {
|
||||
variables = new Map<string, LuaValue>();
|
||||
private readonly consts = new Set<string>();
|
||||
|
||||
constructor(readonly parent?: LuaEnv) {
|
||||
}
|
||||
@@ -67,8 +68,19 @@ export class LuaEnv implements ILuaSettable, ILuaGettable {
|
||||
this.variables.set(name, value);
|
||||
}
|
||||
|
||||
setLocalConst(name: string, value: LuaValue) {
|
||||
this.variables.set(name, value);
|
||||
this.consts.add(name);
|
||||
}
|
||||
|
||||
set(key: string, value: LuaValue, sf?: LuaStackFrame): void {
|
||||
if (this.variables.has(key) || !this.parent) {
|
||||
if (this.consts.has(key)) {
|
||||
throw new LuaRuntimeError(
|
||||
`attempt to assign to const variable '${key}'`,
|
||||
sf || LuaStackFrame.lostFrame,
|
||||
);
|
||||
}
|
||||
this.variables.set(key, value);
|
||||
} else {
|
||||
this.parent.set(key, value, sf);
|
||||
|
||||
Reference in New Issue
Block a user