From 971e19f591d0b800bc168079e6c2212699b359df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20S=2E=20=C5=81ukasiewicz?= Date: Wed, 30 Apr 2025 10:58:45 +0200 Subject: [PATCH] Add Lua features for integers (#1363) * Parse hexadecimal numeric constants in Lua Fix lua grammar to not require a "+" after "e" in numbers. Add tests for parsing correct number value. * Implement bitwise operators in Lua They throw an error when called on fractional values, like Lua 5.4 but with a hint to round them manually. --- common/space_lua/eval.test.ts | 8 +++++++ common/space_lua/eval.ts | 44 ++++++++++++++++++++++++++++++++++ common/space_lua/lua.grammar | 6 ++--- common/space_lua/parse-lua.js | 3 ++- common/space_lua/parse.test.ts | 35 ++++++++++++++++++++++++++- common/space_lua/parse.ts | 7 ++++-- website/API/math.md | 4 +++- website/Space Lua.md | 1 + 8 files changed, 100 insertions(+), 8 deletions(-) diff --git a/common/space_lua/eval.test.ts b/common/space_lua/eval.test.ts index 5d9bc97f..20fb9a89 100644 --- a/common/space_lua/eval.test.ts +++ b/common/space_lua/eval.test.ts @@ -40,6 +40,14 @@ Deno.test("Evaluator test", async () => { assertEquals(evalExpr(`4 // 3`), 1); assertEquals(evalExpr(`4 % 3`), 1); + // Bitwise arithmetic + assertEquals(evalExpr(`~171`), -172); // signed two's complement + assertEquals(evalExpr(`5 & 3`), 1); // 101 & 011 = 001 + assertEquals(evalExpr(`5 | 3`), 7); // 101 | 011 = 111 + assertEquals(evalExpr(`5 ~ 3`), 6); // 101 ^ 011 = 110 + assertEquals(evalExpr(`5 << 3`), 40); // 101 << 3 = 101000 + assertEquals(evalExpr(`5 >> 2`), 1); // 101 >> 2 = 1 + // Strings assertEquals(evalExpr(`"a" .. "b"`), "ab"); diff --git a/common/space_lua/eval.ts b/common/space_lua/eval.ts index 26db8619..333d4586 100644 --- a/common/space_lua/eval.ts +++ b/common/space_lua/eval.ts @@ -191,6 +191,8 @@ export function evalExpression( return +singleResult(value); case "not": return !singleResult(value); + case "~": + return ~exactInt(singleResult(value), e.ctx, sf); case "#": return luaLen(singleResult(value)); default: @@ -207,6 +209,8 @@ export function evalExpression( return +singleResult(value); case "not": return !singleResult(value); + case "~": + return ~exactInt(singleResult(value), e.ctx, sf); case "#": return luaLen(singleResult(value)); default: @@ -534,6 +538,31 @@ const operatorsMetaMethods: Record Math.floor(a / b), }, + "&": { + metaMethod: "__band", + nativeImplementation: (a, b, ctx, sf) => + exactInt(a, ctx, sf) & exactInt(b, ctx, sf), + }, + "|": { + metaMethod: "__bor", + nativeImplementation: (a, b, ctx, sf) => + exactInt(a, ctx, sf) | exactInt(b, ctx, sf), + }, + "~": { + metaMethod: "__bxor", + nativeImplementation: (a, b, ctx, sf) => + exactInt(a, ctx, sf) ^ exactInt(b, ctx, sf), + }, + "<<": { + metaMethod: "__shl", + nativeImplementation: (a, b, ctx, sf) => + exactInt(a, ctx, sf) << exactInt(b, ctx, sf), + }, + ">>": { + metaMethod: "__shr", + nativeImplementation: (a, b, ctx, sf) => + exactInt(a, ctx, sf) >> exactInt(b, ctx, sf), + }, "%": { metaMethod: "__mod", nativeImplementation: (a, b) => a % b }, "^": { metaMethod: "__pow", nativeImplementation: (a, b) => a ** b }, "..": { @@ -896,3 +925,18 @@ function evalLValue( } } } + +function exactInt( + num: number, + ctx: ASTCtx, + sf: LuaStackFrame, +): number { + // See conversion from float to integer https://www.lua.org/manual/5.4/manual.html#3.4.3 + if (!Number.isInteger(num)) { + throw new LuaRuntimeError( + `Number ${num} has no integer representation (consider math.floor or math.ceil)`, + sf.withCtx(ctx), + ); + } + return num; +} diff --git a/common/space_lua/lua.grammar b/common/space_lua/lua.grammar index 49f6d61a..0f87104a 100644 --- a/common/space_lua/lua.grammar +++ b/common/space_lua/lua.grammar @@ -188,8 +188,8 @@ TableConstructor { "{" (field (fieldsep field)* fieldsep?)? "}" } digit { std.digit } Number { - digit+ ("." digit+)? ($[eE] $[+\-] digit+)? | - "0" $[xX] hex+ ("." hex+)? ($[pP] $[+/-] digit+)? + digit+ ("." digit+)? ($[eE] $[+\-]? digit+)? | + "0" $[xX] hex+ ("." hex+)? ($[pP] $[+\-]? digit+)? } Comment { "--" ![\n\r]* } @@ -208,4 +208,4 @@ TableConstructor { "{" (field (fieldsep field)* fieldsep?)? "}" } "(" ")" "[" "]" "{" "}" "." "," ";" ":" "::" -} \ No newline at end of file +} diff --git a/common/space_lua/parse-lua.js b/common/space_lua/parse-lua.js index a315891a..9e7bdbdf 100644 --- a/common/space_lua/parse-lua.js +++ b/common/space_lua/parse-lua.js @@ -13,10 +13,11 @@ export const parser = LRParser.deserialize({ ], skippedNodes: [0,1], repeatNodeCount: 11, - tokenData: "Dg~RuXY#fYZ$Q[]#f]^$_pq#fqr$grs$rst)Yuv)_vw)dwx)ixy-zyz.Pz{.U{|.Z|}.`}!O.g!O!P/Z!P!Q/p!Q!R0Q!R![1f![!]3j!]!^3w!^!_4O!_!`4b!`!a4j!c!}4|!}#O5_#O#P#w#P#QCl#Q#RCy#R#S4|#T#o4|#o#pDO#p#qDT#q#rDY#r#sD_~#kS#i~XY#f[]#fpq#f#O#P#w~#zQYZ#f]^#f~$VP#h~]^$Y~$_O#h~~$dP#h~YZ$Ym$jP!_!`$mm$rOzmk$uXOY$rZ]$r^r$rrs%bs#O$r#O#P%g#P;'S$r;'S;=`'w<%lO$rk%gO#mkk%jZrs$rwx$r!Q![&]#O#P$r#T#U$r#U#V$r#Y#Z$r#b#c$r#i#j'}#l#m(p#n#o$rk&`ZOY$rZ]$r^r$rrs%bs!Q$r!Q!['R![#O$r#O#P%g#P;'S$r;'S;=`'w<%lO$rk'UZOY$rZ]$r^r$rrs%bs!Q$r!Q![$r![#O$r#O#P%g#P;'S$r;'S;=`'w<%lO$rk'zP;=`<%l$rk(QP#o#p(Tk(WR!Q![(a!c!i(a#T#Z(ak(dS!Q![(a!c!i(a#T#Z(a#q#r$rk(sR!Q![(|!c!i(|#T#Z(|k)PR!Q![$r!c!i$r#T#Z$r~)_O$S~~)dO$P~~)iO#w~k)lXOY)iZ])i^w)iwx%bx#O)i#O#P*X#P;'S)i;'S;=`,i<%lO)ik*[Zrs)iwx)i!Q![*}#O#P)i#T#U)i#U#V)i#Y#Z)i#b#c)i#i#j,o#l#m-b#n#o)ik+QZOY)iZ])i^w)iwx%bx!Q)i!Q![+s![#O)i#O#P*X#P;'S)i;'S;=`,i<%lO)ik+vZOY)iZ])i^w)iwx%bx!Q)i!Q![)i![#O)i#O#P*X#P;'S)i;'S;=`,i<%lO)ik,lP;=`<%l)ik,rP#o#p,uk,xR!Q![-R!c!i-R#T#Z-Rk-US!Q![-R!c!i-R#T#Z-R#q#r)ik-eR!Q![-n!c!i-n#T#Z-nk-qR!Q![)i!c!i)i#T#Z)i~.POl~~.UOm~~.ZO#}~~.`O#{~o.gOvc#s[~.lP#|~}!O.o~.tTP~OY.oZ].o^;'S.o;'S;=`/T<%lO.o~/WP;=`<%l.oo/`Pgi!O!P/co/hP!Pm!O!P/kQ/pOcQ~/uQ$O~!P!Q/{!_!`$m~0QO$Q~~0VUd~!O!P0i!Q![1f!g!h0}!z!{1w#X#Y0}#l#m1w~0lP!Q![0o~0tRd~!Q![0o!g!h0}#X#Y0}~1QQ{|1W}!O1W~1ZP!Q![1^~1cPd~!Q![1^~1kSd~!O!P0i!Q![1f!g!h0}#X#Y0}~1zR!Q![2T!c!i2T#T#Z2T~2YUd~!O!P2l!Q![2T!c!i2T!r!s3^#T#Z2T#d#e3^~2oR!Q![2x!c!i2x#T#Z2x~2}Td~!Q![2x!c!i2x!r!s3^#T#Z2x#d#e3^~3aR{|1W}!O1W!P!Q1W~3oPo~![!]3r~3wOU~_4OOSR#s[o4VQ$[Qzm!^!_4]!_!`$mm4bO#ym~4gP#r~!_!`$mo4qQ$]Qzm!_!`$m!`!a4wm4|O#zm~5RS#k~!Q![4|!c!}4|#R#S4|#T#o4|o5dQik!_!`5j!}#OB[k5mQ!_!`5s!}#O=lk5vP!}#O5yk5|TO#P5y#P#Q6]#Q;'S5y;'S;=`8T<%lO5yk6`VO!_6u!_!`:p!`#P6u#P#Q;t#Q;'S6u;'S;=`=f<%lO6uk6xVO!_7_!_!`8Z!`#P7_#P#Q8s#Q;'S7_;'S;=`=`<%lO7_k7bTO#P5y#P#Q7q#Q;'S5y;'S;=`8T<%lO5yk7tP!_!`7wk7zP!_!`7}k8QP#P#Q%bk8WP;=`<%l5yk8^TO#P8Z#P#Q7q#Q;'S8Z;'S;=`8m<%lO8Zk8pP;=`<%l8Zk8vUO!_5y!_!`9Y!`#P5y#Q;'S5y;'S;=`8T<%lO5yk9]VO!_5y!_!`9r!`#P5y#P#Q6]#Q;'S5y;'S;=`8T<%lO5yk9uTO#P5y#P#Q:U#Q;'S5y;'S;=`8T<%lO5yk:ZV#mkO!_6u!_!`:p!`#P6u#P#Q;t#Q;'S6u;'S;=`=f<%lO6uk:sVO!_8Z!_!`;Y!`#P8Z#P#Q7q#Q;'S8Z;'S;=`8m<%lO8Zk;]TO#P8Z#P#Q;l#Q;'S8Z;'S;=`8m<%lO8Zk;qP#mk!_!`7wk;wVO!_7_!_!`:p!`#P7_#P#Q<^#Q;'S7_;'S;=`=`<%lO7_kO#Q;'S=l;'S;=`>z<%lO=lk>RVO!_>h!_!`?Q!`#P>h#P#Q@U#Q;'S>h;'S;=`Al<%lO>hk>kTO#P=l#P#Q7w#Q;'S=l;'S;=`>z<%lO=lk>}P;=`<%l=lk?TTO#P?d#P#Q?|#Q;'S?d;'S;=`?v<%lO?dk?gTO#P?d#P#Q7w#Q;'S?d;'S;=`?v<%lO?dk?yP;=`<%l?dk@RP#mk!_!`7}k@XVO!_=l!_!`@n!`#P=l#P#QAr#Q;'S=l;'S;=`>z<%lO=lk@qTO#P=l#P#QAQ#Q;'S=l;'S;=`>z<%lO=lkAVV#mkO!_>h!_!`?Q!`#P>h#P#Q@U#Q;'S>h;'S;=`Al<%lO>hkAoP;=`<%l>hkAuVO!_?d!_!`?Q!`#P?d#P#QAr#Q;'S?d;'S;=`?v<%lO?doBaT$USO#PBp#P#QCS#Q;'SBp;'S;=`Cf<%lOBpkBsTO#PBp#P#QCS#Q;'SBp;'S;=`Cf<%lOBpkCVTO#PBp#P#Q%b#Q;'SBp;'S;=`Cf<%lOBpkCiP;=`<%lBpoCqPjP#P#QCtnCyO$Wn~DOO$R~~DTOq~~DYO#v~~D_Ou~~DdP#x~!_!`$m", + tokenData: "D^~RuXY#fYZ$Q[]#f]^$_pq#fqr$grs$rst)Yuv)_vw)dwx)ixy-zyz.Pz{.U{|.Z|}.`}!O.g!O!P/Z!P!Q/p!Q!R0Q!R![1i![!]3a!]!^3n!^!_3u!_!`4X!`!a4a!c!}4s!}#O5U#O#P#w#P#QCc#Q#RCp#R#S4s#T#o4s#o#pCu#p#qCz#q#rDP#r#sDU~#kS#i~XY#f[]#fpq#f#O#P#w~#zQYZ#f]^#f~$VP#h~]^$Y~$_O#h~~$dP#h~YZ$Ym$jP!_!`$mm$rOzmk$uXOY$rZ]$r^r$rrs%bs#O$r#O#P%g#P;'S$r;'S;=`'w<%lO$rk%gO#mkk%jZrs$rwx$r!Q![&]#O#P$r#T#U$r#U#V$r#Y#Z$r#b#c$r#i#j'}#l#m(p#n#o$rk&`ZOY$rZ]$r^r$rrs%bs!Q$r!Q!['R![#O$r#O#P%g#P;'S$r;'S;=`'w<%lO$rk'UZOY$rZ]$r^r$rrs%bs!Q$r!Q![$r![#O$r#O#P%g#P;'S$r;'S;=`'w<%lO$rk'zP;=`<%l$rk(QP#o#p(Tk(WR!Q![(a!c!i(a#T#Z(ak(dS!Q![(a!c!i(a#T#Z(a#q#r$rk(sR!Q![(|!c!i(|#T#Z(|k)PR!Q![$r!c!i$r#T#Z$r~)_O$S~~)dO$P~~)iO#w~k)lXOY)iZ])i^w)iwx%bx#O)i#O#P*X#P;'S)i;'S;=`,i<%lO)ik*[Zrs)iwx)i!Q![*}#O#P)i#T#U)i#U#V)i#Y#Z)i#b#c)i#i#j,o#l#m-b#n#o)ik+QZOY)iZ])i^w)iwx%bx!Q)i!Q![+s![#O)i#O#P*X#P;'S)i;'S;=`,i<%lO)ik+vZOY)iZ])i^w)iwx%bx!Q)i!Q![)i![#O)i#O#P*X#P;'S)i;'S;=`,i<%lO)ik,lP;=`<%l)ik,rP#o#p,uk,xR!Q![-R!c!i-R#T#Z-Rk-US!Q![-R!c!i-R#T#Z-R#q#r)ik-eR!Q![-n!c!i-n#T#Z-nk-qR!Q![)i!c!i)i#T#Z)i~.POl~~.UOm~~.ZO#}~~.`O#{~o.gOvc#s[~.lP#|~}!O.o~.tTP~OY.oZ].o^;'S.o;'S;=`/T<%lO.o~/WP;=`<%l.oo/`Pgi!O!P/co/hP!Pm!O!P/kQ/pOcQ~/uQ$O~!P!Q/{!_!`$m~0QO$Q~~0VUd~!O!P0i!Q![1i!g!h0}!z!{1z#X#Y0}#l#m1z~0lP!Q![0o~0tRd~!Q![0o!g!h0}#X#Y0}~1QR{|1Z}!O1Z!Q![1a~1^P!Q![1a~1fPd~!Q![1a~1nSd~!O!P0i!Q![1i!g!h0}#X#Y0}~1}R!Q![2W!c!i2W#T#Z2W~2]Ud~!O!P2o!Q![2W!c!i2W!r!s0}#T#Z2W#d#e0}~2rR!Q![2{!c!i2{#T#Z2{~3QTd~!Q![2{!c!i2{!r!s0}#T#Z2{#d#e0}~3fPo~![!]3i~3nOU~_3uOSR#s[o3|Q$[Qzm!^!_4S!_!`$mm4XO#ym~4^P#r~!_!`$mo4hQ$]Qzm!_!`$m!`!a4nm4sO#zm~4xS#k~!Q![4s!c!}4s#R#S4s#T#o4so5ZQik!_!`5a!}#OBRk5dQ!_!`5j!}#O=ck5mP!}#O5pk5sTO#P5p#P#Q6S#Q;'S5p;'S;=`7z<%lO5pk6VVO!_6l!_!`:g!`#P6l#P#Q;k#Q;'S6l;'S;=`=]<%lO6lk6oVO!_7U!_!`8Q!`#P7U#P#Q8j#Q;'S7U;'S;=`=V<%lO7Uk7XTO#P5p#P#Q7h#Q;'S5p;'S;=`7z<%lO5pk7kP!_!`7nk7qP!_!`7tk7wP#P#Q%bk7}P;=`<%l5pk8TTO#P8Q#P#Q7h#Q;'S8Q;'S;=`8d<%lO8Qk8gP;=`<%l8Qk8mUO!_5p!_!`9P!`#P5p#Q;'S5p;'S;=`7z<%lO5pk9SVO!_5p!_!`9i!`#P5p#P#Q6S#Q;'S5p;'S;=`7z<%lO5pk9lTO#P5p#P#Q9{#Q;'S5p;'S;=`7z<%lO5pk:QV#mkO!_6l!_!`:g!`#P6l#P#Q;k#Q;'S6l;'S;=`=]<%lO6lk:jVO!_8Q!_!`;P!`#P8Q#P#Q7h#Q;'S8Q;'S;=`8d<%lO8Qk;STO#P8Q#P#Q;c#Q;'S8Q;'S;=`8d<%lO8Qk;hP#mk!_!`7nk;nVO!_7U!_!`:g!`#P7U#P#Qq<%lO=ck=xVO!_>_!_!`>w!`#P>_#P#Q?{#Q;'S>_;'S;=`Ac<%lO>_k>bTO#P=c#P#Q7n#Q;'S=c;'S;=`>q<%lO=ck>tP;=`<%l=ck>zTO#P?Z#P#Q?s#Q;'S?Z;'S;=`?m<%lO?Zk?^TO#P?Z#P#Q7n#Q;'S?Z;'S;=`?m<%lO?Zk?pP;=`<%l?Zk?xP#mk!_!`7tk@OVO!_=c!_!`@e!`#P=c#P#QAi#Q;'S=c;'S;=`>q<%lO=ck@hTO#P=c#P#Q@w#Q;'S=c;'S;=`>q<%lO=ck@|V#mkO!_>_!_!`>w!`#P>_#P#Q?{#Q;'S>_;'S;=`Ac<%lO>_kAfP;=`<%l>_kAlVO!_?Z!_!`>w!`#P?Z#P#QAi#Q;'S?Z;'S;=`?m<%lO?ZoBWT$USO#PBg#P#QBy#Q;'SBg;'S;=`C]<%lOBgkBjTO#PBg#P#QBy#Q;'SBg;'S;=`C]<%lOBgkB|TO#PBg#P#Q%b#Q;'SBg;'S;=`C]<%lOBgkC`P;=`<%lBgoChPjP#P#QCknCpO$Wn~CuO$R~~CzOq~~DPO#v~~DUOu~~DZP#x~!_!`$m", tokenizers: [0, 1, 2, 3, 4], topRules: {"Chunk":[0,2]}, dynamicPrecedences: {"128":1}, specialized: [{term: 119, get: (value) => spec_identifier[value] || -1}], tokenPrec: 3791 }) + diff --git a/common/space_lua/parse.test.ts b/common/space_lua/parse.test.ts index 78cff97b..27f8fd26 100644 --- a/common/space_lua/parse.test.ts +++ b/common/space_lua/parse.test.ts @@ -1,4 +1,9 @@ -import { parse, stripLuaComments } from "$common/space_lua/parse.ts"; +import { + parse, + parseExpressionString, + stripLuaComments, +} from "$common/space_lua/parse.ts"; +import type { LuaNumberLiteral } from "$common/space_lua/ast.ts"; import { assertEquals } from "@std/assert/equals"; Deno.test("Test Lua parser", () => { @@ -136,3 +141,31 @@ Deno.test("Test query parsing", () => { ); parse(`_(query[[from p = index.tag("page") order by p.lastModified]])`); }); + +Deno.test("Test numeric constant parsing", () => { + // Examples from Lua 5.4 Reference Manual, except hexadecimal constants with fractional part + assertEquals((parseExpressionString(`3`) as LuaNumberLiteral).value, 3); + assertEquals((parseExpressionString(`345`) as LuaNumberLiteral).value, 345); + assertEquals((parseExpressionString(`0xff`) as LuaNumberLiteral).value, 0xff); + assertEquals( + (parseExpressionString(`0xBEBADA`) as LuaNumberLiteral).value, + 0xBEBADA, + ); + assertEquals((parseExpressionString(`3.0`) as LuaNumberLiteral).value, 3.0); + assertEquals( + (parseExpressionString(`3.1416`) as LuaNumberLiteral).value, + 3.1416, + ); + assertEquals( + (parseExpressionString(`314.16e-2`) as LuaNumberLiteral).value, + 314.16e-2, + ); + assertEquals( + (parseExpressionString(`0.31416E1`) as LuaNumberLiteral).value, + 0.31416E1, + ); + assertEquals( + (parseExpressionString(`34e1`) as LuaNumberLiteral).value, + 34e1, + ); +}); diff --git a/common/space_lua/parse.ts b/common/space_lua/parse.ts index 52daffb2..69082009 100644 --- a/common/space_lua/parse.ts +++ b/common/space_lua/parse.ts @@ -399,12 +399,15 @@ function parseExpression(t: ParseTree, ctx: ASTCtx): LuaExpression { ctx: context(t, ctx), }; } - case "Number": + case "Number": { + const text = t.children![0].text!.toLowerCase(); return { type: "Number", - value: parseFloat(t.children![0].text!), + // Use the integer parser fox 0x literals + value: text.includes("x") ? parseInt(text) : parseFloat(text), ctx: context(t, ctx), }; + } case "BinaryExpression": return { type: "Binary", diff --git a/website/API/math.md b/website/API/math.md index b3ee9754..ec3d948a 100644 --- a/website/API/math.md +++ b/website/API/math.md @@ -40,6 +40,8 @@ print(math.floor(4.8)) -- prints: 4 print(math.floor(-4.8)) -- prints: -5 ``` +Note: `math.floor(x + 0.5)` rounds to closest integer, similar to `round(x)` function from other languages + ## math.max(...) Returns the maximum value among its arguments. @@ -226,4 +228,4 @@ Example: local vec1 = {1, 2, 3} local vec2 = {4, 5, 6} print(math.cosineSimilarity(vec1, vec2)) -- prints: 0.9746318461970762 -``` \ No newline at end of file +``` diff --git a/website/Space Lua.md b/website/Space Lua.md index 674cc252..cba31673 100644 --- a/website/Space Lua.md +++ b/website/Space Lua.md @@ -131,6 +131,7 @@ Space Lua is intended to be a more or less complete implementation of [Lua 5.4]( * coroutines (not planned, not useful in the SilverBullet context) * _ENV (planned) * Full metatable support (only partial now, planned) +* Hexadecimal numeric constants with a fractional part, or binary exponents (not supported by JavaScript number parser either) # Frequently Asked Questions ## Why Lua?