From 80f31da16b9b0a554c752efdd1204937a67d8b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Jan=20Fialka?= Date: Wed, 1 Oct 2025 11:30:58 +0200 Subject: [PATCH] Fix arithmetic coercion - refactored and enhanced (#1576) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Lua 5.4 compatible `tonumber` implementation completely reworked - custom char-by-char parser (no regexes) - proper Lua space handling - support for heximal integers and floats - support for scientific float notation - extended tests - updated website docs Signed-off-by: Matouš Jan Fialka --- client/space_lua/eval.ts | 51 ++- client/space_lua/language_core_test.lua | 30 +- client/space_lua/lua.test.ts | 4 + client/space_lua/stdlib.ts | 20 +- client/space_lua/tonumber.ts | 462 ++++++++++++++++++++++++ client/space_lua/tonumber_test.lua | 101 ++++++ website/Space Lua.md | 36 ++ 7 files changed, 656 insertions(+), 48 deletions(-) create mode 100644 client/space_lua/tonumber.ts create mode 100644 client/space_lua/tonumber_test.lua diff --git a/client/space_lua/eval.ts b/client/space_lua/eval.ts index a95689ad..c6b91a31 100644 --- a/client/space_lua/eval.ts +++ b/client/space_lua/eval.ts @@ -30,6 +30,21 @@ import { ArrayQueryCollection, type LuaCollectionQuery, } from "./query_collection.ts"; +import { + luaToNumber, +} from "./tonumber.ts"; + +// Wrapper for arithmetical operators +function luaCoerceToNumber(val: unknown): number { + if (typeof val === "number") return val; + if (typeof val === "string") { + const n = luaToNumber(val); + if (n !== null) { + return n; + } + } + throw new Error(`attempt to perform arithmetic on a non-number`,); +} async function handleTableFieldSync( table: LuaTable, @@ -141,7 +156,7 @@ export function evalExpression( return value.then((value) => { switch (e.operator) { case "-": - return -singleResult(value); + return -luaCoerceToNumber(singleResult(value)); case "+": return +singleResult(value); case "not": @@ -159,7 +174,7 @@ export function evalExpression( } else { switch (e.operator) { case "-": - return -singleResult(value); + return -luaCoerceToNumber(singleResult(value)); case "+": return +singleResult(value); case "not": @@ -435,13 +450,33 @@ const operatorsMetaMethods: Record LuaValue; }> = { - "+": { metaMethod: "__add", nativeImplementation: (a, b) => a + b }, - "-": { metaMethod: "__sub", nativeImplementation: (a, b) => a - b }, - "*": { metaMethod: "__mul", nativeImplementation: (a, b) => a * b }, - "/": { metaMethod: "__div", nativeImplementation: (a, b) => a / b }, + "+": { + metaMethod: "__add", + nativeImplementation: (a, b) => luaCoerceToNumber(a) + luaCoerceToNumber(b), + }, + "-": { + metaMethod: "__sub", + nativeImplementation: (a, b) => luaCoerceToNumber(a) - luaCoerceToNumber(b), + }, + "*": { + metaMethod: "__mul", + nativeImplementation: (a, b) => luaCoerceToNumber(a) * luaCoerceToNumber(b), + }, + "/": { + metaMethod: "__div", + nativeImplementation: (a, b) => luaCoerceToNumber(a) / luaCoerceToNumber(b), + }, "//": { metaMethod: "__idiv", - nativeImplementation: (a, b) => Math.floor(a / b), + nativeImplementation: (a, b) => Math.floor(luaCoerceToNumber(a) / luaCoerceToNumber(b)), + }, + "%": { + metaMethod: "__mod", + nativeImplementation: (a, b) => luaCoerceToNumber(a) % luaCoerceToNumber(b), + }, + "^": { + metaMethod: "__pow", + nativeImplementation: (a, b) => luaCoerceToNumber(a) ** luaCoerceToNumber(b), }, "&": { metaMethod: "__band", @@ -468,8 +503,6 @@ const operatorsMetaMethods: Record exactInt(a, ctx, sf) >> exactInt(b, ctx, sf), }, - "%": { metaMethod: "__mod", nativeImplementation: (a, b) => a % b }, - "^": { metaMethod: "__pow", nativeImplementation: (a, b) => a ** b }, "..": { metaMethod: "__concat", nativeImplementation: (a, b) => { diff --git a/client/space_lua/language_core_test.lua b/client/space_lua/language_core_test.lua index 232e7f62..e024a014 100644 --- a/client/space_lua/language_core_test.lua +++ b/client/space_lua/language_core_test.lua @@ -540,26 +540,14 @@ local r = query [[from p = data where type(p.favorite) == "table" and p.favorite assertEqual(#r, 1) assertEqual(r[1].name, "John") --- Test tonumber function -assertEqual(tonumber("123"), 123) -assertEqual(tonumber("123.45"), 123.45) -assertEqual(tonumber("-123"), -123) -assertEqual(tonumber("0"), 0) -assertEqual(tonumber(""), nil) -assertEqual(tonumber("abc"), nil) -assertEqual(tonumber("12.34.56"), nil) - --- Test tonumber with base -assertEqual(tonumber("1010", 2), 10) -- Binary -assertEqual(tonumber("FF", 16), 255) -- Hexadecimal -assertEqual(tonumber("377", 8), 255) -- Octal -assertEqual(tonumber("z", 36), 35) -- Base 36 -assertEqual(tonumber("1010", 10), 1010) -- Decimal (explicit) -assertEqual(tonumber("1010", 1), nil) -- Invalid base -assertEqual(tonumber("1010", 37), nil) -- Invalid base -assertEqual(tonumber("FF", 10), nil) -- Invalid hex in decimal -assertEqual(tonumber("8", 8), nil) -- Invalid octal digit - +-- Test string to number coercion for arithmetical operations +assertEqual(-1*' 16 ' == -16, true) +assertEqual(0-'0X10' == -16, true) +assertEqual('-0x10'/1 == -16, true) +assertEqual('-0X10p0'//1 == -16, true) +assertEqual('0x10P0'%15 == 1, true) +assertEqual('0X10P-1'^-1 == 0.125, true) +assertEqual(-'16' == -16, true) -- select tests -- Base case @@ -603,4 +591,4 @@ function varArgTest(a0, ...) assertEqual(a2, 3) end -varArgTest(1, 2, 3, 4) \ No newline at end of file +varArgTest(1, 2, 3, 4) diff --git a/client/space_lua/lua.test.ts b/client/space_lua/lua.test.ts index cd154e06..96f5618b 100644 --- a/client/space_lua/lua.test.ts +++ b/client/space_lua/lua.test.ts @@ -13,6 +13,10 @@ Deno.test("[Lua] Table tests", async () => { await runLuaTest("./stdlib/table_test.lua"); }); +Deno.test("[Lua] String to number tests", async () => { + await runLuaTest("./tonumber_test.lua"); +}); + Deno.test("[Lua] String tests", async () => { await runLuaTest("./stdlib/string_test.lua"); // await runLuaTest("./stdlib/string_test2.lua"); diff --git a/client/space_lua/stdlib.ts b/client/space_lua/stdlib.ts index b957e836..38ac84a4 100644 --- a/client/space_lua/stdlib.ts +++ b/client/space_lua/stdlib.ts @@ -21,6 +21,7 @@ import { mathApi } from "./stdlib/math.ts"; import { parse } from "./parse.ts"; import { evalStatement } from "./eval.ts"; import { encodingApi } from "./stdlib/encoding.ts"; +import { luaToNumber } from "./tonumber.ts"; const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => { console.log( @@ -112,24 +113,7 @@ const tostringFunction = new LuaBuiltinFunction((_sf, value: any) => { const tonumberFunction = new LuaBuiltinFunction( (_sf, value: LuaValue, base?: number) => { - if (typeof value === "string" && value.trim() === "") { - return null; - } - if (base !== undefined) { - if (base < 2 || base > 36) { - return null; - } - const n = parseInt(String(value), base); - if (isNaN(n)) { - return null; - } - return n; - } - const n = Number(value); - if (isNaN(n)) { - return null; - } - return n; + return luaToNumber(value, base); }, ); diff --git a/client/space_lua/tonumber.ts b/client/space_lua/tonumber.ts new file mode 100644 index 00000000..78eb048f --- /dev/null +++ b/client/space_lua/tonumber.ts @@ -0,0 +1,462 @@ +/** + * Space Lua `tonumber` implementation + * + * Matches Lua 5.4 semantics: + * - parses decimal or hex numerals + * - if base is given parses signed integers in the given base (2..36) + * - leading and trailing Lua spaces are trimmed (`\t\r\n\h\v`) + * - returns null on failure + * + * Notes: + * - hex without exponent is an integer + * - hex float requires `p` or `P` exponent + * - decimal float allows '.' and `e` or `E` exponent + */ + +function skipSpace(s: string, i: number): number { + const n = s.length; + while (i < n) { + const c = s.charCodeAt(i); + if (c === 32 || (c >= 9 && c <= 13)) { + i++; + } else { + break; + } + } + return i; +} + +function charToDigitBase(c: number, base: number): number { + if (c >= 48 && c <= 57) { + const v = c - 48; + if (v < base) { + return v; + } else { + return -1; + } + } else if (c >= 65 && c <= 90) { + const v = 10 + (c - 65); + if (v < base) { + return v; + } else { + return -1; + } + } else if (c >= 97 && c <= 122) { + const v = 10 + (c - 97); + if (v < base) { + return v; + } else { + return -1; + } + } else { + return -1; + } +} + +function parseIntWithBase( + s: string, + base: number, +): { ok: boolean; end: number; value: number } { + const n = s.length; + let i = 0; + i = skipSpace(s, i); + if (i >= n) { + return { ok: false, end: i, value: 0 }; + } + + let sign = 1; + if (s.charCodeAt(i) === 45) { + sign = -1; + i++; + } else if (s.charCodeAt(i) === 43) { + i++; + } + + if (i >= n) { + return { ok: false, end: i, value: 0 }; + } + + let acc = 0; + let any = false; + + while (i < n) { + const c = s.charCodeAt(i); + const isAlnum = + (c >= 48 && c <= 57) || + (c >= 65 && c <= 90) || + (c >= 97 && c <= 122); + + if (!isAlnum) { + break; + } + + const d = charToDigitBase(c, base); + if (d < 0) { + return { ok: false, end: i, value: 0 }; + } + + acc = acc * base + d; + any = true; + i++; + } + + if (!any) { + return { ok: false, end: i, value: 0 }; + } + + i = skipSpace(s, i); + if (i !== n) { + return { ok: false, end: i, value: 0 }; + } + + return { ok: true, end: i, value: sign * acc }; +} + +function parseInt(s: string): { ok: boolean; value: number } { + const n = s.length; + let i = 0; + i = skipSpace(s, i); + if (i >= n) { + return { ok: false, value: 0 }; + } + + let neg = false; + if (s.charCodeAt(i) === 45) { // '-' + neg = true; + i++; + } else if (s.charCodeAt(i) === 43) { // '+' + i++; + } + + if (i >= n) { + return { ok: false, value: 0 }; + } + + let acc = 0; + let any = false; + + // hex? + if (s.charCodeAt(i) === 48 && i + 1 < n) { + const x = s.charCodeAt(i + 1); + if (x === 120 || x === 88) { + i += 2; + while (i < n) { + const c = s.charCodeAt(i); + let d = -1; + if (c >= 48 && c <= 57) { + d = c - 48; + } else if (c >= 65 && c <= 70) { + d = 10 + (c - 65); + } else if (c >= 97 && c <= 102) { + d = 10 + (c - 97); + } else { + d = -1; + } + if (d < 0) { + break; + } + acc = acc * 16 + d; + any = true; + i++; + } + i = skipSpace(s, i); + if (!any || i !== n) { + return { ok: false, value: 0 }; + } else { + return { ok: true, value: neg ? -acc : acc }; + } + } + } + + // decimal integer + while (i < n) { + const c = s.charCodeAt(i); + if (c < 48 || c > 57) { + break; + } + acc = acc * 10 + (c - 48); + any = true; + i++; + } + + i = skipSpace(s, i); + if (!any || i !== n) { + return { ok: false, value: 0 }; + } else { + return { ok: true, value: neg ? -acc : acc }; + } +} + +function parseDecFloat(s: string): { ok: boolean; value: number } { + const n = s.length; + let i = 0; + i = skipSpace(s, i); + if (i >= n) { + return { ok: false, value: 0 }; + } + + let sign = 1; + const c0 = s.charCodeAt(i); + if (c0 === 45) { + sign = -1; + i++; + } else if (c0 === 43) { + i++; + } + + let intAny = false; + let fracAny = false; + let val = 0; + + // integer part + while (i < n) { + const c = s.charCodeAt(i); + if (c < 48 || c > 57) { + break; + } + val = val * 10 + (c - 48); + intAny = true; + i++; + } + + // fractional part + if (i < n) { + if (s.charCodeAt(i) === 46) { + i++; + let scale = 1; + while (i < n) { + const c = s.charCodeAt(i); + if (c < 48 || c > 57) { + break; + } + scale *= 0.1; + val += (c - 48) * scale; + fracAny = true; + i++; + } + } + } + + // exponent + let exp = 0; + let hasExp = false; + if (i < n) { + const ec = s.charCodeAt(i); + if (ec === 101 || ec === 69) { + hasExp = true; + i++; + if (i >= n) { + return { ok: false, value: 0 }; + } + let expSign = 1; + if (i < n) { + const es = s.charCodeAt(i); + if (es === 45) { + expSign = -1; + i++; + } else if (es === 43) { + i++; + } + } + if (i >= n) { + return { ok: false, value: 0 }; + } + let anyExp = false; + while (i < n) { + const c = s.charCodeAt(i); + if (c < 48 || c > 57) { + break; + } + exp = exp * 10 + (c - 48); + anyExp = true; + i++; + } + if (!anyExp) { + return { ok: false, value: 0 }; + } + exp *= expSign; + } + } + + if (!intAny && !fracAny) { + return { ok: false, value: 0 }; + } + + i = skipSpace(s, i); + if (i !== n) { + return { ok: false, value: 0 }; + } + + const result = sign * (hasExp ? val * Math.pow(10, exp) : val); + return { ok: true, value: result }; +} + +function parseHexFloat(s: string): { ok: boolean; value: number } { + const n = s.length; + let i = 0; + i = skipSpace(s, i); + if (i >= n) { + return { ok: false, value: 0 }; + } + + let sign = 1; + const c0 = s.charCodeAt(i); + if (c0 === 45) { + sign = -1; + i++; + } else if (c0 === 43) { + i++; + } + + if (!(i + 1 < n && s.charCodeAt(i) === 48)) { + return { ok: false, value: 0 }; + } + const x = s.charCodeAt(i + 1); + if (!(x === 120 || x === 88)) { + return { ok: false, value: 0 }; + } + i += 2; + + let intVal = 0; + let fracVal = 0; + let fracScale = 1; + let anyHex = false; + + // integer hex digits + while (i < n) { + const c = s.charCodeAt(i); + let d = -1; + if (c >= 48 && c <= 57) { + d = c - 48; + } else if (c >= 65 && c <= 70) { + d = 10 + (c - 65); + } else if (c >= 97 && c <= 102) { + d = 10 + (c - 97); + } else { + d = -1; + } + if (d < 0) { + break; + } + intVal = intVal * 16 + d; + anyHex = true; + i++; + } + + // optional fractional part + if (i < n) { + if (s.charCodeAt(i) === 46) { + i++; + while (i < n) { + const c = s.charCodeAt(i); + let d = -1; + if (c >= 48 && c <= 57) { + d = c - 48; + } else if (c >= 65 && c <= 70) { + d = 10 + (c - 65); + } else if (c >= 97 && c <= 102) { + d = 10 + (c - 97); + } else { + d = -1; + } + if (d < 0) { + break; + } + fracScale *= 16; + fracVal = fracVal * 16 + d; + anyHex = true; + i++; + } + } + } + + // exponent (required) + if (i >= n) { + return { ok: false, value: 0 }; + } + const ec = s.charCodeAt(i); + if (!(ec === 112 || ec === 80)) { + return { ok: false, value: 0 }; + } + i++; + + let expSign = 1; + if (i < n) { + const sc = s.charCodeAt(i); + if (sc === 45) { + expSign = -1; + i++; + } else if (sc === 43) { + i++; + } + } + if (i >= n) { + return { ok: false, value: 0 }; + } + + let anyExp = false; + let exp = 0; + while (i < n) { + const c = s.charCodeAt(i); + if (c < 48 || c > 57) { + break; + } + exp = exp * 10 + (c - 48); + anyExp = true; + i++; + } + if (!anyHex || !anyExp) { + return { ok: false, value: 0 }; + } + + i = skipSpace(s, i); + if (i !== n) { + return { ok: false, value: 0 }; + } + + const frac = fracVal === 0 ? 0 : (fracVal / fracScale); + const result = sign * (intVal + frac) * Math.pow(2, expSign * exp); + return { ok: true, value: result }; +} + +export function luaToNumber(s: string, base?: number): number | null { + if (typeof s !== 'string') { + return null; + } + + if (base !== undefined) { + if (!(typeof base === 'number' && base >= 2 && base <= 36)) { + return null; + } + + const parsed = parseIntWithBase(s, base); + if (parsed.ok) { + return parsed.value; + } else { + return null; + } + } + + { + const parsed = parseInt(s); + if (parsed.ok) { + return parsed.value; + } + } + + { + const parsed = parseHexFloat(s); + if (parsed.ok) { + return parsed.value; + } + } + + { + const parsed = parseDecFloat(s); + if (parsed.ok) { + return parsed.value; + } + } + + return null; +} diff --git a/client/space_lua/tonumber_test.lua b/client/space_lua/tonumber_test.lua new file mode 100644 index 00000000..fd20f927 --- /dev/null +++ b/client/space_lua/tonumber_test.lua @@ -0,0 +1,101 @@ +local function assert_eq(a, b, message) + if a ~= b then + error('Assertion failed: ' .. tostring(a) .. ' is not equal to ' .. tostring(b) .. (message and (' | ' .. message) or '')) + end +end + +-- helper for whitespace handling tests +local chr = string.char +-- no base: whitespace handling +assert_eq(tonumber( chr(32, 9, 10, 11, 12, 13) .. '42'), 42) +assert_eq(tonumber('42' .. chr(32, 9, 10, 11, 12, 13)), 42) + +-- no base: zeroes +assert_eq(tonumber('-0'), 0 ) +assert_eq(tonumber('0x0p0'), 0.0) +assert_eq(tonumber('0x.0p10'), 0.0) + +-- no base: valid decimal integers and floats +assert_eq(tonumber('123' ), 123 ) +assert_eq(tonumber('+123' ), 123 ) +assert_eq(tonumber('-123' ), -123 ) +assert_eq(tonumber('0' ), 0 ) +assert_eq(tonumber('000123'), 123 ) +assert_eq(tonumber('123.45'), 123.45) +assert_eq(tonumber('5.' ), 5.0 ) +assert_eq(tonumber('.5' ), 0.5 ) +assert_eq(tonumber('5.e1' ), 50.0 ) +assert_eq(tonumber('.5e2' ), 50.0 ) +assert_eq(tonumber('-.5e2' ), -50.0 ) +assert_eq(tonumber('1e-2' ), 0.01) +assert_eq(tonumber('1E+2' ), 100.0 ) + +-- no base: valid hexadecimal integers (no exponent) +assert_eq(tonumber('0x10' ), 16) +assert_eq(tonumber('0X10' ), 16) +assert_eq(tonumber('+0x10' ), 16) +assert_eq(tonumber('-0x10' ), -16) +assert_eq(tonumber(' 0x0 ' ), 0) +assert_eq(tonumber('0x000F' ), 15) +assert_eq(tonumber('0XdeadBEEF'), 3735928559) + +-- no base: valid hexadecimal floats (with exponent) +assert_eq(tonumber('0x1p0' ), 1.0 ) +assert_eq(tonumber('0x1p+0' ), 1.0 ) +assert_eq(tonumber('0x1p-2' ), 0.25 ) +assert_eq(tonumber('0x1.0p-2' ), 0.25 ) +assert_eq(tonumber('0x1.8p1' ), 3.0 ) +assert_eq(tonumber('0x.8p0' ), 0.5 ) +assert_eq(tonumber('0x.Fp4' ), 15.0 ) +assert_eq(tonumber('0XA.P-1' ), 5.0 ) +assert_eq(tonumber(' 0x10P0 '), 16.0 ) +assert_eq(tonumber('-0x10p0' ), -16.0 ) +assert_eq(tonumber('0x10.2p0' ), 16.125 ) +assert_eq(tonumber('0x10.3P-1' ), 8.09375) +assert_eq(tonumber('-0X10.3P-1'), -8.09375) + +-- no base: invalid +assert_eq(tonumber('' ), nil) +assert_eq(tonumber(' ' ), nil) +assert_eq(tonumber('abc' ), nil) +assert_eq(tonumber('12.34.56'), nil) +assert_eq(tonumber('e10' ), nil) +assert_eq(tonumber('.e1' ), nil) +assert_eq(tonumber('1e' ), nil) +assert_eq(tonumber('1e+' ), nil) +assert_eq(tonumber('0x' ), nil) +assert_eq(tonumber('0xG' ), nil) +assert_eq(tonumber('0x1.2' ), nil) +assert_eq(tonumber('+ 10' ), nil) +assert_eq(tonumber('- 10' ), nil) +assert_eq(tonumber('123x' ), nil) + +-- with base: whitespace handling +assert_eq(tonumber( chr(32, 9, 10, 11, 12, 13) .. 'FF', 16), 255) +assert_eq(tonumber('FF' .. chr(32, 9, 10, 11, 12, 13), 16), 255) + +-- with base: valid +assert_eq(tonumber('1010', 2), 10) +assert_eq(tonumber('+1011', 2), 11) +assert_eq(tonumber('ff', 16), 255) +assert_eq(tonumber('FF', 16), 255) +assert_eq(tonumber('fF', 16), 255) +assert_eq(tonumber('377', 8), 255) +assert_eq(tonumber('z', 36), 35) +assert_eq(tonumber('Aa', 11), 120) +assert_eq(tonumber(' ff ', 16), 255) +assert_eq(tonumber('1010', 10), 1010) + +-- with base: invalid +assert_eq(tonumber('1010', 1), nil) +assert_eq(tonumber('1010', 37), nil) +assert_eq(tonumber('FF', 10), nil) +assert_eq(tonumber('8', 8), nil) +assert_eq(tonumber('2', 2), nil) +assert_eq(tonumber('', 16), nil) +assert_eq(tonumber(' ', 16), nil) +assert_eq(tonumber('+', 16), nil) +assert_eq(tonumber('-', 16), nil) +assert_eq(tonumber('+ FF', 16), nil) +assert_eq(tonumber('1_0', 10), nil) +assert_eq(tonumber('123x', 10), nil) diff --git a/website/Space Lua.md b/website/Space Lua.md index 483614c5..63cab283 100644 --- a/website/Space Lua.md +++ b/website/Space Lua.md @@ -57,6 +57,42 @@ A SilveBullet specific [[Markdown]] [[Markdown/Extensions]] is the `${lua expres For example: 10 + 2 = ${adder(10, 2)} (Alt-click, or select to see the expression) is using the just defined `adder` function. +## Conversion of Strings to Numbers + +Space Lua converts strings to numbers like standard Lua. + +Syntax: +* Leading and trailing whitespace (space, tab, newline, carriage return, form feed and vertical tab) is trimmed. +* The entire string after timming trailing whitespace must form a number. +* Optional `+` or `-` signs are accepted before the number. +* _Decimal integers and floats_ are supported with optional exponent (e.g., `42`, `-3.5`, `.5`, `5.`, `1e3`, `-2.5E-2`). +* _Hexadecimal integers and floats_ are also supported: + * integers (`0x10`, `-0XFF`), + * floats require `p` or `P` exponent (e.g., `0x1.8p1`, `-0X10.3P-1`). + +Failure handling: +* In _arithmetic_ and _unary minus_ expressions invalid strings cause an exception: + * `Lua error: attempt to perform arithmetic on a non-number` +* `tonumber(s)` returns `nil` on failure +* `tonumber(s, base)` parses signed integers in bases 2..36 (no decimal points or exponents) and returns `nil` on invalid input string or base + +Examples (`tonumber` function): +```lua +tonumber(' 42 ') --> 42 +tonumber('-0xFF') --> -255 +tonumber('0x1.8p1') --> 3.0 +tonumber('1e-2') --> 0.01 +tonumber('abc') --> nil + +tonumber('1010', 2) --> 10 +tonumber(' +FF', 16) --> 255 +tonumber('8', 8) --> nil +``` + +Examples (arithmetic expressions): +- ${'0xffffP-1'} (string) vs ${'0xffffP-3' + 0} (addition), +- ${-'123E-12'} (unary minus). + ## Queries Space Lua has a feature called [[Space Lua/Lua Integrated Query]], which integrate SQL-like queries into Lua. Here’s a small example querying the last 3 modifies pages: