Files
plainleaf/client/space_lua/syscalls.ts
T

271 lines
8.3 KiB
TypeScript

import type { ASTCtx, LuaBlock, LuaExpression } from "./ast.ts";
import { evalExpression } from "./eval.ts";
import { parseBlock, parseExpressionString } from "./parse.ts";
import {
type PrintOptions,
prettyPrintBlock,
prettyPrintExpression,
} from "./pretty_print.ts";
import {
isILuaFunction,
LuaEnv,
LuaStackFrame,
LuaTable,
luaGet,
luaKeys,
luaToString,
luaTypeOf,
luaValueToJS,
} from "./runtime.ts";
import { buildThreadLocalEnv } from "../space_lua_api.ts";
import type { SysCallMapping, System } from "../plugos/system.ts";
import { isSendable } from "../plugos/util.ts";
import type {
LuaFunctionInfo,
LuaPropertyInspection,
LuaValueInspection,
} from "../../plug-api/types/index.ts";
import { encodeRef } from "../../plug-api/lib/ref.ts";
import { resolveASTReference } from "../space_lua.ts";
async function inspectValue(
value: unknown,
path: string[],
): Promise<Omit<LuaValueInspection, "properties">> {
const type = value instanceof LuaEnv ? "table" : await luaTypeOf(value);
if (!isILuaFunction(value)) {
return { type };
}
const functionInfo: LuaFunctionInfo = {
...(value.info ?? { kind: "builtin" }),
name: value.info?.name ?? path.join("."),
};
const definitionRef =
functionInfo.kind === "lua" && functionInfo.source
? resolveASTReference(functionInfo.source as ASTCtx)
: null;
return {
type,
functionInfo,
definition: definitionRef ? encodeRef(definitionRef) : undefined,
};
}
async function inspectLuaPath(
env: LuaEnv,
path: string[],
): Promise<LuaValueInspection | null> {
const sf = LuaStackFrame.createWithGlobalEnv(env);
let value: unknown = env;
for (const key of path) {
value = await luaGet(value, key, null, sf);
if (value === null || value === undefined) {
return null;
}
}
const properties: LuaPropertyInspection[] = [];
const keys =
value instanceof LuaEnv
? value.keys()
: value instanceof LuaTable ||
Array.isArray(value) ||
(typeof value === "object" && value !== null)
? luaKeys(value)
: [];
for (const key of [...new Set(keys)]
.filter((candidate): candidate is string => typeof candidate === "string")
.sort()) {
const child = await luaGet(value, key, null, sf);
if (child === null || child === undefined) continue;
properties.push({
key,
...(await inspectValue(child, [...path, key])),
});
}
return {
...(await inspectValue(value, path)),
properties,
};
}
export function luaSyscalls(
system: System<any>,
getEnvironment: () => LuaEnv,
): SysCallMapping {
return {
"lua.parseBlock": {
callback: (_ctx, code: string): LuaBlock => parseBlock(code),
description:
"Parses a Space Lua chunk and returns its AST. Blocks retain comments in source order with their exact text, kind, and source range.",
parameters: [
{ name: "code", type: "string", description: "Lua code to parse." },
],
returns: [{ type: "table", description: "Parsed Lua block AST." }],
examples: [
{
code: 'local ast = lua.parseBlock("print(\\"Hello\\")")',
},
],
see: "API/lua",
},
// Deprecated alias for `lua.parseBlock`, kept for backwards compatibility.
"lua.parse": {
callback: (_ctx, code: string): LuaBlock => parseBlock(code),
description: "Deprecated alias for lua.parseBlock.",
parameters: [
{ name: "code", type: "string", description: "Lua code to parse." },
],
returns: [{ type: "table", description: "Parsed Lua block AST." }],
deprecated: "Use lua.parseBlock instead.",
see: "API/lua",
},
"lua.parseExpression": {
callback: (_ctx, expression: string): LuaExpression =>
parseExpressionString(expression),
description: "Parses a Space Lua expression and returns its AST.",
parameters: [
{
name: "expression",
type: "string",
description: "Lua expression to parse.",
},
],
returns: [{ type: "table", description: "Parsed expression AST." }],
examples: [
{
code: 'local expression = lua.parseExpression("1 + 2 * 3")',
},
],
see: "API/lua",
},
"lua.prettyPrintBlock": {
callback: (_ctx, block: LuaBlock, opts?: PrintOptions): string =>
prettyPrintBlock(block, opts),
description:
"Pretty-prints a parsed Space Lua block. Comments are preserved while their placement and indentation are normalized.",
parameters: [
{ name: "block", type: "table", description: "Parsed block AST." },
{
name: "options",
type: "table",
description:
"Formatting options: `indentWidth`, `quote`, and `trailingComma`.",
optional: true,
},
],
returns: [{ type: "string", description: "Formatted Lua source." }],
examples: [
{
code: 'local formatted = lua.prettyPrintBlock(lua.parseBlock("if a then return 1 end"))',
},
],
see: "API/lua",
},
"lua.prettyPrintExpression": {
callback: (
_ctx,
expression: LuaExpression,
opts?: PrintOptions,
): string => prettyPrintExpression(expression, opts),
description: "Pretty-prints a parsed Space Lua expression.",
parameters: [
{
name: "expression",
type: "table",
description: "Parsed expression AST.",
},
{
name: "options",
type: "table",
description:
"Formatting options: `indentWidth`, `quote`, and `trailingComma`.",
optional: true,
},
],
returns: [{ type: "string", description: "Formatted Lua source." }],
examples: [
{
code: 'local formatted = lua.prettyPrintExpression(lua.parseExpression("{a=1,b=2}"))',
},
],
see: "API/lua",
},
/**
* Evaluates a Lua expression and returns the result as a JavaScript value
* @param _ctx
* @param expression
* @returns
*/
"lua.evalExpression": {
callback: async (_ctx, expression: string) => {
try {
const ast = parseExpressionString(expression);
const globalEnv = getEnvironment();
const env = await buildThreadLocalEnv(system, globalEnv);
const sf = new LuaStackFrame(env, null);
const luaResult = await evalExpression(ast, globalEnv, sf);
const jsResult = luaValueToJS(luaResult, sf);
if (isSendable(jsResult)) {
return jsResult;
} else {
// This may evaluate to e.g. a function, which is not sendable, in this case we'll console.warn and return a stringified version of the result
console.warn(
"Lua eval result is not sendable, returning stringified version",
jsResult,
);
return luaToString(luaResult);
}
} catch (e: any) {
console.error("Lua eval error: ", e.message, e.sf?.astCtx);
throw e;
}
},
description: "Evaluates a Space Lua expression.",
parameters: [
{
name: "expression",
type: "string",
description: "Lua expression to evaluate.",
},
],
returns: [{ description: "Evaluated result." }],
examples: [
{
code: 'local result = lua.evalExpression("1 + 2 * 3")\nprint(result)',
},
],
see: "API/lua",
},
"lua.inspect": {
callback: (_ctx, path: string[] = []) =>
inspectLuaPath(getEnvironment(), path),
description:
"Inspects a value in the live Space Lua environment and returns serializable type, function, definition, and property metadata.",
parameters: [
{
name: "path",
type: "table",
description:
"Sequence of property names from the global environment; omit to inspect globals.",
optional: true,
},
],
returns: [
{
type: "table|nil",
description:
"Inspection metadata, or nil when the requested path does not exist.",
},
],
examples: [
{
code: 'local info = lua.inspect({"editor", "getText"})',
},
],
see: "API/lua",
},
};
}