summaryrefslogtreecommitdiff
path: root/src/js-lang
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2025-11-03 23:40:02 -0800
committerKai Stevenson <kai@kaistevenson.com>2025-11-03 23:40:02 -0800
commit56040f3ff85e77311f0c864a89afd63fcf1bdb50 (patch)
tree2eb0166756e76b0483692e79830329c92e7fdcf3 /src/js-lang
parenta11e6780fbb8bd4143dfec44e2ce147b795772d8 (diff)
add js-lang, refactor some ts-lang stuff
Diffstat (limited to 'src/js-lang')
-rw-r--r--src/js-lang/builtin/builtin.ts39
-rw-r--r--src/js-lang/builtin/index.ts2
-rw-r--r--src/js-lang/builtin/sbuiltin.ts46
-rw-r--r--src/js-lang/core/eval.ts83
-rw-r--r--src/js-lang/core/index.ts3
-rw-r--r--src/js-lang/core/lexer.ts46
-rw-r--r--src/js-lang/core/parser.ts132
-rw-r--r--src/js-lang/index.ts1
8 files changed, 352 insertions, 0 deletions
diff --git a/src/js-lang/builtin/builtin.ts b/src/js-lang/builtin/builtin.ts
new file mode 100644
index 0000000..dde91b6
--- /dev/null
+++ b/src/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<string, BUILTIN> = {
+ arr: V_BUILTIN_Arr,
+ tostring: V_BUILTIN_ToString,
+ add: V_BUILTIN_Add,
+ mul: V_BUILTIN_Mul,
+};
diff --git a/src/js-lang/builtin/index.ts b/src/js-lang/builtin/index.ts
new file mode 100644
index 0000000..00e77f7
--- /dev/null
+++ b/src/js-lang/builtin/index.ts
@@ -0,0 +1,2 @@
+export * from "./builtin";
+export * from "./sbuiltin";
diff --git a/src/js-lang/builtin/sbuiltin.ts b/src/js-lang/builtin/sbuiltin.ts
new file mode 100644
index 0000000..44c969d
--- /dev/null
+++ b/src/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<string, SBUILTIN> = {
+ call: V_SBUITLIN_Call,
+ map: V_SBUILTIN_Map,
+};
diff --git a/src/js-lang/core/eval.ts b/src/js-lang/core/eval.ts
new file mode 100644
index 0000000..60a2059
--- /dev/null
+++ b/src/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 = <const Node extends ASTNode>(
+ node: Node
+): Evaluate<Node> => _evaluate(node, emptyStackFrame) as Evaluate<Node>;
diff --git a/src/js-lang/core/index.ts b/src/js-lang/core/index.ts
new file mode 100644
index 0000000..22ac2d2
--- /dev/null
+++ b/src/js-lang/core/index.ts
@@ -0,0 +1,3 @@
+export * from "./eval";
+export * from "./parser";
+export * from "./lexer";
diff --git a/src/js-lang/core/lexer.ts b/src/js-lang/core/lexer.ts
new file mode 100644
index 0000000..95e0e19
--- /dev/null
+++ b/src/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 = <const Raw extends string>(raw: Raw): Lex<Raw> => {
+ 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<Raw>;
+};
diff --git a/src/js-lang/core/parser.ts b/src/js-lang/core/parser.ts
new file mode 100644
index 0000000..f193d6a
--- /dev/null
+++ b/src/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 = <const Raw extends readonly Token[]>(
+ raw: Raw
+): Parse<Raw> =>
+ _parse({
+ remainingTokens: raw as unknown as Token[],
+ lastToken: null,
+ stack: [{ type: NodeType.EXT, name: "arr", value: null, children: [] }],
+ }) as Parse<Raw>;
diff --git a/src/js-lang/index.ts b/src/js-lang/index.ts
new file mode 100644
index 0000000..8d119de
--- /dev/null
+++ b/src/js-lang/index.ts
@@ -0,0 +1 @@
+export * from "./core";