export * from "./lang"; export * from "./ts-lang"; export * from "./ks-lang"; export * from "./js-lang"; 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, }; export * from "./builtin"; export * from "./sbuiltin"; import { callFn, getEvaluatedChildren } from "../core/eval"; import { ASTNode, FnPrim, StackFrame } from "../../ts-lang"; type SBUILTIN = (node: ASTNode, frame: StackFrame) => any; export const V_SBUILTIN_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_SBUILTIN_Call, map: V_SBUILTIN_Map, }; 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; export * from "./eval"; export * from "./parser"; export * from "./lexer"; 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; }; 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; export * from "./core"; import { callFn, emptyStackFrame, evaluate, lex, parse } from "../js-lang"; import { Evaluate, FnPrim, Lex, Parse, EvalError, CallFn, EmptyStackFrame, } from "../ts-lang"; export const createFn = ( program: Program ): Evaluate>> extends [infer ProgramFn extends FnPrim] ? ( ...args: Args ) => CallFn : EvalError<"Cannot create a function from a program that does not eval to a function"> => { const [programFn] = evaluate(parse(lex(program))) as Array; if (!programFn.fn) { throw new Error( "Cannot create a function from a program that does not eval to a function" ); } return ((...args: any[]) => callFn(programFn, args, emptyStackFrame)) as any; }; import { FnError } from "."; import { AddNumbers, AddStrings, Multiply, ToString, UnarrayIfOnlyHead, } from "../util"; export type BUILTIN_Arr = Args; export type BUILTIN_ToString = ToString< UnarrayIfOnlyHead<{ [Idx in keyof Args]: ToString; }> >; export type BUILTIN_Add = Args extends readonly string[] ? AddStrings : Args extends readonly number[] ? AddNumbers : FnError<`Cannot add operands ${ToString}`>; export type BUILTIN_Mul = Args extends [ infer A, infer B, infer C ] ? FnError<`Can only multiply [number, number], but got ${ToString}`> : Args extends [infer M extends number, infer N extends number] ? Multiply : FnError<`Can only multiply [number, number], but got ${ToString}`>; export type FnError = `Function execution error: ${T}`; export * from "./builtin"; export * from "./sbuiltin"; import { ASTNode, StackFrame } from "../core/common"; import { CallFn, FnPrim, GetEvaluatedChildren, EvalError } from "../core/eval"; import { ExtractNumber, ToString } from "../util"; export type SBUILTIN_Call< Node extends ASTNode, Frame extends StackFrame > = GetEvaluatedChildren extends [ infer Fn extends FnPrim, ...infer Values extends readonly any[] ] ? CallFn : EvalError<`Invalid params for function call: ${ToString< GetEvaluatedChildren >}`>; export type SBUILTIN_Map< Node extends ASTNode, Frame extends StackFrame > = GetEvaluatedChildren extends [ infer Arr extends readonly any[], infer Fn extends FnPrim ] ? { [Idx in keyof Arr]: CallFn], Frame> } : EvalError<`Invalid params for map: ${ToString< GetEvaluatedChildren >}`>; export enum TokenType { OPEN_PAREN = "(", CLOSE_PAREN = ")", SPACE = " ", SEMICOLON = ";", COMMA = ",", NAME = "NAME", } export type Token< Type extends TokenType = TokenType, Name extends string = string > = { type: Type; name: Name; }; export type LexerCtx = { next: string; nameCollection: string; tokens: readonly Token[]; }; export enum NodeType { INT = "INT", EXT = "EXT", PARSER_ERROR = "PARSER_ERROR", } export type ASTNode< Type extends NodeType = NodeType, Name extends string = string, Value extends any = any, Children extends readonly ASTNode[] = readonly ASTNode< NodeType, string, any, any >[] > = { type: Type; name: Name; value: Value; children: Children; }; export type ParserCtx = { remainingTokens: readonly Token[]; lastToken: Token | null; stack: readonly ASTNode[]; }; export type StackFrame< Bindings extends Record = Record, Parent extends StackFrame | null = any > = { bindings: Bindings; parent: Parent; }; export type EmptyStackFrame = StackFrame<{}, null>; export type WithPushedBindings< OldFrame extends StackFrame, Bindings extends StackFrame["bindings"] > = StackFrame; import { BUILTIN_Add, BUILTIN_Arr, BUILTIN_Mul, BUILTIN_ToString, SBUILTIN_Call, SBUILTIN_Map, } from "../builtin"; import { ToString } from "../util"; import { ASTNode, EmptyStackFrame, NodeType, StackFrame } from "./common"; export type SENTINEL_NO_BUILTIN = "__NO_BUILTIN__"; export type MapBuiltins< Node extends ASTNode, Frame extends StackFrame > = GetEvaluatedChildren extends infer Args extends readonly any[] ? Node["name"] extends "call" ? SBUILTIN_Call : Node["name"] extends "map" ? SBUILTIN_Map : Node["name"] extends "tostring" ? BUILTIN_ToString : Node["name"] extends "arr" ? BUILTIN_Arr : Node["name"] extends "add" ? BUILTIN_Add : Node["name"] extends "mul" ? BUILTIN_Mul : SENTINEL_NO_BUILTIN : never; export type EvalError = `Eval error: ${T}`; export type FindInStack< Frame extends StackFrame, NameToFind > = NameToFind extends keyof Frame["bindings"] ? Frame["bindings"][NameToFind] : Frame["parent"] extends null ? EvalError<`Can't find name "${ToString}" on the stack`> : FindInStack; export type FnPrim< Args extends readonly ASTNode[] = readonly ASTNode[], Fn extends ASTNode = ASTNode > = { args: Args; fn: Fn }; export type HandleFn = Node["children"] extends [ ...infer Args extends ASTNode[], infer Fn extends ASTNode ] ? FnPrim : // error? never; // any[] was propertykey export type MapZip< Args extends readonly ASTNode[], Values extends readonly any[] > = { [Idx in Exclude< keyof Args, keyof any[] > as Args[Idx] extends infer Node extends ASTNode ? Node["name"] : never]: Idx extends keyof Values ? Values[Idx] : never; }; export type CallFn< Fn extends FnPrim, Values extends readonly any[], Frame extends StackFrame > = _Evaluate, Frame>>; export type _Evaluate< Node extends ASTNode, Frame extends StackFrame > = Node["type"] extends NodeType.INT ? Node["value"] : Node["type"] extends NodeType.EXT ? // special builtin Node["name"] extends "fn" ? HandleFn : MapBuiltins extends infer BI ? BI extends SENTINEL_NO_BUILTIN ? FindInStack : BI : never : EvalError<`Unhandled node type ${Node["type"]}`>; export type GetEvaluatedChildren< Node extends ASTNode, Frame extends StackFrame > = Node["children"] extends infer Children extends readonly ASTNode[] ? { [Idx in keyof Children]: Children[Idx] extends ASTNode ? _Evaluate : never; } : never; export type Evaluate = _Evaluate; export * from "./common"; export * from "./eval"; export * from "./parser"; export * from "./lexer"; import { LexerCtx, Token, TokenType } from "./common"; export type IsWhitespace = T extends `${TokenType.SPACE}` ? true : T extends `${TokenType.COMMA}` ? true : T extends `${TokenType.SEMICOLON}` ? true : false; export type ProcessNameCollection< Ctx extends LexerCtx, Tail extends string, _Token extends Token | null > = { next: Tail; nameCollection: ""; tokens: _Token extends null ? [ ...Ctx["tokens"], ...(Ctx["nameCollection"] extends "" ? [] : [Token]) ] : [ ...Ctx["tokens"], ...(Ctx["nameCollection"] extends "" ? [_Token] : [Token, _Token]) ]; }; export type IsOpen = T extends `${TokenType.OPEN_PAREN}` ? true : false; export type IsClose = T extends `${TokenType.CLOSE_PAREN}` ? true : false; export type ChunkedLex< Ctx extends LexerCtx, Depth extends any[] = [] > = Depth["length"] extends 50 ? Ctx & { endChunk: true; } : Ctx["next"] extends `${infer Head}${infer Tail}` ? IsWhitespace extends true ? ChunkedLex, [0, ...Depth]> : IsOpen extends true ? ChunkedLex< ProcessNameCollection>, [0, ...Depth] > : IsClose extends true ? ChunkedLex< ProcessNameCollection>, [0, ...Depth] > : ChunkedLex< { next: Tail; nameCollection: `${Ctx["nameCollection"]}${Head}`; tokens: Ctx["tokens"]; }, [0, ...Depth] > : Ctx; export type InnerLex< Next extends string, NameCollection extends LexerCtx["nameCollection"] = "", AccTokens extends Token[] = [] > = Next extends "" ? AccTokens : ChunkedLex<{ next: Next; tokens: []; nameCollection: NameCollection; }> extends infer U ? U extends LexerCtx & { endChunk: true } ? InnerLex : U extends LexerCtx ? [...AccTokens, ...U["tokens"]] : never : never; export type Lex = InnerLex<`${Raw};`>; import { ASTNode, NodeType, ParserCtx, Token, TokenType } from "./common"; export type Error = ASTNode< NodeType.PARSER_ERROR, "Error", T, [] >; export type PushChild = { type: Node["type"]; value: Node["value"]; name: Node["name"]; children: [...Node["children"], Child]; }; export type PushChildToLastElementOfStack< Stack extends ParserCtx["stack"], Child extends ASTNode > = Stack extends [...infer Head, infer Tail extends ASTNode] ? [...Head, PushChild] : Stack extends [infer Only extends ASTNode] ? [PushChild] : never; export type PushChildToSecondLastElementOfStack< Stack extends ParserCtx["stack"], Child extends ASTNode > = Stack extends [ ...infer Head, infer Tail extends ASTNode, infer Final extends ASTNode ] ? [...Head, PushChild, Final] : Stack extends [infer Only extends ASTNode, infer Final extends ASTNode] ? [PushChild, Final] : never; export type GetLastOnStack = Stack extends [ ...infer Head, infer Tail extends ASTNode ] ? Tail : Stack extends [infer Only extends ASTNode] ? Only : never; export type StackWithoutLast = Stack extends [ ...infer Head extends ASTNode[], infer Tail ] ? [...Head] : Stack extends [infer Only extends ASTNode] ? [] : never; type NULL_SENTINEL = { NULL: true; }; export type ParseNumberLiteral = T extends `${infer Inner extends number}` ? Inner : NULL_SENTINEL; export type ParseStringLiteral = T extends `"${infer Inner extends string}"` ? Inner : NULL_SENTINEL; export type ResolveNodeFromToken<_Token extends Token> = ParseNumberLiteral< _Token["name"] > extends number ? ASTNode, []> : ParseStringLiteral<_Token["name"]> extends string ? ASTNode, []> : ASTNode; export type _Parse = Ctx["remainingTokens"] extends [ infer Head extends Token, ...infer Tail extends readonly Token[] ] ? Ctx["lastToken"] extends Token ? Head["type"] extends TokenType.NAME ? // we already have a lastToken // mutate last element of stack to push lastToken as child // lastToken = nextToken // goto start _Parse<{ lastToken: Head; remainingTokens: Tail; stack: PushChildToLastElementOfStack< Ctx["stack"], ResolveNodeFromToken >; }> : //nextToken is openParen or close paren Head["type"] extends TokenType.CLOSE_PAREN ? // handle lastToken // set last element of stack as child of prev element on stack // pop stack // [stack[last - 1].children.push(stack.pop) // goto start _Parse<{ lastToken: null; remainingTokens: Tail; // first push the last name onto the children of the top // then push the top onto the children of the next // then remove the top stack: StackWithoutLast< PushChildToSecondLastElementOfStack< Ctx["stack"], PushChild< GetLastOnStack, ResolveNodeFromToken > > >; }> : Head["type"] extends TokenType.OPEN_PAREN ? // push lastToken onto stack // goto start _Parse<{ lastToken: null; remainingTokens: Tail; stack: [...Ctx["stack"], ResolveNodeFromToken]; }> : Ctx & Error<`Was not expecting ${Head["type"]}`> : // expect nextToken to be a name or close paren Head["type"] extends TokenType.NAME ? // lastToken = nextToken // goto start _Parse<{ lastToken: Head; remainingTokens: Tail; stack: Ctx["stack"]; }> : Head["type"] extends TokenType.CLOSE_PAREN ? _Parse<{ lastToken: null; remainingTokens: Tail; // push the top onto the children of the next // then remove the top stack: StackWithoutLast< PushChildToSecondLastElementOfStack< Ctx["stack"], GetLastOnStack > >; }> : Ctx & Error<`Expected nextToken to be a name or close paren at ${Head["type"]}`> : Ctx["lastToken"] extends Token ? // case where we ended with a name _Parse<{ lastToken: null; remainingTokens: []; stack: PushChildToLastElementOfStack< Ctx["stack"], ResolveNodeFromToken >; }> : Ctx["stack"][0]; export type Parse = _Parse<{ lastToken: null; remainingTokens: Raw; stack: [ASTNode]; }>; export * from "./core"; export * from "./number"; export * from "./utils"; export * from "./string"; export type NumberToArray< Number extends number, Carry extends readonly any[] = [] > = Number extends Carry["length"] ? Carry : NumberToArray; export type NumbersToArray< Numbers extends readonly number[], Carry extends readonly any[] = [] > = Numbers extends [ infer Head extends number, ...infer Tail extends readonly number[] ] ? NumbersToArray]> : Carry; export type AddNumbers = NumbersToArray extends infer T extends readonly any[] ? T["length"] : never; export type MultiplyInner< N extends number, MS extends readonly any[], Carry extends number = 0 > = MS extends [infer Head extends number, ...infer Tail extends readonly any[]] ? MultiplyInner> : Carry; export type Multiply = MultiplyInner< M, NumberToArray >; export type ExtractNumber = T extends `${infer Inner extends number}` ? Inner : never; export type AddStrings< Strings extends readonly string[], Carry extends string = "" > = Strings extends [infer Head extends string, ...infer Tail extends string[]] ? AddStrings : Carry; export type ToString = T extends string | number ? `${T}` : T extends readonly any[] ? T extends readonly [infer Head, ...infer Tail] ? `${ToString< Tail, `${Carry extends "" ? "" : `${Carry}, `}${ToString}` >}` : `[${Carry}]` : never; export type UnarrayIfOnlyHead = T extends [ infer Head, infer Next ] ? T : T extends [infer Head] ? Head : T; import { evaluate, lex, parse } from "./lang"; import { createFn } from "./lang"; const adder = createFn("fn(x, map(arr(10,20,30), fn(y, add(x, y))))"); const result = adder(5); console.log(result);