diff --git a/bend2/comp.ts b/bend2/comp.ts index 0942902b3..9eb6708d6 100644 --- a/bend2/comp.ts +++ b/bend2/comp.ts @@ -63,7 +63,6 @@ type File = { segs: Seg[]; spins: Seg[]; spun: Map; - marsh: Map; clos: Set; tabs: Map; tails: Map>; @@ -472,12 +471,8 @@ function nat_chk(n) { return n; } -function nat_host(n) { - const int = typeof n === "bigint" || Number.isInteger(n); - if (int && n >= 0 && n <= 2 ** 53) { - return Number(n); - } - return { [Symbol.toPrimitive]() { throw "bend: ${ERRS[5]}"; } }; +function nat_in(n) { + return Number.isInteger(n) && n >= 0 && n <= 2 ** 53 ? n : nat_chk(1 / 0); } function f32_show(x) { @@ -1330,7 +1325,6 @@ function file_new(book: Bend.Book, js: boolean): File { segs: [], spins: [], spun: new Map(), - marsh: new Map(), clos: new Set(), tabs: new Map(), tails: new Map(), @@ -3105,11 +3099,11 @@ function js_def(sc: Scope, k: Name, def: Bend.Def): void { } block(sc, `function ${js_sat(k)}(${params.join(", ")}) {`, () => { if (def_foreign(def)) { - const doms = [...live, tele_unbind(def.T).doms.at(-1)!]; - const xs = params.map((p, i) => - `${js_marshal(doms[i][2], true)}(${p})`); + const K = ty_wnf(tele_unbind(def.T).doms.at(-1)![2]); + const f = params.at(-1); return file_push(sc, `return { $: ${JSON.stringify(Bend.name_key(k)) - }, args: [${xs.slice(0, -1).join(", ")}], kont: ${xs.at(-1)} };`); + }, args: [${params.slice(0, -1).join(", ")}], kont: ${K?.$ === "All" + && host_nat(K.A) ? `(x) => ${f}(nat_in(x))` : f} };`); } if (loop.length === 0) { return js_func(sc, h!, def.T, params); @@ -3133,76 +3127,10 @@ function js_def(sc: Scope, k: Name, def: Bend.Def): void { file_push(sc, ""); } -// A part with a converter of its own waits on one work stack, so no depth -// grows the JS call stack; a Nat or function part converts on the spot. -// Arrays stay in-place; ADT nodes are copied. -function js_marshal(A: HTerm | null, out: boolean): string { +// A host hands a Nat in as a number, which nat_in checks. +function host_nat(A: HTerm | null): boolean { const t = ty_wnf(A); - if (t?.$ === "All") { - const y = js_marshal(t.B(DUMMY), out); - if (!quant_live(t.q)) { - return y; - } - const x = js_marshal(t.A, !out); - return x + y === "" ? y : `((f) => (x) => ${y}(f(${x}(x))))`; - } - const seen = new Set(); - const nat = (u: HTerm | null): boolean => u?.$ === "All" - ? [u.A, u.B(DUMMY)].some((v) => ty_holds(v, nat, seen)) - : u?.$ === "ADT" && u.k === "Nat"; - if (t?.$ !== "ADT" || !ty_holds(t, nat, seen)) { - return ""; - } - if (t.k === "Nat") { - return out ? "BigInt" : "nat_host"; - } - const key = (out ? "out " : "in ") + Bend.term_key(Bend.term_lower(t)); - return memo(FL.marsh, key, () => { - const name = "$0m" + FL.marsh.size; - FL.marsh.set(key, name); - const put = (f: string, a: string, i: string): string => f[0] === "$" - ? `q.push(${f}, ${a}, ${i});` : `${a}[${i}] = ${f}(${a}[${i}]);`; - const body: string[] = []; - if (t.k === "Array") { - const f = js_marshal(t.x[0], out); - if (f === "") return ""; - body.push(`r = v; v.forEach((x, i) => { ${put(f, "v", "i")} });`); - } else { - const cs = (FL.book.tlds[t.k] as Bend.ADT).c; - const arms = cs.map((c) => { - const fs = ctr_live(c, t.x).flatMap(([, n, B]) => { - const f = js_marshal(B, out); - return f === "" ? [] : [put(f, "r", `"${n}"`)]; - }); - return [`case "${Bend.name_key(c.k)}": r = ${fs.length ? "{...v}" - : "v"};`, ...fs, "break;"].join(" "); - }); - const tk = Bend.name_key(t.k); - const tags = cs.map((c) => Bend.name_key(c.k)).join(", "); - body.push("switch (v.$) {", ...arms, - `default: throw "bend: ${tk} has no tag " + v?.$ + " (its tags: ${ - tags}); a tag names its constructor as the" - + " loading file sees it, which a later version will make the same" - + " everywhere (#1105)";`, "}"); - } - FL.spins.push({ ...seg_new("", BOX, ["v"]), lines: [`function ${name}(v, q) {`, - "const own = q === undefined;", "if (own) q = [];", "let r;", ...body, - "if (own) while (q.length) {", - "const i = q.pop(), a = q.pop(); a[i] = q.pop()(a[i], q);", - "}", "return r;", "}", ""] }); - return name; - }); -} - -function js_host(k: Name): string { - const { n, live } = fun_of(k); - const ps = live.map((_, i) => "a" + i); - const xs = live.map(([, , A], i) => `${js_marshal(A, false)}(${ps[i]})`); - const ret = tele_fill(FL.book.tlds[k].T, Array(n).fill(DUMMY)); - const back = live.map(([, , A], i) => - `${js_marshal(A, true)}(${ps[i]});`); - return `(${ps.join(", ")}) => { const r = ${js_marshal(ret, true) - }(run_loop(${js_sat(k)}(${xs.join(", ")}))); ${back.join(" ")} return r; }`; + return t?.$ === "ADT" && t.k === "Nat"; } // A module (for the .bend loader and -o .mjs) roots and exports each @@ -3222,9 +3150,13 @@ export function js_lib(book: Bend.Book, mod = false): string { } const effs = effect_srcs(".js", "a foreign def without a .js import: ") .map((t) => `(() => {\n${t}\n})();\n\n`).join(""); - const lib = outs === null ? "" : `export default {\n${outs.map((k) => - ` ${js_key(Bend.name_key(k))}run_lib(${js_host(k)}, ${ - fun_of(k).lays.length}),`).join("\n")}\n};\n`; + const lib = outs === null ? "" : `export default {\n${outs.map((k) => { + const { live, lays } = fun_of(k); + const ps = live.map((_, i) => "a" + i); + const xs = live.map(([, , A], i) => host_nat(A) ? `nat_in(a${i})` : ps[i]); + return ` ${js_key(Bend.name_key(k))}run_lib((${ps}) => ${js_sat(k)}(${ + xs}), ${lays.length}),`; + }).join("\n")}\n};\n`; const jmps = new Map(); const jmp = (k: Name): boolean => k === CLO_APPLY || memo(jmps, k, () => (jmps.set(k, true), [...FL.tails.get(k) ?? []].some(jmp))); @@ -6061,7 +5993,7 @@ function run_loop(r) { function run_lib(f, n) { return (...a) => a.length < n ? run_lib((...b) => f(...a, ...b), n - a.length) - : f(...a); + : run_loop(f(...a)); } // Effect diff --git a/bend2/effs/now.js b/bend2/effs/now.js index 4b2b0fbff..74e252bd5 100644 --- a/bend2/effs/now.js +++ b/bend2/effs/now.js @@ -2,7 +2,7 @@ // == function io_now() { - return BigInt(Math.floor(performance.now())); + return Math.floor(performance.now()); } io_eff(CID(IO.now), io_now); diff --git a/bend2/main.ts b/bend2/main.ts index 6fd7998bf..6e2df253f 100755 --- a/bend2/main.ts +++ b/bend2/main.ts @@ -5,7 +5,7 @@ // (node --import). A .bend module exports every filled, non-base, non-IO // def, wrapped so a JS caller passes the live arguments, in one call or // curried, and gets a plain value back: a constructor is {$: "Name", field: -// value, ...}, a closure is a function, Nat is BigInt, Bool, String and U32 +// value, ...}, a closure is a function, Nat is a number, Bool, String and U32 // are native. A page bundles through Bun.build with the loader on, since // the bun build CLI takes no plugins. diff --git a/demos/app_win_is_bug_2d/web/main.js b/demos/app_win_is_bug_2d/web/main.js index 100116ae8..5366f3ce3 100644 --- a/demos/app_win_is_bug_2d/web/main.js +++ b/demos/app_win_is_bug_2d/web/main.js @@ -9,7 +9,7 @@ import Game from "../main.bend"; // Map // === -const ROWS = Game.grid(Game.map_h() - 1n).split("\n"); +const ROWS = Game.grid(Game.map_h() - 1).split("\n"); const H = ROWS.length; const W = ROWS[0].length; const WALLS = new Set(); @@ -25,7 +25,7 @@ for (let y = 0; y < H; y++) { // Bend bridge // =========== // The conventions: a constructor is {$: "Name", field: value, ...} (live -// fields only), Nat is BigInt, Bool is a plain boolean. Stepping onto the +// fields only), Nat is a number, Bool is a plain boolean. Stepping onto the // flag's cell is the win: the game sets `won` itself. const one = (act) => ({ $: "Con", head: { $: act }, tail: { $: "Nil" } }); diff --git a/guide/EFFECTS.md b/guide/EFFECTS.md index 6d1c39ab6..1f859a66d 100644 --- a/guide/EFFECTS.md +++ b/guide/EFFECTS.md @@ -80,11 +80,13 @@ allocate it in the run function and free it in `pack`. In `bend2/effs/`, The `.js` file is a plain script, run once in a closure of its own, that registers the effect: `io_eff(CID(Clock.now), clock_now)`. The effect takes the def's arguments as JS values: a `U32` is a number, a `Nat` a -`BigInt`, a `String` a string, a constructor +number, a `String` a string, a constructor `{$: CID(Name), field: value}`, a handle its host value (a descriptor). `CID(Name)` is the constructor's tag, read as on the C side. It returns the answer the same way: `io_done(v)`, `io_fail(code)`, -`io_tup(handle, result)`, `{$: CID(Unit)}`. +`io_tup(handle, result)`, `{$: CID(Unit)}`. A `Nat` answer that is not a +whole number from 0 to 2^53 stops the run; a `Nat` inside an answer goes +unchecked, so the host keeps it in that range. A blocking effect takes one more argument, `k`, and parks with `io_park_on(fd, out, k, more)`: it returns `undefined`, and the loop calls diff --git a/guide/GUIDE.md b/guide/GUIDE.md index 87d0162f9..56f91fcb3 100644 --- a/guide/GUIDE.md +++ b/guide/GUIDE.md @@ -411,8 +411,8 @@ underscores. You can add your own effects the same way. Only the event loop runs them, so proofs, termination and the GPU never touch host code. In the other direction, a JS file may `import Game from "./game.bend"` (with `bend2/main.ts` preloaded), or from the `./game.mjs` that `-o game.mjs` writes, and call every -non-IO def, with constructors as `{$: "Name", field: value}` and `Nat` as -`BigInt`. A value crosses without a copy: an `Array` +non-IO def, with constructors as `{$: "Name", field: value}` and `Nat` as a +number. A value crosses without a copy: an `Array` argument is the caller's own array, updated in place, so copy it first if you keep it. diff --git a/tests/io/marshal_array_depth.bend b/tests/io/marshal_array_depth.bend index dbbdca556..c80e84733 100644 --- a/tests/io/marshal_array_depth.bend +++ b/tests/io/marshal_array_depth.bend @@ -1,6 +1,6 @@ -# io (io_marshal_array_depth): nested Array marshalling must use a -# bounded host stack in both directions, preserve every constructor and -# BigInt Nat field, and return the modified deep leaf to Bend. +# io (io_marshal_array_depth): a nested Array crosses to the host +# and back at depth 20000, keeping every constructor and Nat field, and +# the host's change to the deep leaf reaches Bend. import Base type Tree is Type: diff --git a/tests/io/marshal_array_depth.js b/tests/io/marshal_array_depth.js index 044ae7a2a..79ef42d37 100644 --- a/tests/io/marshal_array_depth.js +++ b/tests/io/marshal_array_depth.js @@ -1,5 +1,5 @@ function exchange(tree) { - const input = 123456789n; + const input = 123456789; let node = tree; let depth = 0; while (node.$ === CID(Node)) { @@ -12,7 +12,7 @@ function exchange(tree) { if (node.$ !== CID(Leaf) || node.n !== input || depth !== 20000) { throw new Error("invalid Leaf at depth " + depth); } - node.n = 987654321n; + node.n = 987654321; return tree; } diff --git a/tests/io/marshal_nat_bigint.bend b/tests/io/marshal_nat_bigint.bend deleted file mode 100644 index 45d4e1fc2..000000000 --- a/tests/io/marshal_nat_bigint.bend +++ /dev/null @@ -1,56 +0,0 @@ -# io (io_marshal_nat_bigint): the seam has ONE value model -- a Nat -# is the effect's BigInt, trusted inbound in every lane. This golden -# pinned the dead two-model refusal ('a non-negative Nat answer'): -# the interpreter refused -3n where compiled JS ran the raw negative -# on. Now both lanes run the same text, so the hazard is UNIFORM and -# observable: the boundary answers (0, 5) land canonically (zero, -# succ), and a negative BigInt lands raw and matches as Succ -- both -# lanes print the same three tags byte-identically, which is the law -# this golden guards. -import Base - -law zero: - IO(Nat) - -def zero(): - import "./marshal_nat_bigint.js" - -law five: - IO(Nat) - -def five(): - import "./marshal_nat_bigint.js" - -law neg: - IO(Nat) - -def neg(): - import "./marshal_nat_bigint.js" - -law tag_of: - for n: Nat - String - -def tag_of(n): - match n: - case 0n: - "zero" - case 1n+p: - "succ" - -law main: - IO(U32) - -def main(): - do IO: - a : Nat <- zero() - b : Nat <- five() - c : Nat <- neg() - u : Unit <- IO.print(tag_of(a)) - v : Unit <- IO.print(tag_of(b)) - w : Unit <- IO.print(tag_of(c)) - return 0 - -#|zero -#|succ -#|succ diff --git a/tests/io/marshal_nat_number.bend b/tests/io/marshal_nat_number.bend new file mode 100644 index 000000000..5a9ecd6f0 --- /dev/null +++ b/tests/io/marshal_nat_number.bend @@ -0,0 +1,42 @@ +# io (io_marshal_nat_number): a Nat crosses the effect seam as the +# host's number, unconverted, in every lane: the answers 0 and 5 land +# as zero and a successor; the interpreter and compiled JS print the same +# two tags. +import Base + +law zero: + IO(Nat) + +def zero(): + import "./marshal_nat_number.js" + +law five: + IO(Nat) + +def five(): + import "./marshal_nat_number.js" + +law tag_of: + for n: Nat + String + +def tag_of(n): + match n: + case 0n: + "zero" + case 1n+p: + "succ" + +law main: + IO(U32) + +def main(): + do IO: + a : Nat <- zero() + b : Nat <- five() + u : Unit <- IO.print(tag_of(a)) + v : Unit <- IO.print(tag_of(b)) + return 0 + +#|zero +#|succ diff --git a/tests/io/marshal_nat_bigint.js b/tests/io/marshal_nat_number.js similarity index 52% rename from tests/io/marshal_nat_bigint.js rename to tests/io/marshal_nat_number.js index c62fd1ea8..c8a6e03e7 100644 --- a/tests/io/marshal_nat_bigint.js +++ b/tests/io/marshal_nat_number.js @@ -1,13 +1,9 @@ function zero() { - return 0n; + return 0; } function five() { - return 5n; -} -function neg() { - return -3n; + return 5; } io_eff(CID(zero), zero); io_eff(CID(five), five); -io_eff(CID(neg), neg); diff --git a/tests/io/marshal_nat_pair.bend b/tests/io/marshal_nat_pair.bend index b336b4c8b..d7c989a65 100644 --- a/tests/io/marshal_nat_pair.bend +++ b/tests/io/marshal_nat_pair.bend @@ -1,8 +1,6 @@ # io (io_marshal_nat_pair): a Nat in a pair crosses the effect seam as -# a BigInt at any depth. A pair's second half is a type family, and the -# marshal walk took it for no Nat: it found U32 & Nat's through the -# Sigma's fields, but in U32 & U32 & Nat it had seen Sigma already, so -# that Nat crossed as a number. Both effects answer the BigInt's typeof. +# a number at any depth, the same as a Nat on its own. Both effects +# answer its typeof. import Base law top: @@ -29,5 +27,5 @@ def main(): u : Unit <- IO.print(a) IO.print(b) -#|bigint -#|bigint +#|number +#|number diff --git a/tests/io/marshal_native_forms.bend b/tests/io/marshal_native_forms.bend index 044343ccb..93d624488 100644 --- a/tests/io/marshal_native_forms.bend +++ b/tests/io/marshal_native_forms.bend @@ -1,5 +1,5 @@ # io (io_marshal_native_forms): the native forms cross the effect -# seam canonically, both ways -- a Nat as a BigInt, a Bool as a +# seam canonically, both ways -- a Nat as a number, a Bool as a # boolean, a Cmp by its tag through the generic ctor lane. The legs # escalate as io_marshal_char_type does: direct answers, an answer # inside a Maybe field, a round trip through an argument -- and both diff --git a/tests/io/marshal_native_forms.js b/tests/io/marshal_native_forms.js index 6389ae81a..c4a8a9f52 100644 --- a/tests/io/marshal_native_forms.js +++ b/tests/io/marshal_native_forms.js @@ -1,14 +1,14 @@ function five() { - return 5n; + return 5; } function zero() { - return 0n; + return 0; } function wrap() { - return {$: CID(Some), value: 9n}; + return {$: CID(Some), value: 9}; } function dub(n) { - return n * 2n; + return n * 2; } function flip(b) { return !b; diff --git a/tests/io/marshal_tag_unknown.bend b/tests/io/marshal_tag_unknown.bend deleted file mode 100644 index 76e6f55be..000000000 --- a/tests/io/marshal_tag_unknown.bend +++ /dev/null @@ -1,35 +0,0 @@ -# io (io_marshal_tag_unknown): a value the host hands in converts by -# its constructor tag, so a tag its type lacks fail-stops at the seam, -# naming the tag and the type's tags; it passed through unconverted, and -# its BigInt Nat failed far off (#1105) -import Base - -type Box is Data: - Box{n: Nat} - -law good: - IO(Box) - -def good(): - import "./marshal_tag_unknown.js" - -law bad: - IO(Box) - -def bad(): - import "./marshal_tag_unknown.js" - -def n_of(b: Box) -> Nat: - Box{n} = b - n - -def main() -> IO(Unit): - do IO: - a : Box <- good() - u : Unit <- IO.print(Nat.show(Nat.add(n_of(a), 1n))) - b : Box <- bad() - IO.print(Nat.show(Nat.add(n_of(b), 1n))) - -#|6 -#|bend: Box has no tag Bogus (its tags: Box); a tag names its constructor as the loading file sees it, which a later version will make the same everywhere (#1105) -#|exit 1 diff --git a/tests/io/marshal_tag_unknown.js b/tests/io/marshal_tag_unknown.js deleted file mode 100644 index 9b6019288..000000000 --- a/tests/io/marshal_tag_unknown.js +++ /dev/null @@ -1,9 +0,0 @@ -function good() { - return {$: CID(Box), n: 5n}; -} -function bad() { - return {$: "Bogus", n: 5n}; -} - -io_eff(CID(good), good); -io_eff(CID(bad), bad); diff --git a/tests/io/nat_host_bounds.bend b/tests/io/nat_host_bounds.bend index f893b54e1..02838b206 100644 --- a/tests/io/nat_host_bounds.bend +++ b/tests/io/nat_host_bounds.bend @@ -1,6 +1,6 @@ # io (io_nat_host_bounds): a foreign effect answers Nats at and past the # immediate cap: 2^48-1, 2^48 and 2^53. The C lane takes each word as it -# is; the JS lane takes each exactly, so the unchecked operations (a +# is; the JS lane takes each number exactly, so the unchecked operations (a # word, a quotient, a remainder) print the same on both lanes. import Base diff --git a/tests/io/nat_host_bounds.js b/tests/io/nat_host_bounds.js index 000426de0..b210e0cb1 100644 --- a/tests/io/nat_host_bounds.js +++ b/tests/io/nat_host_bounds.js @@ -1,5 +1,5 @@ function get(k) { - return [281474976710655n, 281474976710656n, 9007199254740992n][k]; + return [281474976710655, 281474976710656, 9007199254740992][k]; } io_eff(CID(get), get); diff --git a/tests/io/nat_host_export.bend b/tests/io/nat_host_export.bend new file mode 100644 index 000000000..6524bd816 --- /dev/null +++ b/tests/io/nat_host_export.bend @@ -0,0 +1,14 @@ +# An exported def takes a Nat as a whole number from 0 to 2^53: a +# negative, fractional, BigInt or larger argument throws the Nat error, +# in one call or curried, and a good one still runs. +import Base + +def Nat.host_export() -> IO(U32): + import "./nat_host_export.js" + +def main() -> IO(Unit): + do IO: + value : U32 <- Nat.host_export() + IO.print(U32.show(value)) + +#|0 diff --git a/tests/io/nat_host_export.js b/tests/io/nat_host_export.js new file mode 100644 index 000000000..e4d388a14 --- /dev/null +++ b/tests/io/nat_host_export.js @@ -0,0 +1,39 @@ +function nat_host_export() { + const fs = require("node:fs"); + const path = require("node:path"); + const child = require("node:child_process"); + const dir = fs.mkdtempSync(path.join(process.cwd(), ".bend-nat-export-")); + try { + const input = path.join(dir, "lib.bend"); + const output = path.join(dir, "lib.mjs"); + fs.writeFileSync(input, `import Base + +def inc(n: Nat) -> Nat: + Nat.add(n, 1n) + +def add(a: Nat, b: Nat) -> Nat: + Nat.add(a, b) +`); + const built = child.spawnSync(process.execPath, + [path.resolve("bend2/main.ts"), input, "-o", output], { + encoding: "utf8", + env: { ...process.env, HOME: dir, BEND_ORIGIN: "http://127.0.0.1:9" }, + }); + if (built.status !== 0) { + return 1; + } + const lib = require(output).default; + const err = "bend: a Nat past the largest immediate 2^48-1"; + const bad = [() => lib.inc(-1), () => lib.inc(1.5), () => lib.inc(1n), + () => lib.inc(2 ** 53 + 2), () => lib.add(1)(-1)]; + if (bad.some((f) => { try { f(); } catch (e) { return e !== err; } + return true; })) { + return 2; + } + return lib.inc(41) === 42 && lib.add(2)(3) === 5 ? 0 : 3; + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } +} + +io_eff(CID(Nat.host_export), nat_host_export); diff --git a/tests/io/nat_host_neg.bend b/tests/io/nat_host_neg.bend index 80ec5f1b0..40b180fbe 100644 --- a/tests/io/nat_host_neg.bend +++ b/tests/io/nat_host_neg.bend @@ -1,8 +1,7 @@ # io (io_nat_host_neg): a foreign effect answers a negative Nat. The C -# lane reads it as a word past the cap, which matches as a successor and -# fail-stops when Nat.show computes with it. The JS lane cannot hold that -# word exactly: it matches as a successor too and fail-stops at its -# first arithmetic use, with the same message and exit code. +# lane reads it as a word past the cap and fail-stops when Nat.show +# computes with it; the JS lane rejects it as the answer comes in. Both +# stop with the same message and exit code. import Base law get: @@ -12,21 +11,8 @@ def get(): import "./nat_host_neg.c" import "./nat_host_neg.js" -def tag(n: Nat) -> String: - match n: - case 0n: - "zero" - case 1n+p: - "succ" - -def report(+n: Nat) -> IO(Unit): - do IO: - a : Unit <- IO.print(tag(n)) - IO.print(Nat.show(n)) - def main() -> IO(Unit): - IO.bind(Nat, Unit, get(), n => report(n)) + IO.bind(Nat, Unit, get(), n => IO.print(Nat.show(n))) -#|succ #|bend: a Nat past the largest immediate 2^48-1 #|exit 1 diff --git a/tests/io/nat_host_neg.js b/tests/io/nat_host_neg.js index adc0a8720..4edf78595 100644 --- a/tests/io/nat_host_neg.js +++ b/tests/io/nat_host_neg.js @@ -1,5 +1,5 @@ function get() { - return -3n; + return -3; } io_eff(CID(get), get);