* Don't know what's going on with the Deno docker image, disabling the deno.lock file for now * Another try * Space Lua: Align numeric and table semantics with Lua This change improves Space Lua compatibility with standard Lua 5.4, focusing on numeric subtypes and table behavior. The test suite is extended to lock in the expected semantics and should pass under both Space Lua and a Lua interpreter. SUMMARY OF CHANGES ------------------ Tighten Lua compatibility across evaluator and runtime: - correct metamethod dispatch (`__index`, `__newindex`, `__call`, comparison metamethods), - loop limits, - raw metamethod lookups. Rework numeric semantics to preserve Lua **integer** vs **float** behavior: - $0$ vs $0.0$ and $-0.0$, - explicit zero kind representation, and - updates arithmetic/bitwise coercions accordingly. Improve parser correctness by rejecting **unary plus** with aligned Lua errors and better parsing errors reporting. Fix `stdlib` behavior to match that of Lua: - `table` function `concat`, `insert`, `remove`, `sort` and `unpack` gain metamethod awareness and enforce Lua errors, - `ipairs` iteration updated to stop on first nil and honor `__index`, - `tonumber` updated to Luae conversion using `luaToNumberDetailed`, - `math.modf` return corrected, - `math.type` accuracy improvements for float/integer and $-0.0$, - `math.pi` added. Fix numeric subtypes for `/` and `^` operators so `math.type` matches Lua results using tagging as well as unary `-`. Expand test coverage: - new `metamethods_test.lua` for Lua metamethod/operator semantics, - extend arithmetic and length tests for zero-kind propagation and `rawlen` vs `__len` metamethod, - Extend `math` test suite to test proper Lua alignment (`math.type` and more), and - update context error expectations for Lua error messages. RATIONALE --------- Lua differs from JavaScript by having two numeric subtypes: **integer** and **float**. Operators depend on the subtype: `+`, `-`, `*`, `//` and `%` use integer mode when both operands are integers and float mode otherwise. Bitwise operators require integers and `math.type(x)` reports "integer" or "float". JavaScript has one numeric primitive type (`number`) so a plain number value cannot record whether Lua considers a value to be a float when the value has no fractional part (for example $2.0$). Lua also differs from "everything is IEEE 754 double" because the rules are defined in terms of integer and float subtypes. Float operations preserve IEEE 754 behavior including `NaN`, infinities and signed zero ($-0.0$) which affects results like $1/0.0$ versus $1/-0.0$. Integer arithmetic does not preserve $-0$ and collapses it to $0$. Lua integer arithmetic is exact within its integer range while JavaScript `number` cannot exactly represent all integers in that range. Lua numbers are integers or floats. Numeric strings coerce to integer or float based on _lexical_ form. Each arithmetic operator selects the result subtype from the operator rules and operand subtypes. Integer only operators (bitwise and `//` as integer division) require integer representability. Mixed arithmetic promotes to float as needed. Two operators are **always float** typed: division (`/`) and exponentiation (`^`) produce floats even if both operands are integers and even if the numeric value has no fractional part. `math.type` reports that internal subtype. Tables are associative arrays and assigning `nil` removes a key. The length operator `#` uses `__len` metamethod if present otherwise it uses the raw length rule. `rawlen(table)` ignores `__len`. Without `__len` Lua defines `#` as some boundary `N` such that `table[N]` is not `nil` and `table[N+1]` is `nil`. If the table has holes (missing or `nil` entries in the positive integer key sequence) the boundary may be non unique so `#` is stable only for proper sequences without holes. PERFORMANCE NOTES ----------------- Numeric changes add small checks to preserve Lua integer and float subtype semantics and avoid allocations except when the subtype would otherwise be lost. Some table operations may be slower due to stricter Lua 5.4 behavior especially around length and sequence boundary handling which currently requires extra metadata tracking and scans and cannot be avoided without a completely different internal table representation. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Another try * Replace `LuaFloatTag` plain objects with boxed Number for float tagging * Restore pre-merge eval/numeric architecture and fix regressions This commit restores the original branch architecture. On top of the restored foundation, float-typed integer results (e.g. `1.0 + 1.0` = `2.0`, `0.0 // 1.0` = `0.0`) are now correctly tagged via `makeLuaFloat` so that `tostring` and `math.type` report them as floats. The *unary minus* fast path for float literals and the `tonumber` function also preserve float tagging. Performance regressions from the merge are addressed by avoiding `Number` boxing for non-integer floats (`3.14` needs no tag — it is unambiguously float), adding *string key* fast paths in `LuaTable` `has`/`rawGet`/`rawSet` to skip numeric normalization for the dominant case, and inlining a `typeof` check in math standard library functions to avoid function call overhead on plain numbers. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Replace boxed `Number` float tagging with plain tagged objects * Replace `new Number()` boxing with plain tagged float objects for Lua float type tracking. * Integer-valued floats that need type disambiguation are now represented as `{ value: number, isFloat: true }` instead of boxed `Number` objects with a symbol property. * Pre-allocated singletons are used for positive and negative float zeros to avoid allocation entirely in common cases. * Updated all detection, unwrapping, and coercion paths across `numeric.ts`, `runtime.ts`, `eval.ts`, `stdlib.ts`, and `stdlib/` modules to use the new `isTaggedFloat` type guard. * Removed all `instanceof Number` checks. * Deleted the `FloatKind` symbol and eliminated redundant helpers `isLuaFloat`, `isFloatTag`, `getZeroBoxKind` and `toPlainNumber` that became dead code. * Simplified `math.type`, `luaToString`, `luaEquals`, `luaTypeName` and various other key normalization paths. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Add fast paths in `coerceNumericPair` for tagged float operands Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix copy/paste typo Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Avoid extra `LuaEnv` allocations in "For" and "ForIn" loops Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * [Space Lua] Reuse loop variable environment in `for` and `for-in` loops Numeric `for` and generic `for-in` loops allocated a fresh `LuaEnv` on every iteration to hold loop variables. But this is only necessary when a closure inside the loop body captures the loop variable. The optimization uses a two-level check computed at parse time. If no function definition exists in the loop's subtree, environment reuse is safe. When a function definition is present a deeper analysis walks the block to determine whether any function body references the loop variable names without them being shadowed by its own parameters. When a closure captures a loop variable the loop fall back to per-iteration allocation. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * [Space Lua] Format numbers using standard Lua rules Standard Lua formats floats via C `sprintf("%.14g")` (14 significant digits, scientific notation when shorter, exponent padded to 2 digits, and a guaranteed `.0` suffix for integer-valued floats). * Replace the old `luaFormatNumber` with JS `toPrecision(14)`-based implementation that reproduces this behavior. * Integrates it so that `${}` expressions in the UI also display numbers correctly. * Fixes tagged floats (`{ value, isFloat }`) were being stripped by `luaValueToJS` or matched as plain objects before reaching the number formatter. This caused `${}` expressions to render raw JS numbers. Examples: ``` - ${tostring(2^63)} - ${2^63} - ${(2^63)} ``` All of the the above examples show correct `9.2233720368548e+18` now. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * [Space Lua] Fix `string.format` for floats and tagged numbers Unwrap tagged floats before `printf`, handle `inf`/`-inf`/`-nan` in `formatDouble`, and fix `%g` producing `0e+00` for zero. Hopefuly it's enough to gain Lua formatting. Tests: ``` - ${string.format("%.14g", 0.0)} - `0` - ${string.format("%.14g", 1.0)} - `1` - ${string.format("%.14g", 1/3)} - `0.33333333333333` - ${string.format("%.14g", math.pi)} - `3.1415926535898` - ${string.format("%.14g", 1e-10)} - `1e-10` - ${string.format("%.14g", 1e18)} - `1e+18` - ${string.format("%.14g", 2^63)} - `9.2233720368548e+18` - ${string.format("%.14g", 2^53)} - `9.007199254741e+15` - ${string.format("%.14g", 1.7976931348623e+308)} - `1.7976931348623e+308` - ${string.format("%.14g", 5e-324)} - `4.9406564584125e-324` - ${string.format("%.14g", 0/0)} - `-nan` - ${string.format("%.14g", 1/0)} - `inf` - ${string.format("%.14g", -1/0)} - `-inf` ``` Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * [Space Lua] Add and integrate new `luaFormat` utility and test suite * Add `luaFormat` string formatting function compatible with Lua, and integrate it across the codebase as a replacement for prior formatting approaches. * Add extensive test suite. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Fix check Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * [Space Lua] Add `%a`/`%A` and `%q` format specifiers to `string.format` Implement hexadecimal floating-point (`%a`/`%A`) and quoted literal (`%q`) specifiers. * Add `%a`/`%A` as IEEE 754 double decomposition with full flag, width and precision support. * Add `%q` as producind valid Lua literals for strings, numbers, booleans and nil. * Use `Math.PI` for `math.pi` to preserve full double precision. * Remove Deno based test suite and replace it with native Lua test suite. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * [Space Lua] Add `%p` format specifier to `string.format` In standard Lua, `%p` formats the internal C heap address of a value, producing output like `0x55a3bc4e2f10`. It works on tables, functions, threads, strings, and userdata (GC-ed objects). For `nil`, booleans, and numbers it returns `(null)`. In Space Lua, there are no *raw memory addresses* since the runtime is JavaScript. Instead, `%p` assigns a *stable sequential integer* to each object via a `WeakMap`, formatted as a 14-digit zero-padded hex value. The key difference is that identifiers are deterministic and sequential rather than random-looking heap addresses: ```lua local t = {} print(string.format("identifier: %p", t)) -- 0x00000000000001 print(string.format("the same: %p", t)) -- 0x00000000000001 print(string.format("another: %p", {}) -- 0x00000000000002 ``` For strings, a regular `Map` is used so identical string content always produces the same identifier. Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Revert merge changes to the deno.json Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Replace `interface` with `type` Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Remove `has_math()` relict function test Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Refactor loop to map Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Remove `Deno.remove("deno.lock")` weirdness Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> * Refactor: Early return undefined in `astNumberKind` instead of assigning Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> --------- Signed-off-by: Matouš Jan Fialka <mjf@mjf.cz> Co-authored-by: Zef Hemel <zef@zef.me>
1523 lines
44 KiB
TypeScript
1523 lines
44 KiB
TypeScript
import { lezerToParseTree } from "../../client/markdown_parser/parse_tree.ts";
|
|
import type { SyntaxNode } from "@lezer/common";
|
|
import {
|
|
cleanTree,
|
|
type ParseTree,
|
|
} from "@silverbulletmd/silverbullet/lib/tree";
|
|
import { parser } from "./parse-lua.js";
|
|
import { styleTags, tags as t } from "@lezer/highlight";
|
|
import { indentNodeProp, LRLanguage } from "@codemirror/language";
|
|
import type {
|
|
ASTCtx,
|
|
LuaAttName,
|
|
LuaBlock,
|
|
LuaExpression,
|
|
LuaFunctionBody,
|
|
LuaFunctionCallExpression,
|
|
LuaFunctionCallStatement,
|
|
LuaFunctionName,
|
|
LuaIfStatement,
|
|
LuaLValue,
|
|
LuaOrderBy,
|
|
LuaPrefixExpression,
|
|
LuaQueryClause,
|
|
LuaStatement,
|
|
LuaTableField,
|
|
} from "./ast.ts";
|
|
import { LuaAttribute } from "./ast.ts";
|
|
import { getBlockGotoMeta } from "./labels.ts";
|
|
import { LuaRuntimeError, LuaStackFrame } from "./runtime.ts";
|
|
|
|
const luaStyleTags = styleTags({
|
|
Name: t.variableName,
|
|
LiteralString: t.string,
|
|
Number: t.number,
|
|
CompareOp: t.operator,
|
|
"true false": t.bool,
|
|
Comment: t.lineComment,
|
|
"return break goto do end while repeat until function local if then else elseif in for nil or and not query from where limit select order by desc":
|
|
t.keyword,
|
|
});
|
|
|
|
const customIndent = indentNodeProp.add({
|
|
"IfStatement FuncBody WhileStatement ForStatement TableConstructor": (
|
|
context,
|
|
) => {
|
|
return context.lineIndent(context.node.from) + context.unit;
|
|
},
|
|
});
|
|
|
|
// Use the customIndent in your language support
|
|
export const luaLanguage = LRLanguage.define({
|
|
name: "space-lua",
|
|
parser: parser.configure({
|
|
props: [
|
|
luaStyleTags,
|
|
customIndent,
|
|
],
|
|
}),
|
|
languageData: {
|
|
commentTokens: { line: "--", block: { open: "--[[", close: "--]]" } },
|
|
},
|
|
});
|
|
|
|
function context(t: ParseTree, ctx: Record<string, any>): ASTCtx {
|
|
return { ...ctx, from: t.from, to: t.to };
|
|
}
|
|
|
|
function parseChunk(t: ParseTree, ctx: ASTCtx): LuaBlock {
|
|
if (t.type !== "Chunk") {
|
|
throw new Error(`Expected Chunk, got ${t.type}`);
|
|
}
|
|
return parseBlock(t.children![0], ctx);
|
|
}
|
|
|
|
function hasCloseLocal(names: LuaAttName[] | undefined): boolean {
|
|
if (!names) {
|
|
return false;
|
|
}
|
|
for (const n of names) {
|
|
if (n.attributes?.includes(LuaAttribute.Close) === true) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function expressionHasFunctionDef(e: LuaExpression): boolean {
|
|
if (!e) return false;
|
|
switch (e.type) {
|
|
case "FunctionDefinition":
|
|
return true;
|
|
case "FunctionCall":
|
|
if (expressionHasFunctionDef(e.prefix)) return true;
|
|
for (let i = 0; i < e.args.length; i++) {
|
|
if (expressionHasFunctionDef(e.args[i])) return true;
|
|
}
|
|
return false;
|
|
case "Binary":
|
|
return expressionHasFunctionDef(e.left) ||
|
|
expressionHasFunctionDef(e.right);
|
|
case "Unary":
|
|
return expressionHasFunctionDef(e.argument);
|
|
case "Parenthesized":
|
|
return expressionHasFunctionDef(e.expression);
|
|
case "TableConstructor":
|
|
for (let i = 0; i < e.fields.length; i++) {
|
|
const f = e.fields[i];
|
|
switch (f.type) {
|
|
case "DynamicField":
|
|
if (expressionHasFunctionDef(f.key)) return true;
|
|
if (expressionHasFunctionDef(f.value)) return true;
|
|
break;
|
|
case "PropField":
|
|
case "ExpressionField":
|
|
if (expressionHasFunctionDef(f.value)) return true;
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
case "TableAccess":
|
|
return expressionHasFunctionDef(e.object) ||
|
|
expressionHasFunctionDef(e.key);
|
|
case "PropertyAccess":
|
|
return expressionHasFunctionDef(e.object);
|
|
case "Query":
|
|
for (let i = 0; i < e.clauses.length; i++) {
|
|
const c = e.clauses[i];
|
|
switch (c.type) {
|
|
case "From":
|
|
if (expressionHasFunctionDef(c.expression)) return true;
|
|
break;
|
|
case "Where":
|
|
case "Select":
|
|
if (expressionHasFunctionDef(c.expression)) return true;
|
|
break;
|
|
case "Limit":
|
|
if (expressionHasFunctionDef(c.limit)) return true;
|
|
if (c.offset && expressionHasFunctionDef(c.offset)) return true;
|
|
break;
|
|
case "OrderBy":
|
|
for (let j = 0; j < c.orderBy.length; j++) {
|
|
if (expressionHasFunctionDef(c.orderBy[j].expression)) {
|
|
return true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function expressionsHaveFunctionDef(
|
|
exprs: LuaExpression[] | undefined,
|
|
): boolean {
|
|
if (!exprs) return false;
|
|
for (let i = 0; i < exprs.length; i++) {
|
|
if (expressionHasFunctionDef(exprs[i])) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Does the expression reference any of `names`?
|
|
// Note: It DOES NOT descend into `FunctionDefinition`.
|
|
function exprReferencesNames(e: LuaExpression, names: Set<string>): boolean {
|
|
if (!e) return false;
|
|
switch (e.type) {
|
|
case "Variable":
|
|
return names.has(e.name);
|
|
case "Binary":
|
|
return exprReferencesNames(e.left, names) ||
|
|
exprReferencesNames(e.right, names);
|
|
case "Unary":
|
|
return exprReferencesNames(e.argument, names);
|
|
case "Parenthesized":
|
|
return exprReferencesNames(e.expression, names);
|
|
case "FunctionCall":
|
|
if (exprReferencesNames(e.prefix, names)) return true;
|
|
for (let i = 0; i < e.args.length; i++) {
|
|
if (exprReferencesNames(e.args[i], names)) return true;
|
|
}
|
|
return false;
|
|
case "TableAccess":
|
|
return exprReferencesNames(e.object, names) ||
|
|
exprReferencesNames(e.key, names);
|
|
case "PropertyAccess":
|
|
return exprReferencesNames(e.object, names);
|
|
case "TableConstructor":
|
|
for (let i = 0; i < e.fields.length; i++) {
|
|
const f = e.fields[i];
|
|
switch (f.type) {
|
|
case "DynamicField":
|
|
if (exprReferencesNames(f.key, names)) return true;
|
|
if (exprReferencesNames(f.value, names)) return true;
|
|
break;
|
|
case "PropField":
|
|
case "ExpressionField":
|
|
if (exprReferencesNames(f.value, names)) return true;
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
case "FunctionDefinition":
|
|
return false;
|
|
case "Query":
|
|
for (let i = 0; i < e.clauses.length; i++) {
|
|
const c = e.clauses[i];
|
|
switch (c.type) {
|
|
case "From":
|
|
if (exprReferencesNames(c.expression, names)) return true;
|
|
break;
|
|
case "Where":
|
|
case "Select":
|
|
if (exprReferencesNames(c.expression, names)) return true;
|
|
break;
|
|
case "Limit":
|
|
if (exprReferencesNames(c.limit, names)) return true;
|
|
if (c.offset && exprReferencesNames(c.offset, names)) return true;
|
|
break;
|
|
case "OrderBy":
|
|
for (let j = 0; j < c.orderBy.length; j++) {
|
|
if (exprReferencesNames(c.orderBy[j].expression, names)) {
|
|
return true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function lvalueReferencesNames(lv: LuaLValue, names: Set<string>): boolean {
|
|
switch (lv.type) {
|
|
case "Variable":
|
|
return names.has(lv.name);
|
|
case "PropertyAccess":
|
|
return exprReferencesNames(lv.object as LuaExpression, names);
|
|
case "TableAccess":
|
|
return exprReferencesNames(lv.object as LuaExpression, names) ||
|
|
exprReferencesNames(lv.key, names);
|
|
}
|
|
}
|
|
|
|
// Does a function body reference any of `names` NOT shadowed by its
|
|
// parameters?
|
|
function functionBodyCapturesNames(
|
|
body: LuaFunctionBody,
|
|
names: Set<string>,
|
|
): boolean {
|
|
let unshadowed: Set<string> | null = null;
|
|
for (let i = 0; i < body.parameters.length; i++) {
|
|
if (names.has(body.parameters[i])) {
|
|
if (!unshadowed) unshadowed = new Set(names);
|
|
unshadowed.delete(body.parameters[i]);
|
|
}
|
|
}
|
|
const check = unshadowed ?? names;
|
|
if (check.size === 0) return false;
|
|
return blockReferencesNames(body.block, check);
|
|
}
|
|
|
|
// Walk block using `exprReferencesNames` (inside a function body).
|
|
function blockReferencesNames(block: LuaBlock, names: Set<string>): boolean {
|
|
for (let i = 0; i < block.statements.length; i++) {
|
|
if (statementReferencesNames(block.statements[i], names)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function statementReferencesNames(
|
|
s: LuaStatement,
|
|
names: Set<string>,
|
|
): boolean {
|
|
switch (s.type) {
|
|
case "Local": {
|
|
const exprs = (s as any).expressions as LuaExpression[] | undefined;
|
|
if (exprs) {
|
|
for (let i = 0; i < exprs.length; i++) {
|
|
if (exprReferencesNames(exprs[i], names)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
case "LocalFunction": {
|
|
const lf = s as any;
|
|
return functionBodyCapturesNames(lf.body as LuaFunctionBody, names);
|
|
}
|
|
case "Function": {
|
|
const fn = s as any;
|
|
return functionBodyCapturesNames(fn.body as LuaFunctionBody, names);
|
|
}
|
|
case "FunctionCallStatement": {
|
|
const call = (s as any).call as LuaFunctionCallExpression;
|
|
if (exprReferencesNames(call.prefix, names)) return true;
|
|
for (let i = 0; i < call.args.length; i++) {
|
|
if (exprReferencesNames(call.args[i], names)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
case "Assignment": {
|
|
const a = s as any;
|
|
const vars = a.variables as LuaLValue[];
|
|
if (vars) {
|
|
for (let i = 0; i < vars.length; i++) {
|
|
if (lvalueReferencesNames(vars[i], names)) return true;
|
|
}
|
|
}
|
|
const exprs = a.expressions as LuaExpression[];
|
|
for (let i = 0; i < exprs.length; i++) {
|
|
if (exprReferencesNames(exprs[i], names)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
case "Return": {
|
|
const exprs = (s as any).expressions as LuaExpression[];
|
|
for (let i = 0; i < exprs.length; i++) {
|
|
if (exprReferencesNames(exprs[i], names)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
case "Block":
|
|
return blockReferencesNames(s as LuaBlock, names);
|
|
case "If": {
|
|
const iff = s as LuaIfStatement;
|
|
for (const c of iff.conditions) {
|
|
if (exprReferencesNames(c.condition, names)) return true;
|
|
if (blockReferencesNames(c.block, names)) return true;
|
|
}
|
|
if (iff.elseBlock && blockReferencesNames(iff.elseBlock, names)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
case "While": {
|
|
const w = s as any;
|
|
if (exprReferencesNames(w.condition, names)) return true;
|
|
return blockReferencesNames(w.block as LuaBlock, names);
|
|
}
|
|
case "Repeat": {
|
|
const r = s as any;
|
|
if (blockReferencesNames(r.block as LuaBlock, names)) return true;
|
|
if (exprReferencesNames(r.condition, names)) return true;
|
|
return false;
|
|
}
|
|
case "For": {
|
|
const fr = s as any;
|
|
if (exprReferencesNames(fr.start, names)) return true;
|
|
if (exprReferencesNames(fr.end, names)) return true;
|
|
if (fr.step && exprReferencesNames(fr.step, names)) return true;
|
|
return blockReferencesNames(fr.block as LuaBlock, names);
|
|
}
|
|
case "ForIn": {
|
|
const fi = s as any;
|
|
const exprs = fi.expressions as LuaExpression[];
|
|
for (let i = 0; i < exprs.length; i++) {
|
|
if (exprReferencesNames(exprs[i], names)) return true;
|
|
}
|
|
return blockReferencesNames(fi.block as LuaBlock, names);
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Walk block looking for `FunctionDefinition` nodes that capture `names`.
|
|
function blockCapturesNames(block: LuaBlock, names: Set<string>): boolean {
|
|
for (let i = 0; i < block.statements.length; i++) {
|
|
if (statementCapturesNames(block.statements[i], names)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function statementCapturesNames(
|
|
s: LuaStatement,
|
|
names: Set<string>,
|
|
): boolean {
|
|
switch (s.type) {
|
|
case "Local": {
|
|
const exprs = (s as any).expressions as LuaExpression[] | undefined;
|
|
if (exprs) {
|
|
for (let i = 0; i < exprs.length; i++) {
|
|
if (exprCapturesNames(exprs[i], names)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
case "LocalFunction": {
|
|
const lf = s as any;
|
|
return functionBodyCapturesNames(lf.body as LuaFunctionBody, names);
|
|
}
|
|
case "Function": {
|
|
const fn = s as any;
|
|
return functionBodyCapturesNames(fn.body as LuaFunctionBody, names);
|
|
}
|
|
case "FunctionCallStatement": {
|
|
const call = (s as any).call as LuaFunctionCallExpression;
|
|
if (exprCapturesNames(call.prefix, names)) return true;
|
|
for (let i = 0; i < call.args.length; i++) {
|
|
if (exprCapturesNames(call.args[i], names)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
case "Assignment": {
|
|
const exprs = (s as any).expressions as LuaExpression[];
|
|
for (let i = 0; i < exprs.length; i++) {
|
|
if (exprCapturesNames(exprs[i], names)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
case "Return": {
|
|
const exprs = (s as any).expressions as LuaExpression[];
|
|
for (let i = 0; i < exprs.length; i++) {
|
|
if (exprCapturesNames(exprs[i], names)) return true;
|
|
}
|
|
return false;
|
|
}
|
|
case "Block":
|
|
return blockCapturesNames(s as LuaBlock, names);
|
|
case "If": {
|
|
const iff = s as LuaIfStatement;
|
|
for (const c of iff.conditions) {
|
|
if (exprCapturesNames(c.condition, names)) return true;
|
|
if (blockCapturesNames(c.block, names)) return true;
|
|
}
|
|
if (iff.elseBlock && blockCapturesNames(iff.elseBlock, names)) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
case "While": {
|
|
const w = s as any;
|
|
if (exprCapturesNames(w.condition, names)) return true;
|
|
return blockCapturesNames(w.block as LuaBlock, names);
|
|
}
|
|
case "Repeat": {
|
|
const r = s as any;
|
|
if (blockCapturesNames(r.block as LuaBlock, names)) return true;
|
|
if (exprCapturesNames(r.condition, names)) return true;
|
|
return false;
|
|
}
|
|
case "For": {
|
|
const fr = s as any;
|
|
if (exprCapturesNames(fr.start, names)) return true;
|
|
if (exprCapturesNames(fr.end, names)) return true;
|
|
if (fr.step && exprCapturesNames(fr.step, names)) return true;
|
|
return blockCapturesNames(fr.block as LuaBlock, names);
|
|
}
|
|
case "ForIn": {
|
|
const fi = s as any;
|
|
const exprs = fi.expressions as LuaExpression[];
|
|
for (let i = 0; i < exprs.length; i++) {
|
|
if (exprCapturesNames(exprs[i], names)) return true;
|
|
}
|
|
return blockCapturesNames(fi.block as LuaBlock, names);
|
|
}
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// At loop block level find `FunctionDefinition` and check if it
|
|
// captures `names`.
|
|
function exprCapturesNames(e: LuaExpression, names: Set<string>): boolean {
|
|
if (!e) return false;
|
|
switch (e.type) {
|
|
case "FunctionDefinition":
|
|
return functionBodyCapturesNames(e.body, names);
|
|
case "Binary":
|
|
return exprCapturesNames(e.left, names) ||
|
|
exprCapturesNames(e.right, names);
|
|
case "Unary":
|
|
return exprCapturesNames(e.argument, names);
|
|
case "Parenthesized":
|
|
return exprCapturesNames(e.expression, names);
|
|
case "FunctionCall":
|
|
if (exprCapturesNames(e.prefix, names)) return true;
|
|
for (let i = 0; i < e.args.length; i++) {
|
|
if (exprCapturesNames(e.args[i], names)) return true;
|
|
}
|
|
return false;
|
|
case "TableAccess":
|
|
return exprCapturesNames(e.object, names) ||
|
|
exprCapturesNames(e.key, names);
|
|
case "PropertyAccess":
|
|
return exprCapturesNames(e.object, names);
|
|
case "TableConstructor":
|
|
for (let i = 0; i < e.fields.length; i++) {
|
|
const f = e.fields[i];
|
|
switch (f.type) {
|
|
case "DynamicField":
|
|
if (exprCapturesNames(f.key, names)) return true;
|
|
if (exprCapturesNames(f.value, names)) return true;
|
|
break;
|
|
case "PropField":
|
|
case "ExpressionField":
|
|
if (exprCapturesNames(f.value, names)) return true;
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
case "Query":
|
|
for (let i = 0; i < e.clauses.length; i++) {
|
|
const c = e.clauses[i];
|
|
switch (c.type) {
|
|
case "From":
|
|
if (exprCapturesNames(c.expression, names)) return true;
|
|
break;
|
|
case "Where":
|
|
case "Select":
|
|
if (exprCapturesNames(c.expression, names)) return true;
|
|
break;
|
|
case "Limit":
|
|
if (exprCapturesNames(c.limit, names)) return true;
|
|
if (c.offset && exprCapturesNames(c.offset, names)) return true;
|
|
break;
|
|
case "OrderBy":
|
|
for (let j = 0; j < c.orderBy.length; j++) {
|
|
if (exprCapturesNames(c.orderBy[j].expression, names)) {
|
|
return true;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return false;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function parseBlock(t: ParseTree, ctx: ASTCtx): LuaBlock {
|
|
if (t.type !== "Block") {
|
|
throw new Error(`Expected Block, got ${t.type}`);
|
|
}
|
|
const stmtNodes = t.children!.filter((c) => c && c.type);
|
|
const statements = stmtNodes.map((s) => parseStatement(s, ctx));
|
|
const block: LuaBlock = { type: "Block", statements, ctx: context(t, ctx) };
|
|
|
|
let hasLabel = false;
|
|
let hasGoto = false;
|
|
let hasLocalDecl = false;
|
|
let dup: { name: string; ctx: ASTCtx } | undefined;
|
|
let hasLabelHere = false;
|
|
let hasCloseHere = false;
|
|
let hasFunctionDef = false;
|
|
|
|
const seen = new Set<string>();
|
|
|
|
for (const s of statements) {
|
|
switch (s.type) {
|
|
case "Label": {
|
|
hasLabel = true;
|
|
hasLabelHere = true;
|
|
// Duplicate labels in the same block are illegal
|
|
const name = (s as any).name as string;
|
|
if (!dup) {
|
|
if (seen.has(name)) {
|
|
dup = { name, ctx: (s as any).ctx as ASTCtx };
|
|
} else {
|
|
seen.add(name);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case "Goto": {
|
|
hasGoto = true;
|
|
break;
|
|
}
|
|
case "Local": {
|
|
hasLocalDecl = true;
|
|
if (!hasCloseHere) {
|
|
hasCloseHere = hasCloseLocal((s as any).names as LuaAttName[]);
|
|
}
|
|
if (!hasFunctionDef) {
|
|
hasFunctionDef = expressionsHaveFunctionDef(
|
|
(s as any).expressions as LuaExpression[] | undefined,
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case "LocalFunction": {
|
|
hasLocalDecl = true;
|
|
hasFunctionDef = true;
|
|
break;
|
|
}
|
|
case "Function": {
|
|
hasFunctionDef = true;
|
|
break;
|
|
}
|
|
case "FunctionCallStatement": {
|
|
if (!hasFunctionDef) {
|
|
const call = (s as any).call as LuaFunctionCallExpression;
|
|
hasFunctionDef = expressionHasFunctionDef(call.prefix) ||
|
|
expressionsHaveFunctionDef(call.args);
|
|
}
|
|
break;
|
|
}
|
|
case "Assignment": {
|
|
if (!hasFunctionDef) {
|
|
hasFunctionDef = expressionsHaveFunctionDef(
|
|
(s as any).expressions as LuaExpression[],
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case "Return": {
|
|
if (!hasFunctionDef) {
|
|
hasFunctionDef = expressionsHaveFunctionDef(
|
|
(s as any).expressions as LuaExpression[],
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
case "Block": {
|
|
const child = s as LuaBlock;
|
|
hasLabel = hasLabel || !!child.hasLabel;
|
|
hasGoto = hasGoto || !!child.hasGoto;
|
|
hasCloseHere = hasCloseHere || !!child.hasCloseHere;
|
|
hasFunctionDef = hasFunctionDef || !!child.hasFunctionDef;
|
|
break;
|
|
}
|
|
case "If": {
|
|
const iff = s as LuaIfStatement;
|
|
for (const c of iff.conditions) {
|
|
hasLabel = hasLabel || !!c.block.hasLabel;
|
|
hasGoto = hasGoto || !!c.block.hasGoto;
|
|
hasCloseHere = hasCloseHere || !!c.block.hasCloseHere;
|
|
hasFunctionDef = hasFunctionDef || !!c.block.hasFunctionDef;
|
|
if (!hasFunctionDef) {
|
|
hasFunctionDef = expressionHasFunctionDef(c.condition);
|
|
}
|
|
}
|
|
if (iff.elseBlock) {
|
|
hasLabel = hasLabel || !!iff.elseBlock.hasLabel;
|
|
hasGoto = hasGoto || !!iff.elseBlock.hasGoto;
|
|
hasCloseHere = hasCloseHere || !!iff.elseBlock.hasCloseHere;
|
|
hasFunctionDef = hasFunctionDef || !!iff.elseBlock.hasFunctionDef;
|
|
}
|
|
break;
|
|
}
|
|
case "While":
|
|
case "Repeat": {
|
|
const child = (s as any).block as LuaBlock;
|
|
hasLabel = hasLabel || !!child.hasLabel;
|
|
hasGoto = hasGoto || !!child.hasGoto;
|
|
hasCloseHere = hasCloseHere || !!child.hasCloseHere;
|
|
hasFunctionDef = hasFunctionDef || !!child.hasFunctionDef;
|
|
if (!hasFunctionDef) {
|
|
hasFunctionDef = expressionHasFunctionDef((s as any).condition);
|
|
}
|
|
break;
|
|
}
|
|
case "For": {
|
|
const child = (s as any).block as LuaBlock;
|
|
hasLabel = hasLabel || !!child.hasLabel;
|
|
hasGoto = hasGoto || !!child.hasGoto;
|
|
hasCloseHere = hasCloseHere || !!child.hasCloseHere;
|
|
hasFunctionDef = hasFunctionDef || !!child.hasFunctionDef;
|
|
if (!hasFunctionDef) {
|
|
hasFunctionDef = expressionHasFunctionDef((s as any).start) ||
|
|
expressionHasFunctionDef((s as any).end) ||
|
|
((s as any).step
|
|
? expressionHasFunctionDef((s as any).step)
|
|
: false);
|
|
}
|
|
break;
|
|
}
|
|
case "ForIn": {
|
|
const child = (s as any).block as LuaBlock;
|
|
hasLabel = hasLabel || !!child.hasLabel;
|
|
hasGoto = hasGoto || !!child.hasGoto;
|
|
hasCloseHere = true;
|
|
hasFunctionDef = hasFunctionDef || !!child.hasFunctionDef;
|
|
if (!hasFunctionDef) {
|
|
hasFunctionDef = expressionsHaveFunctionDef(
|
|
(s as any).expressions as LuaExpression[],
|
|
);
|
|
}
|
|
break;
|
|
}
|
|
default: {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasLabel) {
|
|
block.hasLabel = true;
|
|
}
|
|
if (hasGoto) {
|
|
block.hasGoto = true;
|
|
}
|
|
if (dup) {
|
|
block.dupLabelError = dup;
|
|
}
|
|
if (hasLocalDecl) {
|
|
block.needsEnv = true;
|
|
}
|
|
if (hasLabelHere) {
|
|
block.hasLabelHere = true;
|
|
}
|
|
if (hasCloseHere) {
|
|
block.hasCloseHere = true;
|
|
}
|
|
if (hasFunctionDef) {
|
|
block.hasFunctionDef = true;
|
|
}
|
|
|
|
return block;
|
|
}
|
|
|
|
function parseStatement(t: ParseTree, ctx: ASTCtx): LuaStatement {
|
|
if (!t || !t.type) {
|
|
return {
|
|
type: "Semicolon",
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
switch (t.type) {
|
|
case "Block":
|
|
return parseChunk(t.children![0], ctx);
|
|
case "Semicolon":
|
|
return { type: "Semicolon", ctx: context(t, ctx) };
|
|
case "Label":
|
|
return {
|
|
type: "Label",
|
|
name: t.children![1].children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Break":
|
|
return { type: "Break", ctx: context(t, ctx) };
|
|
case "Goto":
|
|
return {
|
|
type: "Goto",
|
|
name: t.children![1].children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Scope":
|
|
return parseBlock(t.children![1], ctx);
|
|
case ";":
|
|
return { type: "Semicolon", ctx: context(t, ctx) };
|
|
case "WhileStatement":
|
|
return {
|
|
type: "While",
|
|
condition: parseExpression(t.children![1], ctx),
|
|
block: parseBlock(t.children![3], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "RepeatStatement":
|
|
return {
|
|
type: "Repeat",
|
|
block: parseBlock(t.children![1], ctx),
|
|
condition: parseExpression(t.children![3], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "IfStatement": {
|
|
const conditions: {
|
|
condition: LuaExpression;
|
|
block: LuaBlock;
|
|
from?: number;
|
|
to?: number;
|
|
}[] = [];
|
|
let elseBlock: LuaBlock | undefined = undefined;
|
|
for (let i = 0; i < t.children!.length; i += 4) {
|
|
const child = t.children![i];
|
|
if (!child || !child.children || !child.children[0]) {
|
|
continue;
|
|
}
|
|
const token = child.children![0].text;
|
|
if (token === "if" || token === "elseif") {
|
|
conditions.push({
|
|
condition: parseExpression(t.children![i + 1], ctx),
|
|
block: parseBlock(t.children![i + 3], ctx),
|
|
from: child.from,
|
|
to: child.to,
|
|
});
|
|
} else if (token === "else") {
|
|
elseBlock = parseBlock(t.children![i + 1], ctx);
|
|
} else if (token === "end") {
|
|
break;
|
|
} else {
|
|
throw new Error(`Unknown if clause type: ${token}`);
|
|
}
|
|
}
|
|
return {
|
|
type: "If",
|
|
conditions,
|
|
elseBlock,
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "ForStatement": {
|
|
if (t.children![1].type === "ForNumeric") {
|
|
const forNumeric = t.children![1];
|
|
const name = forNumeric.children![0].children![0].text!;
|
|
const block = parseBlock(t.children![3], ctx);
|
|
const node: LuaStatement = {
|
|
type: "For",
|
|
name,
|
|
start: parseExpression(forNumeric.children![2], ctx),
|
|
end: parseExpression(forNumeric.children![4], ctx),
|
|
step: forNumeric.children![5]
|
|
? parseExpression(forNumeric.children![6], ctx)
|
|
: undefined,
|
|
block,
|
|
ctx: context(t, ctx),
|
|
};
|
|
if (block.hasFunctionDef) {
|
|
const names = new Set([name]);
|
|
(node as any).capturesLoopVar = blockCapturesNames(block, names);
|
|
}
|
|
return node;
|
|
}
|
|
const forGeneric = t.children![1];
|
|
const names = parseNameList(forGeneric.children![0]);
|
|
const block = parseBlock(t.children![3], ctx);
|
|
const node: LuaStatement = {
|
|
type: "ForIn",
|
|
names,
|
|
expressions: parseExpList(forGeneric.children![2], ctx),
|
|
block,
|
|
ctx: context(t, ctx),
|
|
};
|
|
if (block.hasFunctionDef) {
|
|
const nameSet = new Set(names);
|
|
(node as any).capturesLoopVar = blockCapturesNames(block, nameSet);
|
|
}
|
|
return node;
|
|
}
|
|
case "Function":
|
|
return {
|
|
type: "Function",
|
|
name: parseFunctionName(t.children![1], ctx),
|
|
body: parseFunctionBody(t.children![2], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "LocalFunction":
|
|
return {
|
|
type: "LocalFunction",
|
|
name: t.children![2].children![0].text!,
|
|
body: parseFunctionBody(t.children![3], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "FunctionCall":
|
|
return {
|
|
type: "FunctionCallStatement",
|
|
call: parseFunctionCall(t, ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Assign":
|
|
return {
|
|
type: "Assignment",
|
|
variables: t.children![0].children!
|
|
.filter((c) => c.type && c.type !== ",")
|
|
.map((lvalue) => parseLValue(lvalue, ctx)),
|
|
expressions: parseExpList(t.children![2], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Local": {
|
|
const names = parseAttNames(t.children![1], ctx);
|
|
|
|
let closeCount = 0;
|
|
for (const n of names) {
|
|
if (n.attributes?.includes(LuaAttribute.Close) === true) {
|
|
closeCount++;
|
|
if (closeCount > 1) {
|
|
throw new Error("multiple <close> variables in local list");
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
type: "Local",
|
|
names,
|
|
expressions: t.children![3] ? parseExpList(t.children![3], ctx) : [],
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "ReturnStatement": {
|
|
const expressions = t.children![1]
|
|
? parseExpList(t.children![1], ctx)
|
|
: [];
|
|
return { type: "Return", expressions, ctx: context(t, ctx) };
|
|
}
|
|
case "break":
|
|
return { type: "Break", ctx: context(t, ctx) };
|
|
default:
|
|
// Gracefully ignore unknown empty nodes
|
|
if (!t.children || t.children.length === 0) {
|
|
return {
|
|
type: "Semicolon",
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
console.error(t);
|
|
throw new Error(
|
|
`Unknown statement type: ${
|
|
t.children![0] && t.children![0].text
|
|
? t.children![0].text
|
|
: String(t.type)
|
|
}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function parseFunctionCall(
|
|
t: ParseTree,
|
|
ctx: ASTCtx,
|
|
): LuaFunctionCallExpression {
|
|
if (t.children![1] && t.children![1].type === ":") {
|
|
return {
|
|
type: "FunctionCall",
|
|
prefix: parsePrefixExpression(t.children![0], ctx),
|
|
name: t.children![2].children![0].text!,
|
|
args: parseFunctionArgs(t.children!.slice(3), ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
return {
|
|
type: "FunctionCall",
|
|
prefix: parsePrefixExpression(t.children![0], ctx),
|
|
args: parseFunctionArgs(t.children!.slice(1), ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
|
|
function parseAttNames(t: ParseTree, ctx: ASTCtx): LuaAttName[] {
|
|
if (t.type !== "AttNameList") {
|
|
throw new Error(`Expected AttNameList, got ${t.type}`);
|
|
}
|
|
return t.children!
|
|
.filter((c) => c.type && c.type !== ",")
|
|
.map((att) => parseAttName(att, ctx));
|
|
}
|
|
|
|
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 &&
|
|
attribute !== LuaAttribute.Close
|
|
) {
|
|
throw new Error(`unknown attribute '${attribute}'`);
|
|
}
|
|
const attributes = attribute ? [attribute as LuaAttribute] : undefined;
|
|
return {
|
|
type: "AttName",
|
|
name: t.children![0].children![0].text!,
|
|
attribute,
|
|
attributes,
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
|
|
function parseLValue(t: ParseTree, ctx: ASTCtx): LuaLValue {
|
|
switch (t.type) {
|
|
case "Name":
|
|
return {
|
|
type: "Variable",
|
|
name: t.children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Property":
|
|
return {
|
|
type: "PropertyAccess",
|
|
object: parsePrefixExpression(t.children![0], ctx),
|
|
property: t.children![2].children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "MemberExpression":
|
|
return {
|
|
type: "TableAccess",
|
|
object: parsePrefixExpression(t.children![0], ctx),
|
|
key: parseExpression(t.children![2], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
default:
|
|
console.error(t);
|
|
throw new Error(`Unknown lvalue type: ${t.type}`);
|
|
}
|
|
}
|
|
|
|
function parseFunctionName(t: ParseTree, ctx: ASTCtx): LuaFunctionName {
|
|
if (t.type !== "FuncName") {
|
|
throw new Error(`Expected FunctionName, got ${t.type}`);
|
|
}
|
|
const propNames: string[] = [];
|
|
let colonName: string | undefined = undefined;
|
|
for (let i = 0; i < t.children!.length; i += 2) {
|
|
const prop = t.children![i];
|
|
propNames.push(prop.children![0].text!);
|
|
if (t.children![i + 1] && t.children![i + 1].type === ":") {
|
|
colonName = t.children![i + 2].children![0].text!;
|
|
break;
|
|
}
|
|
}
|
|
return {
|
|
type: "FunctionName",
|
|
propNames,
|
|
colonName,
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
|
|
function parseNameList(t: ParseTree): string[] {
|
|
if (t.type !== "NameList") {
|
|
throw new Error(`Expected NameList, got ${t.type}`);
|
|
}
|
|
return t.children!
|
|
.filter((c) => c.type === "Name")
|
|
.map((c) => c.children![0].text!);
|
|
}
|
|
|
|
function parseExpList(t: ParseTree, ctx: ASTCtx): LuaExpression[] {
|
|
if (t.type !== "ExpList") {
|
|
throw new Error(`Expected ExpList, got ${t.type}`);
|
|
}
|
|
return t.children!
|
|
.filter((c) => c.type && c.type !== ",")
|
|
.map((e) => parseExpression(e, ctx));
|
|
}
|
|
|
|
const delimiterRegex = /^(\[=*\[)([\s\S]*)(\]=*\])$/;
|
|
|
|
// In case of quoted strings, remove the quotes and unescape the string
|
|
// In case of a [[ type ]] literal string, remove the brackets
|
|
function parseString(s: string): string {
|
|
// Handle long strings with delimiters
|
|
const delimiterMatch = s.match(delimiterRegex);
|
|
if (delimiterMatch) {
|
|
let text = delimiterMatch[2];
|
|
// According to Lua semantics, whenever a [[ formatted string starts with a newline, that newline should be skipped
|
|
if (text[0] === "\n") {
|
|
text = text.slice(1);
|
|
}
|
|
return text;
|
|
}
|
|
return s.slice(1, -1).replace(
|
|
/\\(x[0-9a-fA-F]{2}|u\{[0-9a-fA-F]+\}|[abfnrtv\\'"n])/g,
|
|
(match, capture) => {
|
|
switch (capture) {
|
|
case "a":
|
|
return "\x07"; // Bell
|
|
case "b":
|
|
return "\b"; // Backspace
|
|
case "f":
|
|
return "\f"; // Form feed
|
|
case "n":
|
|
return "\n"; // Newline
|
|
case "r":
|
|
return "\r"; // Carriage return
|
|
case "t":
|
|
return "\t"; // Horizontal tab
|
|
case "v":
|
|
return "\v"; // Vertical tab
|
|
case "\\":
|
|
return "\\"; // Backslash
|
|
case '"':
|
|
return '"'; // Double quote
|
|
case "'":
|
|
return "'"; // Single quote
|
|
default:
|
|
// Handle hexadecimal \x00
|
|
if (capture.startsWith("x")) {
|
|
return String.fromCharCode(parseInt(capture.slice(1), 16));
|
|
}
|
|
// Handle unicode \u{XXXX}
|
|
if (capture.startsWith("u{")) {
|
|
const codePoint = parseInt(capture.slice(2, -1), 16);
|
|
return String.fromCodePoint(codePoint);
|
|
}
|
|
return match; // return the original match if nothing fits
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
function parseExpression(t: ParseTree, ctx: ASTCtx): LuaExpression {
|
|
if (!t || !t.type) {
|
|
throw new Error("Undefined expression node");
|
|
}
|
|
switch (t.type) {
|
|
case "LiteralString": {
|
|
const cleanString = parseString(t.children![0].text!);
|
|
return {
|
|
type: "String",
|
|
value: cleanString,
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "Number": {
|
|
const text = t.children![0].text!.toLowerCase();
|
|
return {
|
|
type: "Number",
|
|
// Use the integer parser fox 0x literals
|
|
value: text.includes("x") ? parseInt(text) : parseFloat(text),
|
|
numericType: /[\.eEpP]/.test(text) ? "float" : "int",
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "BinaryExpression":
|
|
return {
|
|
type: "Binary",
|
|
operator: t.children![1].children![0].text!,
|
|
left: parseExpression(t.children![0], ctx),
|
|
right: parseExpression(t.children![2], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "UnaryExpression": {
|
|
const op = t.children![0].children![0].text!;
|
|
if (op === "+") {
|
|
const err = new Error("unexpected symbol near '+'");
|
|
(err as any).astCtx = context(t.children![0], ctx);
|
|
throw err;
|
|
}
|
|
return {
|
|
type: "Unary",
|
|
operator: op,
|
|
argument: parseExpression(t.children![1], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "Property":
|
|
return {
|
|
type: "PropertyAccess",
|
|
object: parsePrefixExpression(t.children![0], ctx),
|
|
property: t.children![2].children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
|
|
case "MemberExpression":
|
|
return {
|
|
type: "TableAccess",
|
|
object: parsePrefixExpression(t.children![0], ctx),
|
|
key: parseExpression(t.children![2], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
|
|
case "Parens":
|
|
return parseExpression(t.children![1], ctx);
|
|
case "FunctionCall": {
|
|
return parseFunctionCall(t, ctx);
|
|
}
|
|
case "FunctionDef": {
|
|
const body = parseFunctionBody(t.children![1], ctx);
|
|
return {
|
|
type: "FunctionDefinition",
|
|
body,
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "Name":
|
|
return {
|
|
type: "Variable",
|
|
name: t.children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Ellipsis":
|
|
return { type: "Variable", name: "...", ctx: context(t, ctx) };
|
|
case "true":
|
|
return { type: "Boolean", value: true, ctx: context(t, ctx) };
|
|
case "false":
|
|
return { type: "Boolean", value: false, ctx: context(t, ctx) };
|
|
case "TableConstructor":
|
|
return {
|
|
type: "TableConstructor",
|
|
fields: t.children!
|
|
.slice(1, -1)
|
|
.filter((c) =>
|
|
["FieldExp", "FieldProp", "FieldDynamic"].includes(c.type!)
|
|
)
|
|
.map((tf) => parseTableField(tf, ctx)),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "nil":
|
|
return { type: "Nil", ctx: context(t, ctx) };
|
|
case "Query":
|
|
return {
|
|
type: "Query",
|
|
clauses: t.children!.slice(2, -1).map((c) => parseQueryClause(c, ctx)),
|
|
ctx: context(t, ctx),
|
|
};
|
|
default:
|
|
console.error(t);
|
|
throw new Error(`Unknown expression type: ${t.type}`);
|
|
}
|
|
}
|
|
|
|
function parseQueryClause(t: ParseTree, ctx: ASTCtx): LuaQueryClause {
|
|
if (t.type !== "QueryClause") {
|
|
throw new Error(`Expected QueryClause, got ${t.type}`);
|
|
}
|
|
t = t.children![0];
|
|
switch (t.type) {
|
|
case "FromClause": {
|
|
if (t.children!.length === 4) {
|
|
// From clause with a name
|
|
return {
|
|
type: "From",
|
|
name: t.children![1].children![0].text!,
|
|
expression: parseExpression(t.children![3], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
return {
|
|
type: "From",
|
|
expression: parseExpression(t.children![1], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "WhereClause":
|
|
return {
|
|
type: "Where",
|
|
expression: parseExpression(t.children![1], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "LimitClause": {
|
|
const limit = parseExpression(t.children![1], ctx);
|
|
const offset = t.children![2]
|
|
? parseExpression(t.children![3], ctx)
|
|
: undefined;
|
|
return {
|
|
type: "Limit",
|
|
limit,
|
|
offset,
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "OrderByClause": {
|
|
const orderBy: LuaOrderBy[] = [];
|
|
for (const child of t.children!) {
|
|
if (child.type === "OrderBy") {
|
|
orderBy.push({
|
|
type: "Order",
|
|
expression: parseExpression(child.children![0], ctx),
|
|
direction: child.children![1]?.type === "desc" ? "desc" : "asc",
|
|
ctx: context(child, ctx),
|
|
});
|
|
}
|
|
}
|
|
return {
|
|
type: "OrderBy",
|
|
orderBy,
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
case "SelectClause": {
|
|
return {
|
|
type: "Select",
|
|
expression: parseExpression(t.children![1], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
default:
|
|
console.error(t);
|
|
throw new Error(`Unknown query clause type: ${t.type}`);
|
|
}
|
|
}
|
|
|
|
function parseFunctionArgs(ts: ParseTree[], ctx: ASTCtx): LuaExpression[] {
|
|
return ts
|
|
.filter((t) => t.type && ![",", "(", ")"].includes(t.type))
|
|
.map((e) => parseExpression(e, ctx));
|
|
}
|
|
|
|
function parseFunctionBody(t: ParseTree, ctx: ASTCtx): LuaFunctionBody {
|
|
if (t.type !== "FuncBody") {
|
|
throw new Error(`Expected FunctionBody, got ${t.type}`);
|
|
}
|
|
return {
|
|
type: "FunctionBody",
|
|
parameters: t.children![1].children!
|
|
.filter((c) => c.type && ["Name", "Ellipsis"].includes(c.type))
|
|
.map((c) => c.children![0].text!),
|
|
block: parseBlock(t.children![3], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
}
|
|
|
|
function parsePrefixExpression(t: ParseTree, ctx: ASTCtx): LuaPrefixExpression {
|
|
if (!t || !t.type) {
|
|
throw new Error("Undefined prefix expression node");
|
|
}
|
|
switch (t.type) {
|
|
case "Name":
|
|
return {
|
|
type: "Variable",
|
|
name: t.children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Property":
|
|
return {
|
|
type: "PropertyAccess",
|
|
object: parsePrefixExpression(t.children![0], ctx),
|
|
property: t.children![2].children![0].text!,
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "MemberExpression":
|
|
return {
|
|
type: "TableAccess",
|
|
object: parsePrefixExpression(t.children![0], ctx),
|
|
key: parseExpression(t.children![2], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "Parens":
|
|
return {
|
|
type: "Parenthesized",
|
|
expression: parseExpression(t.children![1], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "FunctionCall": {
|
|
return parseFunctionCall(t, ctx);
|
|
}
|
|
default:
|
|
console.error(t);
|
|
throw new Error(`Unknown prefix expression type: ${t.type}`);
|
|
}
|
|
}
|
|
|
|
function parseTableField(t: ParseTree, ctx: ASTCtx): LuaTableField {
|
|
switch (t.type) {
|
|
case "FieldExp":
|
|
return {
|
|
type: "ExpressionField",
|
|
value: parseExpression(t.children![0], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "FieldProp":
|
|
return {
|
|
type: "PropField",
|
|
key: t.children![0].children![0].text!,
|
|
value: parseExpression(t.children![2], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
case "FieldDynamic":
|
|
return {
|
|
type: "DynamicField",
|
|
key: parseExpression(t.children![1], ctx),
|
|
value: parseExpression(t.children![4], ctx),
|
|
ctx: context(t, ctx),
|
|
};
|
|
default:
|
|
console.error(t);
|
|
throw new Error(`Unknown table field type: ${t.type}`);
|
|
}
|
|
}
|
|
|
|
export function stripLuaComments(s: string): string {
|
|
let result = "";
|
|
let i = 0;
|
|
|
|
while (i < s.length) {
|
|
// Check for long string
|
|
if (s[i] === "[") {
|
|
let j = i + 1;
|
|
let equalsCount = 0;
|
|
while (s[j] === "=") {
|
|
equalsCount++;
|
|
j++;
|
|
}
|
|
if (s[j] === "[") {
|
|
// Found long string start
|
|
const openBracket = s.substring(i, j + 1);
|
|
const closeBracket = "]" + "=".repeat(equalsCount) + "]";
|
|
result += openBracket;
|
|
i = j + 1;
|
|
|
|
// Find matching closing bracket
|
|
const content = s.substring(i);
|
|
const closeIndex = content.indexOf(closeBracket);
|
|
if (closeIndex !== -1) {
|
|
// Copy string content verbatim, including any comment-like sequences
|
|
result += content.substring(0, closeIndex) + closeBracket;
|
|
i += closeIndex + closeBracket.length;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for single quoted string
|
|
if (s[i] === '"' || s[i] === "'") {
|
|
const quote = s[i];
|
|
result += quote;
|
|
i++;
|
|
while (i < s.length && s[i] !== quote) {
|
|
if (s[i] === "\\") {
|
|
result += s[i] + s[i + 1];
|
|
i += 2;
|
|
} else {
|
|
result += s[i];
|
|
i++;
|
|
}
|
|
}
|
|
if (i < s.length) {
|
|
result += s[i]; // closing quote
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Check for comments
|
|
if (s[i] === "-" && s[i + 1] === "-") {
|
|
// Replace the -- with spaces
|
|
result += " ";
|
|
i += 2;
|
|
|
|
// Check for long comment
|
|
if (s[i] === "[") {
|
|
let j = i + 1;
|
|
let equalsCount = 0;
|
|
while (s[j] === "=") {
|
|
equalsCount++;
|
|
j++;
|
|
}
|
|
if (s[j] === "[") {
|
|
// Found long comment start
|
|
const closeBracket = "]" + "=".repeat(equalsCount) + "]";
|
|
// Replace opening bracket with spaces
|
|
result += " ".repeat(j - i + 1);
|
|
i = j + 1;
|
|
|
|
// Find matching closing bracket
|
|
const content = s.substring(i);
|
|
const closeIndex = content.indexOf(closeBracket);
|
|
if (closeIndex !== -1) {
|
|
// Replace comment content and closing bracket with spaces
|
|
result += " ".repeat(closeIndex) + " ".repeat(closeBracket.length);
|
|
i += closeIndex + closeBracket.length;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Single line comment - replace rest of line with spaces
|
|
while (i < s.length && s[i] !== "\n") {
|
|
result += " ";
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
result += s[i];
|
|
i++;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function parse(s: string, ctx: ASTCtx = {}): LuaBlock {
|
|
try {
|
|
const t = parseToAST(stripLuaComments(s));
|
|
// console.log("Clean tree", JSON.stringify(t, null, 2));
|
|
const result = parseChunk(t, ctx);
|
|
// console.log("Parsed AST", JSON.stringify(result, null, 2));
|
|
getBlockGotoMeta(result);
|
|
return result;
|
|
} catch (e: any) {
|
|
if (e && typeof e === "object" && "astCtx" in e) {
|
|
throw new LuaRuntimeError(
|
|
e.message,
|
|
LuaStackFrame.lostFrame.withCtx(
|
|
(e as any).astCtx as ASTCtx,
|
|
),
|
|
);
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
export function parseToAST(t: string): ParseTree {
|
|
const tree = parser.parse(t);
|
|
|
|
const errNode = findFirstParseError(tree.topNode);
|
|
if (errNode) {
|
|
const err = new Error(luaUnexpectedSymbolMessage(t, errNode.from));
|
|
(err as any).astCtx = { from: errNode.from, to: errNode.to };
|
|
throw err;
|
|
}
|
|
|
|
const n = lezerToParseTree(t, tree.topNode);
|
|
return cleanTree(n, true);
|
|
}
|
|
|
|
function findFirstParseError(node: SyntaxNode): SyntaxNode | null {
|
|
if (node.type.isError) {
|
|
return node;
|
|
}
|
|
for (let ch = node.firstChild; ch; ch = ch.nextSibling) {
|
|
const hit = findFirstParseError(ch);
|
|
if (hit) {
|
|
return hit;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function luaUnexpectedSymbolMessage(src: string, from: number): string {
|
|
let i = from;
|
|
while (i < src.length && /\s/.test(src[i])) i++;
|
|
const sym = i < src.length ? src[i] : "?";
|
|
return `unexpected symbol near '${sym}'`;
|
|
}
|
|
|
|
/**
|
|
* Helper function to parse a Lua expression string
|
|
*/
|
|
export function parseExpressionString(
|
|
expr: string,
|
|
): LuaExpression {
|
|
const parsedLua = parse(`_(${expr})`) as LuaBlock;
|
|
return (parsedLua.statements[0] as LuaFunctionCallStatement).call.args[0];
|
|
}
|