flan/lib/js.ml
Joseph Ferano 3e68089cde A parameter with no type is dyn, decided where the type names are all known
The type itself, the ABI its operations call into, and the one decision the
feature could not avoid: (defn f [x y]) is one parameter or two, and which one
depends on whether y names a type.

Parse does not decide it. That lookup is the one its defn comment records being
removed for being wrong twice in one day -- the set of type names is incomplete
at parse time by construction, and macros generating definitions is what
widened the failure. So the vector is carried undecided, as Ast.pitems, and
paired in Check, after every file is loaded, every macro expanded and every
header imported. The set is complete there. It is not complete across time, and
the comment says so: a defstruct written later changes a signature with no edit
to the function.

The return slot stays mandatory and dyn is written out in it. The ambiguity
there has no syntactic resolution at all -- a capitalised head in a list is both
a type application and a struct literal -- so the third state the parameters
needed does not exist for the return type, and ret = None goes on meaning Unit.

What the feature costs, and what is taken back: a slot with no type used to be a
syntax error, so a mistyped type now reads as an extra parameter with no
diagnostic. A name within one edit of a type's gets the resolver's own
did-you-mean, and an unknown capitalised name is reported as the unknown type it
is -- not one parameter in the corpus is capitalised. A lowercase name
resembling no type is the feature working, and is the residual.

The x86 backend refuses dyn by name; both callers already name --llvm, and the
daemon takes that backend by default, so this is the first thing a user of dyn
sees. The JS dialect refuses it too, for the opposite reason -- every value
there is already dynamic and what is missing is only the lowering.

runtime/flan_dyn.h is the fixed ABI. flan_dyn_stub.c stands in until the real
collector lands and says in its header that it verifies nothing about roots.
2026-09-19 05:47:49 +07:00

1549 lines
65 KiB
OCaml

(** 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],
[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.
{1 Where this stops, and what the next lane picks up}
[spike/js/survey.sh] is the standing measurement: 24 MATCH, 0 DIFFER, 77
refused by name, 0 that node would not run, over the corpus and this
file's own two probes. What the refusals say about the order to work in:
- {b [Vec], and with it the type-erased container runtime.} The largest
single bucket after the FFI. A [Vec] reaches this file as
[Prim (Rt "flan_vec_push", ...)] over an [AddrOf] and an [Allocator],
so it needs two things that are refused on purpose today: a {e named
table} of [flan_vec_*] symbols answered over a JS array, and a narrow
hole in [AddrOf] for the arguments those take. Both refusals are
load-bearing and neither should be widened further than that. A [Vec] is
move-only and owns its storage, so a JS array aliases correctly and
needs none of the copying above.
- {b The dev loop.} docs/DISCUSS.md item 5's unexpected upside: evaluating
new code is the one thing JavaScript makes easy, and wasm cannot have
this at all. [Build.executable] refuses [--dev] here in its own words
and that refusal is where the lane starts.
- {b Interop}, which is what the whole target is for: a [declare-c]
binding has no meaning here, but a [declare-js] would, and 14 of the 77
refusals are the agent package's C.
- {b Conditions} are a lane and not a stub. [fdefers] is dropped below on
the grounds that nothing can start a transfer; the first thing that can
makes that note wrong, and it says so where it is. *)
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"
(* The irony is not lost: JavaScript is the one target where a dyn value
needs no boxing at all, because every value there is already one. What is
missing is not the representation but the lowering — dyn ops are calls
into runtime/flan_dyn.h, and this dialect has no such runtime. It is a
lane, not a difficulty. *)
| Types.Dyn ->
at loc
"dyn is not in the JS dialect yet — every JavaScript value is already \
dynamic, so this is a matter of lowering the dyn operations onto the \
host's own, and that work has not been done"
| 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 }; }
// Uint8Array.from rather than a view onto the Buffer: node pools small
// Buffers inside one ArrayBuffer, so a view would let a write through one
// string literal's slice reach the bytes of another. Native puts a literal in
// .rodata, where a write is a fault; this is the nearest thing to that which
// costs nothing.
function $str(s) { const b = Buffer.from(s, "utf8"); const a = Uint8Array.from(b); return $view(a, 0, a.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;
datas : (string, Tast.data) Hashtbl.t; (* the tagged sums, defdata *)
cunions : (string, unit) Hashtbl.t; (* C's untagged unions, refused *)
checks : bool;
mutable strs : (string * string) list; (* literal -> its const name *)
mutable nstr : int;
mutable copies : string list; (* struct names needing a $copy *)
mutable tags : string list; (* union names needing a $tag *)
}
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.datas 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
(* [v] is named once, through the parameter: spelling it twice would
evaluate the argument twice, and the argument may be a checked index
whose own subexpression has an effect. *)
| Some c ->
Some
(Printf.sprintf
"((o) => (o === null ? null : { v: ((x) => %s)(o.v) }))(%s)" 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.
[s] is parenthesised first, and that is not decoration: [>>>] binds tighter
than [&], [|] and [^] in JavaScript, so [a & b >>> 0] is [a & (b >>> 0)] and
a [bit-and] on a [u32] would answer -1 where the other backends answer
4294967295. The probe caught exactly that. *)
let norm (k : Types.ikind) s =
let s = "(" ^ s ^ ")" in
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
| _ ->
(* The parentheses are not decoration: an arrow whose body starts with
a brace is parsed as a block, so [() => { x: 0 }] is a label and a
syntax error rather than an object. *)
Printf.sprintf "Array.from({ length: %Ld }, () => (%s))" n
(zero m loc e))
| Types.Named n when Hashtbl.mem m.cunions n ->
at loc
"%s is an untagged union, which is not in the JS dialect — its members \
share one run of bytes and two properties of an object cannot. A \
defdata carries its case and does map"
n
| 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.datas 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.datas 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 ─────────────────────────────────────────── *)
(* C's untagged union, which arrives as a [structure] whose every field sits
at offset zero. Two fields sharing one run of bytes is a layout, and an
object has none here — so it is named rather than met as "not a struct". *)
let refuse_cunion m loc n =
if Hashtbl.mem m.cunions n then
at loc
"%s is an untagged union, which is not in the JS dialect — its members \
share one run of bytes and two properties of an object cannot. A \
defdata carries its case and does map"
n
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
| Types.Named n ->
refuse_cunion m loc n;
at loc "a field of %s, which is not a struct" (Types.to_string t)
| 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))
(* [Field] is a struct's field almost everywhere, and [Render] is the
exception: the structural printer reads an [Option]'s tag as field 0 and
its payload as field 1, and a union's tag as field 0, because that is the
LLVM layout. Neither shape exists here — an Option is null-or-a-box and a
union carries its case by name — so both are answered rather than
refused, and a union's tag goes through a generated [Shape$tag] that maps
the name back to the declaration order [Tast.case_index] fixed. *)
| Tast.Field (x, i) -> (
match x.Tast.ty with
| Types.Option _ ->
let v = spill f x in
if i = 0 then Printf.sprintf "(%s === null ? 0 : 1)" v
else Printf.sprintf "%s.v" v
| Types.Named n when Hashtbl.mem f.md.datas n ->
if i <> 0 then
at e.Tast.loc "field %d of the union %s, which has only a tag" i n;
if not (List.mem n f.md.tags) then f.md.tags <- n :: f.md.tags;
Printf.sprintf "%s$tag(%s)" (ident n) (spill f x)
| _ ->
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.datas 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.datas 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
(* The location on the check is the *index expression's*, not the form's:
emit.ml's [element_addr] passes [i.Tast.loc] to [check_at], and the
survey diffs stderr, so a bounds message that names a different column is
a DIFFER. [slice] is the other way round and uses the form's, which is
why the two are not shared. *)
let step base (bty : Types.t) (i : Tast.expr) 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 i.Tast.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 i (value f i)
| i :: rest ->
let one = step base bty i (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
if sbig && not dbig then
(* The truncation has to happen in BigInt and not after: a 64-bit value
through Number() is rounded to 53 bits first, so (i32 i64hi) would
answer 0 where every other backend answers -1. *)
let k = match bk with Some k -> k | None -> Types.I32 in
Printf.sprintf "Number(BigInt.as%sN(%d, %s))"
(if Types.signed k then "Int" else "Uint")
(Types.bits k) v
else
let v = 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
(* A form whose value is discarded and whose text is [undefined] did
nothing; emitting it as a statement is noise in the output and nothing
else. *)
| None -> if not (String.equal v "undefined") then 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 i.Tast.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 i.Tast.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.datas 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.datas 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;
(* [fdefers] is the same defer list again, for the *transfer* exit path.
Nothing in this dialect can start a transfer — signal, handler-bind,
restart-case and invoke-restart are all refused above, and a bounds or
arithmetic failure here dies where the native runtime would signal — so
that path is unreachable and the list is dropped rather than emitted. The
defers on the ordinary path are already spliced into [body] and do run.
This is the one place a refusal would have been wrong: it would refuse
every program that writes a plain [defer]. *)
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);
(* The body is a list of forms and the last one is the return value — the
same rule [emit.ml] follows. Every form before it is a statement, and a
statement it is not is the bug this used to have: a unit-typed call
compiled to an expression nobody emitted, so [(load-xs)] silently did not
happen. A Unit function's body may end on a form of any type; the value is
discarded there, exactly as emit.ml discards it. *)
let void =
Types.equal fn.Tast.ret Types.Unit || Types.equal fn.Tast.ret Types.Never
in
let n = List.length fn.Tast.body in
List.iteri
(fun i (e : Tast.expr) ->
if i < n - 1 || void then stmt f None e
else
match e.Tast.e with
| Tast.Do _ | Tast.Let _ | Tast.If _ | Tast.While _ | Tast.Match _
| Tast.UnwrapSome _ | Tast.Set _ | Tast.Return _ | Tast.Break _
| Tast.Continue _ ->
let t = fresh f in
line f "let %s;" t;
stmt f (Some t) e;
(match copy_of m fn.Tast.ret t with
| Some c -> line f "return %s;" c
| None -> line f "return %s;" t)
| _ -> line f "return %s;" (bind_value f e))
fn.Tast.body;
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. *)
(* A union's declaration order is its tag ([Tast.case_index]), and that order
is a union's contract — an all-bytes-zero union is its first case. The
printer asks for the number; the value carries the name. *)
let tag_fn m name =
let u = Hashtbl.find m.datas name in
let arms =
List.mapi
(fun i (c : Tast.variant) ->
Printf.sprintf "v.case === %s ? %d : " (js_string c.Tast.vname) i)
u.Tast.cases
in
Printf.sprintf "function %s$tag(v) { return %s-1; }\n" (ident name)
(String.concat "" arms)
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.datas 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;
datas = Hashtbl.create 8; cunions = Hashtbl.create 4; checks;
strs = []; nstr = 0; copies = []; tags = [] }
in
List.iter
(fun (s : Tast.structure) -> Hashtbl.replace m.structs s.Tast.sname s)
p.Tast.structs;
List.iter
(fun (u : Tast.data) -> Hashtbl.replace m.datas u.Tast.dname u)
p.Tast.datas;
(* C's untagged unions, which arrive as [structure] values whose every field
is at offset zero. That is a layout, and a layout is the one thing an
object has none of here: two fields sharing storage cannot be two
properties. Registered so the refusal can name the type rather than
failing as "not a struct". *)
List.iter
(fun (u : Tast.structure) -> Hashtbl.replace m.cunions u.Tast.sname ())
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. A constant initialiser is a value this IR can spell, and it
is the only kind that gets here: the language runs a computed one from a
function [main] calls at startup, and this dialect has grown no equivalent
— so one is refused below rather than silently started as zero. The
ordering question the startup path answers is therefore not one this file
has. *)
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 computed, and the JS dialect has no \
init-at-startup path to run it from"
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))
^ String.concat "" (List.map (tag_fn m) (List.sort compare m.tags))
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
(* [main] takes the command line or nothing, which is the whole of the
entry point's contract. node's own argv starts at the interpreter, so
the slice starts at the script — which is where a native argv[0] is
the program. *)
let arg = if mainfn.Tast.params = [] then "" else "$argv()" in
if Types.equal mainfn.Tast.ret Types.Unit then
Printf.sprintf "\n%s(%s);\nprocess.exit(0);\n" (fname "main") arg
else
Printf.sprintf "\nprocess.exit(Number(%s(%s)));\n" (fname "main") arg
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