From 56040f3ff85e77311f0c864a89afd63fcf1bdb50 Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Mon, 3 Nov 2025 23:40:02 -0800 Subject: add js-lang, refactor some ts-lang stuff --- src/index.ts | 3 +- src/js-lang/builtin/builtin.ts | 39 +++++++++ src/js-lang/builtin/index.ts | 2 + src/js-lang/builtin/sbuiltin.ts | 46 +++++++++++ src/js-lang/core/eval.ts | 83 +++++++++++++++++++ src/js-lang/core/index.ts | 3 + src/js-lang/core/lexer.ts | 46 +++++++++++ src/js-lang/core/parser.ts | 132 ++++++++++++++++++++++++++++++ src/js-lang/index.ts | 1 + src/lang/builtin/builtin.ts | 33 -------- src/lang/builtin/index.ts | 3 - src/lang/builtin/sbuiltin.ts | 27 ------- src/lang/core/common.ts | 72 ----------------- src/lang/core/eval.ts | 111 -------------------------- src/lang/core/index.ts | 0 src/lang/core/lexer.ts | 91 --------------------- src/lang/core/parser.ts | 173 ---------------------------------------- src/lang/index.ts | 1 - src/lang/util/index.ts | 3 - src/lang/util/number.ts | 37 --------- src/lang/util/string.ts | 17 ---- src/lang/util/utils.ts | 8 -- src/ts-lang/builtin/builtin.ts | 33 ++++++++ src/ts-lang/builtin/index.ts | 3 + src/ts-lang/builtin/sbuiltin.ts | 27 +++++++ src/ts-lang/core/common.ts | 66 +++++++++++++++ src/ts-lang/core/eval.ts | 102 +++++++++++++++++++++++ src/ts-lang/core/index.ts | 4 + src/ts-lang/core/lexer.ts | 84 +++++++++++++++++++ src/ts-lang/core/parser.ts | 165 ++++++++++++++++++++++++++++++++++++++ src/ts-lang/index.ts | 1 + src/ts-lang/util/index.ts | 3 + src/ts-lang/util/number.ts | 37 +++++++++ src/ts-lang/util/string.ts | 17 ++++ src/ts-lang/util/utils.ts | 8 ++ 35 files changed, 904 insertions(+), 577 deletions(-) create mode 100644 src/js-lang/builtin/builtin.ts create mode 100644 src/js-lang/builtin/index.ts create mode 100644 src/js-lang/builtin/sbuiltin.ts create mode 100644 src/js-lang/core/eval.ts create mode 100644 src/js-lang/core/index.ts create mode 100644 src/js-lang/core/lexer.ts create mode 100644 src/js-lang/core/parser.ts create mode 100644 src/js-lang/index.ts delete mode 100644 src/lang/builtin/builtin.ts delete mode 100644 src/lang/builtin/index.ts delete mode 100644 src/lang/builtin/sbuiltin.ts delete mode 100644 src/lang/core/common.ts delete mode 100644 src/lang/core/eval.ts delete mode 100644 src/lang/core/index.ts delete mode 100644 src/lang/core/lexer.ts delete mode 100644 src/lang/core/parser.ts delete mode 100644 src/lang/index.ts delete mode 100644 src/lang/util/index.ts delete mode 100644 src/lang/util/number.ts delete mode 100644 src/lang/util/string.ts delete mode 100644 src/lang/util/utils.ts create mode 100644 src/ts-lang/builtin/builtin.ts create mode 100644 src/ts-lang/builtin/index.ts create mode 100644 src/ts-lang/builtin/sbuiltin.ts create mode 100644 src/ts-lang/core/common.ts create mode 100644 src/ts-lang/core/eval.ts create mode 100644 src/ts-lang/core/index.ts create mode 100644 src/ts-lang/core/lexer.ts create mode 100644 src/ts-lang/core/parser.ts create mode 100644 src/ts-lang/index.ts create mode 100644 src/ts-lang/util/index.ts create mode 100644 src/ts-lang/util/number.ts create mode 100644 src/ts-lang/util/string.ts create mode 100644 src/ts-lang/util/utils.ts (limited to 'src') diff --git a/src/index.ts b/src/index.ts index 8cd5167..ab79901 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1 +1,2 @@ -export * from "./lib"; +export * from "./ts-lang"; +export * from "./js-lang"; 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 = { + 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 = { + 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 = ( + node: Node +): Evaluate => _evaluate(node, emptyStackFrame) as Evaluate; 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 = (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/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 = ( + 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/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"; diff --git a/src/lang/builtin/builtin.ts b/src/lang/builtin/builtin.ts deleted file mode 100644 index a289867..0000000 --- a/src/lang/builtin/builtin.ts +++ /dev/null @@ -1,33 +0,0 @@ -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}`>; diff --git a/src/lang/builtin/index.ts b/src/lang/builtin/index.ts deleted file mode 100644 index de2bee3..0000000 --- a/src/lang/builtin/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export type FnError = `Function execution error: ${T}`; -export * from "./builtin"; -export * from "./sbuiltin"; diff --git a/src/lang/builtin/sbuiltin.ts b/src/lang/builtin/sbuiltin.ts deleted file mode 100644 index 01f197e..0000000 --- a/src/lang/builtin/sbuiltin.ts +++ /dev/null @@ -1,27 +0,0 @@ -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 - >}`>; diff --git a/src/lang/core/common.ts b/src/lang/core/common.ts deleted file mode 100644 index 9e3840b..0000000 --- a/src/lang/core/common.ts +++ /dev/null @@ -1,72 +0,0 @@ -export enum TokenType { - OPEN_PAREN = "(", - CLOSE_PAREN = ")", - SPACE = " ", - SEMICOLON = ";", - COMMA = ",", - NAME = "NAME", -} - -export enum TokenSubType { - NA = "NA", - LITERAL = "LITERAL", - REFERENCE = "REFERENCE", -} - -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; diff --git a/src/lang/core/eval.ts b/src/lang/core/eval.ts deleted file mode 100644 index 9181c81..0000000 --- a/src/lang/core/eval.ts +++ /dev/null @@ -1,111 +0,0 @@ -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"; -import { Lex } from "./lexer"; -import { Parse } from "./parser"; - -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 MapOnStack< - Node extends ASTNode, - Frame extends StackFrame -> = 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 - : never; - -export type MapZip< - T extends readonly ASTNode[], - U extends readonly PropertyKey[] -> = { - [Idx in Exclude< - keyof T, - keyof any[] - > as T[Idx] extends infer Node extends ASTNode - ? Node["name"] - : never]: Idx extends keyof U ? U[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 = EmptyStackFrame -> = 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 - ? MapOnStack - : 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; - -const input = - `map(arr("hello", "world"), fn(s, i, add(tostring(i), ":", s)))` as const; -const lex_result = null as unknown as Lex; -const parse_result = null as unknown as Parse; -const eval_result = null as unknown as Evaluate; diff --git a/src/lang/core/index.ts b/src/lang/core/index.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/lang/core/lexer.ts b/src/lang/core/lexer.ts deleted file mode 100644 index 567964f..0000000 --- a/src/lang/core/lexer.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { LexerCtx, Token, TokenSubType, TokenType } from "./common"; - -export type BreakingToken = - | TokenType.OPEN_PAREN - | TokenType.CLOSE_PAREN - | TokenType.COMMA - | TokenType.SEMICOLON - | TokenType.SPACE; - -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; diff --git a/src/lang/core/parser.ts b/src/lang/core/parser.ts deleted file mode 100644 index 2b481fd..0000000 --- a/src/lang/core/parser.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { - ASTNode, - NodeType, - ParserCtx, - Token, - TokenSubType, - TokenType, -} from "./common"; -import { Lex } from "./lexer"; - -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]; -}>; diff --git a/src/lang/index.ts b/src/lang/index.ts deleted file mode 100644 index 8d119de..0000000 --- a/src/lang/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./core"; diff --git a/src/lang/util/index.ts b/src/lang/util/index.ts deleted file mode 100644 index 00a3e54..0000000 --- a/src/lang/util/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from "./number"; -export * from "./utils"; -export * from "./string"; diff --git a/src/lang/util/number.ts b/src/lang/util/number.ts deleted file mode 100644 index 6e4e360..0000000 --- a/src/lang/util/number.ts +++ /dev/null @@ -1,37 +0,0 @@ -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; diff --git a/src/lang/util/string.ts b/src/lang/util/string.ts deleted file mode 100644 index 5772f40..0000000 --- a/src/lang/util/string.ts +++ /dev/null @@ -1,17 +0,0 @@ -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; diff --git a/src/lang/util/utils.ts b/src/lang/util/utils.ts deleted file mode 100644 index ac36ca1..0000000 --- a/src/lang/util/utils.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type UnarrayIfOnlyHead = T extends [ - infer Head, - infer Next -] - ? T - : T extends [infer Head] - ? Head - : T; diff --git a/src/ts-lang/builtin/builtin.ts b/src/ts-lang/builtin/builtin.ts new file mode 100644 index 0000000..a289867 --- /dev/null +++ b/src/ts-lang/builtin/builtin.ts @@ -0,0 +1,33 @@ +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}`>; diff --git a/src/ts-lang/builtin/index.ts b/src/ts-lang/builtin/index.ts new file mode 100644 index 0000000..de2bee3 --- /dev/null +++ b/src/ts-lang/builtin/index.ts @@ -0,0 +1,3 @@ +export type FnError = `Function execution error: ${T}`; +export * from "./builtin"; +export * from "./sbuiltin"; diff --git a/src/ts-lang/builtin/sbuiltin.ts b/src/ts-lang/builtin/sbuiltin.ts new file mode 100644 index 0000000..01f197e --- /dev/null +++ b/src/ts-lang/builtin/sbuiltin.ts @@ -0,0 +1,27 @@ +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 + >}`>; diff --git a/src/ts-lang/core/common.ts b/src/ts-lang/core/common.ts new file mode 100644 index 0000000..fa5cb7c --- /dev/null +++ b/src/ts-lang/core/common.ts @@ -0,0 +1,66 @@ +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; diff --git a/src/ts-lang/core/eval.ts b/src/ts-lang/core/eval.ts new file mode 100644 index 0000000..5f22299 --- /dev/null +++ b/src/ts-lang/core/eval.ts @@ -0,0 +1,102 @@ +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; diff --git a/src/ts-lang/core/index.ts b/src/ts-lang/core/index.ts new file mode 100644 index 0000000..881cacf --- /dev/null +++ b/src/ts-lang/core/index.ts @@ -0,0 +1,4 @@ +export * from "./common"; +export * from "./eval"; +export * from "./parser"; +export * from "./lexer"; diff --git a/src/ts-lang/core/lexer.ts b/src/ts-lang/core/lexer.ts new file mode 100644 index 0000000..bcd5785 --- /dev/null +++ b/src/ts-lang/core/lexer.ts @@ -0,0 +1,84 @@ +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};`>; diff --git a/src/ts-lang/core/parser.ts b/src/ts-lang/core/parser.ts new file mode 100644 index 0000000..db6f3aa --- /dev/null +++ b/src/ts-lang/core/parser.ts @@ -0,0 +1,165 @@ +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]; +}>; diff --git a/src/ts-lang/index.ts b/src/ts-lang/index.ts new file mode 100644 index 0000000..8d119de --- /dev/null +++ b/src/ts-lang/index.ts @@ -0,0 +1 @@ +export * from "./core"; diff --git a/src/ts-lang/util/index.ts b/src/ts-lang/util/index.ts new file mode 100644 index 0000000..00a3e54 --- /dev/null +++ b/src/ts-lang/util/index.ts @@ -0,0 +1,3 @@ +export * from "./number"; +export * from "./utils"; +export * from "./string"; diff --git a/src/ts-lang/util/number.ts b/src/ts-lang/util/number.ts new file mode 100644 index 0000000..6e4e360 --- /dev/null +++ b/src/ts-lang/util/number.ts @@ -0,0 +1,37 @@ +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; diff --git a/src/ts-lang/util/string.ts b/src/ts-lang/util/string.ts new file mode 100644 index 0000000..5772f40 --- /dev/null +++ b/src/ts-lang/util/string.ts @@ -0,0 +1,17 @@ +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; diff --git a/src/ts-lang/util/utils.ts b/src/ts-lang/util/utils.ts new file mode 100644 index 0000000..ac36ca1 --- /dev/null +++ b/src/ts-lang/util/utils.ts @@ -0,0 +1,8 @@ +export type UnarrayIfOnlyHead = T extends [ + infer Head, + infer Next +] + ? T + : T extends [infer Head] + ? Head + : T; -- cgit v1.2.3-70-g09d2