diff --git a/bin/main.ml b/bin/main.ml index 52ac753..ebb503a 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -23,6 +23,14 @@ let with_errors path f = prerr_endline ("x86: " ^ m); ignore path; exit 3 + (* The JS dialect's refusal, and the same status for the same reason. It is + a wider category than the x86 one — that backend is behind on a node, and + this one is a dialect that deliberately does not carry the memory model — + but a sweep counts them the same way: refused by name, not a failure. *) + | Flan.Js.Unsupported m -> + prerr_endline ("js: " ^ m); + ignore path; + exit 3 (* A refusal with no location: a combination of flags this command does not offer, or a build step that failed. Every [failwith] this binary can reach is one of those, and a sentence is what a user can act on where an @@ -518,14 +526,17 @@ let () = (* A web build is three files — the page, its JS and the module — and the page is the one named here: emcc derives the other two from it, and it is the one a browser opens. *) + (* The JS dialect's output is one file and it is source, so it is + named the way source is: node runs it by name. *) (match target with | Some t when Flan.Build.is_web t -> base ^ ".html" + | Some t when Flan.Build.is_js t -> base ^ ".js" | Some t when String.starts_with ~prefix:"wasm32" t -> base ^ ".wasm" | _ -> base) | _ -> prerr_endline "usage: flan build [-o out] [--no-bounds-checks] \ - [--dev] [--debug] [--sanitize] [--target=wasm32-wasi|web]"; + [--dev] [--debug] [--sanitize] [--target=wasm32-wasi|web|js]"; exit 2 in with_errors path (fun () -> @@ -646,7 +657,7 @@ let () = \ flan import-c [package.flan...] [clang flags...]\n\ \ flan generate-c \n\ \ flan build [-o out] [--no-bounds-checks] [--dev] \ - [--debug] [--sanitize] [--x86] [--target=wasm32-wasi|web]\n\ + [--debug] [--sanitize] [--x86] [--target=wasm32-wasi|web|js]\n\ \ flan run [args...]\n\ \ flan reload [-o out.so] [--x86]\n\ \ flan dev [-s socket] [--x86]"; diff --git a/lib/build.ml b/lib/build.ml index 319a396..7d680ad 100644 --- a/lib/build.ml +++ b/lib/build.ml @@ -221,6 +221,20 @@ let web_target opts = let wasi_target opts = match opts.target with Some t when is_wasi t -> true | _ -> false +(* ── The fourth target, and the only one with no clang in it ────────── + + [--target=js] is a *dialect*, not a machine: docs/DISCUSS.md item 5 settled + object mapping over linear memory, and object mapping leaves the memory + model behind. See [lib/js.ml]'s header for what maps to what and what is + refused. Nothing below the fork in [executable] applies to it — there is no + object to compile, no runtime C to link and no linker to run — so it leaves + through its own two lines rather than threading a fourth case through + [target_flags], [compiler] and [cflags]. *) +let is_js t = t = "js" || t = "javascript" || t = "node" + +let js_target opts = + match opts.target with Some t when is_js t -> true | _ -> false + (* Which compiler a target is built by. This is not a flag difference: emcc is a different program with a different driver, and the object cache key below carries it for the same reason it carries clang's mtime. *) @@ -687,6 +701,26 @@ let compile_c ~opts ?tflags ~src ~name () = it binds to. *) let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = []) (p : Tast.program) ~out = + (* The JS dialect leaves here, before anything that assumes a clang. Its + flags are refused rather than ignored, for the reason [flan emit] refuses + [--target]: a flag that is silently swallowed is the shape the house rule + exists to prevent. [--dev] is the one worth a sentence of its own — the + dev loop on JS is a real possibility and deliberately not this lane. *) + if js_target opts then begin + if opts.dev then + failwith + "js: --dev is not built yet — evaluating new code is the one thing \ + JavaScript makes easy, so this is a lane and not a limit"; + if opts.debug then + failwith "js: --debug is native only — there is no DWARF in a .js file"; + if opts.sanitize then + failwith "js: --sanitize is native only"; + if opts.x86 then + failwith "js: --x86 and --target=js are two different backends"; + write out (Js.program ~checks:opts.checks p); + out + end + else (* A dev build is the REPL's, and the REPL reaches a running process through [-rdynamic] and [dlopen]. Neither exists on wasm32, so the combination is refused rather than quietly producing a module nothing can attach to. *) diff --git a/lib/js.ml b/lib/js.ml new file mode 100644 index 0000000..f13548a --- /dev/null +++ b/lib/js.ml @@ -0,0 +1,1394 @@ +(** Tast -> JavaScript, by object mapping. A dialect, not a third target. + + docs/DISCUSS.md item 5 settled the design fork before any of this was + written, and the settlement is the whole shape of this file: {b a Flan + struct becomes a plain JS object and a [Vec] becomes a JS array.} The + alternative — an [ArrayBuffer] standing in for linear memory, with typed + array reads and writes — is asm.js, which is what wasm exists to replace, + and it is precisely wrong for a backend whose reason to exist is calling + JS libraries. Games go to wasm. This is for web apps. + + {1 What this is a dialect of} + + Object mapping means the host's garbage collector owns every value, and + that is not a detail that can be papered over: {b the memory model does not + come along.} There are no addresses in JavaScript, so there is no + [(Ptr T)], no [free], no arena, no allocator and no [with-allocator]. A + program that uses them is not a program this backend compiles badly — it is + a program this backend {e refuses}, by name and with a location, which is + the house rule at [check.ml:14] applied at the one place that can see the + target. Nothing here approximates. + + The refusals are in [refuse_ty] and in the [unsupported] calls through the + walk below. Each one names the feature and says which property of the + object mapping excludes it, because "js: unsupported" is a bug report + addressed to nobody. + + {1 The mapping, in one table} + + - [i8 i16 i32 u8 u16 u32], an enum, a [bool], [f32], [f64] — a JS number + (a boolean for [bool]). Every arithmetic result is normalised back into + the type's range at the site that produced it: see {!section:ints}. + - [i64] and [u64] — a [BigInt]. A double holds 53 bits exactly and these + types hold 64, so there is no honest alternative; it is slower and it is + right, and a wrong number silently is the outcome this repository refuses + everywhere else. + - A struct — a plain object, [{x: 1, y: 2}], keyed by the field's own name. + Readable output is a goal, and this is most of it. + - [[n T]], a fixed array — a JS array, or a [Uint8Array] when [T] is [u8]. + - [[T]], a slice, and [string] — [{a, o, n}]: the backing store, an offset + into it and a length. A slice is a non-owning view in Flan and this is a + non-owning view here: writing through one writes the original, and a + copy of a slice shares its storage, both of which are what the other two + backends do with a (ptr, len) pair. + - A union — [{case: "Name", field: ...}]. [(Option T)] — [null] for + [None] and [{v: x}] for [Some], so that [(Some 0)] and [None] are not the + same value, which [0]-as-falsy would make them. + - [(Fn [..] R)] — a JS function. Function {e values} come along; they carry + no capture in Flan either. + + {b [string] and [[u8]] are the same representation, and that is not a + shortcut.} [Tast.Bytes] and [Tast.StrOfBytes] are documented there as + non-instructions — in both existing backends a [string] {e is} a (ptr, len) + pair over bytes, and the two prims reinterpret rather than convert. Making + [string] a JS string would make [(len s)] count UTF-16 code units where + every other backend counts bytes, and the prelude is byte-oriented + throughout ([rune-count], [valid-utf8?], [decode-rune] all take [[u8]]). + So the JS string appears in exactly one place: the argument of + [$str(...)], where a literal is spelled, and it is UTF-8 encoded on the + way in. Decoding happens at [write-stdout] and nowhere else. + + The backing store of a [u8] sequence is a [Uint8Array] rather than an + [Array] — the brief's "honest mapping" — and it buys more than honesty: + a [Uint8Array] masks on store, so [u8] wrapping is the store itself, and + it is the type a JS library that wants bytes already accepts. + + {1:copies Value semantics, which is the trap} + + A Flan struct and a fixed array are {e values}: binding, passing, + returning or storing one copies it, which is what [emit.ml] spends a + memcpy on at every one of those sites. A JS object assigns by reference. + Left alone, [(let [b a] ...)] on a struct would alias where the LLVM build + copied, and a mutation after the copy would diverge — silently, and only + in a program that actually mutates a copy. + + So every site where [emit.ml] copies, this emits a clone: a [let] binding, + a [set], a call argument, a [return], a field or element store, a struct + or array literal's parts, and a [match] arm's binds. A generated + [Point$copy] per struct does it, recursively, because a field may be a + struct or an array of them. The clone is skipped where the value is + already fresh — a literal, a call's result, a new slice — which is what + keeps the output readable rather than a wall of [$copy]. + + A [Vec] is move-only in Flan and owns its storage, so a JS array is the + right thing for it and aliasing is the right behaviour: the trap is only + where Flan copies. Slices are views and copy as views, by the same rule. + + {1:ints Integer semantics} + + The known hard part, and the one place a silent wrong number is reachable. + The rule is that {b every value of type [T] is, at rest, always in [T]'s + range}, so comparisons, printing and equality need no normalisation of + their own and only the arithmetic sites do: + + - 32 bits — [| 0] for the signed kinds, [>>> 0] for the unsigned ones, and + [Math.imul] for a multiply, which is exact modulo 2^32 where [a * b] is + not. + - 8 and 16 bits — [<< 24 >> 24], [<< 16 >> 16], [& 0xff], [& 0xffff]. + - 64 bits — [BigInt.asIntN(64, ...)] and [BigInt.asUintN(64, ...)]. + - A shift's count is masked to the operand's width, mirroring [emit.ml]'s + shift arm, which masks because LLVM makes an over-wide shift poison. The + mechanism is different — JS already masks a 32-bit shift and does not + mask an 8-bit one, because it has no 8-bit shift — so the mask is + written out rather than relied on. + - A divide truncates toward zero ([Math.trunc], not [| 0], which is only + the same below 2^31), and a remainder follows C, which JS's [%] already + does. + - [f32] rounds through [Math.fround] after every operation, because a JS + number is a double and a [float] is not. + + A divide by zero, [INT_MIN / -1] and a float-to-integer cast out of range + all produce the same message and the same exit status [flan_rt.c] does — + [ArithError]'s unhandled sentence, verbatim, and 134. Without that, + [(/ x 0)] in JS is [Infinity], and [Infinity | 0] is [0]: a wrong number, + silently, which is the worst outcome available. + + {1 What is refused, and why each} + + Pointers and [deref], [free], allocators and [with-allocator], [Map], + [Pool] and [(Handle T)], [declare-c] and the FFI, [embed], conditions and + restarts ([signal], [handler-bind], [restart-case], [invoke-restart]), and + the type-erased container runtime's own entry points. The first group has + no counterpart in a garbage-collected object graph; the FFI and [embed] + are a host boundary this backend does not have; conditions are a stack + walk over a transfer channel and are a lane of their own, not a stub. + + {1 Output} + + One CommonJS file, with a small runtime at the top and the program under + it, ending in [process.exit(main())]. [require] rather than [import] so + that a bare [node out.js] runs it with no [package.json] to arrange. + Output goes through [fs.writeSync(1, ...)] rather than + [process.stdout.write], because node's stdout is asynchronous and a + [process.exit] after a write on it truncates the output — which would + look exactly like a codegen bug. *) + +exception Unsupported of string + +let unsupported fmt = Printf.ksprintf (fun s -> raise (Unsupported s)) fmt + +let at loc fmt = + Printf.ksprintf + (fun s -> raise (Unsupported (Loc.to_string loc ^ ": " ^ s))) + fmt + +(* ── Names ────────────────────────────────────────────────────────── + + A Flan name is not a JS identifier: [rand-u32], [bytes=?], [append!] and + [fn/sort-bytes!/0] are all ordinary. The rule below is injective, which is + what matters — two Flan names must never land on one JS name — and readable + second: [-] is the common case and becomes [_], so [rand-u32] reads as + [rand_u32], and an underscore that was actually written becomes [$_] so + that the two cannot collide. Everything else becomes [$] and two hex + digits. Nothing the rule produces starts with [$], which is how every + identifier this file invents for itself stays out of the way. *) + +let reserved = + [ "break"; "case"; "catch"; "class"; "const"; "continue"; "debugger"; + "default"; "delete"; "do"; "else"; "export"; "extends"; "finally"; "for"; + "function"; "if"; "import"; "in"; "instanceof"; "new"; "return"; "super"; + "switch"; "this"; "throw"; "try"; "typeof"; "var"; "void"; "while"; + "with"; "yield"; "let"; "static"; "enum"; "await"; "implements"; + "package"; "protected"; "interface"; "private"; "public"; "null"; "true"; + "false"; "arguments"; "eval"; "undefined"; "NaN"; "Infinity" ] + +let ident name = + let b = Buffer.create (String.length name + 4) in + String.iter + (fun c -> + match c with + | 'a' .. 'z' | 'A' .. 'Z' | '0' .. '9' -> Buffer.add_char b c + | '-' -> Buffer.add_char b '_' + | '_' -> Buffer.add_string b "$_" + | c -> Buffer.add_string b (Printf.sprintf "$%02x" (Char.code c))) + name; + let s = Buffer.contents b in + let s = + match s.[0] with '0' .. '9' -> "_" ^ s | _ -> s | exception _ -> "_empty" + in + if List.mem s reserved then s ^ "_" else s + +(* A field or a case name reaches the output as a property. The same rule, so + that a struct with fields [x] and [x-y] keeps them apart, and so that + [v.pos_x] reads as the field the source spelled. *) +let prop name = ident name + +let fname n = ident n +let gvar n = "g_" ^ ident n + +(* ── Types: what the mapping admits ─────────────────────────────────── *) + +let rec refuse_ty loc (t : Types.t) = + match t with + | Types.Int _ | Types.Float _ | Types.Bool | Types.String | Types.Unit + | Types.Never | Types.Named _ | Types.Enum _ -> () + | Types.Slice t | Types.Array (_, t) | Types.Option t -> refuse_ty loc t + | Types.Vec t -> refuse_ty loc t + | Types.Fn (ps, r) -> List.iter (refuse_ty loc) ps; refuse_ty loc r + | Types.Ptr _ -> + at loc + "(Ptr T) is not in the JS dialect — JavaScript has no addresses, so a \ + pointer has nothing to be. Pass the value, or a (Vec T) for storage \ + that is shared" + | Types.Alloc -> + at loc + "Allocator is not in the JS dialect — the host's collector owns every \ + value here, so there is no allocation to direct" + | Types.Map (_, _) -> + at loc + "(Map K V) is not in the JS dialect yet — Odin's open-addressed map is a \ + type-erased runtime over raw bytes and the JS answer is a Map keyed by \ + a structural key, which is its own lane" + | Types.Pool _ -> + at loc + "(Pool T) is not in the JS dialect — a pool hands out slot indices into \ + storage it owns, which is the memory model this dialect leaves behind" + | Types.Handle _ -> + at loc + "(Handle T) is not in the JS dialect — a handle is an index into a Pool, \ + and there is no Pool here" + | Types.Var n -> + at loc "a type variable (%s) reached the backend, which cannot happen" n + +(* Aggregates in the sense that matters here: the types whose assignment + copies in Flan and would alias in JS. A slice is deliberately not one — + it is a view, and a copy of a view shares storage in every backend. *) +let rec is_agg (t : Types.t) = + match t with + | Types.Named _ | Types.Array _ -> true + | Types.Option t -> is_agg t + | _ -> false + +let _ = is_agg + +(* ── The emitted runtime ──────────────────────────────────────────── + + Small, and every line of it is either a representation decision from the + header or a message that has to agree with runtime/flan_rt.c character for + character. The survey diffs stderr, so a reworded sentence here is a + DIFFER. *) + +let runtime = + {js|"use strict"; +// The Flan runtime for the JS dialect. See lib/js.ml for what maps to what. +const $fs = require("fs"); + +// A slice, a string and a [u8] are one shape: a backing store, an offset and +// a length. The store is a Uint8Array for bytes and an Array otherwise; both +// answer a[i] and both are written through, which is what a view must do. +function $view(a, o, n) { return { a: a, o: o, n: n }; } +function $str(s) { const b = Buffer.from(s, "utf8"); return $view(new Uint8Array(b.buffer, b.byteOffset, b.length), 0, b.length); } +function $bytes(xs) { const a = Uint8Array.from(xs); return $view(a, 0, a.length); } +function $buf(s) { + const a = s.a; + return a instanceof Uint8Array ? a.subarray(s.o, s.o + s.n) + : Uint8Array.from(a.slice(s.o, s.o + s.n)); +} +function $out(s) { if (s.n > 0) $fs.writeSync(1, $buf(s)); } + +// Exit 134 is abort()'s status without abort()'s signal, and stdout is +// written synchronously above, so the message lands after the output that +// led to it exactly as flan_rt.c's fflush(stdout) arranges. +function $die(msg) { $fs.writeSync(2, msg); process.exit(134); } +function $bounds(loc, i, n) { + $die(loc + ": index " + i + " is out of bounds for length " + n + "\n"); +} +function $slicefail(loc, lo, hi, n) { + $die(loc + ": slice [" + lo + " " + hi + ") is out of bounds for length " + n + "\n"); +} +function $divzero(loc, a) { $die(loc + ": divide by zero: (/ " + a + " 0)\n"); } +function $remzero(loc, a) { $die(loc + ": remainder by zero: (% " + a + " 0)\n"); } +function $divovf(loc, op, a, b) { + $die(loc + ": (" + op + " " + a + " " + b + ") overflows: the quotient is one past the largest value the type holds, and this is the only pair of operands for which that is true\n"); +} +function $castrange(loc, lo, hi) { + $die(loc + ": this value does not fit the integer type it is cast to, which holds [" + lo + " " + hi + "]\n"); +} + +// Checked indexing and slicing. The comparison is the unsigned one flan_rt.c +// makes: a negative index is a huge length there and out of bounds here. +function $at(s, i, loc) { + if (i < 0 || i >= s.n) $bounds(loc, i, s.n); + return s.a[s.o + i]; +} +function $set(s, i, v, loc) { + if (i < 0 || i >= s.n) $bounds(loc, i, s.n); + s.a[s.o + i] = v; +} +function $slice(s, lo, hi, loc) { + if (lo > hi || hi > s.n || lo < 0) $slicefail(loc, lo, hi, s.n); + return $view(s.a, s.o + lo, hi - lo); +} +// A fixed array is its own store; indexing one is the same check without a view. +function $aat(a, i, loc) { + if (i < 0 || i >= a.length) $bounds(loc, i, a.length); + return a[i]; +} +function $aset(a, i, v, loc) { + if (i < 0 || i >= a.length) $bounds(loc, i, a.length); + a[i] = v; +} +function $aslice(a, lo, hi, loc) { + if (lo > hi || hi > a.length || lo < 0) $slicefail(loc, lo, hi, a.length); + return $view(a, lo, hi - lo); +} + +// %lld and %llu. A BigInt prints its digits and nothing else, which is what +// the two shims in flan_rt.c do. +function $i64s(x) { return $str(String(x)); } +// C's "%g": six significant digits, exponent form outside [1e-4, 1e6), and +// trailing zeros stripped. Written out because String(x) is none of those. +function $g(x) { + if (Number.isNaN(x)) return "nan"; + if (!Number.isFinite(x)) return x > 0 ? "inf" : "-inf"; + if (x === 0) return Object.is(x, -0) ? "-0" : "0"; + const e = Number(x.toExponential(5).split("e")[1]); + if (e < -4 || e >= 6) { + let m = x.toExponential(5).split("e")[0]; + if (m.indexOf(".") >= 0) m = m.replace(/0+$/, "").replace(/\.$/, ""); + const s = e < 0 ? "-" : "+"; + const a = Math.abs(e); + return m + "e" + s + (a < 10 ? "0" + a : String(a)); + } + let t = x.toFixed(Math.max(0, 5 - e)); + if (t.indexOf(".") >= 0) t = t.replace(/0+$/, "").replace(/\.$/, ""); + return t; +} +function $f64s(x) { return $str($g(x)); } + +// strtoll and strtod over the leading text, which is what the C shims do: +// no digits is 0, and strtoll saturates at the ends of the type. +function $tobytesi64(s) { + const t = Buffer.from($buf(s)).toString("latin1"); + const m = /^[ \t\n\r\f\v]*[+-]?[0-9]+/.exec(t); + if (!m) return 0n; + let v = BigInt(m[0].replace(/^[ \t\n\r\f\v]*/, "")); + const lo = -(2n ** 63n), hi = 2n ** 63n - 1n; + return v < lo ? lo : v > hi ? hi : v; +} +function $tobytesf64(s) { + const t = Buffer.from($buf(s)).toString("latin1"); + const m = /^[ \t\n\r\f\v]*[+-]?(?:[0-9]*\.?[0-9]+(?:[eE][+-]?[0-9]+)?|[0-9]+\.)/.exec(t); + return m ? parseFloat(m[0]) : 0; +} + +// The escape table of flan_escape_bytes, including its 1024-byte buffer and +// its ellipsis, because a printed structure must read the same on both. +function $esc(s) { + const p = $buf(s); + const o = [34]; + let cut = 0; + for (let i = 0; i < p.length; i++) { + if (o.length + 5 + 4 >= 1024) { cut = 1; break; } + const c = p[i]; + if (c === 34) { o.push(92, 34); } + else if (c === 92) { o.push(92, 92); } + else if (c === 10) { o.push(92, 110); } + else if (c === 9) { o.push(92, 116); } + else if (c === 13) { o.push(92, 114); } + else if (c < 0x20) { + const h = "\\x" + c.toString(16).padStart(2, "0"); + for (let k = 0; k < h.length; k++) o.push(h.charCodeAt(k)); + } else o.push(c); + } + if (cut) o.push(46, 46, 46); + o.push(34); + return $bytes(o); +} + +function $argv() { + const xs = process.argv.slice(1).map($str); + return $view(xs, 0, xs.length); +} +|js} + +(* ── The module state ───────────────────────────────────────────────── *) + +type m = { + out : Buffer.t; (* the program text *) + structs : (string, Tast.structure) Hashtbl.t; + unions : (string, Tast.union) Hashtbl.t; + checks : bool; + mutable strs : (string * string) list; (* literal -> its const name *) + mutable nstr : int; + mutable copies : string list; (* struct names needing a $copy *) +} + +type f = { + md : m; + b : Buffer.t; + mutable ind : int; + mutable n : int; (* temporaries *) + names : string array; (* slot index -> JS name *) + slots : Types.t array; + ret : Types.t; + mutable loops : (string * string) list; (* break label, continue label *) +} + +let line f fmt = + Printf.ksprintf + (fun s -> + Buffer.add_string f.b (String.make (f.ind * 2) ' '); + Buffer.add_string f.b s; + Buffer.add_char f.b '\n') + fmt + +let fresh f = f.n <- f.n + 1; Printf.sprintf "t%d" f.n + +(* A JS string literal for a run of bytes. Printable ASCII stays readable; + anything else goes out as numbers, because the output file's own encoding + must not decide what a byte literal meant. *) +let js_string s = + let b = Buffer.create (String.length s + 2) in + Buffer.add_char b '"'; + String.iter + (fun c -> + match c with + | '"' -> Buffer.add_string b "\\\"" + | '\\' -> Buffer.add_string b "\\\\" + | '\n' -> Buffer.add_string b "\\n" + | '\t' -> Buffer.add_string b "\\t" + | '\r' -> Buffer.add_string b "\\r" + | c when Char.code c >= 0x20 && Char.code c < 0x7f -> Buffer.add_char b c + | c -> Buffer.add_string b (Printf.sprintf "\\u%04x" (Char.code c))) + s; + Buffer.add_char b '"'; + Buffer.contents b + +let printable s = + let ok = ref true in + String.iter + (fun c -> if Char.code c < 0x20 || Char.code c >= 0x7f then ok := false) + s; + !ok + +(* Literals are interned: one const per distinct text, built once at load, so + that a string in a loop is not re-encoded per iteration and so that the + identity of a literal is stable the way a .rodata address is. *) +let string_const m s = + match List.assoc_opt s m.strs with + | Some n -> n + | None -> + let n = Printf.sprintf "$s%d" m.nstr in + m.nstr <- m.nstr + 1; + m.strs <- (s, n) :: m.strs; + n + +let locstr loc = js_string (Loc.to_string loc) + +(* ── Copying a value ──────────────────────────────────────────────── + + See the header's "Value semantics" section. [copy_of] is the JS expression + that produces a private copy of [v] at type [t], and [None] means the type + copies by assignment already. *) + +let rec copy_of m (t : Types.t) v = + match t with + | Types.Named n when Hashtbl.mem m.structs n -> + if not (List.mem n m.copies) then m.copies <- n :: m.copies; + Some (Printf.sprintf "%s$copy(%s)" (ident n) v) + | Types.Named n when Hashtbl.mem m.unions n -> + if not (List.mem n m.copies) then m.copies <- n :: m.copies; + Some (Printf.sprintf "%s$copy(%s)" (ident n) v) + | Types.Array (_, e) -> ( + match copy_of m e "x" with + | None -> Some (Printf.sprintf "%s.slice()" v) + | Some c -> Some (Printf.sprintf "%s.map((x) => %s)" v c)) + | Types.Option e -> ( + match copy_of m e "x" with + | None -> None + | Some c -> + Some (Printf.sprintf "(%s === null ? null : { v: ((x) => %s)(%s.v) })" v c v)) + | _ -> None + +(* A value that was just built is already private; cloning it would be a copy + of a copy. Everything else may be a second name for storage someone else + holds. A call's result is fresh because [return] clones on the way out. *) +let rec fresh_value (e : Tast.expr) = + match e.Tast.e with + | Tast.Make _ | Tast.Arr _ | Tast.Zero _ | Tast.Uninit _ | Tast.Call _ + | Tast.CallPtr _ | Tast.MakeCase _ | Tast.Int _ | Tast.Float _ + | Tast.Bool _ | Tast.Str _ | Tast.Unit | Tast.None_ -> true + | Tast.Some_ v -> fresh_value v + | Tast.Prim (Tast.Rt _, _) -> true + | _ -> false + +(* ── Integer normalisation ──────────────────────────────────────────── *) + +let big k = match k with Types.I64 | Types.U64 -> true | _ -> false + +(* Put an expression back inside its type's range. The header's table. *) +let norm (k : Types.ikind) s = + match k with + | Types.I64 -> Printf.sprintf "BigInt.asIntN(64, %s)" s + | Types.U64 -> Printf.sprintf "BigInt.asUintN(64, %s)" s + | Types.I32 -> Printf.sprintf "(%s | 0)" s + | Types.U32 -> Printf.sprintf "(%s >>> 0)" s + | Types.I16 -> Printf.sprintf "((%s) << 16 >> 16)" s + | Types.U16 -> Printf.sprintf "((%s) & 0xffff)" s + | Types.I8 -> Printf.sprintf "((%s) << 24 >> 24)" s + | Types.U8 -> Printf.sprintf "((%s) & 0xff)" s + +let fround (k : Types.fkind) s = + match k with Types.F32 -> Printf.sprintf "Math.fround(%s)" s | Types.F64 -> s + +(* The literal spelling of an integer of a given kind: a BigInt gets its [n]. *) +let int_lit k (v : int64) = + match k with + | Types.I64 -> Printf.sprintf "%Ldn" v + | Types.U64 -> + if Int64.compare v 0L >= 0 then Printf.sprintf "%Ldn" v + else Printf.sprintf "%sn" (Printf.sprintf "%Lu" v) + | Types.U32 | Types.U16 | Types.U8 -> + Printf.sprintf "%Ld" (Int64.logand v (Int64.of_int 0xffffffff)) + | _ -> Printf.sprintf "%Ld" v + +let float_lit (x : float) = + if Float.is_integer x && Float.abs x < 1e21 then Printf.sprintf "%.1f" x + else if Float.is_nan x then "NaN" + else if x = Float.infinity then "Infinity" + else if x = Float.neg_infinity then "-Infinity" + else + (* Shortest round-tripping decimal. OCaml's %.17g always round-trips; the + shorter spellings are tried first so the output stays readable. *) + let rec go p = + if p > 17 then Printf.sprintf "%.17g" x + else + let s = Printf.sprintf "%.*g" p x in + if float_of_string s = x then s else go (p + 1) + in + go 1 + +(* ── Zero of a type, which is ZII settled here ──────────────────────── *) + +let rec zero m loc (t : Types.t) = + refuse_ty loc t; + match t with + | Types.Int k -> if big k then "0n" else "0" + | Types.Float _ -> "0.0" + | Types.Bool -> "false" + | Types.Enum _ -> "0" + | Types.Unit -> "undefined" + | Types.String | Types.Slice _ -> "$view([], 0, 0)" + | Types.Option _ -> "null" + | Types.Array (n, e) -> + (match e with + | Types.Int Types.U8 -> Printf.sprintf "new Uint8Array(%Ld)" n + | _ -> + Printf.sprintf "Array.from({ length: %Ld }, () => %s)" n + (zero m loc e)) + | Types.Named n when Hashtbl.mem m.structs n -> + let s = Hashtbl.find m.structs n in + Printf.sprintf "{ %s }" + (String.concat ", " + (List.map + (fun (fl : Tast.field) -> + Printf.sprintf "%s: %s" (prop fl.Tast.fname) + (zero m loc fl.Tast.fty)) + s.Tast.fields)) + | Types.Named n when Hashtbl.mem m.unions n -> + (* Declaration order from zero is the tag, so an all-bytes-zero union is + the first case with a zeroed payload. Tast.case_index says so. *) + let u = Hashtbl.find m.unions n in + (match u.Tast.cases with + | [] -> at loc "union %s has no cases" n + | c :: _ -> + Printf.sprintf "{ case: %s%s }" (js_string c.Tast.vname) + (String.concat "" + (List.map + (fun (fl : Tast.field) -> + Printf.sprintf ", %s: %s" (prop fl.Tast.fname) + (zero m loc fl.Tast.fty)) + c.Tast.vfields))) + | Types.Fn _ -> "null" + | Types.Vec _ -> "[]" + | t -> at loc "no zero value for %s in the JS dialect" (Types.to_string t) + +(* ── Places and expressions ─────────────────────────────────────────── *) + +let struct_of m loc (t : Types.t) = + match t with + | Types.Named n when Hashtbl.mem m.structs n -> Hashtbl.find m.structs n + | t -> at loc "a field of %s, which is not a struct" (Types.to_string t) + +let elem_ty loc (t : Types.t) = + match t with + | Types.Slice e | Types.Array (_, e) -> e + | Types.String -> Types.Int Types.U8 + | t -> at loc "indexing %s is not in the JS dialect" (Types.to_string t) + +let is_bytes (t : Types.t) = + match t with + | Types.String -> true + | Types.Slice (Types.Int Types.U8) | Types.Array (_, Types.Int Types.U8) -> + true + | _ -> false + +let rec value f (e : Tast.expr) : string = + refuse_ty e.Tast.loc e.Tast.ty; + match e.Tast.e with + | Tast.Int (v, k) -> + (match e.Tast.ty with + | Types.Enum _ -> Printf.sprintf "%Ld" v + | _ -> int_lit k v) + | Tast.Float (x, _) -> float_lit x + | Tast.Bool b -> if b then "true" else "false" + | Tast.Str s -> + let n = string_const f.md s in + n + | Tast.Unit -> "undefined" + | Tast.Zero t -> zero f.md e.Tast.loc t + (* [uninit] is the opt-out from zeroing. There is no uninitialised memory + here to opt out of, so it is the zero — which is more than the program + asked for and never less. *) + | Tast.Uninit t -> zero f.md e.Tast.loc t + | Tast.Local i -> f.names.(i) + | Tast.Global n -> gvar n + | Tast.FnAddr (Tast.Flanfn n) | Tast.FnAddr (Tast.Fnval n) -> fname n + | Tast.FnAddr (Tast.Rtfn n) -> + at e.Tast.loc + "the runtime entry point %s has no JS counterpart — it is C in \ + flan_rt.c, and this dialect has no C" + n + | Tast.Prim (p, args) -> prim f e p args + | Tast.Call (n, args) -> + Printf.sprintf "%s(%s)" (fname n) (String.concat ", " (call_args f args)) + | Tast.CallPtr (fn, args) -> + let fv = spill f fn in + Printf.sprintf "%s(%s)" fv (String.concat ", " (call_args f args)) + | Tast.Field (x, i) -> + let s = struct_of f.md e.Tast.loc x.Tast.ty in + let fl = List.nth s.Tast.fields i in + Printf.sprintf "%s.%s" (spill f x) (prop fl.Tast.fname) + | Tast.CaseField (x, case, i) -> + let n = + match x.Tast.ty with + | Types.Named n -> n + | t -> at e.Tast.loc "case field of %s" (Types.to_string t) + in + let u = Hashtbl.find f.md.unions n in + (match Tast.case_index u case with + | Some (_, c) -> + Printf.sprintf "%s.%s" (spill f x) + (prop (List.nth c.Tast.vfields i).Tast.fname) + | None -> at e.Tast.loc "no case %s of %s" case n) + | Tast.Make (n, parts) -> + let s = Hashtbl.find f.md.structs n in + let vs = List.map (fun p -> bind_value f p) parts in + Printf.sprintf "{ %s }" + (String.concat ", " + (List.map2 + (fun (fl : Tast.field) v -> + Printf.sprintf "%s: %s" (prop fl.Tast.fname) v) + s.Tast.fields vs)) + | Tast.MakeCase (n, case, parts) -> + let u = Hashtbl.find f.md.unions n in + (match Tast.case_index u case with + | None -> at e.Tast.loc "no case %s of %s" case n + | Some (_, c) -> + let vs = List.map (fun p -> bind_value f p) parts in + Printf.sprintf "{ case: %s%s }" (js_string case) + (String.concat "" + (List.map2 + (fun (fl : Tast.field) v -> + Printf.sprintf ", %s: %s" (prop fl.Tast.fname) v) + c.Tast.vfields vs))) + | Tast.Arr parts -> + let vs = List.map (fun p -> bind_value f p) parts in + (match e.Tast.ty with + | Types.Array (_, Types.Int Types.U8) -> + Printf.sprintf "Uint8Array.of(%s)" (String.concat ", " vs) + | _ -> Printf.sprintf "[%s]" (String.concat ", " vs)) + | Tast.Some_ v -> Printf.sprintf "{ v: %s }" (bind_value f v) + | Tast.None_ -> "null" + | Tast.Addr _ | Tast.Deref _ -> + at e.Tast.loc + "an address is not in the JS dialect — JavaScript has no addresses, so \ + (addr-of x) and a deref have nothing to name" + | Tast.Signal _ -> + at e.Tast.loc + "signalling a condition is not in the JS dialect yet — the handler \ + stack and the transfer channel are a lane of their own" + | Tast.Handled _ -> + at e.Tast.loc + "handler-bind is not in the JS dialect yet — the handler stack and the \ + transfer channel are a lane of their own" + | Tast.RestartCase _ -> + at e.Tast.loc + "restart-case is not in the JS dialect yet — a restart is a frame on a \ + stack this backend does not build" + | Tast.InvokeRestart _ -> + at e.Tast.loc + "invoke-restart is not in the JS dialect yet — a restart is a frame on \ + a stack this backend does not build" + | Tast.WithAlloc _ -> + at e.Tast.loc + "with-allocator is not in the JS dialect — the host's collector owns \ + every value here, so there is no allocator to rebind" + (* Everything below is statement-shaped: it needs a temporary to be a value. + [stmt] writes into the name this hands it. *) + | Tast.Do _ | Tast.Let _ | Tast.If _ | Tast.While _ | Tast.Match _ + | Tast.UnwrapSome _ | Tast.Set _ | Tast.Return _ | Tast.Break _ + | Tast.Continue _ -> + if Types.equal e.Tast.ty Types.Unit || Types.equal e.Tast.ty Types.Never + then begin + stmt f None e; + "undefined" + end + else begin + let t = fresh f in + line f "let %s;" t; + stmt f (Some t) e; + t + end + +(* An expression whose JS text has no side effect and no evaluation order to + respect. Used to decide whether an operand has to go through a temporary + before the next one is compiled. *) +and simple (e : Tast.expr) = + match e.Tast.e with + | Tast.Int _ | Tast.Float _ | Tast.Bool _ | Tast.Str _ | Tast.Unit + | Tast.Local _ | Tast.Global _ | Tast.None_ | Tast.FnAddr _ -> true + | Tast.Field (x, _) | Tast.CaseField (x, _, _) -> simple x + | _ -> false + +(* Evaluate into a name if the expression is not already one. *) +and spill f (e : Tast.expr) = + if simple e then value f e + else begin + let t = fresh f in + line f "const %s = %s;" t (value f e); + t + end + +(* A list of expressions, left to right, with the order preserved: once one of + them has to emit a statement, every operand before it is already in a + temporary, so nothing is re-ordered. *) +and evals f (es : Tast.expr list) = + let rec go acc = function + | [] -> List.rev acc + | [ last ] -> List.rev (value f last :: acc) + | e :: rest -> + let v = if simple e then value f e else spill f e in + go (v :: acc) rest + in + go [] es + +(* A value about to be bound to something that outlives the expression. See + the header's "Value semantics". *) +and bind_value f (e : Tast.expr) = + let v = value f e in + if fresh_value e then v + else match copy_of f.md e.Tast.ty v with Some c -> c | None -> v + +and call_args f (args : Tast.expr list) = + let vs = evals f args in + List.map2 + (fun (a : Tast.expr) v -> + if fresh_value a then v + else match copy_of f.md a.Tast.ty v with Some c -> c | None -> v) + args vs + +(* ── Primitives ─────────────────────────────────────────────────────── *) + +and prim f (e : Tast.expr) (p : Tast.prim) (args : Tast.expr list) = + let loc = e.Tast.loc in + match (p, args) with + | (Tast.Add | Tast.Sub | Tast.Mul | Tast.Div | Tast.Rem), [ x; y ] -> + arith f e p x y + | (Tast.Eq | Tast.Ne | Tast.Lt | Tast.Le | Tast.Gt | Tast.Ge), [ x; y ] -> + let op = + match p with + | Tast.Eq -> "===" | Tast.Ne -> "!==" | Tast.Lt -> "<" + | Tast.Le -> "<=" | Tast.Gt -> ">" | _ -> ">=" + in + (match evals f [ x; y ] with + | [ a; b ] -> Printf.sprintf "(%s %s %s)" a op b + | _ -> assert false) + | Tast.Not, [ x ] -> Printf.sprintf "(!%s)" (value f x) + | (Tast.BitAnd | Tast.BitOr | Tast.BitXor | Tast.Shl | Tast.Shr), [ x; y ] + -> bitwise f e p x y + | Tast.Len, [ x ] -> ( + match x.Tast.ty with + | Types.Array (n, _) -> Printf.sprintf "%Ld" n + | Types.String | Types.Slice _ -> Printf.sprintf "%s.n" (spill f x) + | t -> at loc "(len %s) is not in the JS dialect" (Types.to_string t)) + | Tast.At, target :: idx -> index f loc target idx + | Tast.Slice, [ target; lo; hi ] -> ( + match evals f [ target; lo; hi ] with + | [ t; a; b ] -> + let fn = match target.Tast.ty with + | Types.Array _ -> "$aslice" | _ -> "$slice" + in + Printf.sprintf "%s(%s, %s, %s, %s)" fn t a b (locstr loc) + | _ -> assert false) + | Tast.SliceFromPtr, _ -> + at loc + "(slice-from-ptr p n) is not in the JS dialect — it makes a slice out of \ + an address, and JavaScript has no addresses" + (* [Bytes] and [StrOfBytes] are reinterprets in every backend: a string is a + run of bytes here as it is there. See the header. *) + | (Tast.Bytes | Tast.StrOfBytes), [ x ] -> value f x + | Tast.BytesToF64, [ x ] -> Printf.sprintf "$tobytesf64(%s)" (value f x) + | Tast.BytesToI64, [ x ] -> Printf.sprintf "$tobytesi64(%s)" (value f x) + | Tast.F64ToBytes, [ x ] -> Printf.sprintf "$f64s(%s)" (value f x) + | (Tast.I64ToBytes | Tast.U64ToBytes), [ x ] -> + Printf.sprintf "$i64s(%s)" (value f x) + | Tast.EscapeBytes, [ x ] -> Printf.sprintf "$esc(%s)" (value f x) + | Tast.WriteStdout, [ x ] -> Printf.sprintf "$out(%s)" (value f x) + | Tast.Exit, [ x ] -> + Printf.sprintf "process.exit(Number(%s))" (value f x) + | Tast.Argv, [] -> "$argv()" + | Tast.Cast t, [ x ] -> cast f loc t x + | Tast.AddrOf, _ -> + at loc + "(addr-of x) is not in the JS dialect — JavaScript has no addresses" + | (Tast.SizeOf _ | Tast.AlignOf _), _ -> + at loc + "a size or an alignment is not in the JS dialect — an object has no \ + layout here, which is the whole point of the object mapping" + | Tast.Rt sym, _ -> + at loc + "%s is the type-erased container runtime, which is C in flan_rt.c — the \ + JS dialect has no counterpart for it yet" + sym + | _ -> at loc "this primitive is not in the JS dialect yet" + +(* [(at grid y x)] is one node with a list of indices, innermost last. Each + step but the last goes through a temporary, because the element it names is + the container the next step indexes. *) +and index f loc target idxs = + let tv = spill f target in + let step base (bty : Types.t) iv = + let fn = match bty with Types.Array _ -> "$aat" | _ -> "$at" in + if f.md.checks then Printf.sprintf "%s(%s, %s, %s)" fn base iv (locstr loc) + else + match bty with + | Types.Array _ -> Printf.sprintf "%s[%s]" base iv + | _ -> Printf.sprintf "%s.a[%s.o + %s]" base base iv + in + let rec go base bty = function + | [] -> base + | [ i ] -> step base bty (value f i) + | i :: rest -> + let one = step base bty (value f i) in + let t = fresh f in + line f "const %s = %s;" t one; + go t (elem_ty loc bty) rest + in + go tv target.Tast.ty idxs + +and arith f (e : Tast.expr) p x y = + let loc = e.Tast.loc in + let a, b = + match evals f [ x; y ] with [ a; b ] -> (a, b) | _ -> assert false + in + match e.Tast.ty with + | Types.Float k -> + let op = + match p with + | Tast.Add -> "+" | Tast.Sub -> "-" | Tast.Mul -> "*" + | Tast.Div -> "/" | _ -> "%" + in + fround k (Printf.sprintf "(%s %s %s)" a op b) + | Types.Int k -> + (match p with + | Tast.Add -> norm k (Printf.sprintf "%s + %s" a b) + | Tast.Sub -> norm k (Printf.sprintf "%s - %s" a b) + | Tast.Mul -> + if big k then norm k (Printf.sprintf "%s * %s" a b) + else + (* Math.imul is exact modulo 2^32 where a * b loses the low bits + once the product passes 2^53. Every kind below 32 bits is exact + in it too, since the mask that follows only keeps low bits. *) + norm k (Printf.sprintf "Math.imul(%s, %s)" a b) + | Tast.Div | Tast.Rem -> + let is_rem = p = Tast.Rem in + check_div f loc k ~is_rem a b; + if is_rem then norm k (Printf.sprintf "%s %% %s" a b) + else if big k then norm k (Printf.sprintf "%s / %s" a b) + else norm k (Printf.sprintf "Math.trunc(%s / %s)" a b) + | _ -> assert false) + | t -> at loc "arithmetic on %s" (Types.to_string t) + +(* The two tests emit.ml's [check_div] emits, with the same elisions: a + literal divisor that cannot be zero needs no zero test, and one that cannot + be -1 needs no overflow test. The sentences are flan_rt.c's, verbatim, + because the survey diffs stderr. *) +and check_div f loc (k : Types.ikind) ~is_rem a b = + if f.md.checks then begin + let zero_lit = if big k then "0n" else "0" in + let neg1 = if big k then "-1n" else "-1" in + let need_zero = not (b = zero_lit) in + let need_ovf = Types.signed k && b <> zero_lit in + if need_zero then + line f "if (%s === %s) $%s(%s, %s);" b zero_lit + (if is_rem then "remzero" else "divzero") + (locstr loc) a; + if need_ovf then begin + let lo = + let bits = Types.bits k in + if bits = 64 then "-9223372036854775808n" + else Printf.sprintf "%d" (-(1 lsl (bits - 1))) + in + line f "if (%s === %s && %s === %s) $divovf(%s, %s, %s, %s);" a lo b neg1 + (locstr loc) + (js_string (if is_rem then "%" else "/")) + a b + end + end + +and bitwise f (e : Tast.expr) p x y = + let a, b = + match evals f [ x; y ] with [ a; b ] -> (a, b) | _ -> assert false + in + let k = + match x.Tast.ty with + | Types.Int k -> k + | t -> at e.Tast.loc "a bitwise operation on %s" (Types.to_string t) + in + (* The count is masked to the operand's width, mirroring emit.ml's shift + arm. JS masks a 32-bit shift itself and has no 8- or 16-bit shift at + all, so the mask is written rather than relied on. *) + let b = + match p with + | Tast.Shl | Tast.Shr -> + if big k then Printf.sprintf "(%s & %dn)" b (Types.bits k - 1) + else Printf.sprintf "(%s & %d)" b (Types.bits k - 1) + | _ -> b + in + match p with + | Tast.BitAnd -> norm k (Printf.sprintf "%s & %s" a b) + | Tast.BitOr -> norm k (Printf.sprintf "%s | %s" a b) + | Tast.BitXor -> norm k (Printf.sprintf "%s ^ %s" a b) + | Tast.Shl -> norm k (Printf.sprintf "%s << %s" a b) + | Tast.Shr -> + if big k then norm k (Printf.sprintf "%s >> %s" a b) + else if Types.signed k then norm k (Printf.sprintf "%s >> %s" a b) + else + (* An unsigned value below 32 bits is already a non-negative number, so + >> and >>> agree on it; at 32 bits only >>> does. *) + norm k (Printf.sprintf "%s >>> %s" a b) + | _ -> assert false + +and cast f loc (t : Types.t) (x : Tast.expr) = + refuse_ty loc t; + let src = x.Tast.ty in + let v = value f x in + match (src, t) with + | Types.Int a, Types.Int b when a = b -> v + | (Types.Enum _ | Types.Int _), (Types.Enum _ | Types.Int _) -> + let bk = match t with Types.Int k -> Some k | _ -> None in + let sbig = match src with Types.Int k -> big k | _ -> false in + let dbig = match bk with Some k -> big k | None -> false in + let v = + if sbig && not dbig then Printf.sprintf "Number(%s)" v + else if dbig && not sbig then Printf.sprintf "BigInt(%s)" v + else v + in + (match bk with Some k -> norm k v | None -> Printf.sprintf "(%s | 0)" v) + | Types.Float _, Types.Float k -> fround k v + | (Types.Int _ | Types.Enum _), Types.Float k -> + let sbig = match src with Types.Int k -> big k | _ -> false in + fround k (if sbig then Printf.sprintf "Number(%s)" v else v) + | Types.Float _, Types.Int k -> + let t0 = fresh f in + line f "const %s = %s;" t0 v; + if f.md.checks then begin + let bits = Types.bits k in + let lo, hi = + if Types.signed k then + ( Printf.sprintf "-%s" (two_pow (bits - 1)), + Printf.sprintf "%s" (two_pow (bits - 1)) ) + else ("0", two_pow bits) + in + let ilo, ihi = + if Types.signed k then + (Printf.sprintf "-%s" (two_pow (bits - 1)), + Printf.sprintf "%s" (dec_sub1 (two_pow (bits - 1)))) + else ("0", dec_sub1 (two_pow bits)) + in + line f "if (!(%s >= %s && %s < %s)) $castrange(%s, %s, %s);" t0 lo t0 hi + (locstr loc) ilo ihi + end; + if big k then norm k (Printf.sprintf "BigInt(Math.trunc(%s))" t0) + else norm k (Printf.sprintf "Math.trunc(%s)" t0) + | Types.Bool, Types.Int k -> norm k (Printf.sprintf "(%s ? 1 : 0)" v) + | a, b -> + at loc "a cast from %s to %s is not in the JS dialect" + (Types.to_string a) (Types.to_string b) + +(* 2^n as a decimal literal, exact for every n a machine integer can need. *) +and two_pow n = + let rec go acc i = if i = 0 then acc else go (mul2 acc) (i - 1) in + go "1" n + +and mul2 s = + let n = String.length s in + let out = Bytes.make (n + 1) '0' in + let carry = ref 0 in + for i = n - 1 downto 0 do + let d = (Char.code s.[i] - 48) * 2 + !carry in + Bytes.set out (i + 1) (Char.chr (48 + (d mod 10))); + carry := d / 10 + done; + Bytes.set out 0 (Char.chr (48 + !carry)); + let s = Bytes.to_string out in + if s.[0] = '0' then String.sub s 1 n else s + +and dec_sub1 s = + let b = Bytes.of_string s in + let rec go i = + if i < 0 then () + else if Bytes.get b i = '0' then (Bytes.set b i '9'; go (i - 1)) + else Bytes.set b i (Char.chr (Char.code (Bytes.get b i) - 1)) + in + go (Bytes.length b - 1); + let s = Bytes.to_string b in + if String.length s > 1 && s.[0] = '0' then String.sub s 1 (String.length s - 1) + else s + +(* ── Statements ─────────────────────────────────────────────────────── *) + +and assign f dest v = + match dest with None -> line f "%s;" v | Some d -> line f "%s = %s;" d v + +and stmt f dest (e : Tast.expr) = + match e.Tast.e with + | Tast.Do body -> block f dest body + | Tast.Let (binds, body) -> + List.iter + (fun (slot, v) -> + let x = bind_value f v in + line f "%s = %s;" f.names.(slot) x) + binds; + block f dest body + | Tast.If (c, a, b) -> + let cv = value f c in + line f "if (%s) {" cv; + f.ind <- f.ind + 1; + stmt f dest a; + f.ind <- f.ind - 1; + line f "} else {"; + f.ind <- f.ind + 1; + stmt f dest b; + f.ind <- f.ind - 1; + line f "}" + | Tast.While (c, body, latch) -> + let d = List.length f.loops in + let lb = Printf.sprintf "$b%d" d and lc = Printf.sprintf "$c%d" d in + line f "%s: for (;;) {" lb; + f.ind <- f.ind + 1; + let cv = value f c in + line f "if (!(%s)) break %s;" cv lb; + (* The body is a labelled block and the latch is after it, so a continue + runs the latch — emit.ml's reason for the latch existing at all: a + [dotimes] step written in the body would be skipped by a continue and + the loop would never advance. *) + line f "%s: {" lc; + f.ind <- f.ind + 1; + f.loops <- (lb, lc) :: f.loops; + List.iter (fun x -> stmt f None x) body; + f.loops <- List.tl f.loops; + f.ind <- f.ind - 1; + line f "}"; + List.iter (fun x -> stmt f None x) latch; + f.ind <- f.ind - 1; + line f "}"; + (match dest with Some d -> line f "%s = undefined;" d | None -> ()) + | Tast.Break n -> + let lb, _ = List.nth f.loops n in + line f "break %s;" lb + | Tast.Continue n -> + let _, lc = List.nth f.loops n in + line f "break %s;" lc + | Tast.Return None -> + if Types.equal f.ret Types.Unit then line f "return;" + else line f "return %s;" (zero f.md e.Tast.loc f.ret) + | Tast.Return (Some v) -> + if Types.equal f.ret Types.Unit then begin + ignore (value f v); + line f "return;" + end + else line f "return %s;" (bind_value f v) + | Tast.Set (p, v) -> set f e.Tast.loc p v + | Tast.Match (s, arms) -> emit_match f dest s arms + | Tast.UnwrapSome v -> + let ov = spill f v in + line f "if (%s === null) return null;" ov; + assign f dest (Printf.sprintf "%s.v" ov) + | _ -> assign f dest (value f e) + +and block f dest (body : Tast.expr list) = + match body with + | [] -> (match dest with Some d -> line f "%s = undefined;" d | None -> ()) + | [ last ] -> stmt f dest last + | x :: rest -> + stmt f None x; + block f dest rest + +and set f loc (p : Tast.place) (v : Tast.expr) = + match p with + | Tast.Plocal i -> line f "%s = %s;" f.names.(i) (bind_value f v) + | Tast.Pglobal n -> line f "%s = %s;" (gvar n) (bind_value f v) + | Tast.Pfield (x, i) -> + let s = struct_of f.md loc x.Tast.ty in + let fl = List.nth s.Tast.fields i in + let t = spill f x in + line f "%s.%s = %s;" t (prop fl.Tast.fname) (bind_value f v) + | Tast.Pindex (x, idxs) -> + (* Every index but the last names the container the next one indexes, so + the walk down is [index]'s and only the final store is written here. *) + let tv = spill f x in + let rec walk base (bty : Types.t) = function + | [] -> assert false + | [ i ] -> + let iv = value f i in + let rhs = bind_value f v in + if f.md.checks then + let fn = match bty with Types.Array _ -> "$aset" | _ -> "$set" in + line f "%s(%s, %s, %s, %s);" fn base iv rhs (locstr loc) + else ( + match bty with + | Types.Array _ -> line f "%s[%s] = %s;" base iv rhs + | _ -> line f "%s.a[%s.o + %s] = %s;" base base iv rhs) + | i :: rest -> + let iv = value f i in + let fn = match bty with Types.Array _ -> "$aat" | _ -> "$at" in + let one = + if f.md.checks then + Printf.sprintf "%s(%s, %s, %s)" fn base iv (locstr loc) + else + match bty with + | Types.Array _ -> Printf.sprintf "%s[%s]" base iv + | _ -> Printf.sprintf "%s.a[%s.o + %s]" base base iv + in + let t = fresh f in + line f "const %s = %s;" t one; + walk t (elem_ty loc bty) rest + in + walk tv x.Tast.ty idxs + | Tast.Pderef _ -> + at loc + "a store through a pointer is not in the JS dialect — JavaScript has no \ + addresses" + +and emit_match f dest (s : Tast.expr) (arms : Tast.arm list) = + let sv = spill f s in + let opt = match s.Tast.ty with Types.Option _ -> true | _ -> false in + let payload = + match s.Tast.ty with Types.Option t -> Some t | _ -> None + in + let uname = + match s.Tast.ty with + | Types.Named n when Hashtbl.mem f.md.unions n -> Some n + | Types.Option _ -> None + | t -> at s.Tast.loc "match on %s" (Types.to_string t) + in + let first = ref true in + let closed = ref false in + List.iter + (fun (a : Tast.arm) -> + if not !closed then begin + (match a.Tast.acase with + | None -> + if !first then line f "{" else line f "} else {"; + closed := true + | Some c -> + let test = + if opt then + if String.equal c "Some" then + Printf.sprintf "%s !== null" sv + else Printf.sprintf "%s === null" sv + else Printf.sprintf "%s.case === %s" sv (js_string c) + in + if !first then line f "if (%s) {" test + else line f "} else if (%s) {" test); + first := false; + f.ind <- f.ind + 1; + (* The binds, in field order. Each one copies, for the reason in the + header: a bound field is a value in Flan. *) + List.iteri + (fun i slot -> + let name = f.names.(slot) in + match (opt, a.Tast.acase, uname) with + | true, _, _ -> + let t = Option.get payload in + let v = Printf.sprintf "%s.v" sv in + let v = + match copy_of f.md t v with Some c -> c | None -> v + in + line f "%s = %s;" name v + | false, Some c, Some n -> + let u = Hashtbl.find f.md.unions n in + (match Tast.case_index u c with + | Some (_, cs) -> + let fl = List.nth cs.Tast.vfields i in + let v = + Printf.sprintf "%s.%s" sv (prop fl.Tast.fname) + in + let v = + match copy_of f.md fl.Tast.fty v with + | Some cc -> cc + | None -> v + in + line f "%s = %s;" name v + | None -> at s.Tast.loc "no case %s" c) + | _ -> at s.Tast.loc "a default arm cannot bind fields") + a.Tast.binds; + block f dest a.Tast.abody; + f.ind <- f.ind - 1 + end) + arms; + if not !first then begin + if not !closed then + (* The checker proved exhaustiveness, so the fall-through is + unreachable; it is written out so that a wrong proof is a message and + not a silently-undefined value. *) + line f "} else { throw new Error(\"unreachable match\"); }" + else line f "}" + end + +(* ── A function ─────────────────────────────────────────────────────── *) + +(* Slot names come from [snames] where the source wrote one, which is most of + what makes the output readable. A name used by two different slots gets its + index appended, so the mapping stays injective. *) +let slot_names (fn : Tast.fn) = + let n = Array.length fn.Tast.slots in + let seen = Hashtbl.create 16 in + Array.iteri + (fun i s -> + match s with + | Some s when i < n -> + Hashtbl.replace seen s (1 + try Hashtbl.find seen s with Not_found -> 0) + | _ -> ()) + fn.Tast.snames; + Array.init n (fun i -> + match fn.Tast.snames.(i) with + | Some s when Hashtbl.find seen s = 1 -> ident s + | Some s -> Printf.sprintf "%s$%d" (ident s) i + | None -> Printf.sprintf "t$%d" i) + +let func m (fn : Tast.fn) = + List.iter (refuse_ty fn.Tast.floc) fn.Tast.params; + refuse_ty fn.Tast.floc fn.Tast.ret; + Array.iter (refuse_ty fn.Tast.floc) fn.Tast.slots; + if fn.Tast.fdefers <> [] then + at fn.Tast.floc + "a defer on the transfer path is not in the JS dialect yet — the \ + transfer channel is the conditions lane"; + let names = slot_names fn in + let f = + { md = m; b = Buffer.create 512; ind = 1; n = 0; names; + slots = fn.Tast.slots; ret = fn.Tast.ret; loops = [] } + in + let nparams = List.length fn.Tast.params in + (* Every slot that is not a parameter is declared once at the top, which is + what the frame is: the checker already numbered them and they do not + nest. *) + let locals = + Array.to_list (Array.sub names nparams (Array.length names - nparams)) + in + if locals <> [] then + line f "let %s;" (String.concat ", " locals); + let last = ref "undefined" in + List.iter (fun e -> last := value f e) fn.Tast.body; + if not (Types.equal fn.Tast.ret Types.Unit) + && not (Types.equal fn.Tast.ret Types.Never) + then begin + let tail = List.rev fn.Tast.body in + match tail with + | e :: _ when not (fresh_value e) -> ( + match copy_of m fn.Tast.ret !last with + | Some c -> line f "return %s;" c + | None -> line f "return %s;" !last) + | _ -> line f "return %s;" !last + end; + Buffer.add_string m.out + (Printf.sprintf "\nfunction %s(%s) {\n%s}\n" (fname fn.Tast.name) + (String.concat ", " + (List.mapi (fun i _ -> names.(i)) fn.Tast.params)) + (Buffer.contents f.b)) + +(* ── A whole program ────────────────────────────────────────────────── *) + +(* The per-struct copy, which is where the value-semantics rule is actually + spent. Recursive, because a field may be a struct or an array of them. *) +let copy_fn m name = + let field_copy (fl : Tast.field) = + let v = Printf.sprintf "v.%s" (prop fl.Tast.fname) in + let v = match copy_of m fl.Tast.fty v with Some c -> c | None -> v in + Printf.sprintf "%s: %s" (prop fl.Tast.fname) v + in + if Hashtbl.mem m.structs name then + let s = Hashtbl.find m.structs name in + Printf.sprintf "function %s$copy(v) { return { %s }; }\n" (ident name) + (String.concat ", " (List.map field_copy s.Tast.fields)) + else + let u = Hashtbl.find m.unions name in + let arm (c : Tast.variant) = + Printf.sprintf + " if (v.case === %s) return { case: v.case%s };\n" + (js_string c.Tast.vname) + (String.concat "" + (List.map + (fun (fl : Tast.field) -> ", " ^ field_copy fl) + c.Tast.vfields)) + in + Printf.sprintf "function %s$copy(v) {\n%s return { case: v.case };\n}\n" + (ident name) + (String.concat "" (List.map arm u.Tast.cases)) + +let program ?(checks = true) (p : Tast.program) : string = + let m = + { out = Buffer.create 8192; structs = Hashtbl.create 16; + unions = Hashtbl.create 8; checks; strs = []; nstr = 0; copies = [] } + in + List.iter + (fun (s : Tast.structure) -> Hashtbl.replace m.structs s.Tast.sname s) + p.Tast.structs; + List.iter + (fun (u : Tast.union) -> Hashtbl.replace m.unions u.Tast.uname u) + p.Tast.unions; + if p.Tast.externs <> [] then begin + let e = List.hd p.Tast.externs in + unsupported + "%s is a (declare-c ...) binding, and the JS dialect has no C boundary \ + — the FFI crosses through a generated C shim, which has nowhere to be \ + here" + e.Tast.ename + end; + if p.Tast.cshim <> [] then + unsupported + "this program generates a C shim, and the JS dialect has no C boundary"; + (* Globals first: an initialiser is a compile-time constant in this IR, so + there is no ordering question and no init-at-startup path. *) + let gbuf = Buffer.create 256 in + List.iter + (fun (g : Tast.global) -> + refuse_ty g.Tast.ginit.Tast.loc g.Tast.gty; + let f = + { md = m; b = Buffer.create 64; ind = 0; n = 0; names = [||]; + slots = [||]; ret = g.Tast.gty; loops = [] } + in + let v = value f g.Tast.ginit in + if Buffer.length f.b > 0 then + at g.Tast.ginit.Tast.loc + "the initialiser of %s is not a constant, which this IR does not \ + produce" + g.Tast.gname; + Buffer.add_string gbuf + (Printf.sprintf "%s %s = %s;\n" + (if g.Tast.gconst then "const" else "let") + (gvar g.Tast.gname) v)) + p.Tast.globals; + List.iter (fun fn -> func m fn) p.Tast.fns; + (* The copies are discovered while walking, so they are written after it and + hoisted above by JS's own function hoisting. *) + let copies = + String.concat "" (List.map (copy_fn m) (List.sort compare m.copies)) + in + let strs = + String.concat "" + (List.map + (fun (s, n) -> + if printable s then + Printf.sprintf "const %s = $str(%s);\n" n (js_string s) + else + Printf.sprintf "const %s = $bytes([%s]);\n" n + (String.concat ", " + (List.map (fun c -> string_of_int (Char.code c)) + (List.init (String.length s) (String.get s))))) + (List.rev m.strs)) + in + let has_main = + List.exists (fun (fn : Tast.fn) -> String.equal fn.Tast.name "main") + p.Tast.fns + in + let tail = + if has_main then + let mainfn = + List.find (fun (fn : Tast.fn) -> String.equal fn.Tast.name "main") + p.Tast.fns + in + if Types.equal mainfn.Tast.ret Types.Unit then + Printf.sprintf "\n%s();\nprocess.exit(0);\n" (fname "main") + else Printf.sprintf "\nprocess.exit(Number(%s()));\n" (fname "main") + else "" + in + runtime ^ "\n// ── string literals ──\n" ^ strs + ^ (if copies = "" then "" else "\n// ── value-semantics copies ──\n" ^ copies) + ^ (if Buffer.length gbuf = 0 then "" + else "\n// ── globals ──\n" ^ Buffer.contents gbuf) + ^ Buffer.contents m.out ^ tail