pull/1110/head
Zef Hemel 2024-09-30 12:50:54 +02:00
parent a5c4bcc43b
commit c0a248daba
11 changed files with 1070 additions and 1075 deletions

View File

@ -1,252 +1,252 @@
type ASTPosition = { type ASTPosition = {
from?: number; from?: number;
to?: number; to?: number;
}; };
export type LuaBlock = { export type LuaBlock = {
type: "Block"; type: "Block";
statements: LuaStatement[]; statements: LuaStatement[];
} & ASTPosition; } & ASTPosition;
// STATEMENTS // STATEMENTS
export type LuaReturnStatement = { export type LuaReturnStatement = {
type: "Return"; type: "Return";
expressions: LuaExpression[]; expressions: LuaExpression[];
} & ASTPosition; } & ASTPosition;
export type LuaStatement = export type LuaStatement =
| LuaSemicolonStatement | LuaSemicolonStatement
| LuaLabelStatement | LuaLabelStatement
| LuaBreakStatement | LuaBreakStatement
| LuaGotoStatement | LuaGotoStatement
| LuaReturnStatement | LuaReturnStatement
| LuaBlock | LuaBlock
| LuaWhileStatement | LuaWhileStatement
| LuaRepeatStatement | LuaRepeatStatement
| LuaIfStatement | LuaIfStatement
| LuaForStatement | LuaForStatement
| LuaForInStatement | LuaForInStatement
| LuaFunctionStatement | LuaFunctionStatement
| LuaLocalFunctionStatement | LuaLocalFunctionStatement
| LuaAssignmentStatement | LuaAssignmentStatement
| LuaLocalStatement | LuaLocalStatement
| LuaFunctionCallStatement; | LuaFunctionCallStatement;
export type LuaSemicolonStatement = { export type LuaSemicolonStatement = {
type: "Semicolon"; type: "Semicolon";
} & ASTPosition; } & ASTPosition;
export type LuaLabelStatement = { export type LuaLabelStatement = {
type: "Label"; type: "Label";
name: string; name: string;
} & ASTPosition; } & ASTPosition;
export type LuaBreakStatement = { export type LuaBreakStatement = {
type: "Break"; type: "Break";
} & ASTPosition; } & ASTPosition;
export type LuaGotoStatement = { export type LuaGotoStatement = {
type: "Goto"; type: "Goto";
name: string; name: string;
} & ASTPosition; } & ASTPosition;
export type LuaWhileStatement = { export type LuaWhileStatement = {
type: "While"; type: "While";
condition: LuaExpression; condition: LuaExpression;
block: LuaBlock; block: LuaBlock;
} & ASTPosition; } & ASTPosition;
export type LuaRepeatStatement = { export type LuaRepeatStatement = {
type: "Repeat"; type: "Repeat";
block: LuaBlock; block: LuaBlock;
condition: LuaExpression; condition: LuaExpression;
} & ASTPosition; } & ASTPosition;
export type LuaIfStatement = { export type LuaIfStatement = {
type: "If"; type: "If";
conditions: { condition: LuaExpression; block: LuaBlock }[]; conditions: { condition: LuaExpression; block: LuaBlock }[];
elseBlock?: LuaBlock; elseBlock?: LuaBlock;
} & ASTPosition; } & ASTPosition;
export type LuaForStatement = { export type LuaForStatement = {
type: "For"; type: "For";
name: string; name: string;
start: LuaExpression; start: LuaExpression;
end: LuaExpression; end: LuaExpression;
step?: LuaExpression; step?: LuaExpression;
block: LuaBlock; block: LuaBlock;
} & ASTPosition; } & ASTPosition;
export type LuaForInStatement = { export type LuaForInStatement = {
type: "ForIn"; type: "ForIn";
names: string[]; names: string[];
expressions: LuaExpression[]; expressions: LuaExpression[];
block: LuaBlock; block: LuaBlock;
} & ASTPosition; } & ASTPosition;
export type LuaFunctionStatement = { export type LuaFunctionStatement = {
type: "Function"; type: "Function";
name: LuaFunctionName; name: LuaFunctionName;
body: LuaFunctionBody; body: LuaFunctionBody;
} & ASTPosition; } & ASTPosition;
export type LuaLocalFunctionStatement = { export type LuaLocalFunctionStatement = {
type: "LocalFunction"; type: "LocalFunction";
name: string; name: string;
body: LuaFunctionBody; body: LuaFunctionBody;
} & ASTPosition; } & ASTPosition;
export type LuaFunctionName = { export type LuaFunctionName = {
type: "FunctionName"; type: "FunctionName";
propNames: string[]; propNames: string[];
colonName?: string; colonName?: string;
} & ASTPosition; } & ASTPosition;
export type LuaFunctionBody = { export type LuaFunctionBody = {
type: "FunctionBody"; type: "FunctionBody";
parameters: string[]; parameters: string[];
block: LuaBlock; block: LuaBlock;
} & ASTPosition; } & ASTPosition;
export type LuaAssignmentStatement = { export type LuaAssignmentStatement = {
type: "Assignment"; type: "Assignment";
variables: LuaLValue[]; variables: LuaLValue[];
expressions: LuaExpression[]; expressions: LuaExpression[];
} & ASTPosition; } & ASTPosition;
export type LuaLValue = export type LuaLValue =
| LuaVariable | LuaVariable
| LuaPropertyAccessExpression | LuaPropertyAccessExpression
| LuaTableAccessExpression; | LuaTableAccessExpression;
export type LuaLocalStatement = { export type LuaLocalStatement = {
type: "Local"; type: "Local";
names: LuaAttName[]; names: LuaAttName[];
expressions?: LuaExpression[]; expressions?: LuaExpression[];
} & ASTPosition; } & ASTPosition;
export type LuaAttName = { export type LuaAttName = {
type: "AttName"; type: "AttName";
name: string; name: string;
attribute?: string; attribute?: string;
} & ASTPosition; } & ASTPosition;
export type LuaFunctionCallStatement = { export type LuaFunctionCallStatement = {
type: "FunctionCallStatement"; type: "FunctionCallStatement";
call: LuaFunctionCallExpression; call: LuaFunctionCallExpression;
} & ASTPosition; } & ASTPosition;
// EXPRESSIONS // EXPRESSIONS
export type LuaExpression = export type LuaExpression =
| LuaNilLiteral | LuaNilLiteral
| LuaBooleanLiteral | LuaBooleanLiteral
| LuaNumberLiteral | LuaNumberLiteral
| LuaStringLiteral | LuaStringLiteral
| LuaPrefixExpression | LuaPrefixExpression
| LuaBinaryExpression | LuaBinaryExpression
| LuaUnaryExpression | LuaUnaryExpression
| LuaTableConstructor | LuaTableConstructor
| LuaFunctionDefinition; | LuaFunctionDefinition;
export type LuaNilLiteral = { export type LuaNilLiteral = {
type: "Nil"; type: "Nil";
} & ASTPosition; } & ASTPosition;
export type LuaBooleanLiteral = { export type LuaBooleanLiteral = {
type: "Boolean"; type: "Boolean";
value: boolean; value: boolean;
} & ASTPosition; } & ASTPosition;
export type LuaNumberLiteral = { export type LuaNumberLiteral = {
type: "Number"; type: "Number";
value: number; value: number;
} & ASTPosition; } & ASTPosition;
export type LuaStringLiteral = { export type LuaStringLiteral = {
type: "String"; type: "String";
value: string; value: string;
} & ASTPosition; } & ASTPosition;
export type LuaPrefixExpression = export type LuaPrefixExpression =
| LuaVariableExpression | LuaVariableExpression
| LuaParenthesizedExpression | LuaParenthesizedExpression
| LuaFunctionCallExpression; | LuaFunctionCallExpression;
export type LuaParenthesizedExpression = { export type LuaParenthesizedExpression = {
type: "Parenthesized"; type: "Parenthesized";
expression: LuaExpression; expression: LuaExpression;
} & ASTPosition; } & ASTPosition;
export type LuaVariableExpression = export type LuaVariableExpression =
| LuaVariable | LuaVariable
| LuaPropertyAccessExpression | LuaPropertyAccessExpression
| LuaTableAccessExpression; | LuaTableAccessExpression;
export type LuaVariable = { export type LuaVariable = {
type: "Variable"; type: "Variable";
name: string; name: string;
} & ASTPosition; } & ASTPosition;
export type LuaPropertyAccessExpression = { export type LuaPropertyAccessExpression = {
type: "PropertyAccess"; type: "PropertyAccess";
object: LuaPrefixExpression; object: LuaPrefixExpression;
property: string; property: string;
} & ASTPosition; } & ASTPosition;
export type LuaTableAccessExpression = { export type LuaTableAccessExpression = {
type: "TableAccess"; type: "TableAccess";
object: LuaPrefixExpression; object: LuaPrefixExpression;
key: LuaExpression; key: LuaExpression;
} & ASTPosition; } & ASTPosition;
export type LuaFunctionCallExpression = { export type LuaFunctionCallExpression = {
type: "FunctionCall"; type: "FunctionCall";
prefix: LuaPrefixExpression; prefix: LuaPrefixExpression;
name?: string; name?: string;
args: LuaExpression[]; args: LuaExpression[];
} & ASTPosition; } & ASTPosition;
export type LuaBinaryExpression = { export type LuaBinaryExpression = {
type: "Binary"; type: "Binary";
operator: string; operator: string;
left: LuaExpression; left: LuaExpression;
right: LuaExpression; right: LuaExpression;
} & ASTPosition; } & ASTPosition;
export type LuaUnaryExpression = { export type LuaUnaryExpression = {
type: "Unary"; type: "Unary";
operator: string; operator: string;
argument: LuaExpression; argument: LuaExpression;
} & ASTPosition; } & ASTPosition;
export type LuaTableConstructor = { export type LuaTableConstructor = {
type: "TableConstructor"; type: "TableConstructor";
fields: LuaTableField[]; fields: LuaTableField[];
} & ASTPosition; } & ASTPosition;
export type LuaTableField = export type LuaTableField =
| LuaDynamicField | LuaDynamicField
| LuaPropField | LuaPropField
| LuaExpressionField; | LuaExpressionField;
export type LuaDynamicField = { export type LuaDynamicField = {
type: "DynamicField"; type: "DynamicField";
key: LuaExpression; key: LuaExpression;
value: LuaExpression; value: LuaExpression;
} & ASTPosition; } & ASTPosition;
export type LuaPropField = { export type LuaPropField = {
type: "PropField"; type: "PropField";
key: string; key: string;
value: LuaExpression; value: LuaExpression;
} & ASTPosition; } & ASTPosition;
export type LuaExpressionField = { export type LuaExpressionField = {
type: "ExpressionField"; type: "ExpressionField";
value: LuaExpression; value: LuaExpression;
} & ASTPosition; } & ASTPosition;
export type LuaFunctionDefinition = { export type LuaFunctionDefinition = {
type: "FunctionDefinition"; type: "FunctionDefinition";
body: LuaFunctionBody; body: LuaFunctionBody;
} & ASTPosition; } & ASTPosition;

View File

@ -5,133 +5,133 @@ import type { LuaBlock, LuaFunctionCallStatement } from "./ast.ts";
import { evalExpression, evalStatement } 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(
(parse(`e(${s})`).statements[0] as LuaFunctionCallStatement).call (parse(`e(${s})`).statements[0] as LuaFunctionCallStatement).call
.args[0], .args[0],
e, e,
); );
} }
function evalBlock(s: string, e = new LuaEnv()): Promise<void> { function evalBlock(s: string, e = new LuaEnv()): Promise<void> {
return evalStatement(parse(s) as LuaBlock, e); 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));
env.set("asyncTest", new LuaNativeJSFunction((n) => Promise.resolve(n))); env.set("asyncTest", new LuaNativeJSFunction((n) => Promise.resolve(n)));
// Basic arithmetic // Basic arithmetic
assertEquals(evalExpr(`1 + 2 + 3 - 3`), 3); assertEquals(evalExpr(`1 + 2 + 3 - 3`), 3);
assertEquals(evalExpr(`4 // 3`), 1); assertEquals(evalExpr(`4 // 3`), 1);
assertEquals(evalExpr(`4 % 3`), 1); assertEquals(evalExpr(`4 % 3`), 1);
// Strings // Strings
assertEquals(evalExpr(`"a" .. "b"`), "ab"); assertEquals(evalExpr(`"a" .. "b"`), "ab");
// Logic // Logic
assertEquals(evalExpr(`true and false`), false); assertEquals(evalExpr(`true and false`), false);
assertEquals(evalExpr(`true or false`), true); assertEquals(evalExpr(`true or false`), true);
assertEquals(evalExpr(`not true`), false); assertEquals(evalExpr(`not true`), false);
// Tables // Tables
const tbl = evalExpr(`{3, 1, 2}`); const tbl = evalExpr(`{3, 1, 2}`);
assertEquals(tbl.get(1), 3); assertEquals(tbl.get(1), 3);
assertEquals(tbl.get(2), 1); assertEquals(tbl.get(2), 1);
assertEquals(tbl.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(), {
name: "Zef", name: "Zef",
age: 100, age: 100,
}); });
assertEquals( assertEquals(
(await evalExpr(`{name="Zef", age=asyncTest(100)}`, env)).toObject(), (await evalExpr(`{name="Zef", age=asyncTest(100)}`, env)).toObject(),
{ {
name: "Zef", name: "Zef",
age: 100, age: 100,
}, },
); );
assertEquals(evalExpr(`{[3+2]=1, ["a".."b"]=2}`).toObject(), { assertEquals(evalExpr(`{[3+2]=1, ["a".."b"]=2}`).toObject(), {
5: 1, 5: 1,
ab: 2, ab: 2,
}); });
assertEquals(evalExpr(`#{}`), 0); assertEquals(evalExpr(`#{}`), 0);
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 // 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 () => { Deno.test("Statement evaluation", async () => {
const env = new LuaEnv(); const env = new LuaEnv();
env.set("test", new LuaNativeJSFunction((n) => n)); env.set("test", new LuaNativeJSFunction((n) => n));
env.set("asyncTest", new LuaNativeJSFunction((n) => Promise.resolve(n))); env.set("asyncTest", new LuaNativeJSFunction((n) => Promise.resolve(n)));
assertEquals(undefined, await evalBlock(`a = 3`, env)); assertEquals(undefined, await evalBlock(`a = 3`, env));
assertEquals(env.get("a"), 3); assertEquals(env.get("a"), 3);
assertEquals(undefined, await evalBlock(`b = test(3)`, env)); assertEquals(undefined, await evalBlock(`b = test(3)`, env));
assertEquals(env.get("b"), 3); assertEquals(env.get("b"), 3);
await evalBlock(`c = asyncTest(3)`, env); await evalBlock(`c = asyncTest(3)`, env);
assertEquals(env.get("c"), 3); assertEquals(env.get("c"), 3);
// Multiple assignments // Multiple assignments
const env2 = new LuaEnv(); const env2 = new LuaEnv();
assertEquals(undefined, await evalBlock(`a, b = 1, 2`, env2)); assertEquals(undefined, await evalBlock(`a, b = 1, 2`, env2));
assertEquals(env2.get("a"), 1); assertEquals(env2.get("a"), 1);
assertEquals(env2.get("b"), 2); assertEquals(env2.get("b"), 2);
// Other lvalues // Other lvalues
const env3 = new LuaEnv(); const env3 = new LuaEnv();
await evalBlock(`tbl = {1, 2, 3}`, env3); await evalBlock(`tbl = {1, 2, 3}`, env3);
await evalBlock(`tbl[1] = 3`, env3); await evalBlock(`tbl[1] = 3`, env3);
assertEquals(env3.get("tbl").toArray(), [3, 2, 3]); assertEquals(env3.get("tbl").toArray(), [3, 2, 3]);
await evalBlock("tbl.name = 'Zef'", env3); await evalBlock("tbl.name = 'Zef'", env3);
assertEquals(env3.get("tbl").get("name"), "Zef"); assertEquals(env3.get("tbl").get("name"), "Zef");
await evalBlock(`tbl[2] = {age=10}`, env3); await evalBlock(`tbl[2] = {age=10}`, env3);
await evalBlock(`tbl[2].age = 20`, env3); await evalBlock(`tbl[2].age = 20`, env3);
assertEquals(env3.get("tbl").get(2).get("age"), 20); assertEquals(env3.get("tbl").get(2).get("age"), 20);
// Blocks and scopes // Blocks and scopes
const env4 = new LuaEnv(); const env4 = new LuaEnv();
env4.set("print", new LuaNativeJSFunction(console.log)); env4.set("print", new LuaNativeJSFunction(console.log));
await evalBlock( await evalBlock(
` `
a = 1 a = 1
do do
-- sets global a to 3 -- sets global a to 3
a = 3 a = 3
print("The number is: " .. a) print("The number is: " .. a)
end`, end`,
env4, env4,
); );
assertEquals(env4.get("a"), 3); assertEquals(env4.get("a"), 3);
const env5 = new LuaEnv(); const env5 = new LuaEnv();
env5.set("print", new LuaNativeJSFunction(console.log)); env5.set("print", new LuaNativeJSFunction(console.log));
await evalBlock( await evalBlock(
` `
a = 1 a = 1
if a > 0 then if a > 0 then
a = 3 a = 3
else else
a = 0 a = 0
end`, end`,
env5, env5,
); );
assertEquals(env5.get("a"), 3); assertEquals(env5.get("a"), 3);
await evalBlock( await evalBlock(
` `
if a < 0 then if a < 0 then
a = -1 a = -1
elseif a > 0 then elseif a > 0 then
@ -139,25 +139,25 @@ Deno.test("Statement evaluation", async () => {
else else
a = 0 a = 0
end`, end`,
env5, env5,
); );
assertEquals(env5.get("a"), 1); assertEquals(env5.get("a"), 1);
await evalBlock( await evalBlock(
` `
var = 1 var = 1
do do
local var local var
var = 2 var = 2
end`, end`,
env5, env5,
); );
assertEquals(env5.get("var"), 1); assertEquals(env5.get("var"), 1);
// While loop // While loop
const env6 = new LuaEnv(); const env6 = new LuaEnv();
await evalBlock( await evalBlock(
` `
c = 0 c = 0
while true do while true do
c = c + 1 c = c + 1
@ -166,14 +166,14 @@ Deno.test("Statement evaluation", async () => {
end end
end end
`, `,
env6, env6,
); );
assertEquals(env6.get("c"), 3); assertEquals(env6.get("c"), 3);
// Repeat loop // Repeat loop
const env7 = new LuaEnv(); const env7 = new LuaEnv();
await evalBlock( await evalBlock(
` `
c = 0 c = 0
repeat repeat
c = c + 1 c = c + 1
@ -182,20 +182,20 @@ Deno.test("Statement evaluation", async () => {
end end
until false until false
`, `,
env7, env7,
); );
assertEquals(env7.get("c"), 3); assertEquals(env7.get("c"), 3);
// Function definition and calling // Function definition and calling
const env8 = new LuaEnv(); const env8 = new LuaEnv();
env8.set("print", new LuaNativeJSFunction(console.log)); env8.set("print", new LuaNativeJSFunction(console.log));
await evalBlock( await evalBlock(
` `
function test(a) function test(a)
return a + 1 return a + 1
end end
print("3 + 1 = " .. test(3)) print("3 + 1 = " .. test(3))
`, `,
env8, env8,
); );
}); });

View File

@ -1,86 +1,85 @@
import { parse } from "$common/space_lua/parse.ts"; import { parse } from "$common/space_lua/parse.ts";
Deno.test("Test Lua parser", () => { Deno.test("Test Lua parser", () => {
// Basic block test // Basic block test
parse(` parse(`
print("Hello, World!") print("Hello, World!")
print(10) print(10)
`); `);
parse(""); parse("");
// Expression tests // Expression tests
parse( parse(
`e(1, 1.2, -3.8, +4, #lst, true, false, nil, "string", "Hello there \x00", ...)`, `e(1, 1.2, -3.8, +4, #lst, true, false, nil, "string", "Hello there \x00", ...)`,
); );
parse(`e(10 << 10, 10 >> 10, 10 & 10, 10 | 10, 10 ~ 10)`); parse(`e(10 << 10, 10 >> 10, 10 & 10, 10 | 10, 10 ~ 10)`);
parse(`e(true and false or true)`); parse(`e(true and false or true)`);
parse(`e(a < 3 and b > 4 or b == 5 or c <= 6 and d >= 7 or a /= 8)`); parse(`e(a < 3 and b > 4 or b == 5 or c <= 6 and d >= 7 or a /= 8)`);
parse(`e(a.b.c)`); parse(`e(a.b.c)`);
parse(`e((1+2))`); parse(`e((1+2))`);
// Table expressions // Table expressions
parse(`e({})`); parse(`e({})`);
parse(`e({1, 2, 3, })`); parse(`e({1, 2, 3, })`);
parse(`e({1 ; 2 ; 3})`); parse(`e({1 ; 2 ; 3})`);
parse(`e({a = 1, b = 2, c = 3})`); parse(`e({a = 1, b = 2, c = 3})`);
parse(`e({[3] = 1, [10 * 10] = "sup"})`); parse(`e({[3] = 1, [10 * 10] = "sup"})`);
// Function calls // Function calls
parse(`e(func(), func(1, 2, 3), a.b(), a.b.c:hello(), (a.b)(7))`); parse(`e(func(), func(1, 2, 3), a.b(), a.b.c:hello(), (a.b)(7))`);
// Function expression // Function expression
parse(`e(function(a, b) test() end)`); parse(`e(function(a, b) test() end)`);
parse(`e(function(a, b, ...) end)`); parse(`e(function(a, b, ...) end)`);
// Statements // Statements
parse(`do end`); parse(`do end`);
parse(`do print() end`); parse(`do print() end`);
parse(`::hello:: parse(`::hello::
goto hello`); goto hello`);
parse(`while true do print() end`); parse(`while true do print() end`);
parse(`repeat print() until false`); parse(`repeat print() until false`);
parse( parse(
`if 1 == 2 then print() elseif 1 < 2 then print2() else print3() end`, `if 1 == 2 then print() elseif 1 < 2 then print2() else print3() end`,
); );
parse(`if true then print() end`); parse(`if true then print() end`);
parse(`if true then print() else print2() end`); parse(`if true then print() else print2() end`);
parse(`if true then print() elseif false then print2() end`); parse(`if true then print() elseif false then print2() end`);
// For loops // For loops
parse(`for i = 1, 10, 1 do print(i) end`); parse(`for i = 1, 10, 1 do print(i) end`);
parse(`for i = 1, 10 do print(i) end`); parse(`for i = 1, 10 do print(i) end`);
parse(`for el in each({1, 2, 3}) do print(i) end`); parse(`for el in each({1, 2, 3}) do print(i) end`);
parse(`for i, l in 1, pairs() do print(i) end`); parse(`for i, l in 1, pairs() do print(i) end`);
// Function statements // Function statements
parse(`function a() end`); parse(`function a() end`);
parse(`function a:b() end`); parse(`function a:b() end`);
parse(`function a.b.c:d() end`); parse(`function a.b.c:d() end`);
parse(`function a.b.c() end`); parse(`function a.b.c() end`);
parse(`function hello(a, b) end`); parse(`function hello(a, b) end`);
parse(`function hello(a, b, ...) end`); parse(`function hello(a, b, ...) end`);
parse(`local function hello() end`); parse(`local function hello() end`);
// Assignments, local variables etc. // Assignments, local variables etc.
parse(`a = 1`); parse(`a = 1`);
parse(`a, b = 1, 2`); parse(`a, b = 1, 2`);
parse(`a.b.c = 1`); parse(`a.b.c = 1`);
parse(`a["name"] = 1`); parse(`a["name"] = 1`);
parse(`local a, b<const>`); parse(`local a, b<const>`);
parse(`local a = 1`); parse(`local a = 1`);
parse(`local a<const> = 4`); parse(`local a<const> = 4`);
parse(`local a, b = 1, 2`); parse(`local a, b = 1, 2`);
// Function calls // Function calls
parse(`a(1, 2, 3)`); parse(`a(1, 2, 3)`);
parse(`print "Sup"`); parse(`print "Sup"`);
parse(`e(1 + print "8")`); parse(`e(1 + print "8")`);
// Return statements // Return statements
parse(`return`); parse(`return`);
parse(`return 1`); parse(`return 1`);
parse(`return 1, 2, 3`); parse(`return 1, 2, 3`);
// return; // return;
}); });

View File

@ -1,519 +1,515 @@
import { lezerToParseTree } from "$common/markdown_parser/parse_tree.ts"; import { lezerToParseTree } from "$common/markdown_parser/parse_tree.ts";
import { import {
cleanTree, cleanTree,
type ParseTree, type ParseTree,
} from "@silverbulletmd/silverbullet/lib/tree"; } from "@silverbulletmd/silverbullet/lib/tree";
import { parser } from "./parse-lua.js"; import { parser } from "./parse-lua.js";
import { styleTags } from "@lezer/highlight"; import { styleTags } from "@lezer/highlight";
import type { import type {
LuaAttName, LuaAttName,
LuaBlock, LuaBlock,
LuaExpression, LuaExpression,
LuaFunctionBody, LuaFunctionBody,
LuaFunctionCallExpression, LuaFunctionCallExpression,
LuaFunctionName, LuaFunctionName,
LuaLValue, LuaLValue,
LuaPrefixExpression, LuaPrefixExpression,
LuaStatement, LuaStatement,
LuaTableField, LuaTableField,
} from "./ast.ts"; } from "./ast.ts";
const luaStyleTags = styleTags({ const luaStyleTags = styleTags({
// Identifier: t.variableName, // Identifier: t.variableName,
// TagIdentifier: t.variableName, // TagIdentifier: t.variableName,
// GlobalIdentifier: t.variableName, // GlobalIdentifier: t.variableName,
// String: t.string, // String: t.string,
// Number: t.number, // Number: t.number,
// PageRef: ct.WikiLinkTag, // PageRef: ct.WikiLinkTag,
// BinExpression: t.operator, // BinExpression: t.operator,
// TernaryExpression: t.operator, // TernaryExpression: t.operator,
// Regex: t.regexp, // Regex: t.regexp,
// "where limit select render Order OrderKW and or null as InKW NotKW BooleanKW each all": // "where limit select render Order OrderKW and or null as InKW NotKW BooleanKW each all":
// t.keyword, // t.keyword,
}); });
export const highlightingQueryParser = parser.configure({ export const highlightingQueryParser = parser.configure({
props: [ props: [
luaStyleTags, luaStyleTags,
], ],
}); });
function parseChunk(t: ParseTree): LuaBlock { function parseChunk(t: ParseTree): LuaBlock {
if (t.type !== "Chunk") { if (t.type !== "Chunk") {
throw new Error(`Expected Chunk, got ${t.type}`); throw new Error(`Expected Chunk, got ${t.type}`);
} }
return parseBlock(t.children![0]); return parseBlock(t.children![0]);
} }
function parseBlock(t: ParseTree): LuaBlock { function parseBlock(t: ParseTree): LuaBlock {
if (t.type !== "Block") { if (t.type !== "Block") {
throw new Error(`Expected Block, got ${t.type}`); throw new Error(`Expected Block, got ${t.type}`);
} }
const statements = t.children!.map(parseStatement); const statements = t.children!.map(parseStatement);
return { type: "Block", statements, from: t.from, to: t.to }; return { type: "Block", statements, from: t.from, to: t.to };
} }
function parseStatement(t: ParseTree): LuaStatement { function parseStatement(t: ParseTree): LuaStatement {
switch (t.type) { switch (t.type) {
case "Block": case "Block":
return parseChunk(t.children![0]); return parseChunk(t.children![0]);
case "Semicolon": case "Semicolon":
return { type: "Semicolon", from: t.from, to: t.to }; return { type: "Semicolon", from: t.from, to: t.to };
case "Label": case "Label":
return { return {
type: "Label", type: "Label",
name: t.children![1].children![0].text!, name: t.children![1].children![0].text!,
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
case "Break": case "Break":
return { type: "Break", from: t.from, to: t.to }; return { type: "Break", from: t.from, to: t.to };
case "Goto": case "Goto":
return { return {
type: "Goto", type: "Goto",
name: t.children![1].children![0].text!, name: t.children![1].children![0].text!,
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
case "Scope": case "Scope":
return parseBlock(t.children![1]); return parseBlock(t.children![1]);
case ";": case ";":
return { type: "Semicolon", from: t.from, to: t.to }; return { type: "Semicolon", from: t.from, to: t.to };
case "WhileStatement": case "WhileStatement":
return { return {
type: "While", type: "While",
condition: parseExpression(t.children![1]), condition: parseExpression(t.children![1]),
block: parseBlock(t.children![3]), block: parseBlock(t.children![3]),
}; };
case "RepeatStatement": case "RepeatStatement":
return { return {
type: "Repeat", type: "Repeat",
block: parseBlock(t.children![1]), block: parseBlock(t.children![1]),
condition: parseExpression(t.children![3]), condition: parseExpression(t.children![3]),
}; };
case "IfStatement": { case "IfStatement": {
const conditions: { const conditions: {
condition: LuaExpression; condition: LuaExpression;
block: LuaBlock; block: LuaBlock;
from?: number; from?: number;
to?: number; to?: number;
}[] = []; }[] = [];
let elseBlock: LuaBlock | undefined = undefined; let elseBlock: LuaBlock | undefined = undefined;
for (let i = 0; i < t.children!.length; i += 4) { for (let i = 0; i < t.children!.length; i += 4) {
console.log("Looking at", t.children![i]); console.log("Looking at", t.children![i]);
const child = t.children![i]; const child = t.children![i];
if ( if (
child.children![0].text === "if" || child.children![0].text === "if" ||
child.children![0].text === "elseif" child.children![0].text === "elseif"
) { ) {
conditions.push({ conditions.push({
condition: parseExpression(t.children![i + 1]), condition: parseExpression(t.children![i + 1]),
block: parseBlock(t.children![i + 3]), block: parseBlock(t.children![i + 3]),
from: child.from, from: child.from,
to: child.to, to: child.to,
}); });
} else if (child.children![0].text === "else") { } else if (child.children![0].text === "else") {
elseBlock = parseBlock(t.children![i + 1]); elseBlock = parseBlock(t.children![i + 1]);
} else if (child.children![0].text === "end") { } else if (child.children![0].text === "end") {
break; break;
} else { } else {
throw new Error( throw new Error(
`Unknown if clause type: ${child.children![0].text}`, `Unknown if clause type: ${child.children![0].text}`,
); );
}
}
return {
type: "If",
conditions,
elseBlock,
from: t.from,
to: t.to,
};
} }
case "ForStatement": }
if (t.children![1].type === "ForNumeric") { return {
const forNumeric = t.children![1]; type: "If",
return { conditions,
type: "For", elseBlock,
name: forNumeric.children![0].children![0].text!, from: t.from,
start: parseExpression(forNumeric.children![2]), to: t.to,
end: parseExpression(forNumeric.children![4]), };
step: forNumeric.children![5]
? parseExpression(forNumeric.children![6])
: undefined,
block: parseBlock(t.children![3]),
from: t.from,
to: t.to,
};
} else {
const forGeneric = t.children![1];
return {
type: "ForIn",
names: parseNameList(forGeneric.children![0]),
expressions: parseExpList(forGeneric.children![2]),
block: parseBlock(t.children![3]),
from: t.from,
to: t.to,
};
}
case "Function":
return {
type: "Function",
name: parseFunctionName(t.children![1]),
body: parseFunctionBody(t.children![2]),
from: t.from,
to: t.to,
};
case "LocalFunction":
return {
type: "LocalFunction",
name: t.children![2].children![0].text!,
body: parseFunctionBody(t.children![3]),
};
case "FunctionCall":
return {
type: "FunctionCallStatement",
call: parseExpression(
{
type: "FunctionCall",
children: t.children!,
from: t.from,
to: t.to,
},
) as LuaFunctionCallExpression,
};
case "Assign":
return {
type: "Assignment",
variables: t.children![0].children!.filter((t) =>
t.type !== ","
).map(
parseLValue,
),
expressions: parseExpList(t.children![2]),
from: t.from,
to: t.to,
};
case "Local":
return {
type: "Local",
names: parseAttNames(t.children![1]),
expressions: t.children![3] ? parseExpList(t.children![3]) : [],
from: t.from,
to: t.to,
};
case "ReturnStatement": {
const expressions = t.children![1]
? parseExpList(t.children![1])
: [];
return { type: "Return", expressions, from: t.from, to: t.to };
}
case "break":
return { type: "Break", from: t.from, to: t.to };
default:
console.error(t);
throw new Error(`Unknown statement type: ${t.children![0].text}`);
} }
case "ForStatement":
if (t.children![1].type === "ForNumeric") {
const forNumeric = t.children![1];
return {
type: "For",
name: forNumeric.children![0].children![0].text!,
start: parseExpression(forNumeric.children![2]),
end: parseExpression(forNumeric.children![4]),
step: forNumeric.children![5]
? parseExpression(forNumeric.children![6])
: undefined,
block: parseBlock(t.children![3]),
from: t.from,
to: t.to,
};
} else {
const forGeneric = t.children![1];
return {
type: "ForIn",
names: parseNameList(forGeneric.children![0]),
expressions: parseExpList(forGeneric.children![2]),
block: parseBlock(t.children![3]),
from: t.from,
to: t.to,
};
}
case "Function":
return {
type: "Function",
name: parseFunctionName(t.children![1]),
body: parseFunctionBody(t.children![2]),
from: t.from,
to: t.to,
};
case "LocalFunction":
return {
type: "LocalFunction",
name: t.children![2].children![0].text!,
body: parseFunctionBody(t.children![3]),
};
case "FunctionCall":
return {
type: "FunctionCallStatement",
call: parseExpression(
{
type: "FunctionCall",
children: t.children!,
from: t.from,
to: t.to,
},
) as LuaFunctionCallExpression,
};
case "Assign":
return {
type: "Assignment",
variables: t.children![0].children!.filter((t) => t.type !== ",").map(
parseLValue,
),
expressions: parseExpList(t.children![2]),
from: t.from,
to: t.to,
};
case "Local":
return {
type: "Local",
names: parseAttNames(t.children![1]),
expressions: t.children![3] ? parseExpList(t.children![3]) : [],
from: t.from,
to: t.to,
};
case "ReturnStatement": {
const expressions = t.children![1] ? parseExpList(t.children![1]) : [];
return { type: "Return", expressions, from: t.from, to: t.to };
}
case "break":
return { type: "Break", from: t.from, to: t.to };
default:
console.error(t);
throw new Error(`Unknown statement type: ${t.children![0].text}`);
}
} }
function parseAttNames(t: ParseTree): LuaAttName[] { function parseAttNames(t: ParseTree): LuaAttName[] {
if (t.type !== "AttNameList") { if (t.type !== "AttNameList") {
throw new Error(`Expected AttNameList, got ${t.type}`); throw new Error(`Expected AttNameList, got ${t.type}`);
} }
return t.children!.filter((t) => t.type !== ",").map(parseAttName); return t.children!.filter((t) => t.type !== ",").map(parseAttName);
} }
function parseAttName(t: ParseTree): LuaAttName { function parseAttName(t: ParseTree): LuaAttName {
if (t.type !== "AttName") { if (t.type !== "AttName") {
throw new Error(`Expected AttName, got ${t.type}`); throw new Error(`Expected AttName, got ${t.type}`);
} }
return { return {
type: "AttName", type: "AttName",
name: t.children![0].children![0].text!, name: t.children![0].children![0].text!,
attribute: t.children![1].children![1] attribute: t.children![1].children![1]
? t.children![1].children![1].children![0].text! ? t.children![1].children![1].children![0].text!
: undefined, : undefined,
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
} }
function parseLValue(t: ParseTree): LuaLValue { function parseLValue(t: ParseTree): LuaLValue {
switch (t.type) { switch (t.type) {
case "Name": case "Name":
return { return {
type: "Variable", type: "Variable",
name: t.children![0].text!, name: t.children![0].text!,
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
case "Property": case "Property":
return { return {
type: "PropertyAccess", type: "PropertyAccess",
object: parsePrefixExpression(t.children![0]), object: parsePrefixExpression(t.children![0]),
property: t.children![2].children![0].text!, property: t.children![2].children![0].text!,
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
case "MemberExpression": case "MemberExpression":
return { return {
type: "TableAccess", type: "TableAccess",
object: parsePrefixExpression(t.children![0]), object: parsePrefixExpression(t.children![0]),
key: parseExpression(t.children![2]), key: parseExpression(t.children![2]),
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
default: default:
console.error(t); console.error(t);
throw new Error(`Unknown lvalue type: ${t.type}`); throw new Error(`Unknown lvalue type: ${t.type}`);
} }
} }
function parseFunctionName(t: ParseTree): LuaFunctionName { function parseFunctionName(t: ParseTree): LuaFunctionName {
if (t.type !== "FuncName") { if (t.type !== "FuncName") {
throw new Error(`Expected FunctionName, got ${t.type}`); 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;
} }
const propNames: string[] = []; }
let colonName: string | undefined = undefined; return {
for (let i = 0; i < t.children!.length; i += 2) { type: "FunctionName",
const prop = t.children![i]; propNames,
propNames.push(prop.children![0].text!); colonName,
if (t.children![i + 1] && t.children![i + 1].type === ":") { from: t.from,
colonName = t.children![i + 2].children![0].text!; to: t.to,
break; };
}
}
return {
type: "FunctionName",
propNames,
colonName,
from: t.from,
to: t.to,
};
} }
function parseNameList(t: ParseTree): string[] { function parseNameList(t: ParseTree): string[] {
if (t.type !== "NameList") { if (t.type !== "NameList") {
throw new Error(`Expected NameList, got ${t.type}`); throw new Error(`Expected NameList, got ${t.type}`);
} }
return t.children!.filter((t) => t.type === "Name").map((t) => return t.children!.filter((t) => t.type === "Name").map((t) =>
t.children![0].text! t.children![0].text!
); );
} }
function parseExpList(t: ParseTree): LuaExpression[] { function parseExpList(t: ParseTree): LuaExpression[] {
if (t.type !== "ExpList") { if (t.type !== "ExpList") {
throw new Error(`Expected ExpList, got ${t.type}`); throw new Error(`Expected ExpList, got ${t.type}`);
} }
return t.children!.filter((t) => t.type !== ",").map(parseExpression); return t.children!.filter((t) => t.type !== ",").map(parseExpression);
} }
function parseExpression(t: ParseTree): LuaExpression { function parseExpression(t: ParseTree): LuaExpression {
switch (t.type) { switch (t.type) {
case "LiteralString": { case "LiteralString": {
let cleanString = t.children![0].text!; let cleanString = t.children![0].text!;
// Remove quotes etc // Remove quotes etc
cleanString = cleanString.slice(1, -1); cleanString = cleanString.slice(1, -1);
return { return {
type: "String", type: "String",
value: cleanString, value: cleanString,
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
}
case "Number":
return {
type: "Number",
value: parseFloat(t.children![0].text!),
from: t.from,
to: t.to,
};
case "BinaryExpression":
return {
type: "Binary",
operator: t.children![1].children![0].text!,
left: parseExpression(t.children![0]),
right: parseExpression(t.children![2]),
from: t.from,
to: t.to,
};
case "UnaryExpression":
return {
type: "Unary",
operator: t.children![0].children![0].text!,
argument: parseExpression(t.children![1]),
from: t.from,
to: t.to,
};
case "Property":
return {
type: "PropertyAccess",
object: parsePrefixExpression(t.children![0]),
property: t.children![2].children![0].text!,
from: t.from,
to: t.to,
};
case "Parens":
return parseExpression(t.children![1]);
case "FunctionCall": {
if (t.children![1].type === ":") {
return {
type: "FunctionCall",
prefix: parsePrefixExpression(t.children![0]),
name: t.children![2].children![0].text!,
args: parseFunctionArgs(t.children!.slice(3)),
from: t.from,
to: t.to,
};
}
return {
type: "FunctionCall",
prefix: parsePrefixExpression(t.children![0]),
args: parseFunctionArgs(t.children!.slice(1)),
from: t.from,
to: t.to,
};
}
case "FunctionDef": {
const body = parseFunctionBody(t.children![1]);
return {
type: "FunctionDefinition",
body,
from: t.from,
to: t.to,
};
}
case "Name":
return {
type: "Variable",
name: t.children![0].text!,
from: t.from,
to: t.to,
};
case "Ellipsis":
return { type: "Variable", name: "...", from: t.from, to: t.to };
case "true":
return { type: "Boolean", value: true, from: t.from, to: t.to };
case "false":
return { type: "Boolean", value: false, from: t.from, to: t.to };
case "TableConstructor":
return {
type: "TableConstructor",
fields: t.children!.slice(1, -1).filter((t) =>
["FieldExp", "FieldProp", "FieldDynamic"].includes(t.type!)
).map(parseTableField),
from: t.from,
to: t.to,
};
case "nil":
return { type: "Nil", from: t.from, to: t.to };
default:
console.error(t);
throw new Error(`Unknown expression type: ${t.type}`);
} }
case "Number":
return {
type: "Number",
value: parseFloat(t.children![0].text!),
from: t.from,
to: t.to,
};
case "BinaryExpression":
return {
type: "Binary",
operator: t.children![1].children![0].text!,
left: parseExpression(t.children![0]),
right: parseExpression(t.children![2]),
from: t.from,
to: t.to,
};
case "UnaryExpression":
return {
type: "Unary",
operator: t.children![0].children![0].text!,
argument: parseExpression(t.children![1]),
from: t.from,
to: t.to,
};
case "Property":
return {
type: "PropertyAccess",
object: parsePrefixExpression(t.children![0]),
property: t.children![2].children![0].text!,
from: t.from,
to: t.to,
};
case "Parens":
return parseExpression(t.children![1]);
case "FunctionCall": {
if (t.children![1].type === ":") {
return {
type: "FunctionCall",
prefix: parsePrefixExpression(t.children![0]),
name: t.children![2].children![0].text!,
args: parseFunctionArgs(t.children!.slice(3)),
from: t.from,
to: t.to,
};
}
return {
type: "FunctionCall",
prefix: parsePrefixExpression(t.children![0]),
args: parseFunctionArgs(t.children!.slice(1)),
from: t.from,
to: t.to,
};
}
case "FunctionDef": {
const body = parseFunctionBody(t.children![1]);
return {
type: "FunctionDefinition",
body,
from: t.from,
to: t.to,
};
}
case "Name":
return {
type: "Variable",
name: t.children![0].text!,
from: t.from,
to: t.to,
};
case "Ellipsis":
return { type: "Variable", name: "...", from: t.from, to: t.to };
case "true":
return { type: "Boolean", value: true, from: t.from, to: t.to };
case "false":
return { type: "Boolean", value: false, from: t.from, to: t.to };
case "TableConstructor":
return {
type: "TableConstructor",
fields: t.children!.slice(1, -1).filter((t) =>
["FieldExp", "FieldProp", "FieldDynamic"].includes(t.type!)
).map(parseTableField),
from: t.from,
to: t.to,
};
case "nil":
return { type: "Nil", from: t.from, to: t.to };
default:
console.error(t);
throw new Error(`Unknown expression type: ${t.type}`);
}
} }
function parseFunctionArgs(ts: ParseTree[]): LuaExpression[] { function parseFunctionArgs(ts: ParseTree[]): LuaExpression[] {
console.log("Parsing function args", JSON.stringify(ts, null, 2)); console.log("Parsing function args", JSON.stringify(ts, null, 2));
return ts.filter((t) => ![",", "(", ")"].includes(t.type!)).map( return ts.filter((t) => ![",", "(", ")"].includes(t.type!)).map(
parseExpression, parseExpression,
); );
} }
function parseFunctionBody(t: ParseTree): LuaFunctionBody { function parseFunctionBody(t: ParseTree): LuaFunctionBody {
if (t.type !== "FuncBody") { if (t.type !== "FuncBody") {
throw new Error(`Expected FunctionBody, got ${t.type}`); throw new Error(`Expected FunctionBody, got ${t.type}`);
} }
return { return {
type: "FunctionBody", type: "FunctionBody",
parameters: t.children![1].children!.filter((t) => parameters: t.children![1].children!.filter((t) =>
["Name", "Ellipsis"].includes(t.type!) ["Name", "Ellipsis"].includes(t.type!)
) )
.map((t) => t.children![0].text!), .map((t) => t.children![0].text!),
block: parseBlock(t.children![3]), block: parseBlock(t.children![3]),
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
} }
function parsePrefixExpression(t: ParseTree): LuaPrefixExpression { function parsePrefixExpression(t: ParseTree): LuaPrefixExpression {
switch (t.type) { switch (t.type) {
case "Name": case "Name":
return { return {
type: "Variable", type: "Variable",
name: t.children![0].text!, name: t.children![0].text!,
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
case "Property": case "Property":
return { return {
type: "PropertyAccess", type: "PropertyAccess",
object: parsePrefixExpression(t.children![0]), object: parsePrefixExpression(t.children![0]),
property: t.children![2].children![0].text!, property: t.children![2].children![0].text!,
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
case "MemberExpression": case "MemberExpression":
return { return {
type: "TableAccess", type: "TableAccess",
object: parsePrefixExpression(t.children![0]), object: parsePrefixExpression(t.children![0]),
key: parseExpression(t.children![2]), key: parseExpression(t.children![2]),
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
case "Parens": case "Parens":
return { return {
type: "Parenthesized", type: "Parenthesized",
expression: parseExpression(t.children![1]), expression: parseExpression(t.children![1]),
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
default: default:
console.error(t); console.error(t);
throw new Error(`Unknown prefix expression type: ${t.type}`); throw new Error(`Unknown prefix expression type: ${t.type}`);
} }
} }
function parseTableField(t: ParseTree): LuaTableField { function parseTableField(t: ParseTree): LuaTableField {
switch (t.type) { switch (t.type) {
case "FieldExp": case "FieldExp":
return { return {
type: "ExpressionField", type: "ExpressionField",
value: parseExpression(t.children![0]), value: parseExpression(t.children![0]),
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
case "FieldProp": case "FieldProp":
return { return {
type: "PropField", type: "PropField",
key: t.children![0].children![0].text!, key: t.children![0].children![0].text!,
value: parseExpression(t.children![2]), value: parseExpression(t.children![2]),
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
case "FieldDynamic": case "FieldDynamic":
return { return {
type: "DynamicField", type: "DynamicField",
key: parseExpression(t.children![1]), key: parseExpression(t.children![1]),
value: parseExpression(t.children![4]), value: parseExpression(t.children![4]),
from: t.from, from: t.from,
to: t.to, to: t.to,
}; };
default: default:
console.error(t); console.error(t);
throw new Error(`Unknown table field type: ${t.type}`); throw new Error(`Unknown table field type: ${t.type}`);
} }
} }
export function parse(s: string): LuaBlock { export function parse(s: string): LuaBlock {
const t = parseToCrudeAST(s); const t = parseToCrudeAST(s);
console.log("Clean tree", JSON.stringify(t, null, 2)); console.log("Clean tree", JSON.stringify(t, null, 2));
const result = parseChunk(t); const result = parseChunk(t);
console.log("Parsed AST", JSON.stringify(result, null, 2)); console.log("Parsed AST", JSON.stringify(result, null, 2));
return result; return result;
} }
export function parseToCrudeAST(t: string): ParseTree { export function parseToCrudeAST(t: string): ParseTree {
return cleanTree(lezerToParseTree(t, parser.parse(t).topNode), true); return cleanTree(lezerToParseTree(t, parser.parse(t).topNode), true);
} }

View File

@ -2,52 +2,52 @@ import type { LuaFunctionBody } from "./ast.ts";
import { evalStatement } from "$common/space_lua/eval.ts"; import { evalStatement } from "$common/space_lua/eval.ts";
export class LuaEnv implements ILuaSettable, ILuaGettable { export class LuaEnv implements ILuaSettable, ILuaGettable {
variables = new Map<string, LuaValue>(); variables = new Map<string, LuaValue>();
constructor(readonly parent?: LuaEnv) { constructor(readonly parent?: LuaEnv) {
} }
setLocal(name: string, value: LuaValue) { setLocal(name: string, value: LuaValue) {
this.variables.set(name, value); this.variables.set(name, value);
} }
set(key: string, value: LuaValue): void { set(key: string, value: LuaValue): void {
if (this.variables.has(key) || !this.parent) { if (this.variables.has(key) || !this.parent) {
this.variables.set(key, value); this.variables.set(key, value);
} else { } else {
this.parent.set(key, value); this.parent.set(key, value);
}
} }
}
get(name: string): LuaValue | undefined { get(name: string): LuaValue | undefined {
if (this.variables.has(name)) { if (this.variables.has(name)) {
return this.variables.get(name); return this.variables.get(name);
}
if (this.parent) {
return this.parent.get(name);
}
return undefined;
} }
if (this.parent) {
return this.parent.get(name);
}
return undefined;
}
} }
export class LuaMultiRes { export class LuaMultiRes {
constructor(readonly values: any[]) { constructor(readonly values: any[]) {
} }
unwrap(): any { unwrap(): any {
if (this.values.length !== 1) { if (this.values.length !== 1) {
throw new Error("Cannot unwrap multiple values"); throw new Error("Cannot unwrap multiple values");
}
return this.values[0];
} }
return this.values[0];
}
} }
export function singleResult(value: any): any { export function singleResult(value: any): any {
if (value instanceof LuaMultiRes) { if (value instanceof LuaMultiRes) {
return value.unwrap(); return value.unwrap();
} else { } else {
return value; return value;
} }
} }
// These types are for documentation only // These types are for documentation only
@ -55,207 +55,207 @@ export type LuaValue = any;
export type JSValue = any; export type JSValue = any;
export interface ILuaFunction { export interface ILuaFunction {
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue; call(...args: LuaValue[]): Promise<LuaValue> | LuaValue;
} }
export interface ILuaSettable { export interface ILuaSettable {
set(key: LuaValue, value: LuaValue): void; set(key: LuaValue, value: LuaValue): void;
} }
export interface ILuaGettable { export interface ILuaGettable {
get(key: LuaValue): LuaValue | undefined; get(key: LuaValue): LuaValue | undefined;
} }
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: LuaValue[]): Promise<LuaValue> | LuaValue { call(...args: LuaValue[]): Promise<LuaValue> | LuaValue {
// Create a new environment for this function call // Create a new environment for this function call
const env = new LuaEnv(this.closure); const env = new LuaEnv(this.closure);
// Assign the passed arguments to the parameters // Assign the passed arguments to the parameters
for (let i = 0; i < this.body.parameters.length; i++) { for (let i = 0; i < this.body.parameters.length; i++) {
let arg = args[i]; let arg = args[i];
if (arg === undefined) { if (arg === undefined) {
arg = null; arg = null;
} }
env.set(this.body.parameters[i], arg); env.set(this.body.parameters[i], arg);
}
return evalStatement(this.body.block, env).catch((e: any) => {
if (e instanceof LuaReturn) {
if (e.values.length === 0) {
return;
} else if (e.values.length === 1) {
return e.values[0];
} else {
return new LuaMultiRes(e.values);
}
} else {
throw e;
}
});
} }
return evalStatement(this.body.block, env).catch((e: any) => {
if (e instanceof LuaReturn) {
if (e.values.length === 0) {
return;
} else if (e.values.length === 1) {
return e.values[0];
} else {
return new LuaMultiRes(e.values);
}
} else {
throw e;
}
});
}
} }
export class LuaNativeJSFunction implements ILuaFunction { export class LuaNativeJSFunction implements ILuaFunction {
constructor(readonly fn: (...args: JSValue[]) => JSValue) { constructor(readonly fn: (...args: JSValue[]) => JSValue) {
} }
call(...args: LuaValue[]): Promise<LuaValue> | LuaValue { call(...args: LuaValue[]): Promise<LuaValue> | LuaValue {
const result = this.fn(...args.map(luaValueToJS)); const result = this.fn(...args.map(luaValueToJS));
if (result instanceof Promise) { if (result instanceof Promise) {
return result.then(jsToLuaValue); return result.then(jsToLuaValue);
} else { } else {
return jsToLuaValue(result); return jsToLuaValue(result);
}
} }
}
} }
export class LuaTable implements ILuaSettable, ILuaGettable { export class LuaTable implements ILuaSettable, ILuaGettable {
// To optimize the table implementation we use a combination of different data structures // 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 // 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>; private stringKeys: Record<string, any>;
// Other keys we can support using a Map as a fallback // Other keys we can support using a Map as a fallback
private otherKeys: Map<any, any> | null; private otherKeys: Map<any, any> | null;
// When tables are used as arrays, we use a native JavaScript array for that // When tables are used as arrays, we use a native JavaScript array for that
private arrayPart: any[]; private arrayPart: any[];
// TODO: Actually implement metatables // TODO: Actually implement metatables
private metatable: LuaTable | null; private metatable: LuaTable | null;
constructor() { constructor() {
// For efficiency and performance reasons we pre-allocate these (modern JS engines are very good at optimizing this) // For efficiency and performance reasons we pre-allocate these (modern JS engines are very good at optimizing this)
this.stringKeys = {}; this.stringKeys = {};
this.arrayPart = []; this.arrayPart = [];
this.otherKeys = null; // Only create this when needed this.otherKeys = null; // Only create this when needed
this.metatable = null; this.metatable = null;
}
get length(): number {
return this.arrayPart.length;
}
set(key: LuaValue, value: LuaValue) {
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 length(): number { get(key: LuaValue): LuaValue | undefined {
return this.arrayPart.length; if (typeof key === "string") {
return this.stringKeys[key];
} else if (Number.isInteger(key) && key >= 1) {
return this.arrayPart[key - 1];
} else if (this.otherKeys) {
return this.otherKeys.get(key);
} }
return undefined;
}
set(key: LuaValue, value: LuaValue) { toArray(): JSValue[] {
if (typeof key === "string") { return this.arrayPart;
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 | undefined { toObject(): Record<string, JSValue> {
if (typeof key === "string") { const result = { ...this.stringKeys };
return this.stringKeys[key]; for (const i in this.arrayPart) {
} else if (Number.isInteger(key) && key >= 1) { result[parseInt(i) + 1] = this.arrayPart[i];
return this.arrayPart[key - 1];
} else if (this.otherKeys) {
return this.otherKeys.get(key);
}
return undefined;
} }
return result;
}
toArray(): JSValue[] { static fromArray(arr: JSValue[]): LuaTable {
return this.arrayPart; const table = new LuaTable();
for (let i = 0; i < arr.length; i++) {
table.set(i + 1, arr[i]);
} }
return table;
}
toObject(): Record<string, JSValue> { static fromObject(obj: Record<string, JSValue>): LuaTable {
const result = { ...this.stringKeys }; const table = new LuaTable();
for (const i in this.arrayPart) { for (const key in obj) {
result[parseInt(i) + 1] = this.arrayPart[i]; table.set(key, obj[key]);
}
return result;
}
static fromArray(arr: JSValue[]): LuaTable {
const table = new LuaTable();
for (let i = 0; i < arr.length; i++) {
table.set(i + 1, arr[i]);
}
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;
} }
return table;
}
} }
export type LuaLValueContainer = { env: ILuaSettable; key: LuaValue }; 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);
} else { } else {
obj[key] = value; obj[key] = value;
} }
} }
export function luaGet(obj: any, key: any): any { export function luaGet(obj: any, key: any): any {
if (obj instanceof LuaTable) { if (obj instanceof LuaTable) {
return obj.get(key); return obj.get(key);
} else { } else {
return obj[key]; return obj[key];
} }
} }
export function luaLen(obj: any): number { export function luaLen(obj: any): number {
if (obj instanceof LuaTable) { if (obj instanceof LuaTable) {
return obj.toArray().length; return obj.toArray().length;
} else if (Array.isArray(obj)) { } else if (Array.isArray(obj)) {
return obj.length; return obj.length;
} else { } else {
return 0; return 0;
} }
} }
export class LuaBreak extends Error { export class LuaBreak extends Error {
} }
export class LuaReturn extends Error { export class LuaReturn extends Error {
constructor(readonly values: LuaValue[]) { constructor(readonly values: LuaValue[]) {
super(); super();
} }
} }
export function luaTruthy(value: any): boolean { export function luaTruthy(value: any): boolean {
if (value === undefined || value === null || value === false) { if (value === undefined || value === null || value === false) {
return false; return false;
} }
if (value instanceof LuaTable) { if (value instanceof LuaTable) {
return value.length > 0; return value.length > 0;
} }
return true; return true;
} }
export function jsToLuaValue(value: any): any { export function jsToLuaValue(value: any): any {
if (value instanceof LuaTable) { if (value instanceof LuaTable) {
return value; return value;
} else if (Array.isArray(value)) { } else if (Array.isArray(value)) {
return LuaTable.fromArray(value.map(jsToLuaValue)); return LuaTable.fromArray(value.map(jsToLuaValue));
} else if (typeof value === "object") { } else if (typeof value === "object") {
return LuaTable.fromObject(value); return LuaTable.fromObject(value);
} else { } else {
return value; return value;
} }
} }
export function luaValueToJS(value: any): any { export function luaValueToJS(value: any): any {
if (value instanceof LuaTable) { if (value instanceof LuaTable) {
// This is a heuristic: if this table is used as an array, we return an array // This is a heuristic: if this table is used as an array, we return an array
if (value.length > 0) { if (value.length > 0) {
return value.toArray(); return value.toArray();
} else {
return value.toObject();
}
} else { } else {
return value; return value.toObject();
} }
} else {
return value;
}
} }

View File

@ -3,19 +3,19 @@ import { assertEquals } from "@std/assert/equals";
import { assert } from "@std/assert"; import { assert } from "@std/assert";
Deno.test("Test promise helpers", async () => { Deno.test("Test promise helpers", async () => {
const r = evalPromiseValues([1, 2, 3]); const r = evalPromiseValues([1, 2, 3]);
// should return the same array not as a promise // should return the same array not as a promise
assertEquals(r, [1, 2, 3]); assertEquals(r, [1, 2, 3]);
const asyncR = evalPromiseValues([ const asyncR = evalPromiseValues([
new Promise((resolve) => { new Promise((resolve) => {
setTimeout(() => { setTimeout(() => {
resolve(1); resolve(1);
}, 5); }, 5);
}), }),
Promise.resolve(2), Promise.resolve(2),
3, 3,
]); ]);
// should return a promise // should return a promise
assert(asyncR instanceof Promise); assert(asyncR instanceof Promise);
assertEquals(await asyncR, [1, 2, 3]); assertEquals(await asyncR, [1, 2, 3]);
}); });

View File

@ -1,16 +1,16 @@
export function evalPromiseValues(vals: any[]): Promise<any[]> | any[] { export function evalPromiseValues(vals: any[]): Promise<any[]> | any[] {
const promises = []; const promises = [];
const promiseResults = new Array(vals.length); const promiseResults = new Array(vals.length);
for (let i = 0; i < vals.length; i++) { for (let i = 0; i < vals.length; i++) {
if (vals[i] instanceof Promise) { if (vals[i] instanceof Promise) {
promises.push(vals[i].then((v: any) => promiseResults[i] = v)); promises.push(vals[i].then((v: any) => promiseResults[i] = v));
} else {
promiseResults[i] = vals[i];
}
}
if (promises.length === 0) {
return promiseResults;
} else { } else {
return Promise.all(promises).then(() => promiseResults); promiseResults[i] = vals[i];
} }
}
if (promises.length === 0) {
return promiseResults;
} else {
return Promise.all(promises).then(() => promiseResults);
}
} }

View File

@ -1,5 +1,5 @@
import { localDateString } from "$lib/dates.ts"; import { localDateString } from "$lib/dates.ts";
Deno.test("Dates", () => { Deno.test("Dates", () => {
console.log("Local date string", localDateString(new Date())); console.log("Local date string", localDateString(new Date()));
}); });

View File

@ -2,63 +2,63 @@ import { assertEquals } from "@std/assert/equals";
import { determineType, jsonTypeToString } from "./attributes.ts"; import { determineType, jsonTypeToString } from "./attributes.ts";
Deno.test("JSON Determine type", () => { Deno.test("JSON Determine type", () => {
// Determine type tests // Determine type tests
assertEquals(determineType(null), { type: "null" }); assertEquals(determineType(null), { type: "null" });
assertEquals(determineType(undefined), { type: "null" }); assertEquals(determineType(undefined), { type: "null" });
assertEquals(determineType("hello"), { type: "string" }); assertEquals(determineType("hello"), { type: "string" });
assertEquals(determineType(10), { type: "number" }); assertEquals(determineType(10), { type: "number" });
assertEquals(determineType(true), { type: "boolean" }); assertEquals(determineType(true), { type: "boolean" });
assertEquals(determineType({}), { type: "object", properties: {} }); assertEquals(determineType({}), { type: "object", properties: {} });
assertEquals(determineType([]), { type: "array" }); assertEquals(determineType([]), { type: "array" });
assertEquals(determineType([1]), { assertEquals(determineType([1]), {
type: "array", type: "array",
items: { type: "number" }, items: { type: "number" },
}); });
assertEquals( assertEquals(
determineType({ name: "Pete", age: 10, siblings: ["Sarah"] }), determineType({ name: "Pete", age: 10, siblings: ["Sarah"] }),
{ {
type: "object", type: "object",
properties: { properties: {
name: { type: "string" }, name: { type: "string" },
age: { type: "number" }, age: { type: "number" },
siblings: { type: "array", items: { type: "string" } }, siblings: { type: "array", items: { type: "string" } },
}, },
}, },
); );
}); });
Deno.test("Serialize JSON Type to string", () => { Deno.test("Serialize JSON Type to string", () => {
assertEquals(jsonTypeToString({ type: "string" }), "string"); assertEquals(jsonTypeToString({ type: "string" }), "string");
assertEquals(jsonTypeToString({ type: "null" }), "null"); assertEquals(jsonTypeToString({ type: "null" }), "null");
assertEquals(jsonTypeToString({ type: "number" }), "number"); assertEquals(jsonTypeToString({ type: "number" }), "number");
assertEquals(jsonTypeToString({ type: "boolean" }), "boolean"); assertEquals(jsonTypeToString({ type: "boolean" }), "boolean");
assertEquals(jsonTypeToString({ type: "array" }), "any[]"); assertEquals(jsonTypeToString({ type: "array" }), "any[]");
assertEquals( assertEquals(
jsonTypeToString({ type: "array", items: { type: "number" } }), jsonTypeToString({ type: "array", items: { type: "number" } }),
"number[]", "number[]",
); );
assertEquals( assertEquals(
jsonTypeToString({ type: "object", properties: {} }), jsonTypeToString({ type: "object", properties: {} }),
"{}", "{}",
); );
assertEquals( assertEquals(
jsonTypeToString({ jsonTypeToString({
type: "object", type: "object",
properties: { properties: {
name: { type: "string" }, name: { type: "string" },
age: { type: "number" }, age: { type: "number" },
}, },
}), }),
"{name: string; age: number;}", "{name: string; age: number;}",
); );
assertEquals( assertEquals(
jsonTypeToString({ jsonTypeToString({
anyOf: [ anyOf: [
{ type: "string" }, { type: "string" },
{ type: "number" }, { type: "number" },
{ type: "boolean" }, { type: "boolean" },
], ],
}), }),
"string | number | boolean", "string | number | boolean",
); );
}); });

View File

@ -10,24 +10,24 @@ const itemsMd = `
`; `;
Deno.test("Test item extraction", async () => { Deno.test("Test item extraction", async () => {
const t = parseMarkdown(itemsMd); const t = parseMarkdown(itemsMd);
const items = await extractItems("test", t); const items = await extractItems("test", t);
assertEquals(items[0].name, "Item 1"); assertEquals(items[0].name, "Item 1");
assertEquals(items[0].age, 100); assertEquals(items[0].age, 100);
assertEquals(items[0].page, "test"); assertEquals(items[0].page, "test");
assertEquals(items[0].parent, undefined); assertEquals(items[0].parent, undefined);
assertEquals(items[0].text, "Item 1 #tag1 #tag2 [age: 100]"); assertEquals(items[0].text, "Item 1 #tag1 #tag2 [age: 100]");
assertEquals(new Set(items[0].tags), new Set(["tag1", "tag2"])); assertEquals(new Set(items[0].tags), new Set(["tag1", "tag2"]));
assertEquals(new Set(items[0].itags), new Set(["item", "tag1", "tag2"])); assertEquals(new Set(items[0].itags), new Set(["item", "tag1", "tag2"]));
assertEquals(items[1].name, "Item 1.1"); assertEquals(items[1].name, "Item 1.1");
assertEquals(new Set(items[1].tags), new Set(["tag3", "tag1"])); assertEquals(new Set(items[1].tags), new Set(["tag3", "tag1"]));
assertEquals( assertEquals(
new Set(items[1].itags), new Set(items[1].itags),
new Set(["tag3", "tag2", "tag1", "item"]), new Set(["tag3", "tag2", "tag1", "item"]),
); );
assertEquals(items[1].parent, items[0].ref); assertEquals(items[1].parent, items[0].ref);
assertEquals(items[2].parent, items[1].ref); assertEquals(items[2].parent, items[1].ref);
}); });

View File

@ -11,34 +11,34 @@ const itemsMd = `
`; `;
Deno.test("Test task extraction", async () => { Deno.test("Test task extraction", async () => {
const t = parseMarkdown(itemsMd); const t = parseMarkdown(itemsMd);
const tasks = await extractTasks("test", t); const tasks = await extractTasks("test", t);
assertEquals(tasks.length, 3); assertEquals(tasks.length, 3);
assertEquals(tasks[0].name, "Task 1"); assertEquals(tasks[0].name, "Task 1");
assertEquals(tasks[0].age, 200); assertEquals(tasks[0].age, 200);
assertEquals(tasks[0].page, "test"); assertEquals(tasks[0].page, "test");
assertEquals(tasks[0].text, "Task 1 [age: 200]"); assertEquals(tasks[0].text, "Task 1 [age: 200]");
assertEquals(new Set(tasks[0].itags), new Set(["tag1", "tag2", "task"])); assertEquals(new Set(tasks[0].itags), new Set(["tag1", "tag2", "task"]));
assertEquals(tasks[0].parent, "test@1"); assertEquals(tasks[0].parent, "test@1");
assertEquals(tasks[1].name, "Task 2"); assertEquals(tasks[1].name, "Task 2");
// Don't inherit attributes // Don't inherit attributes
assertEquals(tasks[1].age, undefined); assertEquals(tasks[1].age, undefined);
// But inherit tags through itags, not tags // But inherit tags through itags, not tags
assertEquals( assertEquals(
new Set(tasks[1].tags), new Set(tasks[1].tags),
new Set(["tag1", "tag3"]), new Set(["tag1", "tag3"]),
); );
assertEquals( assertEquals(
new Set(tasks[1].itags), new Set(tasks[1].itags),
new Set(["tag1", "tag3", "task", "tag2"]), new Set(["tag1", "tag3", "task", "tag2"]),
); );
assertEquals(tasks[1].parent, "test@1"); assertEquals(tasks[1].parent, "test@1");
// Deeply // Deeply
assertEquals(tasks[2].name, "Task 2.1"); assertEquals(tasks[2].name, "Task 2.1");
assertEquals(tasks[2].tags, []); assertEquals(tasks[2].tags, []);
assertEquals( assertEquals(
new Set(tasks[2].itags), new Set(tasks[2].itags),
new Set(["tag1", "tag3", "task", "tag2"]), new Set(["tag1", "tag3", "task", "tag2"]),
); );
}); });