More work on Lua
parent
f08c66d305
commit
aa712ed8f4
|
@ -1,8 +1,8 @@
|
||||||
import { assertEquals } from "@std/assert/equals";
|
import { assertEquals } from "@std/assert/equals";
|
||||||
import { LuaEnv, LuaNativeJSFunction, singleResult } from "./runtime.ts";
|
import { LuaEnv, LuaNativeJSFunction, singleResult } from "./runtime.ts";
|
||||||
import { parse } from "./parse.ts";
|
import { parse } from "./parse.ts";
|
||||||
import type { LuaFunctionCallStatement } from "./ast.ts";
|
import type { LuaBlock, LuaFunctionCallStatement } from "./ast.ts";
|
||||||
import { evalExpression } from "./eval.ts";
|
import { evalExpression, evalStatement } from "./eval.ts";
|
||||||
|
|
||||||
function evalExpr(s: string, e = new LuaEnv()): any {
|
function evalExpr(s: string, e = new LuaEnv()): any {
|
||||||
return evalExpression(
|
return evalExpression(
|
||||||
|
@ -12,6 +12,10 @@ function evalExpr(s: string, e = new LuaEnv()): any {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function evalBlock(s: string, e = new LuaEnv()): Promise<void> {
|
||||||
|
return evalStatement(parse(s) as LuaBlock, e);
|
||||||
|
}
|
||||||
|
|
||||||
Deno.test("Evaluator test", async () => {
|
Deno.test("Evaluator test", async () => {
|
||||||
const env = new LuaEnv();
|
const env = new LuaEnv();
|
||||||
env.set("test", new LuaNativeJSFunction((n) => n));
|
env.set("test", new LuaNativeJSFunction((n) => n));
|
||||||
|
@ -22,11 +26,19 @@ Deno.test("Evaluator test", async () => {
|
||||||
assertEquals(evalExpr(`4 // 3`), 1);
|
assertEquals(evalExpr(`4 // 3`), 1);
|
||||||
assertEquals(evalExpr(`4 % 3`), 1);
|
assertEquals(evalExpr(`4 % 3`), 1);
|
||||||
|
|
||||||
|
// Strings
|
||||||
|
assertEquals(evalExpr(`"a" .. "b"`), "ab");
|
||||||
|
|
||||||
|
// Logic
|
||||||
|
assertEquals(evalExpr(`true and false`), false);
|
||||||
|
assertEquals(evalExpr(`true or false`), true);
|
||||||
|
assertEquals(evalExpr(`not true`), false);
|
||||||
|
|
||||||
// Tables
|
// Tables
|
||||||
const tbl = evalExpr(`{3, 1, 2}`);
|
const tbl = evalExpr(`{3, 1, 2}`);
|
||||||
assertEquals(tbl.entries.get(1), 3);
|
assertEquals(tbl.get(1), 3);
|
||||||
assertEquals(tbl.entries.get(2), 1);
|
assertEquals(tbl.get(2), 1);
|
||||||
assertEquals(tbl.entries.get(3), 2);
|
assertEquals(tbl.get(3), 2);
|
||||||
assertEquals(tbl.toArray(), [3, 1, 2]);
|
assertEquals(tbl.toArray(), [3, 1, 2]);
|
||||||
|
|
||||||
assertEquals(evalExpr(`{name=test("Zef"), age=100}`, env).toObject(), {
|
assertEquals(evalExpr(`{name=test("Zef"), age=100}`, env).toObject(), {
|
||||||
|
@ -51,9 +63,126 @@ Deno.test("Evaluator test", async () => {
|
||||||
assertEquals(evalExpr(`#{1, 2, 3}`), 3);
|
assertEquals(evalExpr(`#{1, 2, 3}`), 3);
|
||||||
|
|
||||||
// Unary operators
|
// Unary operators
|
||||||
|
|
||||||
assertEquals(await evalExpr(`-asyncTest(3)`, env), -3);
|
assertEquals(await evalExpr(`-asyncTest(3)`, env), -3);
|
||||||
|
|
||||||
|
// Function calls
|
||||||
assertEquals(singleResult(evalExpr(`test(3)`, env)), 3);
|
assertEquals(singleResult(evalExpr(`test(3)`, env)), 3);
|
||||||
assertEquals(singleResult(await evalExpr(`asyncTest(3) + 1`, env)), 4);
|
assertEquals(singleResult(await evalExpr(`asyncTest(3) + 1`, env)), 4);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Deno.test("Statement evaluation", async () => {
|
||||||
|
const env = new LuaEnv();
|
||||||
|
env.set("test", new LuaNativeJSFunction((n) => n));
|
||||||
|
env.set("asyncTest", new LuaNativeJSFunction((n) => Promise.resolve(n)));
|
||||||
|
|
||||||
|
assertEquals(undefined, await evalBlock(`a = 3`, env));
|
||||||
|
assertEquals(env.get("a"), 3);
|
||||||
|
assertEquals(undefined, await evalBlock(`b = test(3)`, env));
|
||||||
|
assertEquals(env.get("b"), 3);
|
||||||
|
|
||||||
|
await evalBlock(`c = asyncTest(3)`, env);
|
||||||
|
assertEquals(env.get("c"), 3);
|
||||||
|
|
||||||
|
// Multiple assignments
|
||||||
|
const env2 = new LuaEnv();
|
||||||
|
assertEquals(undefined, await evalBlock(`a, b = 1, 2`, env2));
|
||||||
|
assertEquals(env2.get("a"), 1);
|
||||||
|
assertEquals(env2.get("b"), 2);
|
||||||
|
|
||||||
|
// Other lvalues
|
||||||
|
const env3 = new LuaEnv();
|
||||||
|
await evalBlock(`tbl = {1, 2, 3}`, env3);
|
||||||
|
await evalBlock(`tbl[1] = 3`, env3);
|
||||||
|
assertEquals(env3.get("tbl").toArray(), [3, 2, 3]);
|
||||||
|
await evalBlock("tbl.name = 'Zef'", env3);
|
||||||
|
assertEquals(env3.get("tbl").get("name"), "Zef");
|
||||||
|
await evalBlock(`tbl[2] = {age=10}`, env3);
|
||||||
|
await evalBlock(`tbl[2].age = 20`, env3);
|
||||||
|
assertEquals(env3.get("tbl").get(2).get("age"), 20);
|
||||||
|
|
||||||
|
// Blocks and scopes
|
||||||
|
const env4 = new LuaEnv();
|
||||||
|
env4.set("print", new LuaNativeJSFunction(console.log));
|
||||||
|
await evalBlock(
|
||||||
|
`
|
||||||
|
a = 1
|
||||||
|
do
|
||||||
|
-- sets global a to 3
|
||||||
|
a = 3
|
||||||
|
print("The number is: " .. a)
|
||||||
|
end`,
|
||||||
|
env4,
|
||||||
|
);
|
||||||
|
assertEquals(env4.get("a"), 3);
|
||||||
|
|
||||||
|
const env5 = new LuaEnv();
|
||||||
|
env5.set("print", new LuaNativeJSFunction(console.log));
|
||||||
|
|
||||||
|
await evalBlock(
|
||||||
|
`
|
||||||
|
a = 1
|
||||||
|
if a > 0 then
|
||||||
|
a = 3
|
||||||
|
else
|
||||||
|
a = 0
|
||||||
|
end`,
|
||||||
|
env5,
|
||||||
|
);
|
||||||
|
assertEquals(env5.get("a"), 3);
|
||||||
|
|
||||||
|
await evalBlock(
|
||||||
|
`
|
||||||
|
if a < 0 then
|
||||||
|
a = -1
|
||||||
|
elseif a > 0 then
|
||||||
|
a = 1
|
||||||
|
else
|
||||||
|
a = 0
|
||||||
|
end`,
|
||||||
|
env5,
|
||||||
|
);
|
||||||
|
assertEquals(env5.get("a"), 1);
|
||||||
|
|
||||||
|
await evalBlock(
|
||||||
|
`
|
||||||
|
var = 1
|
||||||
|
do
|
||||||
|
local var
|
||||||
|
var = 2
|
||||||
|
end`,
|
||||||
|
env5,
|
||||||
|
);
|
||||||
|
assertEquals(env5.get("var"), 1);
|
||||||
|
|
||||||
|
// While loop
|
||||||
|
const env6 = new LuaEnv();
|
||||||
|
await evalBlock(
|
||||||
|
`
|
||||||
|
c = 0
|
||||||
|
while true do
|
||||||
|
c = c + 1
|
||||||
|
if c == 3 then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
`,
|
||||||
|
env6,
|
||||||
|
);
|
||||||
|
assertEquals(env6.get("c"), 3);
|
||||||
|
|
||||||
|
// Repeat loop
|
||||||
|
const env7 = new LuaEnv();
|
||||||
|
await evalBlock(
|
||||||
|
`
|
||||||
|
c = 0
|
||||||
|
repeat
|
||||||
|
c = c + 1
|
||||||
|
if c == 3 then
|
||||||
|
break
|
||||||
|
end
|
||||||
|
until false
|
||||||
|
`,
|
||||||
|
env7,
|
||||||
|
);
|
||||||
|
assertEquals(env7.get("c"), 3);
|
||||||
|
});
|
||||||
|
|
|
@ -1,18 +1,26 @@
|
||||||
import type { LuaExpression } from "$common/space_lua/ast.ts";
|
import type {
|
||||||
|
LuaExpression,
|
||||||
|
LuaLValue,
|
||||||
|
LuaStatement,
|
||||||
|
} from "$common/space_lua/ast.ts";
|
||||||
import { evalPromiseValues } from "$common/space_lua/util.ts";
|
import { evalPromiseValues } from "$common/space_lua/util.ts";
|
||||||
import {
|
import {
|
||||||
type ILuaFunction,
|
type ILuaFunction,
|
||||||
type LuaEnv,
|
LuaBreak,
|
||||||
|
LuaEnv,
|
||||||
luaGet,
|
luaGet,
|
||||||
luaLen,
|
luaLen,
|
||||||
|
type LuaLValueContainer,
|
||||||
LuaTable,
|
LuaTable,
|
||||||
|
luaTruthy,
|
||||||
|
type LuaValue,
|
||||||
singleResult,
|
singleResult,
|
||||||
} from "./runtime.ts";
|
} from "./runtime.ts";
|
||||||
|
|
||||||
export function evalExpression(
|
export function evalExpression(
|
||||||
e: LuaExpression,
|
e: LuaExpression,
|
||||||
env: LuaEnv,
|
env: LuaEnv,
|
||||||
): Promise<any> | any {
|
): Promise<LuaValue> | LuaValue {
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case "String":
|
case "String":
|
||||||
// TODO: Deal with escape sequences
|
// TODO: Deal with escape sequences
|
||||||
|
@ -101,13 +109,13 @@ export function evalExpression(
|
||||||
const value = evalExpression(field.value, env);
|
const value = evalExpression(field.value, env);
|
||||||
if (value instanceof Promise) {
|
if (value instanceof Promise) {
|
||||||
promises.push(value.then((value) => {
|
promises.push(value.then((value) => {
|
||||||
table.entries.set(
|
table.set(
|
||||||
field.key,
|
field.key,
|
||||||
singleResult(value),
|
singleResult(value),
|
||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
table.entries.set(field.key, singleResult(value));
|
table.set(field.key, singleResult(value));
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@ -126,14 +134,14 @@ export function evalExpression(
|
||||||
? value
|
? value
|
||||||
: Promise.resolve(value),
|
: Promise.resolve(value),
|
||||||
]).then(([key, value]) => {
|
]).then(([key, value]) => {
|
||||||
table.entries.set(
|
table.set(
|
||||||
singleResult(key),
|
singleResult(key),
|
||||||
singleResult(value),
|
singleResult(value),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
table.entries.set(
|
table.set(
|
||||||
singleResult(key),
|
singleResult(key),
|
||||||
singleResult(value),
|
singleResult(value),
|
||||||
);
|
);
|
||||||
|
@ -145,15 +153,15 @@ export function evalExpression(
|
||||||
if (value instanceof Promise) {
|
if (value instanceof Promise) {
|
||||||
promises.push(value.then((value) => {
|
promises.push(value.then((value) => {
|
||||||
// +1 because Lua tables are 1-indexed
|
// +1 because Lua tables are 1-indexed
|
||||||
table.entries.set(
|
table.set(
|
||||||
table.entries.size + 1,
|
table.length + 1,
|
||||||
singleResult(value),
|
singleResult(value),
|
||||||
);
|
);
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
// +1 because Lua tables are 1-indexed
|
// +1 because Lua tables are 1-indexed
|
||||||
table.entries.set(
|
table.set(
|
||||||
table.entries.size + 1,
|
table.length + 1,
|
||||||
singleResult(value),
|
singleResult(value),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -175,7 +183,7 @@ export function evalExpression(
|
||||||
function evalPrefixExpression(
|
function evalPrefixExpression(
|
||||||
e: LuaExpression,
|
e: LuaExpression,
|
||||||
env: LuaEnv,
|
env: LuaEnv,
|
||||||
): Promise<any> | any {
|
): Promise<LuaValue> | LuaValue {
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case "Variable": {
|
case "Variable": {
|
||||||
const value = env.get(e.name);
|
const value = env.get(e.name);
|
||||||
|
@ -262,3 +270,160 @@ function luaOp(op: string, left: any, right: any): any {
|
||||||
throw new Error(`Unknown operator ${op}`);
|
throw new Error(`Unknown operator ${op}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function evalStatement(
|
||||||
|
s: LuaStatement,
|
||||||
|
env: LuaEnv,
|
||||||
|
): Promise<void> {
|
||||||
|
switch (s.type) {
|
||||||
|
case "Assignment": {
|
||||||
|
const values = await evalPromiseValues(
|
||||||
|
s.expressions.map((value) => evalExpression(value, env)),
|
||||||
|
);
|
||||||
|
const lvalues = await evalPromiseValues(s.variables
|
||||||
|
.map((lval) => evalLValue(lval, env)));
|
||||||
|
|
||||||
|
for (let i = 0; i < lvalues.length; i++) {
|
||||||
|
lvalues[i].env.set(lvalues[i].key, values[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Local": {
|
||||||
|
for (let i = 0; i < s.names.length; i++) {
|
||||||
|
if (!s.expressions || s.expressions[i] === undefined) {
|
||||||
|
env.setLocal(s.names[i].name, null);
|
||||||
|
} else {
|
||||||
|
const value = await evalExpression(s.expressions[i], env);
|
||||||
|
env.setLocal(s.names[i].name, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Semicolon":
|
||||||
|
break;
|
||||||
|
case "Label":
|
||||||
|
case "Goto":
|
||||||
|
throw new Error("Labels and gotos are not supported yet");
|
||||||
|
case "Block": {
|
||||||
|
const newEnv = new LuaEnv(env);
|
||||||
|
for (const statement of s.statements) {
|
||||||
|
await evalStatement(statement, newEnv);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "If": {
|
||||||
|
for (const cond of s.conditions) {
|
||||||
|
if (luaTruthy(await evalExpression(cond.condition, env))) {
|
||||||
|
return evalStatement(cond.block, env);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (s.elseBlock) {
|
||||||
|
return evalStatement(s.elseBlock, env);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "While": {
|
||||||
|
while (luaTruthy(await evalExpression(s.condition, env))) {
|
||||||
|
try {
|
||||||
|
await evalStatement(s.block, env);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e instanceof LuaBreak) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Repeat": {
|
||||||
|
do {
|
||||||
|
try {
|
||||||
|
await evalStatement(s.block, env);
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e instanceof LuaBreak) {
|
||||||
|
break;
|
||||||
|
} else {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} while (!luaTruthy(await evalExpression(s.condition, env)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "Break":
|
||||||
|
throw new LuaBreak();
|
||||||
|
case "FunctionCallStatement": {
|
||||||
|
return evalExpression(s.call, env);
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown statement type ${s.type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function evalLValue(
|
||||||
|
lval: LuaLValue,
|
||||||
|
env: LuaEnv,
|
||||||
|
): LuaLValueContainer | Promise<LuaLValueContainer> {
|
||||||
|
switch (lval.type) {
|
||||||
|
case "Variable":
|
||||||
|
return { env, key: lval.name };
|
||||||
|
case "TableAccess": {
|
||||||
|
const objValue = evalExpression(
|
||||||
|
lval.object,
|
||||||
|
env,
|
||||||
|
);
|
||||||
|
const keyValue = evalExpression(lval.key, env);
|
||||||
|
if (
|
||||||
|
objValue instanceof Promise ||
|
||||||
|
keyValue instanceof Promise
|
||||||
|
) {
|
||||||
|
return Promise.all([
|
||||||
|
objValue instanceof Promise
|
||||||
|
? objValue
|
||||||
|
: Promise.resolve(objValue),
|
||||||
|
keyValue instanceof Promise
|
||||||
|
? keyValue
|
||||||
|
: Promise.resolve(keyValue),
|
||||||
|
]).then(([objValue, keyValue]) => ({
|
||||||
|
env: singleResult(objValue),
|
||||||
|
key: singleResult(keyValue),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
env: singleResult(objValue),
|
||||||
|
key: singleResult(keyValue),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "PropertyAccess": {
|
||||||
|
const objValue = evalExpression(
|
||||||
|
lval.object,
|
||||||
|
env,
|
||||||
|
);
|
||||||
|
if (objValue instanceof Promise) {
|
||||||
|
return objValue.then((objValue) => {
|
||||||
|
if (!objValue.set) {
|
||||||
|
throw new Error(
|
||||||
|
`Not a settable object: ${objValue}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
env: objValue,
|
||||||
|
key: lval.property,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (!objValue.set) {
|
||||||
|
throw new Error(
|
||||||
|
`Not a settable object: ${objValue}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
env: objValue,
|
||||||
|
key: lval.property,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -165,6 +165,8 @@ function parseStatement(n: CrudeAST): LuaStatement {
|
||||||
names: parseAttNames(t[2]),
|
names: parseAttNames(t[2]),
|
||||||
expressions: t[4] ? parseExpList(t[4]) : [],
|
expressions: t[4] ? parseExpList(t[4]) : [],
|
||||||
};
|
};
|
||||||
|
case "break":
|
||||||
|
return { type: "Break" };
|
||||||
default:
|
default:
|
||||||
console.error(t);
|
console.error(t);
|
||||||
throw new Error(`Unknown statement type: ${t[0]}`);
|
throw new Error(`Unknown statement type: ${t[0]}`);
|
||||||
|
@ -359,6 +361,12 @@ function parsePrefixExpression(n: CrudeAST): LuaPrefixExpression {
|
||||||
object: parsePrefixExpression(t[1]),
|
object: parsePrefixExpression(t[1]),
|
||||||
property: t[3][1] as string,
|
property: t[3][1] as string,
|
||||||
};
|
};
|
||||||
|
case "MemberExpression":
|
||||||
|
return {
|
||||||
|
type: "TableAccess",
|
||||||
|
object: parsePrefixExpression(t[1]),
|
||||||
|
key: parseExpression(t[3]),
|
||||||
|
};
|
||||||
case "Parens":
|
case "Parens":
|
||||||
return { type: "Parenthesized", expression: parseExpression(t[2]) };
|
return { type: "Parenthesized", expression: parseExpression(t[2]) };
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1,15 +1,24 @@
|
||||||
import type { LuaFunctionBody } from "./ast.ts";
|
import type { LuaFunctionBody } from "./ast.ts";
|
||||||
|
|
||||||
export class LuaEnv {
|
export class LuaEnv implements ILuaSettable {
|
||||||
variables = new Map<string, any>();
|
variables = new Map<string, LuaValue>();
|
||||||
|
|
||||||
constructor(readonly parent?: LuaEnv) {
|
constructor(readonly parent?: LuaEnv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
set(name: string, value: any) {
|
setLocal(name: string, value: LuaValue) {
|
||||||
this.variables.set(name, value);
|
this.variables.set(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(name: string): any {
|
set(key: string, value: LuaValue): void {
|
||||||
|
if (this.variables.has(key) || !this.parent) {
|
||||||
|
this.variables.set(key, value);
|
||||||
|
} else {
|
||||||
|
this.parent.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get(name: string): LuaValue {
|
||||||
if (this.variables.has(name)) {
|
if (this.variables.has(name)) {
|
||||||
return this.variables.get(name);
|
return this.variables.get(name);
|
||||||
}
|
}
|
||||||
|
@ -40,71 +49,130 @@ export function singleResult(value: any): any {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These types are for documentation only
|
||||||
|
export type LuaValue = any;
|
||||||
|
export type JSValue = any;
|
||||||
|
|
||||||
export interface ILuaFunction {
|
export interface ILuaFunction {
|
||||||
call(...args: any[]): Promise<LuaMultiRes> | LuaMultiRes;
|
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILuaSettable {
|
||||||
|
set(key: LuaValue, value: LuaValue): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LuaFunction implements ILuaFunction {
|
export class LuaFunction implements ILuaFunction {
|
||||||
constructor(private body: LuaFunctionBody, private closure: LuaEnv) {
|
constructor(private body: LuaFunctionBody, private closure: LuaEnv) {
|
||||||
}
|
}
|
||||||
|
|
||||||
call(..._args: any[]): Promise<LuaMultiRes> | LuaMultiRes {
|
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue {
|
||||||
|
// Create a new environment for this function call
|
||||||
|
const env = new LuaEnv(this.closure);
|
||||||
|
// Assign the passed arguments to the parameters
|
||||||
|
for (let i = 0; i < this.body.parameters.length; i++) {
|
||||||
|
let arg = args[i];
|
||||||
|
if (arg === undefined) {
|
||||||
|
arg = null;
|
||||||
|
}
|
||||||
|
env.set(this.body.parameters[i], arg);
|
||||||
|
}
|
||||||
throw new Error("Not yet implemented funciton call");
|
throw new Error("Not yet implemented funciton call");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LuaNativeJSFunction implements ILuaFunction {
|
export class LuaNativeJSFunction implements ILuaFunction {
|
||||||
constructor(readonly fn: (...args: any[]) => any) {
|
constructor(readonly fn: (...args: JSValue[]) => JSValue) {
|
||||||
}
|
}
|
||||||
|
|
||||||
call(...args: any[]): Promise<LuaMultiRes> | LuaMultiRes {
|
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue {
|
||||||
const result = this.fn(...args);
|
const result = this.fn(...args.map(luaValueToJS));
|
||||||
if (result instanceof Promise) {
|
if (result instanceof Promise) {
|
||||||
return result.then((result) => new LuaMultiRes([result]));
|
return result.then(jsToLuaValue);
|
||||||
} else {
|
} else {
|
||||||
return new LuaMultiRes([result]);
|
return jsToLuaValue(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class LuaTable {
|
export class LuaTable implements ILuaSettable {
|
||||||
constructor(readonly entries: Map<any, any> = new Map()) {
|
// To optimize the table implementation we use a combination of different data structures
|
||||||
|
// When tables are used as maps, the common case is that they are string keys, so we use a simple object for that
|
||||||
|
private stringKeys: Record<string, any>;
|
||||||
|
// Other keys we can support using a Map as a fallback
|
||||||
|
private otherKeys: Map<any, any> | null;
|
||||||
|
// When tables are used as arrays, we use a native JavaScript array for that
|
||||||
|
private arrayPart: any[];
|
||||||
|
|
||||||
|
// TODO: Actually implement metatables
|
||||||
|
private metatable: LuaTable | null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// For efficiency and performance reasons we pre-allocate these (modern JS engines are very good at optimizing this)
|
||||||
|
this.stringKeys = {};
|
||||||
|
this.arrayPart = [];
|
||||||
|
this.otherKeys = null; // Only create this when needed
|
||||||
|
this.metatable = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key: any): any {
|
get length(): number {
|
||||||
return this.entries.get(key);
|
return this.arrayPart.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
set(key: any, value: any) {
|
set(key: LuaValue, value: LuaValue) {
|
||||||
this.entries.set(key, value);
|
if (typeof key === "string") {
|
||||||
|
this.stringKeys[key] = value;
|
||||||
|
} else if (Number.isInteger(key) && key >= 1) {
|
||||||
|
this.arrayPart[key - 1] = value;
|
||||||
|
} else {
|
||||||
|
if (!this.otherKeys) {
|
||||||
|
this.otherKeys = new Map();
|
||||||
|
}
|
||||||
|
this.otherKeys.set(key, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
get(key: LuaValue): LuaValue {
|
||||||
* Convert the table to a a JavaScript array, assuming it uses integer keys
|
if (typeof key === "string") {
|
||||||
* @returns
|
return this.stringKeys[key];
|
||||||
*/
|
} else if (Number.isInteger(key) && key >= 1) {
|
||||||
toArray(): any[] {
|
return this.arrayPart[key - 1];
|
||||||
const result = [];
|
} else if (this.otherKeys) {
|
||||||
const keys = Array.from(this.entries.keys()).sort();
|
return this.otherKeys.get(key);
|
||||||
for (const key of keys) {
|
}
|
||||||
result.push(this.entries.get(key));
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
toArray(): JSValue[] {
|
||||||
|
return this.arrayPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
toObject(): Record<string, JSValue> {
|
||||||
|
const result = { ...this.stringKeys };
|
||||||
|
for (const i in this.arrayPart) {
|
||||||
|
result[parseInt(i) + 1] = this.arrayPart[i];
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
static fromArray(arr: JSValue[]): LuaTable {
|
||||||
* Convert the table to a JavaScript object, assuming it uses string keys
|
const table = new LuaTable();
|
||||||
* @returns
|
for (let i = 0; i < arr.length; i++) {
|
||||||
*/
|
table.set(i + 1, arr[i]);
|
||||||
toObject(): Record<string, any> {
|
|
||||||
const result: Record<string, any> = {};
|
|
||||||
for (const [key, value] of this.entries.entries()) {
|
|
||||||
result[key] = value;
|
|
||||||
}
|
}
|
||||||
return result;
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromObject(obj: Record<string, JSValue>): LuaTable {
|
||||||
|
const table = new LuaTable();
|
||||||
|
for (const key in obj) {
|
||||||
|
table.set(key, obj[key]);
|
||||||
|
}
|
||||||
|
return table;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type LuaLValueContainer = { env: ILuaSettable; key: LuaValue };
|
||||||
|
|
||||||
export function luaSet(obj: any, key: any, value: any) {
|
export function luaSet(obj: any, key: any, value: any) {
|
||||||
if (obj instanceof LuaTable) {
|
if (obj instanceof LuaTable) {
|
||||||
obj.set(key, value);
|
obj.set(key, value);
|
||||||
|
@ -130,3 +198,41 @@ export function luaLen(obj: any): number {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class LuaBreak extends Error {
|
||||||
|
}
|
||||||
|
|
||||||
|
export function luaTruthy(value: any): boolean {
|
||||||
|
if (value === undefined || value === null || value === false) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (value instanceof LuaTable) {
|
||||||
|
return value.length > 0;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function jsToLuaValue(value: any): any {
|
||||||
|
if (value instanceof LuaTable) {
|
||||||
|
return value;
|
||||||
|
} else if (Array.isArray(value)) {
|
||||||
|
return LuaTable.fromArray(value.map(jsToLuaValue));
|
||||||
|
} else if (typeof value === "object") {
|
||||||
|
return LuaTable.fromObject(value);
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function luaValueToJS(value: any): any {
|
||||||
|
if (value instanceof LuaTable) {
|
||||||
|
// This is a heuristic: if this table is used as an array, we return an array
|
||||||
|
if (value.length > 0) {
|
||||||
|
return value.toArray();
|
||||||
|
} else {
|
||||||
|
return value.toObject();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -226,7 +226,7 @@ export function parseTreeToAST(tree: ParseTree, omitTrimmable = true): AST {
|
||||||
}
|
}
|
||||||
const ast: AST = [tree.type!];
|
const ast: AST = [tree.type!];
|
||||||
for (const node of tree.children!) {
|
for (const node of tree.children!) {
|
||||||
if (node.type && !node.type.endsWith("Mark")) {
|
if (node.type && !node.type.endsWith("Mark") && node.type !== "Comment") {
|
||||||
ast.push(parseTreeToAST(node, omitTrimmable));
|
ast.push(parseTreeToAST(node, omitTrimmable));
|
||||||
}
|
}
|
||||||
if (node.text && (omitTrimmable && node.text.trim() || !omitTrimmable)) {
|
if (node.text && (omitTrimmable && node.text.trim() || !omitTrimmable)) {
|
||||||
|
|
Loading…
Reference in New Issue