From 8b610f2bcfc223333254ce9679730c42dce6d26e Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Mon, 3 Nov 2025 23:41:31 -0800 Subject: add createFn --- src/lang/js-lang/builtin/builtin.ts | 39 +++++++++++ src/lang/js-lang/builtin/index.ts | 2 + src/lang/js-lang/builtin/sbuiltin.ts | 46 ++++++++++++ src/lang/js-lang/core/eval.ts | 83 ++++++++++++++++++++++ src/lang/js-lang/core/index.ts | 3 + src/lang/js-lang/core/lexer.ts | 46 ++++++++++++ src/lang/js-lang/core/parser.ts | 132 +++++++++++++++++++++++++++++++++++ src/lang/js-lang/index.ts | 1 + 8 files changed, 352 insertions(+) create mode 100644 src/lang/js-lang/builtin/builtin.ts create mode 100644 src/lang/js-lang/builtin/index.ts create mode 100644 src/lang/js-lang/builtin/sbuiltin.ts create mode 100644 src/lang/js-lang/core/eval.ts create mode 100644 src/lang/js-lang/core/index.ts create mode 100644 src/lang/js-lang/core/lexer.ts create mode 100644 src/lang/js-lang/core/parser.ts create mode 100644 src/lang/js-lang/index.ts (limited to 'src/lang/js-lang') diff --git a/src/lang/js-lang/builtin/builtin.ts b/src/lang/js-lang/builtin/builtin.ts new file mode 100644 index 0000000..dde91b6 --- /dev/null +++ b/src/lang/js-lang/builtin/builtin.ts @@ -0,0 +1,39 @@ +type BUILTIN = (args: any[]) => any; + +export const V_BUILTIN_Arr: BUILTIN = (args) => args; + +// FIXME actually implement this properly +export const V_BUILTIN_ToString: BUILTIN = (args) => + args.length === 1 ? JSON.stringify(args[0]) : JSON.stringify(args); + +export const V_BUILTIN_Add: BUILTIN = (args) => { + if (args.every((arg) => ["string", "number"].includes(typeof arg))) { + return args.reduce( + (acc, cur) => acc + cur, + typeof args[0] === "string" ? "" : 0 + ); + } + + throw new Error(`Cannot add operands ${JSON.stringify(args, undefined, 2)}`); +}; + +export const V_BUILTIN_Mul: BUILTIN = (args) => { + if (args.every((arg) => typeof arg === "number") && args.length === 2) { + return args.reduce((acc, cur) => acc * cur, 1); + } + + throw new Error( + `Can only multiply [number, number], but got ${JSON.stringify( + args, + undefined, + 2 + )}` + ); +}; + +export const nameToBUILTIN: Record = { + arr: V_BUILTIN_Arr, + tostring: V_BUILTIN_ToString, + add: V_BUILTIN_Add, + mul: V_BUILTIN_Mul, +}; diff --git a/src/lang/js-lang/builtin/index.ts b/src/lang/js-lang/builtin/index.ts new file mode 100644 index 0000000..00e77f7 --- /dev/null +++ b/src/lang/js-lang/builtin/index.ts @@ -0,0 +1,2 @@ +export * from "./builtin"; +export * from "./sbuiltin"; diff --git a/src/lang/js-lang/builtin/sbuiltin.ts b/src/lang/js-lang/builtin/sbuiltin.ts new file mode 100644 index 0000000..44c969d --- /dev/null +++ b/src/lang/js-lang/builtin/sbuiltin.ts @@ -0,0 +1,46 @@ +import { callFn, getEvaluatedChildren } from "../core/eval"; +import { ASTNode, FnPrim, StackFrame } from "../../ts-lang"; + +type SBUILTIN = (node: ASTNode, frame: StackFrame) => any; + +export const V_SBUITLIN_Call: SBUILTIN = (node, frame) => { + const children = getEvaluatedChildren(node, frame); + const fn = children[0] as FnPrim | undefined; + + if (!fn?.fn) { + throw new Error( + `Invalid params for function call: ${JSON.stringify( + children, + undefined, + 2 + )}` + ); + } + + return callFn(fn, children.slice(1), frame); +}; + +export const V_SBUILTIN_Map: SBUILTIN = (node, frame) => { + const children = getEvaluatedChildren(node, frame); + const fn = children[1] as FnPrim | undefined; + + if (!fn?.fn) { + throw new Error( + `Invalid params for map: ${JSON.stringify(children, undefined, 2)}` + ); + } + + const values = children[0]; + + if (!Array.isArray(values)) { + // add to ts + throw new Error(`Can't map non-array value: ${values}`); + } + + return values.map((v, i) => callFn(fn, [v, i], frame)); +}; + +export const nameToSBUILTIN: Record = { + call: V_SBUITLIN_Call, + map: V_SBUILTIN_Map, +}; diff --git a/src/lang/js-lang/core/eval.ts b/src/lang/js-lang/core/eval.ts new file mode 100644 index 0000000..60a2059 --- /dev/null +++ b/src/lang/js-lang/core/eval.ts @@ -0,0 +1,83 @@ +import { + ASTNode, + StackFrame, + Evaluate, + EmptyStackFrame, + NodeType, + FnPrim, + SENTINEL_NO_BUILTIN, +} from "../../ts-lang"; +import { nameToBUILTIN, nameToSBUILTIN, V_SBUILTIN_Map } from "../builtin"; + +const V_SENTINEL_NO_BUILTIN: SENTINEL_NO_BUILTIN = "__NO_BUILTIN__"; + +const mapBuiltins = (node: ASTNode, frame: StackFrame): any => { + if (node.name in nameToSBUILTIN) { + return nameToSBUILTIN[node.name](node, frame); + } + if (node.name in nameToBUILTIN) { + return nameToBUILTIN[node.name](getEvaluatedChildren(node, frame)); + } + + return V_SENTINEL_NO_BUILTIN; +}; + +const findInStack = (frame: StackFrame, nameToFind: string) => { + if (nameToFind in frame.bindings) { + return frame.bindings[nameToFind]; + } + + if (!frame.parent) { + throw new Error(`Can't find name ${nameToFind} on the stack`); + } + + return findInStack(frame.parent, nameToFind); +}; + +const handleFn = (node: ASTNode): FnPrim => { + const fn = node.children[node.children.length - 1]; + + return { + args: node.children.slice(0, node.children.length - 1), + fn, + }; +}; + +const mapZip = (args: ASTNode[], values: any[]) => + Object.fromEntries(args.map(({ name }, i) => [name, values[i]])); + +export const callFn = (fn: FnPrim, values: any[], frame: StackFrame) => + _evaluate(fn.fn, { + bindings: mapZip(fn.args as ASTNode[], values), + parent: frame, + }); + +export const _evaluate = (node: ASTNode, frame: StackFrame) => { + if (node.type === NodeType.INT) { + return node.value; + } + + if (node.type === NodeType.EXT) { + if (node.name === "fn") { + return handleFn(node); + } + + const builtinResult = mapBuiltins(node, frame); + if (builtinResult !== V_SENTINEL_NO_BUILTIN) { + return builtinResult; + } + + return findInStack(frame, node.name); + } + + throw new Error(`Unhandled node type ${node.type}`); +}; + +export const getEvaluatedChildren = (node: ASTNode, frame: StackFrame) => + node.children.map((child) => _evaluate(child, frame)); + +export const emptyStackFrame: EmptyStackFrame = { bindings: {}, parent: null }; + +export const evaluate = ( + node: Node +): Evaluate => _evaluate(node, emptyStackFrame) as Evaluate; diff --git a/src/lang/js-lang/core/index.ts b/src/lang/js-lang/core/index.ts new file mode 100644 index 0000000..22ac2d2 --- /dev/null +++ b/src/lang/js-lang/core/index.ts @@ -0,0 +1,3 @@ +export * from "./eval"; +export * from "./parser"; +export * from "./lexer"; diff --git a/src/lang/js-lang/core/lexer.ts b/src/lang/js-lang/core/lexer.ts new file mode 100644 index 0000000..95e0e19 --- /dev/null +++ b/src/lang/js-lang/core/lexer.ts @@ -0,0 +1,46 @@ +import { TokenType, Lex, Token } from "../../ts-lang"; + +const WHITESPACE_TOKENS = [ + TokenType.SPACE, + TokenType.COMMA, + TokenType.SEMICOLON, +] as string[]; + +export const lex = (raw: Raw): Lex => { + let _raw: string = raw; + let nameCollection = ""; + const tokens: Token[] = []; + + while (_raw) { + const head = _raw[0]; + _raw = _raw.slice(1); + + const processNameCollection = () => { + if (nameCollection) { + tokens.push({ type: TokenType.NAME, name: nameCollection }); + nameCollection = ""; + } + }; + + if (WHITESPACE_TOKENS.includes(head)) { + processNameCollection(); + continue; + } + + if (head === TokenType.OPEN_PAREN) { + processNameCollection(); + tokens.push({ type: TokenType.OPEN_PAREN, name: "" }); + continue; + } + + if (head === TokenType.CLOSE_PAREN) { + processNameCollection(); + tokens.push({ type: TokenType.CLOSE_PAREN, name: "" }); + continue; + } + + nameCollection += head; + } + + return tokens as Lex; +}; diff --git a/src/lang/js-lang/core/parser.ts b/src/lang/js-lang/core/parser.ts new file mode 100644 index 0000000..f193d6a --- /dev/null +++ b/src/lang/js-lang/core/parser.ts @@ -0,0 +1,132 @@ +import { Token, Parse, TokenType, ASTNode, NodeType } from "../../ts-lang"; +import { lex } from "./lexer"; + +const resolveNodeFromToken = (token: Token): ASTNode => { + // FIXME not correct + if (!isNaN(Number(token.name))) { + return { + type: NodeType.INT, + name: "", + value: Number(token.name), + children: [], + }; + } + if (token.name[0] === '"' && token.name[token.name.length - 1] === '"') { + return { + type: NodeType.INT, + name: "", + value: token.name.slice(1, token.name.length - 1), + children: [], + }; + } + return { + type: NodeType.EXT, + name: token.name, + value: null, + children: [], + }; +}; + +const _parse = ({ + remainingTokens, + lastToken, + stack, +}: { + remainingTokens: Token[]; + lastToken: Token | null; + stack: ASTNode[]; +}): ASTNode | null => { + const head = remainingTokens.shift(); + if (!head) { + if (lastToken) { + (stack[stack.length - 1].children as ASTNode[]).push( + resolveNodeFromToken(lastToken) + ); + + return _parse({ + lastToken: null, + remainingTokens: [], + stack, + }); + } + + return stack[0] ?? null; + } + + if (lastToken) { + if (head.type === TokenType.NAME) { + (stack[stack.length - 1].children as ASTNode[]).push( + resolveNodeFromToken(lastToken) + ); + return _parse({ + lastToken: head, + remainingTokens, + stack, + }); + } + + if (head.type === TokenType.CLOSE_PAREN) { + const top = stack.pop()!; + (top.children as ASTNode[]).push(resolveNodeFromToken(lastToken)); + (stack[stack.length - 1].children as ASTNode[]).push(top); + + return _parse({ + lastToken: null, + remainingTokens, + stack, + }); + } + + if (head.type === TokenType.OPEN_PAREN) { + return _parse({ + lastToken: null, + remainingTokens, + stack: [...stack, resolveNodeFromToken(lastToken)], + }); + } + + throw new Error( + `${JSON.stringify({ + lastToken, + remainingTokens, + stack, + })} Was not expecting ${head.type}` + ); + } + + if (head.type === TokenType.NAME) { + return _parse({ + lastToken: head, + remainingTokens, + stack, + }); + } + + if (head.type === TokenType.CLOSE_PAREN) { + const top = stack.pop()!; + (stack[stack.length - 1].children as ASTNode[]).push(top); + + return _parse({ + lastToken: null, + remainingTokens, + stack, + }); + } + + throw new Error( + `${JSON.stringify({ + lastToken, + remainingTokens, + stack, + })} Expected nextToken to be a name or close paren at ${head.type}` + ); +}; + +export const parse = ( + raw: Raw +): Parse => + _parse({ + remainingTokens: raw as unknown as Token[], + lastToken: null, + stack: [{ type: NodeType.EXT, name: "arr", value: null, children: [] }], + }) as Parse; diff --git a/src/lang/js-lang/index.ts b/src/lang/js-lang/index.ts new file mode 100644 index 0000000..8d119de --- /dev/null +++ b/src/lang/js-lang/index.ts @@ -0,0 +1 @@ +export * from "./core"; -- cgit v1.2.3-70-g09d2