diff --git a/src/index.test.ts b/src/index.test.ts index 081ba07..114fbd6 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -1,6 +1,7 @@ import { jam, cue, cue_bytes, bigintToDataView, bi_cut } from "./serial"; import bits from "./bits"; import { dwim, enjs } from "./noun-helpers"; +import { putIn, putBy } from "./noun-std"; import { Atom, Cell, Noun } from "./noun"; import compiler from "./compiler"; import { bigIntFromStringWithRadix } from "./bigint"; @@ -261,4 +262,28 @@ test('dwim <-> enjs.cord', () => { const cord = enjs.cord(atom); expect(cord).toEqual(str); -}) \ No newline at end of file +}); + +test('putIn', () => { + const nums = [1, 2, 3, 3, 3, 4, 5, 6, 7, 7, 7]; + const ex = [6, [7, [5, 0, 0], 0], 4, [2, [1, 0, 0], 3, 0, 0], 0]; + + let set: Noun = Atom.zero; + for (let num of nums) { + set = putIn(set, Atom.fromInt(num)); + }; + + expect(set.equals(dwim(ex))); +}); + +test('putBy', () => { + const nums = [[1, 11], [2, 22], [3, 33], [3, 34], [3, 35], [4, 44], [5, 55], [6, 66], [7, 77], [7, 78], [7, 79]]; + const ex = [[6, 66], [[7, 79], [[5, 55], 0, 0], 0], [4, 44], [[2, 22], [[1, 11], 0, 0], [3, 35], 0, 0], 0]; + + let map: Noun = Atom.zero; + for (let num of nums) { + map = putBy(map, Atom.fromInt(num[0]), Atom.fromInt(num[1])); + }; + + expect(map.equals(dwim(ex))); +}); diff --git a/src/noun-dejs.ts b/src/noun-dejs.ts new file mode 100644 index 0000000..4905502 --- /dev/null +++ b/src/noun-dejs.ts @@ -0,0 +1,80 @@ +import { Atom, Cell, isNoun } from "./noun"; +import type { Noun } from "./noun"; +import { putIn, putBy } from "./noun-std"; + +// primitives + +type Atomizable = number | string | Atom; + +// "Do What I Mean" +function dwim(a: number): Atom; +function dwim(a: string): Atom; +function dwim(a: Atomizable, b: Atomizable): Cell; +function dwim( + a: Atomizable, + b: Atomizable, + c: Atomizable +): Cell>; +function dwim(a: Atomizable, ...b: any[]): Cell>; +function dwim(...a: any[]): Cell; +// implementation +function dwim(...args: any[]): Noun { + const n = args.length === 1 ? args[0] : args; + if (isNoun(n)) return n; + if (typeof n === "number") { + return Atom.fromInt(n); + } else if (typeof n === "string") { + return Atom.fromCord(n); + } else if (Array.isArray(n)) { + if (n.length < 2) { + return dwim(n[0]); + } + const head = dwim(n[n.length - 2]); + const tail = dwim(n[n.length - 1]); + let cel = new Cell(head, tail); + for (var j = n.length - 3; j >= 0; --j) { + cel = new Cell(dwim(n[j]), cel); + } + return cel; + } else if (n === null) { + return Atom.zero; + } + // objects, undefined, etc + console.error("what do you mean??", typeof n, JSON.stringify(n)); + throw new Error('dwim, but meaning unclear'); +} + +// structures + +function list(args: any[]): Noun { + if (args.length === 0) return Atom.zero; + return dwim([...args, Atom.zero]); +} + +function set(args: any[]): Noun { + if (args.length === 0) return Atom.zero; + let set: Noun = Atom.zero; + for (let arg of args) { + set = putIn(set, dwim(arg)); + } + return set; +} + +function map(args: {key: any, val: any}[]): Noun { + if (args.length === 0) return Atom.zero; + let map: Noun = Atom.zero; + for (let arg of args) { + map = putBy(map, dwim(arg.key), dwim(arg.val)); + } + return map; +} + +const dejs = { + nounify: dwim, + dwim, + list, + set, + map +}; + +export { dejs, dwim }; diff --git a/src/noun-enjs.ts b/src/noun-enjs.ts new file mode 100644 index 0000000..a518568 --- /dev/null +++ b/src/noun-enjs.ts @@ -0,0 +1,204 @@ +import { Atom, Cell } from "./noun"; +import type { Noun } from "./noun"; +import { bitLength } from "./bigint"; + +type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; +export type EnjsFunction = (n: Noun) => Json; +type frondOpt = { tag: string; get: EnjsFunction }; + +const frond = function (opts: frondOpt[]): EnjsFunction { + return function (noun) { + if (!(noun instanceof Cell && noun.head instanceof Atom)) { + throw new Error("frond: noun not cell with tag head"); + } + const tag = Atom.cordToString(noun.head); + for (let i = 0; i < opts.length; i++) { + if (tag === opts[i].tag) { + return { [tag]: opts[i].get(noun.tail) }; + } + } + throw new Error("frond: unknown tag" + tag); + }; +}; + +const tuple = function(funs: EnjsFunction[]): EnjsFunction { + return function (noun) { + let o = []; + while (funs.length > 1) { + if (noun.isAtom()) { + throw new Error("tuple: noun too shallow"); + } + o.push(funs[0](noun.head)); + funs.splice(0, 1); + noun = noun.tail; + } + o.push(funs[0](noun)); + return o; + } +} + +type PairCell = { nom: string; get: EnjsFunction }; +const pairs = function (cels: PairCell[]): EnjsFunction { + return function (noun) { + let i = 0; + let o: Record = {}; + while (i < cels.length - 1) { + if (!(noun instanceof Cell)) { + throw new Error("pairs: noun too shallow"); + } + o[cels[i].nom] = cels[i].get(noun.head); + noun = noun.tail; + i++; + } + o[cels[i].nom] = cels[i].get(noun); + return o; + }; +}; +const pair = function ( + na: string, + ga: EnjsFunction, + nb: string, + gb: EnjsFunction +): EnjsFunction { + return pairs([ + { nom: na, get: ga }, + { nom: nb, get: gb }, + ]); +}; + +const bucwut = function (opts: EnjsFunction[]): EnjsFunction { + return function (noun) { + for (let i = 0; i < opts.length; i++) { + try { + const res = opts[i](noun); + return res; + } catch (e) { + continue; + } + } + throw new Error("bucwut: no matches"); + }; +}; + +// buccen: like frond, but without the wrapper object +const buccen = function (opts: frondOpt[]): EnjsFunction { + return function (noun) { + if (!(noun instanceof Cell && noun.head instanceof Atom)) { + throw new Error("buccen: noun not cell with tag head"); + } + const tag = Atom.cordToString(noun.head); + for (let i = 0; i < opts.length; i++) { + if (tag === opts[i].tag) { + return opts[i].get(noun.tail); + } + } + throw new Error("buccen: unknown tag" + tag); + }; +}; + +const array = function (item: EnjsFunction): (n: Noun) => Json[] { + return function (noun) { + let a: Json[] = []; + while (noun instanceof Cell) { + a.push(item(noun.head)); + noun = noun.tail; + } + return a; + }; +}; + +const tree = function (item: EnjsFunction): (n: Noun) => Json[] { + return function (noun) { + let a: Json[] = []; + if (noun instanceof Cell) { + if (!(noun.tail instanceof Cell)) { + throw new Error("tree: malformed"); + } + a = [ + ...a, + item(noun.head), + ...tree(item)(noun.tail.head), + ...tree(item)(noun.tail.tail), + ]; + } + return a; + }; +}; + +const cord = function (noun: Noun): string { + if (!(noun instanceof Atom)) { + throw new Error(`cord: noun not atom ${noun.toString()}`); + } + return Atom.cordToString(noun); +}; + +const tape = function (noun: Noun): string { + return (array(((n: Noun) => { + if (n.isCell()) { + throw new Error("tape: malformed"); + } + return Atom.cordToString(n); + }))(noun)).join(); +} + +const numb = function (noun: Noun): number | string { + if (!(noun instanceof Atom)) { + throw new Error("numb: noun not atom"); + } + if (bitLength(noun.number) <= 32) { + return Number(noun.number); + } else { + return noun.number.toString(); + } +}; + +const numb32 = function (noun: Noun): number { + if (!(noun instanceof Atom)) { + throw new Error("numb32: noun not atom"); + } + if (bitLength(noun.number) > 32) { + throw new Error("numb32: number too big"); + } + return Number(noun.number); +} + +const numbString = function (noun: Noun): string { + if (!(noun instanceof Atom)) { + throw new Error("numbString: noun not atom"); + } + return noun.number.toString(); +} + +const loob = function (noun: Noun): boolean { + return noun.loob(); +}; + +const nill = function (noun: Noun): null { + if (!(noun instanceof Atom && noun.number === 0n)) { + throw new Error("nill: not null"); + } + return null; +}; + +const path = array(cord); + +const enjs = { + frond, + tuple, + pairs, + pair, + array, + loob, + tree, + cord, + tape, + numb, + numb32, + numbString, + path, + buccen, + bucwut, + nill, +}; + +export { enjs }; diff --git a/src/noun-helpers.ts b/src/noun-helpers.ts index 759da77..2d0a32d 100644 --- a/src/noun-helpers.ts +++ b/src/noun-helpers.ts @@ -1,212 +1,4 @@ -import { Atom, Cell } from "./noun"; -import type { Noun } from "./noun"; -import { bitLength } from "./bigint"; - -type Json = null | boolean | number | string | Json[] | { [key: string]: Json }; -export type EnjsFunction = (n: Noun) => Json; -type frondOpt = { tag: string; get: EnjsFunction }; - -const frond = function (opts: frondOpt[]): EnjsFunction { - return function (noun) { - if (!(noun instanceof Cell && noun.head instanceof Atom)) { - throw new Error("frond: noun not cell with tag head"); - } - const tag = Atom.cordToString(noun.head); - for (let i = 0; i < opts.length; i++) { - if (tag === opts[i].tag) { - return { [tag]: opts[i].get(noun.tail) }; - } - } - throw new Error("frond: unknown tag" + tag); - }; -}; -type PairCell = { nom: string; get: EnjsFunction }; -const pairs = function (cels: PairCell[]): EnjsFunction { - return function (noun) { - let i = 0; - let o: Record = {}; - while (i < cels.length - 1) { - if (!(noun instanceof Cell)) { - throw new Error("pairs: noun too shallow"); - } - o[cels[i].nom] = cels[i].get(noun.head); - noun = noun.tail; - i++; - } - o[cels[i].nom] = cels[i].get(noun); - return o; - }; -}; - -const pair = function ( - na: string, - ga: EnjsFunction, - nb: string, - gb: EnjsFunction -): EnjsFunction { - return pairs([ - { nom: na, get: ga }, - { nom: nb, get: gb }, - ]); -}; - -const bucwut = function (opts: EnjsFunction[]): EnjsFunction { - return function (noun) { - for (let i = 0; i < opts.length; i++) { - try { - const res = opts[i](noun); - return res; - } catch (e) { - continue; - } - } - throw new Error("bucwut: no matches"); - }; -}; -const array = function (item: EnjsFunction): (n: Noun) => Json[] { - return function (noun) { - let a: Json[] = []; - while (noun instanceof Cell) { - a.push(item(noun.head)); - noun = noun.tail; - } - return a; - }; -}; - -const tree = function (item: EnjsFunction): (n: Noun) => Json[] { - return function (noun) { - let a: Json[] = []; - if (noun instanceof Cell) { - if (!(noun.tail instanceof Cell)) { - throw new Error("tree: malformed"); - } - a = [ - ...a, - item(noun.head), - ...tree(item)(noun.tail.head), - ...tree(item)(noun.tail.tail), - ]; - } - return a; - }; -}; - -const cord = function (noun: Noun): string { - if (!(noun instanceof Atom)) { - throw new Error(`cord: noun not atom ${noun.toString()}`); - } - return Atom.cordToString(noun); -}; - -const numb = function (noun: Noun): number | string { - if (!(noun instanceof Atom)) { - throw new Error("numb: noun not atom"); - } - if (bitLength(noun.number) <= 32) { - return Number(noun.number); - } else { - return noun.number.toString(); - } -}; - -const numb32 = function (noun: Noun): number { - if (!(noun instanceof Atom)) { - throw new Error("numb32: noun not atom"); - } - if (bitLength(noun.number) > 32) { - throw new Error("numb32: number too big"); - } - return Number(noun.number); -} - -const numbString = function (noun: Noun): string { - if (!(noun instanceof Atom)) { - throw new Error("numbString: noun not atom"); - } - return noun.number.toString(); -} - -const loob = function (noun: Noun): boolean { - return noun.loob(); -}; - -const nill = function (noun: Noun): null { - if (!(noun instanceof Atom && noun.number === 0n)) { - throw new Error("nill: not null"); - } - return null; -}; - -const path = array(cord); - -const enjs = { - frond, - pairs, - pair, - array, - loob, - tree, - cord, - numb, - numb32, - numbString, - path, - bucwut, - nill, -}; - -// - -function list(args: any[]): Noun { - if (args.length === 0) return Atom.zero; - return dwim([...args, Atom.zero]); -} - -type Atomizable = number | string | Atom; - -// "Do What I Mean" -function dwim(a: number): Atom; -function dwim(a: string): Atom; -function dwim(a: Atomizable, b: Atomizable): Cell; -function dwim( - a: Atomizable, - b: Atomizable, - c: Atomizable -): Cell>; -function dwim(a: Atomizable, ...b: any[]): Cell>; -function dwim(...a: any[]): Cell; -// implementation -function dwim(...args: any[]): Noun { - const n = args.length === 1 ? args[0] : args; - if (n instanceof Atom || n instanceof Cell) return n; - if (typeof n === "number") { - return Atom.fromInt(n); - } else if (typeof n === "string") { - return Atom.fromCord(n); - } else if (Array.isArray(n)) { - if (n.length < 2) { - return dwim(n[0]); - } - const head = dwim(n[n.length - 2]); - const tail = dwim(n[n.length - 1]); - let cel = new Cell(head, tail); - for (var j = n.length - 3; j >= 0; --j) { - cel = new Cell(dwim(n[j]), cel); - } - return cel; - } else if (n === null) { - return Atom.zero; - } - // objects, undefined, etc - console.error("what do you mean??", typeof n, JSON.stringify(n)); - throw new Error('dwim, but meaning unclear'); -} - -const dejs = { - nounify: dwim, - dwim, - list, -} - +import { enjs } from "./noun-enjs"; +import { dejs } from "./noun-dejs"; +const dwim = dejs.dwim; export { enjs, dejs, dwim }; diff --git a/src/noun-std.ts b/src/noun-std.ts new file mode 100644 index 0000000..50bf985 --- /dev/null +++ b/src/noun-std.ts @@ -0,0 +1,157 @@ +import { Atom, Cell } from "./noun"; +import type { Noun } from "./noun"; +import { dwim } from "./noun-dejs"; + +// +dor: depth order +export function dor(a: Noun, b: Noun): boolean { + // ?: =(a b) & + if (a.equals(b)) return true; + // ?. ?=(@ a) + if (a.isCell()) { + // ?: ?=(@ b) | + if (b.isAtom()) return false; + // ?: =(-.a -.b) + if (a.head.equals(b.head)) + // $(a +.a, b +.b) + return dor(a.tail, b.tail); + // $(a -.a, b -.b) + return dor(a.head, b.head); + } + // ?. ?=(@ b) & + if (b.isCell()) return true; + // (lth a b) + return (a < b); +} + +// +gor: mug hash order, collisions fall back to +dor +export function gor(a: Noun, b: Noun): boolean { + // =+ [c=(mug a) d=(mug b)] + const c = a.mug(); + const d = b.mug(); + // ?: =(c d) + if (c === d) + // (dor a b) + return dor(a, b); + // (lth c d) + return c < d; +} + +// +mor: double mug hash order, collisions fall back to +dor +export function mor(a: Noun, b: Noun): boolean { + // =+ [c=(mug (mug a)) d=(mug (mug b))] + const c = Atom.fromInt(a.mug()).mug(); + const d = Atom.fromInt(b.mug()).mug(); + // ?: =(c d) + if (c === d) + // (dor a b) + return dor(a, b); + // (lth c d) + return c < d; +} + +// isSet: check for set with >0 entries, ?=([* * *]) +export function isSet(a: Noun): a is Cell> { + return a.isCell() && a.tail.isCell(); +} + +// +put:in: set insertion +export function putIn(a: Noun, b: Noun): Noun { + // ?~ a + // [b ~ ~] + if (a.equals(Atom.zero)) { + return dwim(b, null, null); + } + if (!isSet(a)) { + throw new Error('malformed set'); + } + // ?: =(b n.a) + // a + if (b.equals(a.head)) { + return a; + } + // ?: (gor b n.a) + if (gor(b, a.head)) { + // =+ c=$(a l.a) + const c = putIn(a.tail.head, b); + // ?> ?=(^ c) + if (!isSet(c)) { + throw new Error('implementation error'); + } + // ?: (mor n.a n.c) + // a(l c) + if (mor(a.head, c.head)) { + return dwim(a.head, c, a.tail.tail); + } + // c(r a(l r.c)) + return dwim(c.head, c.tail.head, [a.head, c.tail.tail, a.tail.tail]); + } + // =+ c=$(a r.a) + const c = putIn(a.tail.tail, b); + // ?> ?=(^ c) + if (!isSet(c)) { + throw new Error('implementation error'); + } + // ?: (mor n.a n.c) + // a(r c) + if (mor(a.head, c.head)) { + return dwim(a.head, a.tail.head, c); + } + // c(l a(r l.c)) + return dwim(c.head, [a.head, a.tail.head, c.tail.head], c.tail.tail); +} + +// isMap: check for map with >0 entries, ?=([[* *] * *]) +export function isMap(a: Noun): a is Cell, Cell> { + return a.isCell() && a.head.isCell() && a.tail.isCell(); +} + +// +put:by: map insertion +export function putBy(a: Noun, b: Noun, c: Noun): Noun { + // ?~ a + // [[b c] ~ ~] + if (a.equals(Atom.zero)) { + return dwim([b, c], null, null); + } + if (!isMap(a)) { + throw new Error('malformed map'); + } + // ?: =(b p.n.a) + if (b.equals(a.head.head)) { + // ?: =(c q.n.a) + // a + if (c.equals(a.head.tail)) { + return a; + } + // a(n [b c]) + return dwim([b, c], a.tail); + } + // ?: (gor b p.n.a) + if (gor(b, a.head.head)) { + // =+ d=$(a l.a) + const d = putBy(a.tail.head, b, c); + // ?> ?=(^ d) + if (!isMap(d)) { + throw new Error('implementation error'); + } + // ?: (mor p.n.a p.n.d) + // a(l d) + if (mor(a.head.head, d.head.head)) { + return dwim(a.head, d, a.tail.tail); + } + // d(r a(l r.d)) + return dwim(d.head, d.tail.head, [a.head, d.tail.tail, a.tail.tail]); + } + // =+ d=$(a r.a) + const d = putBy(a.tail.tail, b, c); + // ?> ?=(^ d) + if (!isMap(d)) { + throw new Error('implementation error'); + } + // ?: (mor p.n.a p.n.d) + // a(r d) + if (mor(a.head.head, d.head.head)) { + return dwim(a.head, a.tail.head, d); + } + // d(l a(r l.d)) + return dwim(d.head, [a.head, a.tail.head, d.tail.head], d.tail.tail); +} diff --git a/src/noun.ts b/src/noun.ts index d52309b..68f6441 100644 --- a/src/noun.ts +++ b/src/noun.ts @@ -51,6 +51,8 @@ class Atom { constructor(public readonly number: bigint) {} // common methods with Cell + isAtom(): this is Atom { return true; } + isCell(): this is Cell { return false; } pretty(out: string[], hasTail = false): void { if (this.number < 65536n) out.push(this.number.toString(10)); else { @@ -207,6 +209,8 @@ class Cell { private _mug = 0; constructor(public readonly head: TH, public readonly tail: TT, public deep = true) {} // common methods + isAtom(): this is Atom { return false; } + isCell(): this is Cell { return true; } pretty(out: string[], hasTail: boolean): void { if (!hasTail) out.push("["); this.head.pretty(out, false); @@ -266,4 +270,14 @@ class Cell { } type Noun = Atom | Cell; +export function isAtom(a: any): a is Atom { + return a instanceof Atom; +} +export function isCell(a: any): a is Cell { + return a instanceof Cell; +} +export function isNoun(a: any): a is Noun { + return isAtom(a) || isCell(a); +} + export { Atom, Cell, Noun };