summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2025-11-09 21:08:12 -0800
committerKai Stevenson <kai@kaistevenson.com>2025-11-09 21:08:30 -0800
commit413eaa284e164143c5416cdce5a1de0f9f992409 (patch)
treed999e8cbaddefcce9df3265c594083177427b6cb
parent93992029bd349185d15de02e0f633e95c62695a9 (diff)
map + reduce
-rw-r--r--examples/README.md5
-rw-r--r--examples/mapReduce.ts21
-rw-r--r--package.json4
-rw-r--r--src/lang/js-lang/builtin/sbuiltin.ts25
-rw-r--r--src/lang/ts-lang/builtin/sbuiltin.ts28
-rw-r--r--src/lang/ts-lang/core/eval.ts3
-rw-r--r--tests/type-consistency/spec/index.ts4
-rw-r--r--tests/type-consistency/spec/mapReduce.ts34
-rw-r--r--tests/type-consistency/spec/recursion.ts25
9 files changed, 145 insertions, 4 deletions
diff --git a/examples/README.md b/examples/README.md
index 33c957d..1d0e0c1 100644
--- a/examples/README.md
+++ b/examples/README.md
@@ -1,6 +1,7 @@
## A guided tour of KaiScript through examples
-Open this in your IDE of choice--make sure your LSP is running so you can inspect types.
+Open this in your IDE of choice--make sure your LSP is running so you can inspect types. Feel free to run `tsx` on each file to verify the runtime outputs.
+If you're reading this without an LSP, I've added type annotations in comments above each result. I promise these match what you'd see if you hovered over the value!
1. [Breaking the second wall](./mapper.ts)
@@ -11,3 +12,5 @@ Open this in your IDE of choice--make sure your LSP is running so you can inspec
4. [Turing completeness](./branching.ts)
5. [Infinite computation](./recursion.ts)
+
+6. [Infinite transformation](./mapReduce.ts)
diff --git a/examples/mapReduce.ts b/examples/mapReduce.ts
new file mode 100644
index 0000000..3904b2a
--- /dev/null
+++ b/examples/mapReduce.ts
@@ -0,0 +1,21 @@
+/*
+You can do anything with map + reduce!
+*/
+
+import { createFn } from "../src";
+
+const concatNumbers = createFn<[number[]]>()(
+ `fn(a, reduce(map(a, fn(n, tostring(n))), fn(acc, cur, add(acc,cur)), ""))`
+);
+
+// const result: "1235"
+const concatted = concatNumbers([1, 2, 35]);
+console.log(concatted);
+
+const arrayLength = createFn<[any[]]>()(
+ `fn(a, reduce(a, fn(acc, add(acc, 1)), 0))`
+);
+
+// const length: 2
+const length = arrayLength(["hello", "world"]);
+console.log(length);
diff --git a/package.json b/package.json
index f4d09b8..e5d98d5 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "@aberrantflux/kai-script",
"description": "A type safe framework for TypeScript",
- "version": "0.2.4",
+ "version": "0.3.0",
"repository": {
"type": "git",
"url": "https://git.aberrantflux.xyz/kai-script.git/"
@@ -9,7 +9,7 @@
"scripts": {
"build": "tsc --declaration",
"generate-tests": "rm -f tests/type-consistency/generated/*.test.ts && tsx tests/type-consistency/generateAll.ts",
- "test": "pnpm run generate-tests && vitest run --reporter verbose && tsc --noEmit --project tests/type-consistency/tsconfig.json",
+ "test": "pnpm run generate-tests && vitest run --reporter verbose && tsc --noEmit --project tests/type-consistency/tsconfig.json && echo 'All tests passed!'",
"safe-publish": "rm -rf dist && pnpm build && pnpm test && npm publish"
},
"exports": {
diff --git a/src/lang/js-lang/builtin/sbuiltin.ts b/src/lang/js-lang/builtin/sbuiltin.ts
index 013854c..7d9b851 100644
--- a/src/lang/js-lang/builtin/sbuiltin.ts
+++ b/src/lang/js-lang/builtin/sbuiltin.ts
@@ -40,6 +40,30 @@ export const V_SBUILTIN_Map: SBUILTIN = (node, frame) => {
return values.map((v, i) => callFn(fn, [v, i], frame));
};
+export const V_SBUILTIN_Reduce: SBUILTIN = (node, frame) => {
+ const children = getEvaluatedChildren(node, frame);
+ const fn = children[1] as FnPrim | undefined;
+ const acc = children[2];
+
+ if (!fn?.fn) {
+ throw new Error(
+ `Invalid params for reduce: ${JSON.stringify(children, undefined, 2)}`
+ );
+ }
+
+ const values = children[0];
+
+ if (!Array.isArray(values)) {
+ // add to ts
+ throw new Error(`Can't reduce non-array value: ${values}`);
+ }
+
+ return values.reduce(
+ (acc, cur, idx) => callFn(fn, [acc, cur, idx], frame),
+ acc
+ );
+};
+
export const V_SBUILTIN_IfElse: SBUILTIN = (node, frame) => {
const children = node.children;
@@ -59,5 +83,6 @@ export const V_SBUILTIN_IfElse: SBUILTIN = (node, frame) => {
export const nameToSBUILTIN: Record<string, SBUILTIN> = {
call: V_SBUILTIN_Call,
map: V_SBUILTIN_Map,
+ reduce: V_SBUILTIN_Reduce,
"?": V_SBUILTIN_IfElse,
};
diff --git a/src/lang/ts-lang/builtin/sbuiltin.ts b/src/lang/ts-lang/builtin/sbuiltin.ts
index c92fd6a..38b4256 100644
--- a/src/lang/ts-lang/builtin/sbuiltin.ts
+++ b/src/lang/ts-lang/builtin/sbuiltin.ts
@@ -42,6 +42,34 @@ export type SBUILTIN_Map<
GetEvaluatedChildren<Node, Frame, Callstack>
>}`>;
+type Reduce<
+ Arr extends readonly any[],
+ Fn extends FnPrim,
+ Acc,
+ IdxLen extends readonly any[] = readonly []
+> = Arr extends [infer Head, ...infer Tail]
+ ? Reduce<
+ Tail,
+ Fn,
+ CallFn<Fn, [Acc, Head, IdxLen["length"]]>,
+ [...IdxLen, any]
+ >
+ : Acc;
+
+export type SBUILTIN_Reduce<
+ Node extends ASTNode,
+ Frame extends StackFrame,
+ Callstack extends readonly string[]
+> = GetEvaluatedChildren<Node, Frame, Callstack> extends [
+ infer Arr extends readonly any[],
+ infer Fn extends FnPrim,
+ infer Acc
+]
+ ? Reduce<Arr, Fn, Acc>
+ : EvalError<`Invalid params for reduce: ${ToString<
+ GetEvaluatedChildren<Node, Frame, Callstack>
+ >}`>;
+
export type SBUILTIN_IfElse<
Node extends ASTNode,
Frame extends StackFrame,
diff --git a/src/lang/ts-lang/core/eval.ts b/src/lang/ts-lang/core/eval.ts
index ebc58e7..bef0ef8 100644
--- a/src/lang/ts-lang/core/eval.ts
+++ b/src/lang/ts-lang/core/eval.ts
@@ -8,6 +8,7 @@ import {
SBUILTIN_Call,
SBUILTIN_IfElse,
SBUILTIN_Map,
+ SBUILTIN_Reduce,
} from "../builtin";
import { ToString } from "../util";
import {
@@ -34,6 +35,8 @@ export type MapBuiltins<
? SBUILTIN_Call<Node, Frame, Callstack>
: Node["name"] extends "map"
? SBUILTIN_Map<Node, Frame, Callstack>
+ : Node["name"] extends "reduce"
+ ? SBUILTIN_Reduce<Node, Frame, Callstack>
: Node["name"] extends "?"
? SBUILTIN_IfElse<Node, Frame, Callstack>
: Node["name"] extends "tostring"
diff --git a/tests/type-consistency/spec/index.ts b/tests/type-consistency/spec/index.ts
index b2da682..780c20c 100644
--- a/tests/type-consistency/spec/index.ts
+++ b/tests/type-consistency/spec/index.ts
@@ -2,5 +2,7 @@ import array from "./array";
import functions from "./function";
import types from "./types";
import tostring from "./tostring";
+import mapreduce from "./mapReduce";
+import recursion from "./recursion";
-export default [array, functions, types, tostring];
+export default [array, functions, types, tostring, mapreduce, recursion];
diff --git a/tests/type-consistency/spec/mapReduce.ts b/tests/type-consistency/spec/mapReduce.ts
new file mode 100644
index 0000000..b5077ff
--- /dev/null
+++ b/tests/type-consistency/spec/mapReduce.ts
@@ -0,0 +1,34 @@
+import path from "path";
+import { createTestHarness } from "../harness";
+
+export default createTestHarness({
+ harnessName: "Map reduce",
+ generatedPath: path.join(__dirname, "..", "generated"),
+})
+ .createFunctionTest({
+ name: "Map: numbers to string",
+ program: "fn(a, map(a, fn(n, tostring(n))))",
+ cases: [
+ { input: [1, 2, 3], output: ["1", "2", "3"] },
+ { input: [50], output: ["50"] },
+ { input: [], output: [] },
+ ],
+ })
+ .createFunctionTest({
+ name: "Reduce: array length",
+ program: "fn(a, reduce(a, fn(acc, add(acc, 1)), 0))",
+ cases: [
+ { input: [1, 2, 3], output: 3 },
+ { input: ["hello", ["hello", "world"]], output: 2 },
+ { input: [], output: 0 },
+ ],
+ })
+ .createFunctionTest({
+ name: "Reduce: sum of numbers times index",
+ program: "fn(a, reduce(a, fn(acc, cur, idx, add(acc, mul(cur, idx))), 0))",
+ cases: [
+ { input: [1, 2, 3], output: 8 },
+ { input: [], output: 0 },
+ { input: [50, 10, 0], output: 10 },
+ ],
+ });
diff --git a/tests/type-consistency/spec/recursion.ts b/tests/type-consistency/spec/recursion.ts
new file mode 100644
index 0000000..c5de83f
--- /dev/null
+++ b/tests/type-consistency/spec/recursion.ts
@@ -0,0 +1,25 @@
+import path from "path";
+import { createTestHarness } from "../harness";
+
+export default createTestHarness({
+ harnessName: "Recursive functions",
+ generatedPath: path.join(__dirname, "..", "generated"),
+})
+ .createFunctionTest({
+ name: "n!",
+ program: "bind(fac,fn(n,?(eq(n, 1),n,mul(n,call(fac,sub(n,1))))))",
+ cases: [
+ { input: 3, output: 6 },
+ { input: 1, output: 1 },
+ { input: 5, output: 120 },
+ ],
+ })
+ .createFunctionTest({
+ name: "Sum of natural numbers on [0, n]",
+ program: "bind(sumnn,fn(n,?(eq(n, 1),n,add(n,call(sumnn,sub(n,1))))))",
+ cases: [
+ { input: 5, output: 15 },
+ { input: 1, output: 1 },
+ { input: 2, output: 3 },
+ ],
+ });