A Flan struct is a JS object, and the memory model does not come along

lib/js.ml lowers the same checked Tast the other two backends take to one
CommonJS file, by object mapping rather than linear memory: docs/DISCUSS.md
item 5 settled that fork before this was written, and item 5's consequence is
the whole shape of the file. Object mapping means the host's collector owns
every value, so there is no (Ptr T), no free, no arena and no allocator, and a
program that uses one is refused by name with a location rather than compiled
badly.

flan build --target=js leaves Build.executable through its own two lines,
before anything that assumes a clang: there is no object to compile and no
linker to run. --dev, --debug, --sanitize and --x86 are refused there rather
than swallowed. Js.Unsupported exits 3 beside X86.Unsupported, so a sweep can
count refused-by-name apart from did-not-compile.

What runs end to end: integer and float arithmetic with the normalisation each
width needs, let, if, while with break and continue, calls, function values,
structs, fixed arrays, slices, unions, options, match, and println through the
same structural printer the other backends walk.

Value semantics is the trap the object mapping sets and the reason the header
carries a section on it. A Flan struct and a fixed array copy on assignment and
a JS object does not, so every site emit.ml memcpys emits a generated
Point$copy here. Fable's JS backend faces the same question for F# structs and
answers it the other way -- it inserts no clone, and its Rust backend does --
so the divergence is deliberate and the survey pins it.
This commit is contained in:
Joseph Ferano 2026-09-17 21:50:32 +07:00
parent 8a430bb50c
commit dfb02ee26a
3 changed files with 1441 additions and 2 deletions

View File

@ -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 <file.flan> [-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 <header.h> [package.flan...] [clang flags...]\n\
\ flan generate-c <package-dir>\n\
\ flan build <file.flan> [-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 <file.flan> [args...]\n\
\ flan reload <program.flan> <forms.flan> [-o out.so] [--x86]\n\
\ flan dev <program.flan> [-s socket] [--x86]";

View File

@ -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. *)

1394
lib/js.ml Normal file

File diff suppressed because it is too large Load Diff