diff --git a/client/space_lua/stdlib.ts b/client/space_lua/stdlib.ts index 450d2bfc..efad38fb 100644 --- a/client/space_lua/stdlib.ts +++ b/client/space_lua/stdlib.ts @@ -14,7 +14,7 @@ import { LuaMultiRes, LuaRuntimeError, type LuaStackFrame, - type LuaTable, + LuaTable, luaToString, luaTypeOf, type LuaValue, @@ -37,9 +37,22 @@ import { isTaggedFloat, makeLuaFloat } from "./numeric.ts"; import { isPromise } from "./rp.ts"; import { isSqlNull } from "./sliq_null.ts"; -const printFunction = new LuaBuiltinFunction(async (_sf, ...args) => { - console.log("[Lua]", ...(await Promise.all(args.map((v) => luaToString(v))))); -}); +const printFunction = new LuaBuiltinFunction( + async (_sf, ...args) => { + console.log( + "[Lua]", + ...(await Promise.all(args.map((v) => luaToString(v)))), + ); + }, + { + kind: "builtin", + description: + "Prints string representations of its arguments to the runtime log.", + signatures: ["print(...)"], + parameters: [{ name: "...", description: "Values to print." }], + examples: [{ code: 'print("Hello, world!")' }], + }, +); const assertFunction = new LuaBuiltinFunction( async (sf, value: any, message?: string) => { @@ -47,22 +60,53 @@ const assertFunction = new LuaBuiltinFunction( throw new LuaRuntimeError(`Assertion failed: ${message}`, sf); } }, + { + kind: "builtin", + description: + "Raises an error when a value is falsy; otherwise completes successfully.", + parameters: [ + { name: "value", description: "Condition to test." }, + { + name: "message", + type: "string", + description: "Error detail.", + optional: true, + }, + ], + examples: [{ code: 'assert(user ~= nil, "user is required")' }], + }, ); -const ipairsFunction = new LuaBuiltinFunction((sf, t: LuaTable | any[]) => { - let i = 0; +const ipairsFunction = new LuaBuiltinFunction( + (sf, t: LuaTable | any[]) => { + let i = 0; - return async () => { - i = i + 1; + return async () => { + i = i + 1; - const v = await luaGet(t, i, sf.astCtx ?? null, sf); - if (v === null || v === undefined) { - return; - } + const v = await luaGet(t, i, sf.astCtx ?? null, sf); + if (v === null || v === undefined) { + return; + } - return new LuaMultiRes([i, v]); - }; -}); + return new LuaMultiRes([i, v]); + }; + }, + { + kind: "builtin", + description: + "Returns an iterator over consecutive integer keys starting at 1 and stopping at the first `nil`.", + parameters: [{ name: "table", type: "table" }], + returns: [ + { type: "function", description: "Iterator yielding index and value." }, + ], + examples: [ + { + code: 'for i, fruit in ipairs({"apple", "banana"}) do\n print(i, fruit)\nend', + }, + ], + }, +); const pairsFunction = new LuaBuiltinFunction( (sf, t: LuaTable | any[] | Record) => { @@ -105,6 +149,23 @@ const pairsFunction = new LuaBuiltinFunction( // Must return (iter, state, control) for generic for return new LuaMultiRes([iter, t, null]); }, + { + kind: "builtin", + description: + "Returns an iterator over all table key-value pairs, respecting `__pairs`.", + parameters: [{ name: "table", type: "table" }], + returns: [ + { + type: "function", + description: "Iterator plus its state and initial control value.", + }, + ], + examples: [ + { + code: 'for key, value in pairs({name = "Ada", age = 36}) do\n print(key, value)\nend', + }, + ], + }, ); export const eachFunction = new LuaBuiltinFunction( @@ -120,12 +181,30 @@ export const eachFunction = new LuaBuiltinFunction( return result; }; }, + { + kind: "builtin", + description: + "Returns a Space Lua iterator over array-like values without yielding indices.", + parameters: [{ name: "table", type: "table" }], + returns: [{ type: "function", description: "Iterator yielding values." }], + examples: [ + { + code: 'for fruit in each({"apple", "banana"}) do\n print(fruit)\nend', + }, + ], + }, ); const typeFunction = new LuaBuiltinFunction( (_sf, value: LuaValue): string | Promise => { return luaTypeOf(value); }, + { + kind: "builtin", + description: "Returns the Lua type name of a value.", + parameters: [{ name: "value" }], + returns: [{ type: "string" }], + }, ); // tostring() checks `__tostring` metamethod first (with live SF), then @@ -153,6 +232,13 @@ const tostringFunction = new LuaBuiltinFunction( } return luaToString(value); }, + { + kind: "builtin", + description: + "Converts a value to a string, respecting its `__tostring` metamethod.", + parameters: [{ name: "value" }], + returns: [{ type: "string" }], + }, ); const tonumberFunction = new LuaBuiltinFunction( @@ -188,11 +274,33 @@ const tonumberFunction = new LuaBuiltinFunction( return result.value; }, + { + kind: "builtin", + description: + "Converts a number or numeric string to a Lua number, optionally in a base from 2 through 36.", + signatures: [ + "tonumber(value): number|nil", + "tonumber(value, base): integer|nil", + ], + parameters: [ + { name: "value", type: "number|string" }, + { name: "base", type: "integer", optional: true }, + ], + returns: [{ type: "number|nil" }], + examples: [{ code: 'print(tonumber("2a", 16)) -- 42' }], + }, ); -const errorFunction = new LuaBuiltinFunction((sf, message: string) => { - throw new LuaRuntimeError(message, sf); -}); +const errorFunction = new LuaBuiltinFunction( + (sf, message: string) => { + throw new LuaRuntimeError(message, sf); + }, + { + kind: "builtin", + description: "Raises a Lua runtime error with the supplied message.", + parameters: [{ name: "message", type: "string" }], + }, +); async function pcallBoundary( sf: LuaStackFrame, @@ -241,6 +349,23 @@ const pcallFunction = new LuaBuiltinFunction( } return new LuaMultiRes([false, res.message]); }, + { + kind: "builtin", + description: + "Calls a function in protected mode and returns a success flag followed by results or an error message.", + signatures: ["pcall(function, ...): boolean, ..."], + parameters: [ + { name: "function", type: "function" }, + { name: "...", description: "Arguments passed to the function." }, + ], + returns: [ + { type: "boolean", description: "Whether the call succeeded." }, + { description: "Call results or error message." }, + ], + examples: [ + { code: "local ok, result = pcall(function() return mightFail() end)" }, + ], + }, ); const xpcallFunction = new LuaBuiltinFunction( @@ -254,6 +379,26 @@ const xpcallFunction = new LuaBuiltinFunction( const outVals = hr instanceof LuaMultiRes ? hr.flatten().values : [hr]; return new LuaMultiRes([false, ...outVals]); }, + { + kind: "builtin", + description: + "Calls a function in protected mode and transforms any error with an error handler.", + signatures: ["xpcall(function, errorHandler, ...): boolean, ..."], + parameters: [ + { name: "function", type: "function" }, + { name: "errorHandler", type: "function" }, + { name: "...", description: "Arguments passed to the function." }, + ], + returns: [ + { type: "boolean", description: "Whether the call succeeded." }, + { description: "Call results or handler results." }, + ], + examples: [ + { + code: 'local ok, message = xpcall(riskyOperation, function(err)\n return "Operation failed: " .. tostring(err)\nend)', + }, + ], + }, ); const setmetatableFunction = new LuaBuiltinFunction( @@ -264,16 +409,47 @@ const setmetatableFunction = new LuaBuiltinFunction( table.metatable = metatable; return table; }, + { + kind: "builtin", + description: "Sets a table's metatable and returns the table.", + parameters: [ + { name: "table", type: "table" }, + { name: "metatable", type: "table" }, + ], + returns: [{ type: "table" }], + }, ); -const rawlenFunction = new LuaBuiltinFunction((_sf, value: LuaValue) => { - return luaLen(value, _sf, true); -}); +const rawlenFunction = new LuaBuiltinFunction( + (_sf, value: LuaValue) => luaLen(value, _sf, true), + { + kind: "builtin", + description: "Returns a string or table length without invoking `__len`.", + parameters: [{ name: "value", type: "string|table" }], + returns: [{ type: "integer" }], + }, +); const rawsetFunction = new LuaBuiltinFunction( (_sf, table: LuaTable, key: LuaValue, value: LuaValue) => { return (table as any).rawSet(key, value); }, + { + kind: "builtin", + description: + "Sets a table key without invoking `__newindex` and returns the table.", + parameters: [ + { name: "table", type: "table" }, + { name: "key" }, + { name: "value" }, + ], + returns: [{ type: "table" }], + examples: [ + { + code: 'local t = setmetatable({}, {__newindex = function() error("blocked") end})\nrawset(t, "name", "Ada")', + }, + ], + }, ); const rawgetFunction = new LuaBuiltinFunction( @@ -328,38 +504,71 @@ const rawgetFunction = new LuaBuiltinFunction( const v = (table as Record)[k as any]; return v === undefined ? null : v; }, + { + kind: "builtin", + description: "Reads a table key without invoking `__index`.", + parameters: [{ name: "table", type: "table" }, { name: "key" }], + returns: [{ description: "Stored value or `nil`." }], + }, ); -const rawequalFunction = new LuaBuiltinFunction((_sf, a: any, b: any) => { - const av = isTaggedFloat(a) ? a.value : a; - const bv = isTaggedFloat(b) ? b.value : b; - return av === bv; -}); +const rawequalFunction = new LuaBuiltinFunction( + (_sf, a: any, b: any) => { + const av = isTaggedFloat(a) ? a.value : a; + const bv = isTaggedFloat(b) ? b.value : b; + return av === bv; + }, + { + kind: "builtin", + description: "Tests two values for equality without invoking `__eq`.", + parameters: [{ name: "a" }, { name: "b" }], + returns: [{ type: "boolean" }], + }, +); -const getmetatableFunction = new LuaBuiltinFunction((_sf, table: LuaTable) => { - return (table as any).metatable; -}); +const getmetatableFunction = new LuaBuiltinFunction( + (_sf, table: LuaTable) => (table as any).metatable, + { + kind: "builtin", + description: "Returns a table's metatable, or `nil` when none is set.", + parameters: [{ name: "table", type: "table" }], + returns: [{ type: "table|nil" }], + }, +); -const dofileFunction = new LuaBuiltinFunction(async (sf, filename: string) => { - const global = sf.threadLocal.get("_GLOBAL") as LuaEnv; - const file = (await luaCall( - (global.get("space") as any).get("readFile"), - [filename], - sf.astCtx!, - sf, - )) as Uint8Array; - const code = new TextDecoder().decode(file); - try { - const parsedExpr = parseBlock(code); - const env = new LuaEnv(global); - await evalStatement(parsedExpr, env, sf.withCtx(parsedExpr.ctx)); - } catch (e: any) { - throw new LuaRuntimeError( - `Error evaluating "${filename}": ${e.message}`, +const dofileFunction = new LuaBuiltinFunction( + async (sf, filename: string) => { + const global = sf.threadLocal.get("_GLOBAL") as LuaEnv; + const file = (await luaCall( + (global.get("space") as any).get("readFile"), + [filename], + sf.astCtx!, sf, - ); - } -}); + )) as Uint8Array; + const code = new TextDecoder().decode(file); + try { + const parsedExpr = parseBlock(code); + const env = new LuaEnv(global); + await evalStatement(parsedExpr, env, sf.withCtx(parsedExpr.ctx)); + } catch (e: any) { + throw new LuaRuntimeError( + `Error evaluating "${filename}": ${e.message}`, + sf, + ); + } + }, + { + kind: "builtin", + description: "Reads and executes a Lua source file from the current space.", + parameters: [ + { + name: "path", + type: "string", + description: "Space-relative Lua file path.", + }, + ], + }, +); /** * From the Lua docs: @@ -381,6 +590,21 @@ const selectFunction = new LuaBuiltinFunction( return new LuaMultiRes(args.slice(args.length + index)); } }, + { + kind: "builtin", + description: + "Returns the count of extra arguments or all arguments from a selected position onward.", + signatures: ['select("#", ...): integer', "select(index, ...): ..."], + parameters: [ + { + name: "index", + type: "integer|string", + description: "One-based index, negative index from the end, or `#`.", + }, + { name: "..." }, + ], + returns: [{ description: "Argument count or selected argument values." }], + }, ); /** @@ -434,24 +658,90 @@ const nextFunction = new LuaBuiltinFunction( } return new LuaMultiRes([key, luaGet(table, key, sf.astCtx ?? null, sf)]); }, + { + kind: "builtin", + description: + "Returns the next table key and value after a given key, or the first pair when the key is omitted.", + parameters: [ + { name: "table", type: "table" }, + { name: "index", description: "Previous key.", optional: true }, + ], + returns: [ + { description: "Next key or `nil`." }, + { description: "Value at the next key." }, + ], + }, ); // Non-standard, but useful -const someFunction = new LuaBuiltinFunction(async (_sf, value: any) => { - switch (await luaTypeOf(value)) { - case "number": - if (!Number.isFinite(value)) return null; - break; - case "string": - if (value.trim() === "") return null; - break; - case "table": - if (luaKeys(value).length === 0) return null; - } - return value; +const someFunction = new LuaBuiltinFunction( + async (_sf, value: any) => { + switch (await luaTypeOf(value)) { + case "number": + if (!Number.isFinite(value)) return null; + break; + case "string": + if (value.trim() === "") return null; + break; + case "table": + if (luaKeys(value).length === 0) return null; + } + return value; + }, + { + kind: "builtin", + description: + "Returns `nil` for empty Space Lua values and otherwise returns the value unchanged.", + parameters: [ + { + name: "value", + description: + "Value to normalize; blank strings, empty tables, infinities, and NaN are empty.", + }, + ], + returns: [{ description: "Original value or `nil`." }], + examples: [ + { + code: 'print(some(" ") or "empty")\nprint(some({}) or "empty")\nprint(some(0))', + }, + ], + }, +); + +const loadFunction = new LuaBuiltinFunction((sf, s) => luaLoad(s, sf), { + kind: "builtin", + description: + "Compiles Lua source into a callable chunk without executing it.", + parameters: [ + { name: "chunk", type: "string", description: "Lua source code." }, + ], + returns: [ + { type: "function|nil", description: "Compiled chunk or `nil`." }, + { type: "string", description: "Compilation error when unsuccessful." }, + ], }); -const loadFunction = new LuaBuiltinFunction((sf, s) => luaLoad(s, sf)); +function annotateBuiltinApi( + value: unknown, + path: string, + page: string, + seen = new WeakSet(), +): void { + if (!value || typeof value !== "object" || seen.has(value)) return; + seen.add(value); + if (isILuaFunction(value)) { + value.info ??= { kind: "builtin" }; + value.info.name ??= path; + value.info.see ??= page; + return; + } + if (value instanceof LuaTable) { + for (const key of value.keys()) { + if (typeof key !== "string") continue; + annotateBuiltinApi(value.rawGet(key), `${path}.${key}`, page, seen); + } + } +} export function luaBuildStandardEnv() { const env = new LuaEnv(); @@ -499,5 +789,11 @@ export function luaBuildStandardEnv() { env.set("crypto", cryptoApi); env.set("net", netApi); env.set("some", someFunction); + + for (const name of env.keys()) { + const value = env.get(name); + const page = value instanceof LuaTable ? `API/${name}` : "API/global"; + annotateBuiltinApi(value, name, page); + } return env; } diff --git a/client/space_lua/stdlib/crypto.ts b/client/space_lua/stdlib/crypto.ts index 3b139951..cb7cff07 100644 --- a/client/space_lua/stdlib/crypto.ts +++ b/client/space_lua/stdlib/crypto.ts @@ -6,5 +6,13 @@ export const cryptoApi = new LuaTable({ (_sf, s: string | Uint8Array): Promise => { return hashSHA256(s); }, + { + kind: "builtin", + description: "Computes the SHA-256 digest of a string or byte buffer.", + parameters: [ + { name: "data", type: "string|bytes", description: "Data to hash." }, + ], + returns: [{ type: "string", description: "Hexadecimal SHA-256 digest." }], + }, ), }); diff --git a/client/space_lua/stdlib/encoding.ts b/client/space_lua/stdlib/encoding.ts index 77f43ad9..2055cb97 100644 --- a/client/space_lua/stdlib/encoding.ts +++ b/client/space_lua/stdlib/encoding.ts @@ -6,14 +6,44 @@ export const encodingApi = new LuaTable({ (_sf, s: string | Uint8Array): string => { return base64Encode(s); }, + { + kind: "builtin", + description: "Encodes a string or byte buffer as Base64.", + parameters: [{ name: "data", type: "string|bytes" }], + returns: [{ type: "string", description: "Base64-encoded data." }], + }, + ), + base64Decode: new LuaBuiltinFunction( + (_sf, s: string): Uint8Array => { + return base64Decode(s); + }, + { + kind: "builtin", + description: "Decodes a Base64 string into a byte buffer.", + parameters: [{ name: "encoded", type: "string" }], + returns: [{ type: "bytes", description: "Decoded bytes." }], + }, + ), + utf8Encode: new LuaBuiltinFunction( + (_sf, s: string): Uint8Array => { + return new TextEncoder().encode(s); + }, + { + kind: "builtin", + description: "Encodes a UTF-8 string into a byte buffer.", + parameters: [{ name: "value", type: "string" }], + returns: [{ type: "bytes", description: "UTF-8 encoded bytes." }], + }, + ), + utf8Decode: new LuaBuiltinFunction( + (_sf, data: Uint8Array): string => { + return new TextDecoder().decode(data); + }, + { + kind: "builtin", + description: "Decodes a UTF-8 byte buffer into a string.", + parameters: [{ name: "data", type: "bytes" }], + returns: [{ type: "string", description: "Decoded text." }], + }, ), - base64Decode: new LuaBuiltinFunction((_sf, s: string): Uint8Array => { - return base64Decode(s); - }), - utf8Encode: new LuaBuiltinFunction((_sf, s: string): Uint8Array => { - return new TextEncoder().encode(s); - }), - utf8Decode: new LuaBuiltinFunction((_sf, data: Uint8Array): string => { - return new TextDecoder().decode(data); - }), }); diff --git a/client/space_lua/stdlib/js.ts b/client/space_lua/stdlib/js.ts index eb36e7a0..e2ddd182 100644 --- a/client/space_lua/stdlib/js.ts +++ b/client/space_lua/stdlib/js.ts @@ -13,22 +13,60 @@ export const jsApi = new LuaTable({ * @param args - The arguments to pass to the constructor. * @returns The new instance. */ - new: new LuaBuiltinFunction((sf, constructorFn: any, ...args) => { - return new constructorFn(...args.map((v) => luaValueToJS(v, sf))); - }), + new: new LuaBuiltinFunction( + (sf, constructorFn: any, ...args) => { + return new constructorFn(...args.map((v) => luaValueToJS(v, sf))); + }, + { + kind: "builtin", + description: "Creates an instance of a JavaScript class.", + signatures: ["js.new(constructor, ...): userdata"], + parameters: [ + { + name: "constructor", + type: "userdata", + description: "JavaScript constructor function.", + }, + { + name: "...", + description: "Constructor arguments converted to JavaScript values.", + }, + ], + returns: [{ type: "userdata", description: "New JavaScript instance." }], + examples: [ + { code: 'local value = js.new(js.window.Date, "2024-03-14")' }, + ], + }, + ), /** * Imports a JavaScript module. * @param url - The URL of the module to import. * @returns The imported module. */ - import: new LuaBuiltinFunction(async (_sf, url) => { - let m = await import(url); - // Unwrap default if it exists - if (Object.keys(m).length === 1 && m.default) { - m = m.default; - } - return m; - }), + import: new LuaBuiltinFunction( + async (_sf, url) => { + let m = await import(url); + // Unwrap default if it exists + if (Object.keys(m).length === 1 && m.default) { + m = m.default; + } + return m; + }, + { + kind: "builtin", + description: "Dynamically imports a JavaScript module from a URL.", + parameters: [{ name: "url", type: "string", description: "Module URL." }], + returns: [ + { + type: "userdata", + description: "Imported module, with a sole default export unwrapped.", + }, + ], + examples: [ + { code: 'local lib = js.import("https://esm.sh/lodash@4.17.21")' }, + ], + }, + ), /** * Like `js.import`, but takes a path to a file in the current space (e.g. * "Library/foo/bar.js") and resolves it to its full same-origin `/.fs` URL @@ -36,51 +74,127 @@ export const jsApi = new LuaTable({ * @param path - Space-relative path to the JS module (leading "/" optional). * @returns The imported module (with a sole `default` export unwrapped). */ - importFromSpace: new LuaBuiltinFunction(async (_sf, path: string) => { - const base = document.baseURI.replace(/\/*$/, "/"); - const rel = String(path).replace(/^\/+/, ""); - let m = await import(base + fsEndpoint.slice(1) + "/" + rel); - // Unwrap default if it exists (same contract as `js.import`). - if (Object.keys(m).length === 1 && m.default) { - m = m.default; - } - return m; - }), - eachIterable: new LuaBuiltinFunction((_sf, val) => { - const iterator = val[Symbol.asyncIterator](); - return async () => { - const result = await iterator.next(); - if (result.done) { - return; + importFromSpace: new LuaBuiltinFunction( + async (_sf, path: string) => { + const base = document.baseURI.replace(/\/*$/, "/"); + const rel = String(path).replace(/^\/+/, ""); + let m = await import(base + fsEndpoint.slice(1) + "/" + rel); + // Unwrap default if it exists (same contract as `js.import`). + if (Object.keys(m).length === 1 && m.default) { + m = m.default; } - return result.value; - }; - }), + return m; + }, + { + kind: "builtin", + description: + "Imports a JavaScript module from a file in the current space.", + parameters: [ + { + name: "path", + type: "string", + description: + "Space-relative module path, with an optional leading slash.", + }, + ], + returns: [ + { + type: "userdata", + description: "Imported module, with a sole default export unwrapped.", + }, + ], + examples: [ + { code: 'local acme = js.importFromSpace("Library/acme/acme.js")' }, + ], + }, + ), + eachIterable: new LuaBuiltinFunction( + (_sf, val) => { + const iterator = val[Symbol.asyncIterator](); + return async () => { + const result = await iterator.next(); + if (result.done) { + return; + } + return result.value; + }; + }, + { + kind: "builtin", + description: "Creates a Lua iterator over a JavaScript async iterable.", + parameters: [ + { + name: "iterable", + type: "userdata", + description: "JavaScript async iterable.", + }, + ], + returns: [ + { + type: "function", + description: "Iterator yielding successive JavaScript values.", + }, + ], + examples: [ + { + code: "for value in js.eachIterable(someJsAsyncIterable) do\n print(value)\nend", + }, + ], + }, + ), /** * Converts a JavaScript value to a Lua value. * @param val - The JavaScript value to convert. * @returns The Lua value. */ - tolua: new LuaBuiltinFunction((_sf, val) => jsToLuaValue(val)), + tolua: new LuaBuiltinFunction((_sf, val) => jsToLuaValue(val), { + kind: "builtin", + description: "Converts a JavaScript value to its Lua representation.", + parameters: [ + { name: "value", description: "JavaScript value to convert." }, + ], + returns: [{ description: "Converted Lua value." }], + examples: [{ code: "local luaTable = js.tolua(jsArray)" }], + }), /** * Converts a Lua value to a JavaScript value. * @param val - The Lua value to convert. * @returns The JavaScript value. */ - tojs: new LuaBuiltinFunction((sf, val) => luaValueToJS(val, sf)), + tojs: new LuaBuiltinFunction((sf, val) => luaValueToJS(val, sf), { + kind: "builtin", + description: "Converts a Lua value to its JavaScript representation.", + parameters: [{ name: "value", description: "Lua value to convert." }], + returns: [{ description: "Converted JavaScript value." }], + examples: [{ code: "local jsArray = js.tojs({1, 2, 3})" }], + }), /** * Logs a message to the console. * @param args - The arguments to log. */ - log: new LuaBuiltinFunction((_sf, ...args) => { - console.log(...args); - }), + log: new LuaBuiltinFunction( + (_sf, ...args) => { + console.log(...args); + }, + { + kind: "builtin", + description: "Logs values to the JavaScript console.", + parameters: [{ name: "...", description: "Values to log." }], + examples: [{ code: 'js.log("User data:", {name = "Ada"})' }], + }, + ), /** * Converts a Lua value to a JSON string. * @param val - The Lua value to convert. * @returns The JSON string. */ - stringify: new LuaBuiltinFunction((_sf, val) => JSON.stringify(val)), + stringify: new LuaBuiltinFunction((_sf, val) => JSON.stringify(val), { + kind: "builtin", + description: "Serializes a value as JSON using JavaScript semantics.", + parameters: [{ name: "value", description: "Value to serialize." }], + returns: [{ type: "string", description: "JSON representation." }], + examples: [{ code: "print(js.stringify({1, 2, 3})) -- [1,2,3]" }], + }), // Expose the global window object window: globalThis, diff --git a/client/space_lua/stdlib/load.ts b/client/space_lua/stdlib/load.ts index 2f07850f..50eabcc7 100644 --- a/client/space_lua/stdlib/load.ts +++ b/client/space_lua/stdlib/load.ts @@ -24,23 +24,35 @@ export function luaLoad(code: LuaValue, sf: LuaStackFrame): LuaValue { const globalEnv: LuaEnv = globalEnvMaybe as LuaEnv; - const runner = new LuaBuiltinFunction(async (innerSf: LuaStackFrame) => { - const res = await evalStatement(block, globalEnv, innerSf, true); - if (res === undefined) { - return null; - } else { - if (res && typeof res === "object" && (res as any).ctrl === "return") { - return new LuaMultiRes((res as any).values); + const runner = new LuaBuiltinFunction( + async (innerSf: LuaStackFrame) => { + const res = await evalStatement(block, globalEnv, innerSf, true); + if (res === undefined) { + return null; + } else { + if ( + res && + typeof res === "object" && + (res as any).ctrl === "return" + ) { + return new LuaMultiRes((res as any).values); + } + if (res && typeof res === "object" && (res as any).ctrl === "break") { + throw new Error("break outside loop"); + } + if (res && typeof res === "object" && (res as any).ctrl === "goto") { + throw new Error("unexpected goto signal"); + } + return null; } - if (res && typeof res === "object" && (res as any).ctrl === "break") { - throw new Error("break outside loop"); - } - if (res && typeof res === "object" && (res as any).ctrl === "goto") { - throw new Error("unexpected goto signal"); - } - return null; - } - }); + }, + { + kind: "builtin", + description: "Executes the Lua chunk compiled by `load`.", + signatures: ["function(...)"], + returns: [{ description: "Values returned by the compiled chunk." }], + }, + ); return runner; } catch (e: any) { diff --git a/client/space_lua/stdlib/math.ts b/client/space_lua/stdlib/math.ts index 58c8d2f9..ed4f1252 100644 --- a/client/space_lua/stdlib/math.ts +++ b/client/space_lua/stdlib/math.ts @@ -23,49 +23,71 @@ export const mathApi = new LuaTable({ pi: Math.PI, // math.type(x) => "integer" | "float" | nil - type: new LuaBuiltinFunction((_sf, x?: any) => { - if (x === undefined) { - throw new LuaRuntimeError( - "bad argument #1 to 'math.type' (value expected)", - _sf, - ); - } - if (isTaggedFloat(x)) { - return "float"; - } - if (typeof x === "number") { - if (!Number.isFinite(x) || isNegativeZero(x)) { + type: new LuaBuiltinFunction( + (_sf, x?: any) => { + if (x === undefined) { + throw new LuaRuntimeError( + "bad argument #1 to 'math.type' (value expected)", + _sf, + ); + } + if (isTaggedFloat(x)) { return "float"; } - return Number.isInteger(x) ? "integer" : "float"; - } - if (typeof x === "bigint") { - return "integer"; - } - return null; - }), + if (typeof x === "number") { + if (!Number.isFinite(x) || isNegativeZero(x)) { + return "float"; + } + return Number.isInteger(x) ? "integer" : "float"; + } + if (typeof x === "bigint") { + return "integer"; + } + return null; + }, + { + kind: "builtin", + description: + "Returns `integer` or `float` for a number, or `nil` for other values.", + parameters: [{ name: "x", description: "Value to inspect." }], + returns: [ + { type: "string|nil", description: "Numeric subtype or `nil`." }, + ], + }, + ), /** * If the value x is representable as a Lua integer, returns an integer * with that value. Otherwise returns nil. * Strings are NOT accepted — only Lua number values. */ - tointeger: new LuaBuiltinFunction((_sf, x?: any) => { - if (typeof x === "number") { - return Number.isInteger(x) && Number.isFinite(x) ? x : null; - } - if (isTaggedFloat(x)) { - const n = x.value; - return Number.isInteger(n) && Number.isFinite(n) ? n : null; - } - if (typeof x === "string") { - const n = untagNumber(x); // Number(x) coerces the string - if (Number.isNaN(n) || !Number.isFinite(n) || !Number.isInteger(n)) - return null; - return n; - } - return null; - }), + tointeger: new LuaBuiltinFunction( + (_sf, x?: any) => { + if (typeof x === "number") { + return Number.isInteger(x) && Number.isFinite(x) ? x : null; + } + if (isTaggedFloat(x)) { + const n = x.value; + return Number.isInteger(n) && Number.isFinite(n) ? n : null; + } + if (typeof x === "string") { + const n = untagNumber(x); // Number(x) coerces the string + if (Number.isNaN(n) || !Number.isFinite(n) || !Number.isInteger(n)) + return null; + return n; + } + return null; + }, + { + kind: "builtin", + description: + "Converts a value to an integer when it has an exact finite integral representation.", + parameters: [{ name: "x", description: "Value to convert." }], + returns: [ + { type: "integer|nil", description: "Converted integer or `nil`." }, + ], + }, + ), /** * When called without arguments, returns a pseudo-random float with @@ -76,118 +98,345 @@ export const mathApi = new LuaTable({ * math.random(1,n). The call math.random(0) produces an integer * with all bits (pseudo)random. */ - random: new LuaBuiltinFunction((_sf, m?: number, n?: number) => { - if (m !== undefined) m = untagNumber(m); - if (n !== undefined) n = untagNumber(n); - try { - return prng.random(m, n); - } catch (e: any) { - throw new LuaRuntimeError(e.message, _sf); - } - }), + random: new LuaBuiltinFunction( + (_sf, m?: number, n?: number) => { + if (m !== undefined) m = untagNumber(m); + if (n !== undefined) n = untagNumber(n); + try { + return prng.random(m, n); + } catch (e: any) { + throw new LuaRuntimeError(e.message, _sf); + } + }, + { + kind: "builtin", + description: + "Returns a pseudo-random float or an integer in a requested inclusive range.", + signatures: [ + "math.random(): number", + "math.random(n): integer", + "math.random(m, n): integer", + ], + parameters: [ + { name: "m", type: "integer", optional: true }, + { name: "n", type: "integer", optional: true }, + ], + returns: [{ type: "number", description: "Pseudo-random result." }], + examples: [ + { + code: "print(math.random())\nprint(math.random(10))\nprint(math.random(5, 10))", + }, + ], + }, + ), /** * Seeds the pseudo-random generator. With no arguments, uses a * time-based seed. Returns the two seed integers used (Lua 5.4 contract). */ - randomseed: new LuaBuiltinFunction((_sf, x?: number, y?: number) => { - if (x !== undefined) x = untagNumber(x); - if (y !== undefined) y = untagNumber(y); - const [s1, s2] = prng.randomseed(x, y); - return new LuaMultiRes([s1, s2]); - }), + randomseed: new LuaBuiltinFunction( + (_sf, x?: number, y?: number) => { + if (x !== undefined) x = untagNumber(x); + if (y !== undefined) y = untagNumber(y); + const [s1, s2] = prng.randomseed(x, y); + return new LuaMultiRes([s1, s2]); + }, + { + kind: "builtin", + description: + "Seeds the pseudo-random generator and returns the two seeds used.", + signatures: [ + "math.randomseed(): integer, integer", + "math.randomseed(x, y): integer, integer", + ], + parameters: [ + { name: "x", type: "integer", optional: true }, + { name: "y", type: "integer", optional: true }, + ], + returns: [ + { type: "integer", description: "First seed." }, + { type: "integer", description: "Second seed." }, + ], + }, + ), // Basic functions - abs: new LuaBuiltinFunction((_sf, x: number) => Math.abs(untagNumber(x))), - ceil: new LuaBuiltinFunction((_sf, x: number) => Math.ceil(untagNumber(x))), - floor: new LuaBuiltinFunction((_sf, x: number) => Math.floor(untagNumber(x))), - max: new LuaBuiltinFunction((_sf, ...args: number[]) => - Math.max(...args.map(untagNumber)), + abs: new LuaBuiltinFunction((_sf, x: number) => Math.abs(untagNumber(x)), { + kind: "builtin", + description: "Returns the absolute value of `x`.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "number" }], + }), + ceil: new LuaBuiltinFunction((_sf, x: number) => Math.ceil(untagNumber(x)), { + kind: "builtin", + description: "Returns the smallest integer greater than or equal to `x`.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "integer" }], + }), + floor: new LuaBuiltinFunction( + (_sf, x: number) => Math.floor(untagNumber(x)), + { + kind: "builtin", + description: "Returns the largest integer less than or equal to `x`.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "integer" }], + }, ), - min: new LuaBuiltinFunction((_sf, ...args: number[]) => - Math.min(...args.map(untagNumber)), + max: new LuaBuiltinFunction( + (_sf, ...args: number[]) => Math.max(...args.map(untagNumber)), + { + kind: "builtin", + description: "Returns the greatest of its arguments.", + signatures: ["math.max(x, ...): number"], + returns: [{ type: "number" }], + }, + ), + min: new LuaBuiltinFunction( + (_sf, ...args: number[]) => Math.min(...args.map(untagNumber)), + { + kind: "builtin", + description: "Returns the least of its arguments.", + signatures: ["math.min(x, ...): number"], + returns: [{ type: "number" }], + }, ), // Rounding and remainder fmod: new LuaBuiltinFunction( (_sf, x: number, y: number) => untagNumber(x) % untagNumber(y), + { + kind: "builtin", + description: + "Returns the remainder of `x / y` with the quotient rounded toward zero.", + parameters: [ + { name: "x", type: "number" }, + { name: "y", type: "number" }, + ], + returns: [{ type: "number" }], + }, + ), + modf: new LuaBuiltinFunction( + (_sf, x: number) => { + const xn = untagNumber(x); + const int = Math.trunc(xn); + // Guarantee that the `frac` part is always Lua float + const frac = makeLuaFloat(xn - int); + return new LuaMultiRes([int, frac]); + }, + { + kind: "builtin", + description: "Splits `x` into its integral and fractional parts.", + parameters: [{ name: "x", type: "number" }], + returns: [ + { type: "integer", description: "Integral part." }, + { type: "float", description: "Fractional part." }, + ], + examples: [{ code: "local integer, fraction = math.modf(3.14)" }], + }, ), - modf: new LuaBuiltinFunction((_sf, x: number) => { - const xn = untagNumber(x); - const int = Math.trunc(xn); - // Guarantee that the `frac` part is always Lua float - const frac = makeLuaFloat(xn - int); - return new LuaMultiRes([int, frac]); - }), // Returns m and e such that x = m * 2^e, 0.5 <= |m| < 1 (or m=0 when x=0). // e is an integer. Mirrors C99/Lua. // Special cases: frexp(0) = (0, 0); frexp(+-inf/nan) = (x, 0). - frexp: new LuaBuiltinFunction((_sf, x: number) => { - const xn = untagNumber(x); - if (xn === 0 || !Number.isFinite(xn) || Number.isNaN(xn)) { - return new LuaMultiRes([xn, 0]); - } - const abs = Math.abs(xn); - let e = Math.floor(Math.log2(abs)) + 1; - let m = xn / 2 ** e; - if (Math.abs(m) >= 1.0) { - e += 1; - m /= 2; - } - if (Math.abs(m) < 0.5) { - e -= 1; - m *= 2; - } - return new LuaMultiRes([m, e]); - }), + frexp: new LuaBuiltinFunction( + (_sf, x: number) => { + const xn = untagNumber(x); + if (xn === 0 || !Number.isFinite(xn) || Number.isNaN(xn)) { + return new LuaMultiRes([xn, 0]); + } + const abs = Math.abs(xn); + let e = Math.floor(Math.log2(abs)) + 1; + let m = xn / 2 ** e; + if (Math.abs(m) >= 1.0) { + e += 1; + m /= 2; + } + if (Math.abs(m) < 0.5) { + e -= 1; + m *= 2; + } + return new LuaMultiRes([m, e]); + }, + { + kind: "builtin", + description: + "Decomposes `x` into a normalized fraction and a power-of-two exponent.", + parameters: [{ name: "x", type: "number" }], + returns: [ + { type: "number", description: "Fraction." }, + { type: "integer", description: "Exponent." }, + ], + }, + ), // Returns m * 2^e (the inverse of frexp). Mirrors C99/Lua. ldexp: new LuaBuiltinFunction( (_sf, m: number, e: number) => untagNumber(m) * 2 ** untagNumber(e), + { + kind: "builtin", + description: "Returns `m * 2^e`, the inverse of `math.frexp`.", + parameters: [ + { name: "m", type: "number" }, + { name: "e", type: "integer" }, + ], + returns: [{ type: "number" }], + }, ), // Power and logarithms - exp: new LuaBuiltinFunction((_sf, x: number) => Math.exp(untagNumber(x))), - log: new LuaBuiltinFunction((_sf, x: number, base?: number) => { - if (base === undefined) { - return Math.log(untagNumber(x)); - } - return Math.log(untagNumber(x)) / Math.log(untagNumber(base)); + exp: new LuaBuiltinFunction((_sf, x: number) => Math.exp(untagNumber(x)), { + kind: "builtin", + description: "Returns `e` raised to `x`.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "number" }], }), + log: new LuaBuiltinFunction( + (_sf, x: number, base?: number) => { + if (base === undefined) { + return Math.log(untagNumber(x)); + } + return Math.log(untagNumber(x)) / Math.log(untagNumber(base)); + }, + { + kind: "builtin", + description: + "Returns the logarithm of `x`, using the natural base unless another base is supplied.", + parameters: [ + { name: "x", type: "number" }, + { name: "base", type: "number", optional: true }, + ], + returns: [{ type: "number" }], + examples: [{ code: "print(math.log(100, 10)) -- 2" }], + }, + ), // Power function (deprecated in Lua 5.4 but retained for compatibility) pow: new LuaBuiltinFunction( (_sf, x: number, y: number) => untagNumber(x) ** untagNumber(y), + { + kind: "builtin", + description: "Returns `x` raised to the power `y`.", + parameters: [ + { name: "x", type: "number" }, + { name: "y", type: "number" }, + ], + returns: [{ type: "number" }], + deprecated: "Use the `^` operator instead.", + }, ), - sqrt: new LuaBuiltinFunction((_sf, x: number) => Math.sqrt(untagNumber(x))), - - // Trigonometric functions - cos: new LuaBuiltinFunction((_sf, x: number) => Math.cos(untagNumber(x))), - sin: new LuaBuiltinFunction((_sf, x: number) => Math.sin(untagNumber(x))), - tan: new LuaBuiltinFunction((_sf, x: number) => Math.tan(untagNumber(x))), - acos: new LuaBuiltinFunction((_sf, x: number) => Math.acos(untagNumber(x))), - asin: new LuaBuiltinFunction((_sf, x: number) => Math.asin(untagNumber(x))), - atan: new LuaBuiltinFunction((_sf, y: number, x?: number) => { - if (x === undefined) { - return Math.atan(untagNumber(y)); - } - return Math.atan2(untagNumber(y), untagNumber(x)); + sqrt: new LuaBuiltinFunction((_sf, x: number) => Math.sqrt(untagNumber(x)), { + kind: "builtin", + description: "Returns the square root of `x`.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "number" }], }), + // Trigonometric functions + cos: new LuaBuiltinFunction((_sf, x: number) => Math.cos(untagNumber(x)), { + kind: "builtin", + description: "Returns the cosine of `x` radians.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "number" }], + }), + sin: new LuaBuiltinFunction((_sf, x: number) => Math.sin(untagNumber(x)), { + kind: "builtin", + description: "Returns the sine of `x` radians.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "number" }], + }), + tan: new LuaBuiltinFunction((_sf, x: number) => Math.tan(untagNumber(x)), { + kind: "builtin", + description: "Returns the tangent of `x` radians.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "number" }], + }), + acos: new LuaBuiltinFunction((_sf, x: number) => Math.acos(untagNumber(x)), { + kind: "builtin", + description: "Returns the arc cosine of `x` in radians.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "number" }], + }), + asin: new LuaBuiltinFunction((_sf, x: number) => Math.asin(untagNumber(x)), { + kind: "builtin", + description: "Returns the arc sine of `x` in radians.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "number" }], + }), + atan: new LuaBuiltinFunction( + (_sf, y: number, x?: number) => { + if (x === undefined) { + return Math.atan(untagNumber(y)); + } + return Math.atan2(untagNumber(y), untagNumber(x)); + }, + { + kind: "builtin", + description: + "Returns the arc tangent of `y/x` in radians, using `1` for omitted `x`.", + parameters: [ + { name: "y", type: "number" }, + { name: "x", type: "number", optional: true }, + ], + returns: [{ type: "number" }], + }, + ), + // Hyperbolic functions (deprecated in Lua 5.4 but retained for compatibility) - cosh: new LuaBuiltinFunction((_sf, x: number) => Math.cosh(untagNumber(x))), - sinh: new LuaBuiltinFunction((_sf, x: number) => Math.sinh(untagNumber(x))), - tanh: new LuaBuiltinFunction((_sf, x: number) => Math.tanh(untagNumber(x))), + cosh: new LuaBuiltinFunction((_sf, x: number) => Math.cosh(untagNumber(x)), { + kind: "builtin", + description: "Returns the hyperbolic cosine of `x`.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "number" }], + deprecated: "Retained for compatibility with older Lua versions.", + }), + sinh: new LuaBuiltinFunction((_sf, x: number) => Math.sinh(untagNumber(x)), { + kind: "builtin", + description: "Returns the hyperbolic sine of `x`.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "number" }], + deprecated: "Retained for compatibility with older Lua versions.", + }), + tanh: new LuaBuiltinFunction((_sf, x: number) => Math.tanh(untagNumber(x)), { + kind: "builtin", + description: "Returns the hyperbolic tangent of `x`.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "number" }], + deprecated: "Retained for compatibility with older Lua versions.", + }), // Additional utility deg: new LuaBuiltinFunction( (_sf, x: number) => (untagNumber(x) * 180) / Math.PI, + { + kind: "builtin", + description: "Converts an angle from radians to degrees.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "number" }], + examples: [{ code: "print(math.deg(math.pi)) -- 180" }], + }, ), rad: new LuaBuiltinFunction( (_sf, x: number) => (untagNumber(x) * Math.PI) / 180, + { + kind: "builtin", + description: "Converts an angle from degrees to radians.", + parameters: [{ name: "x", type: "number" }], + returns: [{ type: "number" }], + }, + ), + ult: new LuaBuiltinFunction( + (_sf, m: number, n: number) => { + return untagNumber(m) >>> 0 < untagNumber(n) >>> 0; + }, + { + kind: "builtin", + description: "Compares two integers as unsigned 32-bit values.", + parameters: [ + { name: "m", type: "integer" }, + { name: "n", type: "integer" }, + ], + returns: [{ type: "boolean" }], + examples: [{ code: "print(math.ult(2, 3)) -- true" }], + }, ), - ult: new LuaBuiltinFunction((_sf, m: number, n: number) => { - return untagNumber(m) >>> 0 < untagNumber(n) >>> 0; - }), // Keep the cosineSimilarity utility function cosineSimilarity: new LuaBuiltinFunction( @@ -215,5 +464,18 @@ export const mathApi = new LuaTable({ return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB)); }, + { + kind: "builtin", + description: + "Returns the cosine similarity between two equal-length numeric vectors.", + parameters: [ + { name: "vecA", type: "table" }, + { name: "vecB", type: "table" }, + ], + returns: [{ type: "number", description: "Cosine similarity." }], + examples: [ + { code: "print(math.cosineSimilarity({1, 2, 3}, {4, 5, 6}))" }, + ], + }, ), }); diff --git a/client/space_lua/stdlib/net.ts b/client/space_lua/stdlib/net.ts index f46a0922..c7190248 100644 --- a/client/space_lua/stdlib/net.ts +++ b/client/space_lua/stdlib/net.ts @@ -70,6 +70,29 @@ export const netApi = new LuaTable({ body: body, }; }, + { + kind: "builtin", + description: + "Performs an HTTP request through the SilverBullet server to avoid browser CORS restrictions.", + signatures: ["net.proxyFetch(url, options?)"], + parameters: [ + { name: "url", type: "string", description: "URL to request." }, + { + name: "options", + type: "table", + description: + "Optional method, headers, body, and responseEncoding values.", + optional: true, + }, + ], + returns: [ + { + type: "table", + description: "Response status, headers, decoded body, and ok flag.", + }, + ], + see: "API/net", + }, ), readURI: new LuaNativeJSFunction( (uri: string, options: { uri?: string; encoding?: string } = {}) => { @@ -79,6 +102,22 @@ export const netApi = new LuaTable({ options, ); }, + { + kind: "builtin", + description: "Reads content from a URI using the best matching service.", + signatures: ["net.readURI(uri, options?)"], + parameters: [ + { name: "uri", type: "string", description: "URI to read." }, + { + name: "options", + type: "table", + description: "Optional service-specific values such as encoding.", + optional: true, + }, + ], + returns: [{ description: "Content returned by the matching service." }], + see: "API/net", + }, ), writeURI: new LuaNativeJSFunction( (uri: string, content: string | Uint8Array) => { @@ -87,6 +126,21 @@ export const netApi = new LuaTable({ { uri, content }, ); }, + { + kind: "builtin", + description: "Writes content to a URI using the best matching service.", + signatures: ["net.writeURI(uri, content)"], + parameters: [ + { name: "uri", type: "string", description: "URI to write." }, + { + name: "content", + type: "string|userdata", + description: "Text or binary content to write.", + }, + ], + returns: [{ description: "Result returned by the matching service." }], + see: "API/net", + }, ), }); diff --git a/client/space_lua/stdlib/os.ts b/client/space_lua/stdlib/os.ts index 0d21396e..9f88e221 100644 --- a/client/space_lua/stdlib/os.ts +++ b/client/space_lua/stdlib/os.ts @@ -282,39 +282,78 @@ function luaFormatTime(fmt: string, d: Date, utc: boolean): string { } export const osApi = new LuaTable({ - time: new LuaBuiltinFunction((_sf, tbl?: LuaTable) => { - if (tbl) { - if (!tbl.has("year")) { - throw new Error("time(): year is required"); + time: new LuaBuiltinFunction( + (_sf, tbl?: LuaTable) => { + if (tbl) { + if (!tbl.has("year")) { + throw new Error("time(): year is required"); + } + + if (!tbl.has("month")) { + throw new Error("time(): month is required"); + } + + if (!tbl.has("day")) { + throw new Error("time(): day is required"); + } + + const year = tbl.get("year"); + const month = tbl.get("month"); + const day = tbl.get("day"); + const hour = tbl.get("hour") ?? 12; + const min = tbl.get("min") ?? 0; + const sec = tbl.get("sec") ?? 0; + const date = new Date(year, month - 1, day, hour, min, sec); + + return Math.floor(date.getTime() / 1000); } - if (!tbl.has("month")) { - throw new Error("time(): month is required"); - } - - if (!tbl.has("day")) { - throw new Error("time(): day is required"); - } - - const year = tbl.get("year"); - const month = tbl.get("month"); - const day = tbl.get("day"); - const hour = tbl.get("hour") ?? 12; - const min = tbl.get("min") ?? 0; - const sec = tbl.get("sec") ?? 0; - const date = new Date(year, month - 1, day, hour, min, sec); - - return Math.floor(date.getTime() / 1000); - } - - return Math.floor(Date.now() / 1000); - }), + return Math.floor(Date.now() / 1000); + }, + { + kind: "builtin", + description: + "Returns the current Unix timestamp or one built from a local date table.", + signatures: ["os.time(): integer", "os.time(dateTable): integer"], + parameters: [ + { + name: "dateTable", + type: "table", + description: + "Local date fields `year`, `month`, `day`, and optional `hour`, `min`, and `sec`.", + optional: true, + }, + ], + returns: [ + { type: "integer", description: "Seconds since the Unix epoch." }, + ], + examples: [ + { + code: "local timestamp = os.time({year = 2020, month = 1, day = 1})", + }, + ], + }, + ), // Returns the difference, from time `t1` to time `t2` in seconds // In POSIX and some other systems, this value is exactly $t2-t1$. - difftime: new LuaBuiltinFunction((_sf, t2: number, t1: number): number => { - return t2 - t1; - }), + difftime: new LuaBuiltinFunction( + (_sf, t2: number, t1: number): number => { + return t2 - t1; + }, + { + kind: "builtin", + description: + "Returns the difference in seconds from timestamp `t1` to `t2`.", + parameters: [ + { name: "t2", type: "number" }, + { name: "t1", type: "number" }, + ], + returns: [ + { type: "number", description: "The value `t2 - t1` in seconds." }, + ], + }, + ), // Returns a string or a table containing date and time, formatted // according to the given string format. @@ -336,29 +375,69 @@ export const osApi = new LuaTable({ // Otherwise, format specifiers follow ISO C `strftime`. // // If format is absent, it defaults to `%c`. - date: new LuaBuiltinFunction((_sf, format?: string, timestamp?: number) => { - let fmt = format ?? "%c"; - let utc = false; + date: new LuaBuiltinFunction( + (_sf, format?: string, timestamp?: number) => { + let fmt = format ?? "%c"; + let utc = false; - if (fmt.startsWith("!")) { - utc = true; - fmt = fmt.slice(1); - } + if (fmt.startsWith("!")) { + utc = true; + fmt = fmt.slice(1); + } - const d = - timestamp !== undefined && timestamp !== null - ? new Date(timestamp * 1000) - : new Date(); + const d = + timestamp !== undefined && timestamp !== null + ? new Date(timestamp * 1000) + : new Date(); - if (fmt === "*t") { - return dateTable(d, utc); - } + if (fmt === "*t") { + return dateTable(d, utc); + } - return luaFormatTime(fmt, d, utc); - }), + return luaFormatTime(fmt, d, utc); + }, + { + kind: "builtin", + description: + "Formats a timestamp as a date string or date table, optionally in UTC.", + parameters: [ + { + name: "format", + type: "string", + description: + "`strftime`-style format, `*t` for a table, and optional leading `!` for UTC.", + optional: true, + }, + { + name: "timestamp", + type: "number", + description: "Unix timestamp; defaults to the current time.", + optional: true, + }, + ], + returns: [ + { type: "string|table", description: "Formatted date or date fields." }, + ], + examples: [ + { code: 'print(os.date("%Y-%m-%d"))\nlocal utc = os.date("!*t")' }, + ], + }, + ), // Returns an approximation of CPU time used by the program in seconds. - clock: new LuaBuiltinFunction((_sf): number => { - return performance.now() / 1000.0; - }), + clock: new LuaBuiltinFunction( + (_sf): number => { + return performance.now() / 1000.0; + }, + { + kind: "builtin", + description: "Returns a high-resolution elapsed time value in seconds.", + returns: [ + { + type: "number", + description: "Browser performance timer in seconds.", + }, + ], + }, + ), }); diff --git a/client/space_lua/stdlib/string.ts b/client/space_lua/stdlib/string.ts index 217118c6..e5f782c2 100644 --- a/client/space_lua/stdlib/string.ts +++ b/client/space_lua/stdlib/string.ts @@ -28,20 +28,39 @@ function capturesToLua(caps: CaptureResult[]): any { } export const stringApi = new LuaTable({ - byte: new LuaBuiltinFunction((_sf, s: string, i?: number, j?: number) => { - i = i ?? 1; - j = j ?? i; - if (j > s.length) j = s.length; - if (i < 1) i = 1; - const result = []; - for (let k = i; k <= j; k++) { - result.push(s.charCodeAt(k - 1)); - } - return new LuaMultiRes(result); - }), - char: new LuaBuiltinFunction((_sf, ...args: number[]) => { - return String.fromCharCode(...args); - }), + byte: new LuaBuiltinFunction( + (_sf, s: string, i?: number, j?: number) => { + i = i ?? 1; + j = j ?? i; + if (j > s.length) j = s.length; + if (i < 1) i = 1; + const result = []; + for (let k = i; k <= j; k++) { + result.push(s.charCodeAt(k - 1)); + } + return new LuaMultiRes(result); + }, + { + kind: "builtin", + description: + "Returns the numeric character codes in the inclusive range from `i` to `j`.", + parameters: [ + { name: "s", type: "string" }, + { name: "i", type: "integer", optional: true }, + { name: "j", type: "integer", optional: true }, + ], + returns: [{ type: "integer", description: "One result per character." }], + }, + ), + char: new LuaBuiltinFunction( + (_sf, ...args: number[]) => String.fromCharCode(...args), + { + kind: "builtin", + description: "Creates a string from numeric character codes.", + signatures: ["string.char(...): string"], + returns: [{ type: "string" }], + }, + ), find: new LuaBuiltinFunction( (_sf, s: string, pattern: string, init = 1, plain = false) => { const r = patternFind(s, pattern, init, plain); @@ -52,13 +71,46 @@ export const stringApi = new LuaTable({ } return new LuaMultiRes(result); }, + { + kind: "builtin", + description: + "Finds the first Lua-pattern match and returns its bounds followed by captures.", + parameters: [ + { name: "s", type: "string" }, + { name: "pattern", type: "string" }, + { name: "init", type: "integer", optional: true }, + { name: "plain", type: "boolean", optional: true }, + ], + returns: [ + { type: "integer|nil", description: "Start index or `nil`." }, + { type: "integer", description: "End index." }, + ], + }, + ), + format: new LuaBuiltinFunction( + (_sf, format: string, ...args: any[]) => { + for (let i = 0; i < args.length; i++) { + args[i] = untagNumber(args[i]); + } + return luaFormat(format, ...args); + }, + { + kind: "builtin", + description: "Formats values according to a C-style format string.", + signatures: ["string.format(format, ...): string"], + parameters: [ + { name: "format", type: "string" }, + { + name: "...", + description: "Values consumed by conversion specifiers.", + }, + ], + returns: [{ type: "string" }], + examples: [ + { code: 'print(string.format("Name: %s, score: %.1f", "Ada", 9.5))' }, + ], + }, ), - format: new LuaBuiltinFunction((_sf, format: string, ...args: any[]) => { - for (let i = 0; i < args.length; i++) { - args[i] = untagNumber(args[i]); - } - return luaFormat(format, ...args); - }), gmatch: new LuaBuiltinFunction( (_sf, s: string, pattern: string, init = 1) => { const iter = patternGmatch(s, pattern, init); @@ -68,6 +120,22 @@ export const stringApi = new LuaTable({ return capturesToLua(caps); }; }, + { + kind: "builtin", + description: + "Returns an iterator over successive Lua-pattern matches and captures.", + parameters: [ + { name: "s", type: "string" }, + { name: "pattern", type: "string" }, + { name: "init", type: "integer", optional: true }, + ], + returns: [{ type: "function", description: "Match iterator." }], + examples: [ + { + code: 'for word in string.gmatch("hello world", "%w+") do\n print(word)\nend', + }, + ], + }, ), gsub: new LuaBuiltinFunction( async (sf, s: string, pattern: string, repl: any, n?: number) => { @@ -103,61 +171,156 @@ export const stringApi = new LuaTable({ const [result, count] = await patternGsub(s, pattern, callbacks, n); return new LuaMultiRes([result, count]); }, + { + kind: "builtin", + description: + "Replaces Lua-pattern matches using a string, table, or function replacement.", + parameters: [ + { name: "s", type: "string" }, + { name: "pattern", type: "string" }, + { name: "replacement", type: "string|table|function" }, + { name: "n", type: "integer", optional: true }, + ], + returns: [ + { type: "string", description: "Result string." }, + { type: "integer", description: "Number of replacements." }, + ], + examples: [ + { + code: 'local result, count = string.gsub("hello hello", "hello", "hi", 1)\nprint(result, count) -- hi hello 1', + }, + ], + }, ), - len: new LuaBuiltinFunction((_sf, s: string) => { - return s.length; - }), - lower: new LuaBuiltinFunction((_sf, s: string) => { - return luaToString(s.toLowerCase()); - }), - upper: new LuaBuiltinFunction((_sf, s: string) => { - return luaToString(s.toUpperCase()); - }), - match: new LuaBuiltinFunction((_sf, s: string, pattern: string, init = 1) => { - const caps = patternMatch(s, pattern, init); - if (!caps) return null; - return capturesToLua(caps); - }), - rep: new LuaBuiltinFunction((_sf, s: string, n: number, sep?: string) => { - if (n <= 0) return ""; - sep = sep ?? ""; - const parts: string[] = []; - for (let i = 0; i < n; i++) { - parts.push(s); - } - return parts.join(sep); - }), - reverse: new LuaBuiltinFunction((_sf, s: string) => { - return s.split("").reverse().join(""); - }), - sub: new LuaBuiltinFunction((_sf, s: string, i: number, j?: number) => { - const len = s.length; - let start: number; - if (i > 0) { - start = i; - } else if (i < -len) { - start = 1; - } else { - start = i === 0 ? 1 : len + i + 1; - } - let end: number; - if (j === undefined || j === null || j > len) { - end = len; - } else if (j >= 0) { - end = j; - } else if (j < -len) { - end = 0; - } else { - end = len + j + 1; - } - if (start <= end) { - return s.substring(start - 1, end); - } - return ""; + len: new LuaBuiltinFunction((_sf, s: string) => s.length, { + kind: "builtin", + description: "Returns the length of a string.", + parameters: [{ name: "s", type: "string" }], + returns: [{ type: "integer" }], }), + lower: new LuaBuiltinFunction( + (_sf, s: string) => luaToString(s.toLowerCase()), + { + kind: "builtin", + description: "Returns a copy of a string converted to lowercase.", + parameters: [{ name: "s", type: "string" }], + returns: [{ type: "string" }], + }, + ), + upper: new LuaBuiltinFunction( + (_sf, s: string) => luaToString(s.toUpperCase()), + { + kind: "builtin", + description: "Returns a copy of a string converted to uppercase.", + parameters: [{ name: "s", type: "string" }], + returns: [{ type: "string" }], + }, + ), + match: new LuaBuiltinFunction( + (_sf, s: string, pattern: string, init = 1) => { + const caps = patternMatch(s, pattern, init); + if (!caps) return null; + return capturesToLua(caps); + }, + { + kind: "builtin", + description: + "Returns captures from the first Lua-pattern match, or `nil` when none is found.", + parameters: [ + { name: "s", type: "string" }, + { name: "pattern", type: "string" }, + { name: "init", type: "integer", optional: true }, + ], + returns: [{ description: "Pattern captures, whole match, or `nil`." }], + examples: [ + { code: 'local year, month = string.match("2024-03", "(%d+)%-(%d+)")' }, + ], + }, + ), + rep: new LuaBuiltinFunction( + (_sf, s: string, n: number, sep?: string) => { + if (n <= 0) return ""; + sep = sep ?? ""; + const parts: string[] = []; + for (let i = 0; i < n; i++) { + parts.push(s); + } + return parts.join(sep); + }, + { + kind: "builtin", + description: + "Returns `n` copies of a string joined by an optional separator.", + parameters: [ + { name: "s", type: "string" }, + { name: "n", type: "integer" }, + { name: "sep", type: "string", optional: true }, + ], + returns: [{ type: "string" }], + }, + ), + reverse: new LuaBuiltinFunction( + (_sf, s: string) => s.split("").reverse().join(""), + { + kind: "builtin", + description: "Returns a string with its characters in reverse order.", + parameters: [{ name: "s", type: "string" }], + returns: [{ type: "string" }], + }, + ), + sub: new LuaBuiltinFunction( + (_sf, s: string, i: number, j?: number) => { + const len = s.length; + let start: number; + if (i > 0) { + start = i; + } else if (i < -len) { + start = 1; + } else { + start = i === 0 ? 1 : len + i + 1; + } + let end: number; + if (j === undefined || j === null || j > len) { + end = len; + } else if (j >= 0) { + end = j; + } else if (j < -len) { + end = 0; + } else { + end = len + j + 1; + } + if (start <= end) { + return s.substring(start - 1, end); + } + return ""; + }, + { + kind: "builtin", + description: + "Returns the substring from inclusive index `i` through `j`, supporting negative indices.", + parameters: [ + { name: "s", type: "string" }, + { name: "i", type: "integer" }, + { name: "j", type: "integer", optional: true }, + ], + returns: [{ type: "string" }], + }, + ), - split: new LuaBuiltinFunction((_sf, s: string, sep: string) => { - return s.split(sep); + split: new LuaBuiltinFunction((_sf, s: string, sep: string) => s.split(sep), { + kind: "builtin", + description: + "Splits a string on a literal separator and returns the substrings.", + parameters: [ + { name: "s", type: "string" }, + { name: "sep", type: "string" }, + ], + returns: [{ type: "table" }], + examples: [ + { + code: 'for part in each(string.split("a,b,c", ",")) do\n print(part)\nend', + }, + ], }), pack: strPackFn, @@ -165,34 +328,97 @@ export const stringApi = new LuaTable({ packsize: strPackSizeFn, // Non-standard extensions - startsWith: new LuaBuiltinFunction((_sf, s: string, prefix: string) => { - return s.startsWith(prefix); + startsWith: new LuaBuiltinFunction( + (_sf, s: string, prefix: string) => s.startsWith(prefix), + { + kind: "builtin", + description: "Returns whether a string starts with a literal prefix.", + parameters: [ + { name: "s", type: "string" }, + { name: "prefix", type: "string" }, + ], + returns: [{ type: "boolean" }], + }, + ), + endsWith: new LuaBuiltinFunction( + (_sf, s: string, suffix: string) => s.endsWith(suffix), + { + kind: "builtin", + description: "Returns whether a string ends with a literal suffix.", + parameters: [ + { name: "s", type: "string" }, + { name: "suffix", type: "string" }, + ], + returns: [{ type: "boolean" }], + }, + ), + trim: new LuaBuiltinFunction((_sf, s: string) => s.trim(), { + kind: "builtin", + description: "Removes whitespace from both ends of a string.", + parameters: [{ name: "s", type: "string" }], + returns: [{ type: "string" }], }), - endsWith: new LuaBuiltinFunction((_sf, s: string, suffix: string) => { - return s.endsWith(suffix); + trimStart: new LuaBuiltinFunction((_sf, s: string) => s.trimStart(), { + kind: "builtin", + description: "Removes whitespace from the beginning of a string.", + parameters: [{ name: "s", type: "string" }], + returns: [{ type: "string" }], }), - trim: new LuaBuiltinFunction((_sf, s: string) => { - return s.trim(); - }), - trimStart: new LuaBuiltinFunction((_sf, s: string) => { - return s.trimStart(); - }), - trimEnd: new LuaBuiltinFunction((_sf, s: string) => { - return s.trimEnd(); - }), - matchRegex: new LuaBuiltinFunction((_sf, s: string, pattern: string) => { - const regex = new RegExp(pattern); - const result = s.match(regex); - return jsToLuaValue(result); - }), - matchRegexAll: new LuaBuiltinFunction((_sf, s: string, pattern: string) => { - const regex = new RegExp(pattern, "g"); - return () => { - const match = regex.exec(s); - if (!match) { - return; - } - return jsToLuaValue(match); - }; + trimEnd: new LuaBuiltinFunction((_sf, s: string) => s.trimEnd(), { + kind: "builtin", + description: "Removes whitespace from the end of a string.", + parameters: [{ name: "s", type: "string" }], + returns: [{ type: "string" }], }), + matchRegex: new LuaBuiltinFunction( + (_sf, s: string, pattern: string) => { + const regex = new RegExp(pattern); + const result = s.match(regex); + return jsToLuaValue(result); + }, + { + kind: "builtin", + description: + "Matches a string with a JavaScript regular expression and returns the match array.", + parameters: [ + { name: "s", type: "string" }, + { name: "pattern", type: "string" }, + ], + returns: [{ type: "table|nil" }], + examples: [ + { + code: 'local match = string.matchRegex("hello123", "([a-z]+)([0-9]+)")\nprint(match[1], match[2], match[3])', + }, + ], + }, + ), + matchRegexAll: new LuaBuiltinFunction( + (_sf, s: string, pattern: string) => { + const regex = new RegExp(pattern, "g"); + return () => { + const match = regex.exec(s); + if (!match) { + return; + } + return jsToLuaValue(match); + }; + }, + { + kind: "builtin", + description: + "Returns an iterator over all JavaScript regular-expression matches.", + parameters: [ + { name: "s", type: "string" }, + { name: "pattern", type: "string" }, + ], + returns: [ + { type: "function", description: "Iterator yielding match arrays." }, + ], + examples: [ + { + code: 'for match in string.matchRegexAll("a1b2", "([a-z])([0-9])") do\n print(match[1], match[2], match[3])\nend', + }, + ], + }, + ), }); diff --git a/client/space_lua/stdlib/string_pack.ts b/client/space_lua/stdlib/string_pack.ts index 9442d209..4da5fb70 100644 --- a/client/space_lua/stdlib/string_pack.ts +++ b/client/space_lua/stdlib/string_pack.ts @@ -364,6 +364,17 @@ export const strPackFn = new LuaBuiltinFunction( for (let i = 0; i < out.length; i++) result += String.fromCharCode(out[i]); return result; }, + { + kind: "builtin", + description: + "Packs values into a binary string according to a Lua 5.4 format string.", + signatures: ["string.pack(format, ...): string"], + parameters: [ + { name: "format", type: "string", description: "Binary packing format." }, + { name: "...", description: "Values consumed by the format options." }, + ], + returns: [{ type: "string", description: "Packed binary string." }], + }, ); export const strUnpackFn = new LuaBuiltinFunction( @@ -468,23 +479,60 @@ export const strUnpackFn = new LuaBuiltinFunction( results.push(pos + 1); return new LuaMultiRes(results); }, + { + kind: "builtin", + description: + "Unpacks values from a binary string according to a Lua 5.4 format string.", + parameters: [ + { + name: "format", + type: "string", + description: "Binary unpacking format.", + }, + { name: "data", type: "string", description: "Packed binary string." }, + { + name: "init", + type: "integer", + description: "One-based starting position.", + optional: true, + }, + ], + returns: [ + { description: "Unpacked values followed by the next unread position." }, + ], + }, ); -export const strPackSizeFn = new LuaBuiltinFunction((_sf, fmt: string) => { - const h = makeHeader(); - let totalsize = 0; - let pos = 0; +export const strPackSizeFn = new LuaBuiltinFunction( + (_sf, fmt: string) => { + const h = makeHeader(); + let totalsize = 0; + let pos = 0; - while (pos < fmt.length) { - let opt: ParsedOption; - [opt, pos] = getDetails(fmt, pos, h, totalsize); + while (pos < fmt.length) { + let opt: ParsedOption; + [opt, pos] = getDetails(fmt, pos, h, totalsize); - if (opt.opt === "string" || opt.opt === "zstr") { - throw new LuaRuntimeError("variable-length format", _sf); + if (opt.opt === "string" || opt.opt === "zstr") { + throw new LuaRuntimeError("variable-length format", _sf); + } + + totalsize += opt.ntoalign + opt.size; } - totalsize += opt.ntoalign + opt.size; - } - - return totalsize; -}); + return totalsize; + }, + { + kind: "builtin", + description: + "Returns the byte size of a fixed-length Lua 5.4 packing format.", + parameters: [ + { + name: "format", + type: "string", + description: "Fixed-length binary packing format.", + }, + ], + returns: [{ type: "integer", description: "Packed byte count." }], + }, +); diff --git a/client/space_lua/stdlib/table.ts b/client/space_lua/stdlib/table.ts index 37bdcf70..ee03bb33 100644 --- a/client/space_lua/stdlib/table.ts +++ b/client/space_lua/stdlib/table.ts @@ -108,6 +108,18 @@ export const tableApi = new LuaTable({ } return out.join(sep); }, + { + kind: "builtin", + description: + "Concatenates table elements from `i` through `j` using an optional separator.", + parameters: [ + { name: "table", type: "table" }, + { name: "sep", type: "string", optional: true }, + { name: "i", type: "integer", optional: true }, + { name: "j", type: "integer", optional: true }, + ], + returns: [{ type: "string" }], + }, ), /** @@ -159,6 +171,32 @@ export const tableApi = new LuaTable({ await luaSet(tbl, pos, v, sf); }, + { + kind: "builtin", + description: + "Inserts a value at a position, shifting later elements, or appends it when no position is supplied.", + signatures: [ + "table.insert(table, value)", + "table.insert(table, pos, value)", + ], + parameters: [ + { name: "table", type: "table" }, + { + name: "posOrValue", + description: "Insertion position or appended value.", + }, + { + name: "value", + description: "Value for positional insertion.", + optional: true, + }, + ], + examples: [ + { + code: 'local fruits = {"apple", "orange"}\ntable.insert(fruits, 2, "banana")\nprint(table.concat(fruits, ", "))', + }, + ], + }, ), /** @@ -202,6 +240,21 @@ export const tableApi = new LuaTable({ return v; }, + { + kind: "builtin", + description: + "Removes and returns an element, shifting later elements down.", + parameters: [ + { name: "table", type: "table" }, + { + name: "pos", + type: "integer", + description: "Position; defaults to the last element.", + optional: true, + }, + ], + returns: [{ description: "Removed value." }], + }, ), /** @@ -252,6 +305,24 @@ export const tableApi = new LuaTable({ return a2; }, + { + kind: "builtin", + description: + "Moves an inclusive element range to a destination table while handling overlaps.", + parameters: [ + { name: "a1", type: "table", description: "Source table." }, + { name: "f", type: "integer", description: "First source index." }, + { name: "e", type: "integer", description: "Last source index." }, + { name: "t", type: "integer", description: "Destination start index." }, + { + name: "a2", + type: "table", + description: "Destination table; defaults to `a1`.", + optional: true, + }, + ], + returns: [{ type: "table", description: "Destination table." }], + }, ), /** @@ -311,6 +382,23 @@ export const tableApi = new LuaTable({ return tbl; }, + { + kind: "builtin", + description: + "Sorts a table in place using ascending order or an optional comparison function.", + parameters: [ + { name: "table", type: "table" }, + { name: "comp", type: "function", optional: true }, + ], + returns: [ + { type: "table", description: "The sorted table in Space Lua." }, + ], + examples: [ + { + code: "local numbers = {3, 1, 2}\ntable.sort(numbers, function(a, b) return a > b end)", + }, + ], + }, ), /** @@ -319,12 +407,21 @@ export const tableApi = new LuaTable({ * @param tbl - The table to get the keys from. * @returns The keys of the table. */ - keys: new LuaBuiltinFunction((_sf, tbl: LuaTable | LuaEnv | any) => { - if (tbl.keys) { - return tbl.keys(); - } - return Object.keys(tbl); - }), + keys: new LuaBuiltinFunction( + (_sf, tbl: LuaTable | LuaEnv | any) => { + if (tbl.keys) { + return tbl.keys(); + } + return Object.keys(tbl); + }, + { + kind: "builtin", + description: + "Returns an array containing all keys of a table or JavaScript object.", + parameters: [{ name: "table", type: "table" }], + returns: [{ type: "table", description: "Array of keys." }], + }, + ), /** * Checks if a table (used as an array) contains a value. @@ -355,6 +452,16 @@ export const tableApi = new LuaTable({ sf, ); }, + { + kind: "builtin", + description: + "Returns whether any table value is Lua-equal to a requested value.", + parameters: [ + { name: "table", type: "table" }, + { name: "value", description: "Value to find." }, + ], + returns: [{ type: "boolean" }], + }, ), /** @@ -383,21 +490,57 @@ export const tableApi = new LuaTable({ } return resultTable; }, + { + kind: "builtin", + description: "Copies selected keys from a table into a new table.", + signatures: [ + "table.select(table, ...keys): table", + "table.select(table, keys): table", + ], + parameters: [ + { name: "table", type: "table" }, + { + name: "keys", + description: "Individual keys or one array-like table of keys.", + }, + ], + returns: [{ type: "table" }], + examples: [ + { + code: '${query[[\n from p = index.pages()\n limit 3\n select table.select(p, "name", "lastModified")\n]]}', + language: "markdown", + }, + ], + }, ), /** * Returns a new table with all arguments stored in keys 1, 2, ..., n * and t.n = n (the total number of arguments). */ - pack: new LuaBuiltinFunction(async (sf, ...args: any[]) => { - const tbl = new LuaTable(); - const n = args.length; - for (let i = 0; i < n; i++) { - await luaSet(tbl, i + 1, args[i], sf); - } - void tbl.rawSet("n", n); - return tbl; - }), + pack: new LuaBuiltinFunction( + async (sf, ...args: any[]) => { + const tbl = new LuaTable(); + const n = args.length; + for (let i = 0; i < n; i++) { + await luaSet(tbl, i + 1, args[i], sf); + } + void tbl.rawSet("n", n); + return tbl; + }, + { + kind: "builtin", + description: + "Packs all arguments into a table with a count stored in field `n`.", + signatures: ["table.pack(...): table"], + returns: [ + { + type: "table", + description: "Arguments at integer keys plus field `n`.", + }, + ], + }, + ), /** * Returns all values t[i], t[i+1], ..., t[j]. @@ -425,6 +568,20 @@ export const tableApi = new LuaTable({ } return new LuaMultiRes(result); }, + { + kind: "builtin", + description: + "Returns the table values from index `i` through `j` as separate results.", + parameters: [ + { name: "table", type: "table" }, + { name: "i", type: "integer", optional: true }, + { name: "j", type: "integer", optional: true }, + ], + returns: [{ description: "One result per selected element." }], + examples: [ + { code: 'local second, third = table.unpack({"a", "b", "c"}, 2, 3)' }, + ], + }, ), // Non-standard Lua functions @@ -457,5 +614,28 @@ export const tableApi = new LuaTable({ } return null; }, + { + kind: "builtin", + description: + "Finds the first array element accepted by a predicate and returns its index and value.", + parameters: [ + { name: "table", type: "table" }, + { + name: "criteriaFn", + type: "function", + description: "Predicate called with each value.", + }, + { name: "fromIndex", type: "integer", optional: true }, + ], + returns: [ + { type: "integer|nil", description: "Matching index or `nil`." }, + { description: "Matching value." }, + ], + examples: [ + { + code: "local index, value = table.find({1, 2, 3, 4}, function(n) return n % 2 == 0 end)", + }, + ], + }, ), });