diff --git a/bend2/comp.ts b/bend2/comp.ts index fb624df71..6907c56d1 100644 --- a/bend2/comp.ts +++ b/bend2/comp.ts @@ -57,7 +57,8 @@ type TLD = Bend.ADT | Def; type Book = Omit & { tlds: Record }; -type Src = { refs: Set; deps: Set; flat: boolean }; +type Src = { refs: Set; deps: Set; flat: boolean; + loop: boolean }; type Carb = { book: Book; @@ -148,6 +149,22 @@ const NATIVE_DIE = " does not match the native format of its type"; // The term nodes one segment may gain by folding calls at compile time. const FOLD_FUEL = 8192; +// The elements one leaf of a lowered Array.map walks in sequence, as a +// power of two: up to 2^12 under a cheap callback (straight-line C: +// intrinsics, constructors and defs that neither loop nor fork), where a +// fork costs more than the elements it would split; up to 2^3 under any +// other, and only once the walk still has 2^6 leaves to fork over, so a +// small array of expensive callbacks splits down to its elements. +const MAP_LEAF_CHEAP = 12; +const MAP_LEAF_DEAR = 3; +const MAP_SPLIT_DEAR = 6; + +// The raw block operations the lowered Array.map is written over. +const MAP_OPS = ["src", "depth", "dst", "split", "leaf", "cnt", "mid", "take", + "drop", "put", "join", "close"].map((k) => "Array.map." + k); + +const MAP_JS: Gen = () => die("an Array.map operation outside the C lane"); + // A native with this many lines or more is a call on both lanes: the // device inlines every native into every caller (hvm5 under a bang: 32 s // of Metal compile, 2.6 s so); at 128 raytrace lost 31% on PAR-CPU. @@ -319,6 +336,54 @@ const OPERATIONS: Record = Object.setPrototypeOf({ call: true, JS: "{$: \"Tuple\", fst: $0, snd: $0.slice()}", }, + array_map_src: { + C: "$0", + JS: MAP_JS, + }, + array_map_depth: { + call: true, + JS: MAP_JS, + }, + array_map_dst: { + call: true, + JS: MAP_JS, + }, + array_map_split: { + C: "($0 - ($0 > $2 + $1 ? $1 : $0 > $2 ? $0 - $2 : 0))", + JS: MAP_JS, + }, + array_map_leaf: { + C: "(1ull << ($0 > $2 + $1 ? $1 : $0 > $2 ? $0 - $2 : 0))", + JS: MAP_JS, + }, + array_map_cnt: { + C: "(err_seen(e.mem) ? 0 : $0)", + JS: MAP_JS, + }, + array_map_mid: { + C: "($0 + ($2 << $1))", + JS: MAP_JS, + }, + array_map_take: { + call: true, + JS: MAP_JS, + }, + array_map_drop: { + call: true, + JS: MAP_JS, + }, + array_map_put: { + call: true, + JS: MAP_JS, + }, + array_map_join: { + C: "((void)$0, $1)", + JS: MAP_JS, + }, + array_map_close: { + C: "(blk_free(e, $0), (void)$2, $1)", + JS: MAP_JS, + }, }, null); // Optimized @@ -611,6 +676,8 @@ const FOLDS: Map = new Map(); const FLATS: Map = new Map(); +const CHEAPS: Map = new Map(); + const SIGS: Map = new Map(); const BRWS: Map = new Map(); @@ -1421,9 +1488,10 @@ function def_body(cb: Carb, k: Bend.Name): TLD | undefined { // each one's source summary (SRCS): what it refers to, what it calls (a // reference used as a value is no call; Clo.apply is never flat), and // whether it is flat: no fork, no bang call, self-calls in tail position. -function carb_book(src: Bend.Book, roots: Bend.Name[]): Carb { +function carb_book(src: Bend.Book, roots: Bend.Name[], + lower = false): Carb { book_owned(src); - [TELES, SRCS, NODES, LAYS, CYCLES, FLATS, SIGS, BRWS].forEach((m) => + [TELES, SRCS, NODES, LAYS, CYCLES, FLATS, CHEAPS, SIGS, BRWS].forEach((m) => m.clear()); LOCAL.clear(); for (const [k, tld] of Object.entries(src.tlds)) { @@ -1440,45 +1508,71 @@ function carb_book(src: Bend.Book, roots: Bend.Name[]): Carb { own: new Set(), lend: new Set(), }; - for (const queue = roots.slice(); queue.length > 0;) { - const d = queue.shift() as Bend.Name; - if (SRCS.has(d)) { - continue; - } - memo_gc(); - const tld = def_body(cb, d); - const own: Src = { refs: new Set(), deps: new Set(), flat: done_live(tld) }; - SRCS.set(d, own); - for (const x of tld?.$ === "ADT" ? tld.c : tld ? [tld] : []) { - queue.push(...type_adts(cb, x.T)); - } - if (!done_live(tld)) { - continue; - } - term_any(cb, tld.h as HTerm, (s, tail) => { - if (s.$ === "Ann") { - queue.push(...type_adts(cb, s.T)); + const walk = (queue: Bend.Name[]): void => { + while (queue.length > 0) { + const d = queue.shift() as Bend.Name; + if (SRCS.has(d)) { + continue; } - if (s.$ === "Ref") { - if (s.b) { - cb.bangs.add(s.k); - } - if (intr_of(cb, s.k) === undefined) { - own.refs.add(s.k); - cb.sites.set(s.k, (cb.sites.get(s.k) ?? 0) + 1); - } + memo_gc(); + const tld = def_body(cb, d); + const own: Src = { refs: new Set(), deps: new Set(), flat: done_live(tld), + loop: false }; + SRCS.set(d, own); + for (const x of tld?.$ === "ADT" ? tld.c : tld ? [tld] : []) { + queue.push(...type_adts(cb, x.T)); } - const ck = call_kind(cb, s); - if (ck !== null && ck.k !== d) { - own.deps.add(ck.k); + if (!done_live(tld)) { + continue; } - if ((s.$ === "Let" && s.k.length >= 2) - || (ck !== null && (ck.bang === true || (ck.k === d && !tail)))) { - own.flat = false; + term_any(cb, tld.h as HTerm, (s, tail) => { + if (s.$ === "Ann") { + queue.push(...type_adts(cb, s.T)); + } + if (s.$ === "Ref") { + if (s.b) { + cb.bangs.add(s.k); + } + if (intr_of(cb, s.k) === undefined) { + own.refs.add(s.k); + cb.sites.set(s.k, (cb.sites.get(s.k) ?? 0) + 1); + } + } + const ck = call_kind(cb, s); + if (ck !== null && ck.k !== d) { + own.deps.add(ck.k); + } + if (ck !== null && ck.k === d) { + own.loop = true; + } + if ((s.$ === "Let" && s.k.length >= 2) + || (ck !== null && (ck.bang === true || (ck.k === d && !tail)))) { + own.flat = false; + } + return false; + }); + queue.push(...own.refs); + } + }; + walk(roots.slice()); + // The C lane's Array.map instances are lowered once every def they + // reach is summarized (the leaf size reads the callback's defs), the + // old body's sites given back, and the new body and its workers + // summarized in turn. + for (const k of lower ? [...SRCS.keys()] : []) { + const old = cb.book.tlds[k]; + const tld = map_lower(cb, k, old); + if (tld === old || old?.$ !== "Def") { + continue; + } + term_any(cb, old.h as HTerm, (s) => { + if (s.$ === "Ref" && intr_of(cb, s.k) === undefined) { + cb.sites.set(s.k, (cb.sites.get(s.k) ?? 1) - 1); } return false; }); - queue.push(...own.refs); + SRCS.delete(k); + walk([k]); } return cb; } @@ -1512,6 +1606,17 @@ function flat_of(k: Bend.Name): boolean { }); } +// A def is cheap when it is flat, never calls itself, and every def it +// calls is: its body is straight-line C, a bounded step per call. +function cheap_of(k: Bend.Name): boolean { + return memo(CHEAPS, k, () => { + const own = SRCS.get(k); + CHEAPS.set(k, false); + return own !== undefined && flat_of(k) && !own.loop + && [...own.deps].every(cheap_of); + }); +} + // Done // ==== @@ -2228,7 +2333,9 @@ function emit_intr(fl: File, it: Intr, x: HTerm, } if (it.call === true && it.C === undefined) { ty_adt(fl.book, m.all[0]) ?? die("an open Array element type"); - return arr_op(fl, op, lay_of(fl.book, m.all[0]), args); + const el = lay_of(fl.book, m.all[0]); + return op.startsWith("array_map_") ? map_op(fl, op, el, args) + : arr_op(fl, op, el, args); } const ws = args.map((v) => (val_own(fl, v), val_word(v))); if (Array.isArray(it.C)) { @@ -2822,6 +2929,264 @@ function compile_def(fl: File, k: Bend.Name, tld: Def): void { emit_body(fl, tld.h as HTerm, tld.T, [], vals, null); } +// Map +// === +// Array.map~k, the minted map instance, is written over the tree the +// language presents: a match on ANode splits the block (blk_half copies +// each half) and ANode{l, r} joins it (blk_node copies both), so a map over +// n elements copies O(n log n) words. The C lane lowers the instance to a +// range walk over one source and one destination instead. The entry takes +// the array as a raw word, allocates the destination once and walks the +// whole range; the walk forks on halves down to a leaf (MAP_LEAF_CHEAP, +// MAP_LEAF_DEAR), and the leaf moves each element out, applies the +// callback and writes its result straight into its slot. The workers are +// defs the emitter compiles like any other, so their fork is the fork it +// always emits (tasks, or frames when the lane winds back) and a callback +// that needs a continuation, or forks itself, gets the one a let of its +// call gets. A range is words nobody owns: the entry frees the source +// shallow once every element has moved. The definition, its signature, the +// interpreter and the JS lane are unchanged. + +type MapShape = { Tin: HTerm; Tout: HTerm; ret: HTerm; leaf: Of<"Lam"> }; + +function map_lower(cb: Carb, k: Bend.Name, + tld: TLD | undefined): TLD | undefined { + if (tld?.$ !== "Def" || !/^Array\.map~\d+$/.test(k)) { + return tld; + } + const shape = map_shape(cb, k, tld); + if (shape === null) { + return tld; + } + map_defs(cb); + const { Tin, Tout, ret, leaf } = shape; + const nat = Bend.Ref("Nat"); + const lam = (n: string, f: (x: HTerm) => HTerm): HTerm => Bend.Lam(n, 0, f); + const call = (n: string, ...xs: HTerm[]): HTerm => + xs.reduce((f, x) => Bend.App(f, x), Bend.Ref(n) as HTerm); + const bind = (n: string, T: HTerm, v: HTerm, + b: (x: HTerm) => HTerm): HTerm => + Bend.Let([n], [0], [Bend.Ann(v, T)], (xs: HTerm[]) => b(xs[0])); + const arm = (n: string, f: (x: HTerm) => HTerm, T: HTerm = nat): HTerm => + Bend.Ann(lam(n, (x) => Bend.Ann(f(x), T)), map_words([n], T)); + const fb = (x: HTerm): HTerm => + (Bend.term_strip(Bend.term_apply(leaf, x)) as Of<"Ctr">).x[0]; + const xo = term_open(leaf); + const field = fb(xo.ps[0]); + const used = rest_use(cb, [field], xo.ps[0]) > 0; + const dear = term_any(cb, field, (s) => { + const ck = call_kind(cb, s); + return ck !== null && !cheap_of(ck.k); + }); + const lit = (n: number): HTerm => Array(n).fill(0) + .reduce((t: HTerm) => Bend.Ctr("Succ", [t]), Bend.Ctr("Zero", [])); + const gl = lit(dear ? MAP_LEAF_DEAR : MAP_LEAF_CHEAP); + const sl = lit(dear ? MAP_SPLIT_DEAR : 0); + const w = k + ".w"; + const sq = k + ".seq"; + const tp = k + ".top"; + // seq(sa, da, ix, n): n elements from ix, each moved out (or dropped when + // the callback ignores it), mapped and written; the index after them. + const step = (sa: HTerm, da: HTerm, ix: HTerm, p: HTerm): HTerm => used + ? bind("x", Tin, call("Array.map.take", Tin, sa, ix), (x) => + bind("y", Tout, fb(x), (y) => call(sq, sa, da, + call("Array.map.put", Tout, da, ix, y), p))) + : bind("y", Tout, fb(DUMMY), (y) => call(sq, sa, da, + call("Array.map.put", Tout, da, call("Array.map.drop", Tin, sa, ix), y), + p)); + const sqH = lam("sa", (sa) => lam("da", (da) => lam("ix", (ix) => + Bend.Mat("Zero", Bend.Ann(ix, nat), + Bend.Mat("Succ", arm("p", (p) => step(sa, da, ix, p)), Bend.Efq()))))); + // w(sa, da, ix, lf, dp): 2^dp leaves of lf elements from ix, forked on + // halves down to the leaf; the index after them, which a join takes + // from its high half once both are done. top is the same walk over the + // whole range, closed: the source freed shallow around the finished + // destination, in the leaf or at the join. + const fork = (sa: HTerm, da: HTerm, ix: HTerm, lf: HTerm, dp: HTerm, + j: (a: HTerm, b: HTerm) => HTerm): HTerm => Bend.Let(["a", "b"], [0, 0], + [Bend.Ann(call(w, sa, da, ix, lf, dp), nat), + Bend.Ann(call(w, sa, da, call("Array.map.mid", ix, dp, lf), lf, dp), + nat)], (xs: HTerm[]) => j(xs[0], xs[1])); + const close = (sa: HTerm, da: HTerm, u: HTerm): HTerm => + Bend.Ann(call("Array.map.close", Tout, sa, da, u), ret); + const walk = (top: boolean): HTerm => lam("sa", (sa) => lam("da", (da) => + lam("ix", (ix) => lam("lf", (lf) => { + const run = call(sq, sa, da, ix, call("Array.map.cnt", lf)); + return Bend.Mat("Zero", top ? bind("u", nat, run, (u) => + close(sa, da, u)) : Bend.Ann(run, nat), + Bend.Mat("Succ", arm("dp", (dp) => fork(sa, da, ix, lf, dp, (a, b) => { + const hi = call("Array.map.join", a, b); + return top ? close(sa, da, hi) : hi; + }), top ? ret : nat), Bend.Efq())); + })))); + const wH = walk(false); + const tpH = walk(true); + const h = lam("a", (a) => + bind("sa", nat, call("Array.map.src", Tin, a), (sa) => + bind("dp", nat, call("Array.map.depth", Tin, sa), (dp) => + bind("da", nat, call("Array.map.dst", Tout, dp), (da) => + Bend.Ann(call(tp, sa, da, lit(0), call("Array.map.leaf", dp, gl, sl), + call("Array.map.split", dp, gl, sl)), ret))))); + const ws = ["sa", "da", "ix", "lf", "dp"]; + cb.book.tlds[sq] = { $: "Def", n: 4, x: 0, T: map_words(["sa", "da", "ix", + "n"], nat), v: sqH, h: sqH }; + cb.book.tlds[w] = { $: "Def", n: 5, x: 0, T: map_words(ws, nat), v: wH, + h: wH }; + cb.book.tlds[tp] = { $: "Def", n: 5, x: 0, T: map_words(ws, ret), v: tpH, + h: tpH }; + const out: Def = { ...tld, h }; + cb.book.tlds[k] = out; + return out; +} + +// A telescope of Nat words ending in `ret`. +function map_words(ks: string[], ret: HTerm): HTerm { + return ks.reduceRight((B: HTerm, k) => + Bend.All(Bend.Lone(), k, 0, Bend.Ref("Nat"), () => B), ret); +} + +// The instance qualifies when it maps one Array of a datatype to another +// and its body is the tree recursion itself: an ALeaf arm that rebuilds a +// leaf from one value, and an ANode arm that forks the instance on both +// halves and joins them in order. +function map_shape(cb: Carb, k: Bend.Name, tld: Def): MapShape | null { + const { doms, ret } = tele_unbind(cb.book, tld.T); + const adtIn = ty_adt(cb.book, doms[0]?.[2] ?? null); + const adtOut = ty_adt(cb.book, ret); + if (doms.length !== 1 || tld.n !== 1 || adtIn?.k !== "Array" + || adtOut?.k !== "Array" || ty_adt(cb.book, adtIn.x[0]) === null + || ty_adt(cb.book, adtOut.x[0]) === null) { + return null; + } + const { arms, end } = mat_arms(tld.h as HTerm); + const leaf = Bend.term_strip(arms.find(([n]) => n === "ALeaf")?.[1] + ?? Bend.Efq()); + const node = Bend.term_strip(arms.find(([n]) => n === "ANode")?.[1] + ?? Bend.Efq()); + if (arms.length !== 2 || Bend.term_strip(end).$ !== "Efq" + || leaf.$ !== "Lam" || node.$ !== "Lam") { + return null; + } + const cell = Bend.term_strip(term_open(leaf).b); + if (cell.$ !== "Ctr" || cell.k !== "ALeaf" || cell.x.length !== 1) { + return null; + } + const xs = term_open(node); + const ysl = Bend.term_strip(xs.b); + if (ysl.$ !== "Lam") { + return null; + } + const ys = term_open(ysl); + const fork = Bend.term_strip(ys.b); + if (fork.$ !== "Let" || fork.k.length !== 2) { + return null; + } + const halves = [xs.ps[0], ys.ps[0]]; + const lr = term_open(fork); + const join = Bend.term_strip(lr.b); + const same = (t: HTerm, p: Probe): boolean => { + const v = Bend.term_strip(t); + return v.$ === "Var" && probe_of(v) === p; + }; + const rec = fork.v.every((v, j) => { + const m = term_spine(cb, v); + return m.call?.k === k && m.args.length === 1 && same(m.args[0], halves[j]); + }); + if (!rec || join.$ !== "Ctr" || join.k !== "ANode" || join.x.length !== 2 + || !join.x.every((x, j) => same(x, lr.ps[j]))) { + return null; + } + return { Tin: adtIn.x[0], Tout: adtOut.x[0], ret, leaf }; +} + +// The raw block operations, bodiless defs the emitter lowers by name (see +// OPERATIONS, and map_op for those that read an element layout): a block +// term as a word (src), its element depth (depth), a fresh destination of +// that depth (dst), the depth above a leaf and that leaf's size, the leaf +// at most 2^g and no wider than leaves a split of 2^s (split, leaf), +// a leaf's count, zero once the run has failed (cnt), the start of the +// high half (mid), an element moved out or dropped (take, drop), a result +// written into its slot (put), the high half's end once both halves are +// done (join), and the source freed shallow around the finished +// destination (close). +function map_defs(cb: Carb): void { + if (cb.book.tlds["Array.map.take"] !== undefined) { + return; + } + const nat = Bend.Ref("Nat"); + const kind = Bend.Typ(Bend.Qua(Bend.Lone())); + const arr = (T: HTerm): HTerm => Bend.ADT("Array", [T]); + const gen = (k: string, n: number, T: (X: HTerm) => HTerm): void => { + cb.book.tlds["Array.map." + k] = { $: "Def", n, x: 0, v: null, b: true, + T: Bend.All(Bend.None(), "T", 0, kind, T) }; + }; + const one = (k: string, n: number, T: HTerm): void => { + cb.book.tlds["Array.map." + k] = { $: "Def", n, x: 0, v: null, b: true, T }; + }; + gen("src", 2, (T) => Bend.All(Bend.Lone(), "a", 0, arr(T), () => nat)); + gen("depth", 2, () => map_words(["s"], nat)); + gen("dst", 2, () => map_words(["d"], nat)); + one("split", 3, map_words(["d", "g", "s"], nat)); + one("leaf", 3, map_words(["d", "g", "s"], nat)); + one("cnt", 1, map_words(["n"], nat)); + one("mid", 3, map_words(["lo", "h", "leaf"], nat)); + gen("take", 3, (T) => map_words(["s", "i"], T)); + gen("drop", 3, () => map_words(["s", "i"], nat)); + gen("put", 4, (T) => Bend.All(Bend.Lone(), "o", 0, nat, () => + Bend.All(Bend.Lone(), "i", 0, nat, () => + Bend.All(Bend.Lone(), "v", 0, T, () => nat)))); + one("join", 2, map_words(["a", "b"], nat)); + gen("close", 4, (T) => map_words(["s", "o", "u"], arr(T))); +} + +// The layout-reading operations over an element layout `el`; the block is +// a term word, the index an element index. +function map_op(fl: File, op: string, el: Lay, args: Val[]): Val { + const { arr, lgs } = lay_arr(el); + const word = (i: number): string => emit_alias(fl, val_word(args[i]), "m"); + switch (op) { + case "array_map_depth": { + return val_new([`((u64)blk_cls(${word(0)}) - ${lgs})`], W64); + } + case "array_map_dst": { + const d = emit_hold(fl, [val_word(args[0])], "d")[0]; + block(fl, `if (${d} + ${lgs} > 31) {`, () => { + file_push(fl, "err_post(e.mem, ERR_ARRS);"); + file_push(fl, `${d} = 0;`); + }); + const oc = emit_hold(fl, [`${d} + ${lgs}`], "oc")[0]; + const cls = arr ? oc : `buf_wcls(${oc})`; + const l = emit_hold(fl, [`heap_alloc(e, ${cls})`], "l")[0]; + return val_new([`(err_seen(e.mem) ? term_buf(0, ${l}) : term_blk(${ + Number(arr)}, ${oc}, ${l}))`], W64); + } + case "array_map_take": + case "array_map_drop": { + const s = word(0); + const i = word(1); + const at = emit_hold(fl, [`${i} << ${lgs}`], "at")[0]; + const got = arr_cells(fl, s, at, el, true); + if (op === "array_map_take") { + return got; + } + val_sink(fl, got); + return val_new([i], W64); + } + case "array_map_put": { + const o = word(0); + const i = word(1); + const at = emit_hold(fl, [`${i} << ${lgs}`], "at")[0]; + const ws = val_own(fl, val_to(fl, args[2], el)); + for (let j = 0; j < (1 << lgs); j += 1) { + file_push(fl, `blk_write(e.mem, ${Number(arr)}, term_loc(${o}), ${ + at} + ${j}, ${ws[j] ?? 0});`); + } + return val_new([`(${i} + 1)`], W64); + } + default: return die("an Array.map operation the C lane lacks: " + op); + } +} + function compile_reqs(fl: File): void { const seen = new Set(); fl.spares = []; @@ -2860,7 +3225,7 @@ const RUNTIME_ADTS = ["Sigma", "String", "Word.Con", "IO.OP", "Result", // file may declare; OWNED adds the types a file without `import Base` may // declare as its own, which check and run, and which the emitters, whose // native shape would not fit, refuse. -export const SYNTH = [CLO_APPLY]; +export const SYNTH = [CLO_APPLY, ...MAP_OPS]; const OWNED = [...SYNTH, "IO", ...RUNTIME_ADTS, ...Object.keys(OPTIMIZED)]; export function book_owned(src: Bend.Book, ks = OWNED): void { @@ -2947,7 +3312,7 @@ function compile_segs(fl: File): string { export function compile_book(book: Bend.Book): string { const show = show_main(book); - const cb = carb_book(book, ["main", ...RUNTIME_ADTS]); + const cb = carb_book(book, ["main", ...RUNTIME_ADTS], true); const facts = () => JSON.stringify([[...cb.own], [...cb.hot], [...cb.stat]]); const pass = (defs: [Bend.Name, Def][]): File => { diff --git a/tests/run/array_map_buf_alloc.bend b/tests/run/array_map_buf_alloc.bend new file mode 100644 index 000000000..8bc82df60 --- /dev/null +++ b/tests/run/array_map_buf_alloc.bend @@ -0,0 +1,24 @@ +# A packed numeric destination is a BUF block: its physical class is one +# below the logical class in the term. Allocating it in the logical class +# frees it into the wrong size list, so repeated maps cannot reuse the block +# and resident memory grows with every turn. This spins 20,000 maps of a +# 4096 element U32 array through unchanged Array.map calls. +import Base + +def rd(r: Array & U32) -> U32: + (a, x) = r + x + +def spin(n: Nat, a: Array) -> Array: + match n: + case 0n: + a + case 1n+p: + b = Array.map(~U32, ~U32, ~(x => U32.add(x, 1)), a) + spin(p, b) + +def main() -> IO(Unit): + a = spin(20000n, [0 : U32*4096n]) + IO.print(U32.show(rd(Array.get(U32, a, 0)))) + +#|20000 diff --git a/tests/run/array_map_cont.bend b/tests/run/array_map_cont.bend new file mode 100644 index 000000000..002879277 --- /dev/null +++ b/tests/run/array_map_cont.bend @@ -0,0 +1,61 @@ +# Array.map lowers in the C lane to workers over index ranges of one source +# and one destination, and its callback keeps every shape a body has: a +# parallel let, a call into a def with no arguments, a call that needs a +# continuation, a let, an element it passes on and one it ignores. The last +# row is wider than one leaf, so its walk forks. +import Base + +def slow(+d: Nat) -> U32: + match d: + case 0n: + 3 + case 1n+p: + a b = slow(p) slow(p) + U32.add(a, b) + +def k() -> U32: + slow(3n) + +def rd(r: Array & U32) -> U32: + (a, x) = r + x + +def u32s(xs: List) -> String: + List.show(~&1, ~U32, ~(x => U32.show(x)), xs) + +def strs(xs: List) -> String: + List.show(~&1, ~String, ~(s => s), xs) + +def main() -> IO(Unit): + do IO: + forks : Array = Array.map(~U32, ~U32, ~(x => + a b = U32.add(x, 1) U32.mul(7, 2) + U32.add(a, b)), Array.set(U32, [3 : U32*4n], 1, 10)) + IO.print(u32s(Array.to_list(~U32, forks))) + consts : Array = Array.map(~U32, ~U32, ~(x => U32.add(x, k())), + [1 : U32*2n]) + IO.print(u32s(Array.to_list(~U32, consts))) + slows : Array = Array.map(~Nat, ~U32, ~(n => slow(n)), + Array.set(Nat, [1n : Nat*4n], 2, 4n)) + IO.print(u32s(Array.to_list(~U32, slows))) + lets : Array = Array.map(~U32, ~U32, ~(x => + y = U32.mul(x, 3) + U32.add(y, 1)), Array.set(U32, [3 : U32*4n], 1, 10)) + IO.print(u32s(Array.to_list(~U32, lets))) + same : Array = Array.map(~String, ~String, ~(s => s), + Array.set(String, ["ab" : String*4n], 3, "xyz")) + IO.print(strs(Array.to_list(~String, same))) + gone : Array = Array.map(~String, ~U32, ~(s => 7), + Array.set(String, ["ab" : String*2n], 1, "xyz")) + IO.print(u32s(Array.to_list(~U32, gone))) + wide : Array = Array.map(~U32, ~U32, ~(x => U32.add(x, 1)), + Array.set(U32, [0 : U32*8192n], 8191, 41)) + IO.print(U32.show(rd(Array.get(U32, wide, 8191)))) + +#|[18, 25, 18, 18] +#|[25, 25] +#|[6, 6, 48, 6] +#|[10, 31, 10, 10] +#|[ab, ab, ab, xyz] +#|[7, 7] +#|42 diff --git a/tests/run/array_map_fast.bend b/tests/run/array_map_fast.bend new file mode 100644 index 000000000..c8c2e3a74 --- /dev/null +++ b/tests/run/array_map_fast.bend @@ -0,0 +1,52 @@ +# Array.map lowers in the C lane to a walk over index ranges of one source +# and one destination: the destination is allocated once, each source cell +# moves out once and each result is written raw into its slot. These rows +# cover a differing layout, a multi-word record with padding, boxed elements, +# and nested arrays on both sides, all through unchanged Array.map calls. +import Base + +type Trip is Data: + Trip{a: U32, b: U32, c: U32} + +def rd(r: Array & U32) -> U32: + (a, x) = r + x + +def rdn(r: Array & Nat) -> Nat: + (a, x) = r + x + +def main() -> IO(Unit): + do IO: + one : Array = Array.map(~U32, ~U32, + ~(x => U32.add(x, 1)), [41 : U32*1n]) + IO.print(U32.show(rd(Array.get(U32, one, 0)))) + non : Array = Array.map(~U32, ~U32, ~(x => U32.mul(x, 10)), + Array.set(U32, Array.set(U32, Array.set(U32, [0 : U32*4n], + 1, 1), 2, 2), 3, 3)) + IO.print(U32.show(rd(Array.get(U32, non, 3)))) + up : Array = Array.map(~U32, ~U32, + ~(x => U32.add(x, 1)), [9 : U32*4n]) + IO.print(U32.show(rd(Array.get(U32, up, 3)))) + nat : Array = Array.map(~U32, ~Nat, + ~(x => U32.to_nat(U32.add(x, 2))), [5 : U32*2n]) + IO.print(Nat.show(rdn(Array.get(Nat, nat, 1)))) + trip : Array = Array.map(~U32, ~Trip, + ~(x => Trip{x, 0, 0}), [7 : U32*2n]) + IO.print(U32.show(rd(Array.get(U32, Array.map(~Trip, ~U32, + ~(t => 0), trip), 0)))) + boxed : Array = Array.map(~String, ~U32, + ~(s => 0), ["" : String*4n]) + IO.print(U32.show(rd(Array.get(U32, boxed, 2)))) + rows : Array> = Array.map(~U32, ~Array, + ~(x => [x : U32*2n]), [6 : U32*2n]) + IO.print(U32.show(rd(Array.get(U32, Array.map(~Array, ~U32, + ~(r => 1), rows), 1)))) + +#|42 +#|30 +#|10 +#|7 +#|0 +#|0 +#|1 diff --git a/tests/run/array_map_layouts.bend b/tests/run/array_map_layouts.bend new file mode 100644 index 000000000..67b0a93a4 --- /dev/null +++ b/tests/run/array_map_layouts.bend @@ -0,0 +1,56 @@ +# Array.map keeps its Type generic signature and its tree definition. These rows +# cover a leaf, a wide array, a different output layout and an element that owns +# a box. +import Base + +def u32s(xs: List) -> String: + List.show(~&1, ~U32, ~(x => U32.show(x)), xs) + +def strs(xs: List) -> String: + List.show(~&1, ~String, ~(s => s), xs) + +def nats(xs: List) -> String: + List.show(~&1, ~Nat, ~(n => Nat.show(n)), xs) + +def rows(xs: List>) -> String: + List.show(~&1, ~Array, ~(r => u32s(Array.to_list(~U32, r))), xs) + +def rowsum.fold(xs: List, acc: U32) -> U32: + List.foldl(~&1, ~U32, ~U32, ~(a => x => U32.add(a, x)), xs, acc) + +def rowsum(r: Array) -> U32: + rowsum.fold(Array.to_list(~U32, r), 0) + +def main() -> IO(Unit): + do IO: + mine : Array = Array.set(U32, [1 : U32*2n], 1, 3) + rows_out : Array> = Array.map(~U32, ~Array, + ~(x => [x : U32*2n]), mine) + IO.print(rows(Array.to_list(~Array, rows_out))) + rows_in : Array> = Array.map(~U32, ~Array, + ~(x => [x : U32*2n]), Array.set(U32, [1 : U32*2n], 1, 3)) + IO.print(u32s(Array.to_list(~U32, + Array.map(~Array, ~U32, ~(r => rowsum(r)), rows_in)))) + one : Array = Array.map(~U32, ~U32, + ~(x => U32.add(x, 1)), [0 : U32*1n]) + IO.print(u32s(Array.to_list(~U32, one))) + wide : Array = Array.map(~U32, ~U32, + ~(x => U32.add(x, 1)), [0 : U32*16n]) + IO.print(u32s(Array.to_list(~U32, wide))) + boxed : Array = Array.map(~String, ~U32, + ~(s => U32.from_nat(String.length(s))), ["" : String*4n]) + IO.print(u32s(Array.to_list(~U32, boxed))) + wide_out : Array = Array.map(~U32, ~Nat, + ~(x => U32.to_nat(U32.add(x, 2))), [0 : U32*4n]) + IO.print(nats(Array.to_list(~Nat, wide_out))) + strs_out : Array = Array.map(~U32, ~String, + ~(x => U32.show(x)), [7 : U32*2n]) + IO.print(strs(Array.to_list(~String, strs_out))) + +#|[[1, 1], [3, 3]] +#|[2, 6] +#|[1] +#|[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] +#|[0, 0, 0, 0] +#|[2, 2, 2, 2] +#|[7, 7]