diff --git a/common/space_lua/lua.grammar b/common/space_lua/lua.grammar new file mode 100644 index 00000000..34a78092 --- /dev/null +++ b/common/space_lua/lua.grammar @@ -0,0 +1,171 @@ +/* Based on: https://github.com/R167/lezer-lua */ +@precedence { + call, + power @right, + prefix, + times @left, + plus @left, + concat @right, + shift @left, + bitand @left, + xor @left, + bitor @left, + compare @left, + and @left, + or @left +} + +@top Chunk { Block } + +Block { statement* ReturnStatement? } + +ReturnStatement { kw<"return"> exp? ";"?} + +@skip { newline | space | Comment } + +statement[@isGroup=Statement] { + ";" | + Label | + kw<"break"> | + Goto{ kw<"goto"> Name } | + Scope { kw<"do"> Block kw<"end"> } | + WhileStatement { kw<"while"> exp kw<"do"> Block kw<"end"> } | + RepeatStatement { kw<"repeat"> Block kw<"until"> exp } | + IfStatement | + ForStatement | + Function { kw<"function"> FuncName FuncBody } | + LocalFunction { kw<"local"> kw<"function"> Name FuncBody } | + Assign { VarList "=" ExpList } | + Local { kw<"local"> AttNameList ("=" ExpList)? } | + FunctionCall ~fcall +} + +IfStatement { + kw<"if"> exp kw<"then"> Block + (kw<"elseif"> exp kw<"then"> Block)* + (kw<"else"> Block) + kw<"end"> +} + +ForNumeric { Name "=" exp "," exp ("," exp)? } + +ForGeneric { NameList kw<"in"> ExpList } + +ForStatement { + kw<"for"> (ForNumeric | ForGeneric) kw<"do"> Block kw<"end"> +} + +FuncName { Name ("." Name)* (":" Name)? } +FuncBody { "(" ArgList ")" Block kw<"end"> } + +list { term ("," term)* } + +NameList { list } +ExpList { list } +VarList { list } +ArgList { list } + +AttNameList { list } +Attrib { ( "<" Name ">" )? } + +exp { + kw<"nil"> | kw<"true"> | kw<"false"> | "..." | + Number | + LiteralString | + prefixexp | + BinaryExpression | + UnaryExpression | + TableConstructor | + FunctionDef { kw<"function"> FuncBody } +} + +field[@isGroup=Field] { + FieldDynamic { "[" exp "]" "=" exp } | + FieldProp { Name "=" exp } | + FieldExp { exp } +} + +prefixexp { + var | + Parens { "(" exp ")" ~parens } | + FunctionCall ~fcall +} +FunctionCall { prefixexp (":" Name)? !call args } +args { + LiteralString | + TableConstructor | + funcParams[@dynamicPrecedence=1] { "(" list? ")" ~parens } +} + +var { + Name | Property { (prefixexp "." Name) } | MemberExpression { (prefixexp "[" exp "]") } +} + +kw { @specialize[@name={term}] } + +Name { identifier } +Label { "::" Name "::" } +LiteralString { simpleString } + +BinaryExpression { + exp !or kw<"or"> exp | + exp !and kw<"and"> exp | + exp !compare CompareOp exp | + exp !bitor BitOp{"|"} exp | + exp !bitand BitOp{"&"} exp | + exp !xor BitOp{"~"} exp | + exp !shift BitOp{"<<" | ">>"} exp | + exp !concat ".." exp | + exp !plus ArithOp{"+" | minus} exp | + exp !times ArithOp{"*" | "/" | "%" | "//"} exp | + exp !power ArithOp{"^"} exp +} + +UnaryExpression { + !prefix kw<"not"> exp | + !prefix (ArithOp{"+" | minus} | BitOp{"~"}) exp +} + +TableConstructor { "{" (field (fieldsep field)* fieldsep?)? "}" } + +@tokens { + CompareOp { "<" | ">" | $[<>=~/] "=" } + + word { std.asciiLetter (std.digit | std.asciiLetter)* } + + identifier { word } + + stringEscape { + "\\" ($[abfnz"'\\] | digit digit? digit?) | + "\\x" hex hex | + // NOTE: this should really be /[0-7]hex{5}/ at max, but that's annoying to write + "\\u{" hex+ "}" + } + + simpleString { "'" (stringEscape | ![\r\n\\'])+ "'" | '"' (stringEscape | ![\r\n\\"])+ '"'} + + hex { $[0-9a-fA-F] } + digit { std.digit } + + Number { + digit+ ("." digit+)? ($[eE] $[+\-] digit+)? | + "0" $[xX] hex+ ("." hex+)? ($[pP] $[+/-] digit+)? + } + + Comment { "--" ![\n\r]* } + + space { ($[ \t\f] | "\\" $[\n\r])+ } + newline { $[\n\r] | "\n\r" | "\r\n" } + + "..."[@name=Ellipsis] + ".."[@name=Concat] + + @precedence { Comment, minus } + + minus {"-"} + fieldsep { $[,;] } + + "(" ")" "[" "]" "{" "}" + + "." "," ";" ":" "::" +} \ No newline at end of file diff --git a/common/space_lua/parse.test.ts b/common/space_lua/parse.test.ts new file mode 100644 index 00000000..617e2fad --- /dev/null +++ b/common/space_lua/parse.test.ts @@ -0,0 +1,42 @@ +import { parse } from "$common/space_lua/parse.ts"; +import { assertEquals } from "@std/assert/equals"; + +Deno.test("Test Lua parser", () => { + // Basic block test + parse(` + print("Hello, World!") + print(10) +`); + parse(""); + // Expression tests + parse( + `e(1, 1.2, -3.8, +4, true, false, nil, "string", "Hello there \x00", ...)`, + ); + parse(`e(10 << 10, 10 >> 10, 10 & 10, 10 | 10, 10 ~ 10)`); + + assertEquals( + parse(`e(1 + 2 - 3 * 4 / 4)`), + parse(`e(1 + 2 - ((3 * 4) / 4))`), + ); + 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.b.c)`); + parse(`e((1+2))`); + + // Table expressions + parse(`e({})`); + parse(`e({1, 2, 3, })`); + parse(`e({1 ; 2 ; 3})`); + parse(`e({a = 1, b = 2, c = 3})`); + parse(`e({[3] = 1, [10 * 10] = "sup"})`); + + // Function calls + parse(`e(func(), func(1, 2, 3), a.b(), a.b.c:hello(), (a.b)(7))`); + + // Function expression + parse(`function sayHi() +print("Hi") +end`); + parse(`e(function(a, b) end)`); + parse(`e(function(a, b, ...) end)`); +}); diff --git a/common/space_lua/parse.ts b/common/space_lua/parse.ts new file mode 100644 index 00000000..9b0b3496 --- /dev/null +++ b/common/space_lua/parse.ts @@ -0,0 +1,443 @@ +import { lezerToParseTree } from "$common/markdown_parser/parse_tree.ts"; +import { + type AST as CrudeAST, + parseTreeToAST, +} from "@silverbulletmd/silverbullet/lib/tree"; +import { parser } from "./parse-lua.js"; +import { styleTags } from "@lezer/highlight"; + +const luaStyleTags = styleTags({ + // Identifier: t.variableName, + // TagIdentifier: t.variableName, + // GlobalIdentifier: t.variableName, + // String: t.string, + // Number: t.number, + // PageRef: ct.WikiLinkTag, + // BinExpression: t.operator, + // TernaryExpression: t.operator, + // Regex: t.regexp, + // "where limit select render Order OrderKW and or null as InKW NotKW BooleanKW each all": + // t.keyword, +}); + +export const highlightingQueryParser = parser.configure({ + props: [ + luaStyleTags, + ], +}); + +export type LuaBlock = { + type: "Block"; + statements: LuaStatement[]; +}; + +// STATEMENTS +export type LuaReturnStatement = { + type: "Return"; + expressions: LuaExpression[]; +}; + +export type LuaStatement = + | LuaSemicolonStatement + | LuaLabelStatement + | LuaBreakStatement + | LuaGotoStatement + | LuaReturnStatement + | LuaBlock + | LuaWhileStatement + | LuaRepeatStatement + | LuaIfStatement + | LuaForStatement + | LuaForInStatement + | LuaFunctionStatement + | LuaLocalFunctionStatement + | LuaAssignmentStatement + | LuaLocalAssignmentStatement + | LuaFunctionCallStatement; + +export type LuaSemicolonStatement = { + type: "Semicolon"; +}; + +export type LuaLabelStatement = { + type: "Label"; + name: string; +}; + +export type LuaBreakStatement = { + type: "Break"; +}; + +export type LuaGotoStatement = { + type: "Goto"; + name: string; +}; + +export type LuaWhileStatement = { + type: "While"; + condition: LuaExpression; + block: LuaBlock; +}; + +export type LuaRepeatStatement = { + type: "Repeat"; + block: LuaBlock; + condition: LuaExpression; +}; + +export type LuaIfStatement = { + type: "If"; + conditions: { condition: LuaExpression; block: LuaBlock }[]; + elseBlock?: LuaBlock; +}; + +export type LuaForStatement = { + type: "For"; + name: string; + start: LuaExpression; + end: LuaExpression; + step?: LuaExpression; + block: LuaBlock; +}; + +export type LuaForInStatement = { + type: "ForIn"; + names: string[]; + expressions: LuaExpression[]; + block: LuaBlock; +}; + +export type LuaFunctionStatement = { + type: "Function"; + name: LuaFunctionName; + body: LuaFunctionBody; +}; + +export type LuaLocalFunctionStatement = { + type: "LocalFunction"; + name: string; + body: LuaFunctionBody; +}; + +export type LuaFunctionName = { + type: "FunctionName"; + name: string; + propNames?: string[]; + colonName?: string; +}; + +export type LuaFunctionBody = { + type: "FunctionBody"; + parameters: string[]; + block: LuaBlock; +}; + +export type LuaAssignmentStatement = { + type: "Assignment"; + variables: LuaExpression[]; + expressions: LuaExpression[]; +}; + +export type LuaLocalAssignmentStatement = { + type: "LocalAssignment"; + names: string[]; + expressions: LuaExpression[]; +}; + +export type LuaFunctionCallStatement = { + type: "FunctionCallStatement"; + name: string; + args: LuaExpression[]; +}; + +// EXPRESSIONS +export type LuaExpression = + | LuaNilLiteral + | LuaBooleanLiteral + | LuaNumberLiteral + | LuaStringLiteral + | LuaPrefixExpression + | LuaBinaryExpression + | LuaUnaryExpression + | LuaTableConstructor + | LuaFunctionDefinition; + +export type LuaNilLiteral = { + type: "Nil"; +}; + +export type LuaBooleanLiteral = { + type: "Boolean"; + value: boolean; +}; + +export type LuaNumberLiteral = { + type: "Number"; + value: number; +}; + +export type LuaStringLiteral = { + type: "String"; + value: string; +}; + +export type LuaPrefixExpression = + | LuaVariableExpression + | LuaParenthesizedExpression + | LuaFunctionCallExpression; + +export type LuaParenthesizedExpression = { + type: "Parenthesized"; + expression: LuaExpression; +}; + +export type LuaVariableExpression = + | LuaVariable + | LuaPropertyAccessExpression + | LuaTableAccessExpression; + +export type LuaVariable = { + type: "Variable"; + name: string; +}; + +export type LuaPropertyAccessExpression = { + type: "PropertyAccess"; + object: LuaPrefixExpression; + property: string; +}; + +export type LuaTableAccessExpression = { + type: "TableAccess"; + object: LuaPrefixExpression; + key: LuaExpression; +}; + +export type LuaFunctionCallExpression = { + type: "FunctionCall"; + prefix: LuaPrefixExpression; + name?: string; + args: LuaExpression[]; +}; + +export type LuaBinaryExpression = { + type: "Binary"; + operator: string; + left: LuaExpression; + right: LuaExpression; +}; + +export type LuaUnaryExpression = { + type: "Unary"; + operator: string; + argument: LuaExpression; +}; + +export type LuaTableConstructor = { + type: "TableConstructor"; + fields: LuaTableField[]; +}; + +export type LuaTableField = + | LuaDynamicField + | LuaPropField + | LuaExpressionField; + +export type LuaDynamicField = { + type: "DynamicField"; + key: LuaExpression; + value: LuaExpression; +}; + +export type LuaPropField = { + type: "PropField"; + key: string; + value: LuaExpression; +}; + +export type LuaExpressionField = { + type: "ExpressionField"; + value: LuaExpression; +}; + +export type LuaFunctionDefinition = { + type: "FunctionDefinition"; + body: LuaFunctionBody; +}; + +function parseChunk(n: CrudeAST): LuaBlock { + const t = n as [string, ...CrudeAST[]]; + if (t[0] !== "Chunk") { + throw new Error(`Expected Chunk, got ${t[0]}`); + } + return parseBlock(t[1]); +} + +function parseBlock(n: CrudeAST): LuaBlock { + const t = n as [string, ...CrudeAST[]]; + if (t[0] !== "Block") { + throw new Error(`Expected Block, got ${t[0]}`); + } + const statements = t.slice(1).map(parseStatement); + return { type: "Block", statements }; +} + +function parseStatement(n: CrudeAST): LuaStatement { + const t = n as [string, ...CrudeAST[]]; + switch (t[0]) { + case "Block": + return parseChunk(t[1]); + case "Semicolon": + return { type: "Semicolon" }; + case "Label": + return { type: "Label", name: t[1] as string }; + case "Break": + return { type: "Break" }; + case "Goto": + return { type: "Goto", name: t[1] as string }; + case "FunctionCall": + return { + type: "FunctionCallStatement", + name: t[1][1] as string, + args: t.slice(2, -1).filter((t) => + ![",", "(", ")"].includes(t[1] as string) + ).map(parseExpression), + }; + default: + console.error(t); + throw new Error(`Unknown statement type: ${t[0]}`); + } +} + +function parseExpression(n: CrudeAST): LuaExpression { + const t = n as [string, ...CrudeAST[]]; + switch (t[0]) { + case "LiteralString": { + let cleanString = t[1] as string; + // Remove quotes etc + cleanString = cleanString.slice(1, -1); + return { type: "String", value: cleanString }; + } + case "Number": + return { type: "Number", value: parseFloat(t[1] as string) }; + case "BinaryExpression": + return { + type: "Binary", + operator: t[2][1] as string, + left: parseExpression(t[1]), + right: parseExpression(t[3]), + }; + case "UnaryExpression": + return { + type: "Unary", + operator: t[1][1] as string, + argument: parseExpression(t[2]), + }; + case "Property": + return { + type: "PropertyAccess", + object: parsePrefixExpression(t[1]), + property: t[3][1] as string, + }; + + case "Parens": + return parseExpression(t[2]); + case "FunctionCall": { + if (t[2][0] === ":") { + return { + type: "FunctionCall", + prefix: parsePrefixExpression(t[1]), + name: t[3][1] as string, + args: t.slice(4, -1).filter((t) => + ![",", "(", ")"].includes(t[1] as string) + ).map(parseExpression), + }; + } + return { + type: "FunctionCall", + prefix: parsePrefixExpression(t[1]), + args: t.slice(3, -1).filter((t) => + ![",", "(", ")"].includes(t[1] as string) + ).map(parseExpression), + }; + } + case "Name": + return { type: "Variable", name: t[1] as string }; + case "Ellipsis": + return { type: "Variable", name: "..." }; + case "true": + return { type: "Boolean", value: true }; + case "false": + return { type: "Boolean", value: false }; + case "TableConstructor": + return { + type: "TableConstructor", + fields: t.slice(2, -1).filter((t) => + !(typeof t === "string" || + ["{", "}"].includes(t[1] as string)) + ).map(parseTableField), + }; + case "nil": + return { type: "Nil" }; + default: + console.error(t); + throw new Error(`Unknown expression type: ${t[0]}`); + } +} + +function parsePrefixExpression(n: CrudeAST): LuaPrefixExpression { + const t = n as [string, ...CrudeAST[]]; + switch (t[0]) { + case "Name": + return { type: "Variable", name: t[1] as string }; + case "Property": + return { + type: "PropertyAccess", + object: parsePrefixExpression(t[1]), + property: t[3][1] as string, + }; + case "Parens": + return { type: "Parenthesized", expression: parseExpression(t[2]) }; + default: + console.error(t); + throw new Error(`Unknown prefix expression type: ${t[0]}`); + } +} + +function parseTableField(n: CrudeAST): LuaTableField { + const t = n as [string, ...CrudeAST[]]; + switch (t[0]) { + case "FieldExp": + return { + type: "ExpressionField", + value: parseExpression(t[1]), + }; + case "FieldProp": + return { + type: "PropField", + key: t[1][1] as string, + value: parseExpression(t[3]), + }; + case "FieldDynamic": + return { + type: "DynamicField", + key: parseExpression(t[2]), + value: parseExpression(t[5]), + }; + default: + console.error(t); + throw new Error(`Unknown table field type: ${t[0]}`); + } +} + +export function parse(t: string): LuaBlock { + const crudeAst = parseToCrudeAST(t); + console.log("Crude AST", JSON.stringify(crudeAst, null, 2)); + const result = parseChunk(crudeAst); + console.log("Parsed AST", JSON.stringify(result, null, 2)); + return result; +} + +export function parseToCrudeAST(t: string): CrudeAST { + return parseTreeToAST(lezerToParseTree(t, parser.parse(t).topNode), true); +} diff --git a/scripts/generate.sh b/scripts/generate.sh index c6bf645b..193fb890 100755 --- a/scripts/generate.sh +++ b/scripts/generate.sh @@ -3,6 +3,7 @@ QUERY_GRAMMAR=common/markdown_parser/query.grammar EXPRESSION_GRAMMAR=common/markdown_parser/expression.grammar.generated TEMPLATE_GRAMMAR=common/template/template.grammar +LUA_GRAMMAR=common/space_lua/lua.grammar LEZER_GENERATOR_VERSION=1.5.1 # Generate a patched grammer for just expressions @@ -11,4 +12,5 @@ tail -n +2 $QUERY_GRAMMAR >> $EXPRESSION_GRAMMAR deno run -A npm:@lezer/generator@$LEZER_GENERATOR_VERSION $QUERY_GRAMMAR -o common/markdown_parser/parse-query.js deno run -A npm:@lezer/generator@$LEZER_GENERATOR_VERSION $EXPRESSION_GRAMMAR -o common/markdown_parser/parse-expression.js -deno run -A npm:@lezer/generator@$LEZER_GENERATOR_VERSION $TEMPLATE_GRAMMAR -o common/template/parse-template.js \ No newline at end of file +deno run -A npm:@lezer/generator@$LEZER_GENERATOR_VERSION $TEMPLATE_GRAMMAR -o common/template/parse-template.js +deno run -A npm:@lezer/generator@$LEZER_GENERATOR_VERSION $LUA_GRAMMAR -o common/space_lua/parse-lua.js \ No newline at end of file