More work on Lua

pull/1102/head
Zef Hemel 2024-09-27 09:11:03 +02:00
parent f08c66d305
commit aa712ed8f4
5 changed files with 462 additions and 54 deletions

View File

@ -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);
});

View File

@ -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,
};
}
}
}
}

View File

@ -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:

View File

@ -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;
}
}

View File

@ -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)) {