A whole program goes through the hand-written backend and runs

x86.ml was an encoder and a frame model with nothing calling it. It now
lowers a whole Tast.program to an assembly file, and `flan build --x86`
hands that file to the same clang invocation the LLVM path uses, against
the same runtime objects. The flag is off by default; LLVM stays the
release backend and the default one.

Three programs, built both ways and compared by what they print and what
they exit with rather than by reading bytes: exit 0; a dotimes that
prints; and a fizz over a call, an if, a remainder and two string
literals. All three agree with the LLVM build.

The measurement decided the target. hist.ml over the fizz program shows
no Signal, no Handled, no RestartCase — a loop that prints does not drag
conditions in. What does is the bounds check and the allocator, and
neither is in the reachable set of a program that prints a number.

That is why there is no transfer guard here, and check_no_transfer is
what makes the omission sound rather than hopeful: if nothing reachable
can write the channel, no call can return with it set. It is a
whole-program property, so it is checked once per build and the build
stops with the node's name when it fails.
This commit is contained in:
Joseph Ferano 2026-09-13 14:56:14 +07:00
parent 68625a535e
commit 2155c41465
6 changed files with 1142 additions and 16 deletions

View File

@ -100,8 +100,16 @@ let sanitize_flag = "--sanitize"
preference, and it goes away with the transport it drives. *)
let two_process_flag = "--two-process"
(* The hand-written x86-64 backend (lib/x86.ml) instead of LLVM. The dev
backend from DISCUSS.md item 15, off by default and named explicitly:
LLVM stays the release path and the default one. It covers a subset of the
IR and refuses the rest by name, so a build that succeeds is one it really
compiled. *)
let x86_flag = "--x86"
let flags =
[ no_checks_flag; dev_flag; debug_flag; sanitize_flag; two_process_flag ]
[ no_checks_flag; dev_flag; debug_flag; sanitize_flag; two_process_flag;
x86_flag ]
(* [--target=wasm32-wasi] and [--target=web], the two cross targets. Unlike
the flags above, a target
@ -422,6 +430,7 @@ let () =
let dev = List.mem dev_flag rest in
let debug = List.mem debug_flag rest in
let sanitize = List.mem sanitize_flag rest in
let x86 = List.mem x86_flag rest in
let target = target_of rest in
let out =
match List.filter (fun a -> not (is_flag a)) rest with
@ -453,7 +462,7 @@ let () =
let p, csrcs, lflags = Flan.Reach.link ~dev l p in
ignore (Flan.Build.executable
~opts:{ Flan.Build.default with checks; dev; debug; sanitize;
target }
target; x86 }
~csrcs ~lflags ~pnames:(if debug then param_names l else [])
p ~out))
(* The daemon an editor talks to: one session, the program it belongs to
@ -536,7 +545,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] [--target=wasm32-wasi|web]\n\
[--debug] [--sanitize] [--x86] [--target=wasm32-wasi|web]\n\
\ flan run <file.flan> [args...]\n\
\ flan reload <program.flan> <forms.flan> [-o out.so]\n\
\ flan dev <program.flan> [-s socket]";

View File

@ -147,6 +147,13 @@ type opts = {
language's arithmetic means; without the exclusion every program
trips on its first [+]. Nothing else is excluded. *)
sanitize : bool;
(* The dev backend: lower the typed IR to x86-64 assembly here instead of
handing LLVM IR to clang. Off by default and off everywhere but the one
flag that asks for it LLVM stays the release backend and the default
one. It refuses rather than degrades: a program holding a node [x86.ml]
does not lower yet stops the build with that node's name, so a build that
succeeds is one this backend really compiled. *)
x86 : bool;
}
(* Checks are deliberately independent of [opt]: the acceptance table runs the
@ -155,7 +162,7 @@ type opts = {
checks. Dropping them is a release decision, not an optimisation one. *)
let default =
{ target = None; opt = "-O2"; keep = false; checks = true; dev = false;
debug = false; sanitize = false }
debug = false; sanitize = false; x86 = false }
(* The flags that are neither [opt] nor the target, spelled once so that the
compile command and the object-cache key cannot disagree. They did before:
@ -725,11 +732,26 @@ let executable ?(opts = default) ?(csrcs = []) ?(lflags = []) ?(pnames = [])
does not do this: see [opts]. *)
let opts = if opts.debug then { opts with opt = "-O0" } else opts in
let tflags = target_flags opts in
if opts.x86 && (wasm_target opts || opts.dev || opts.debug || opts.sanitize)
then
failwith
"--x86 is the native dev backend on its own: it emits no DWARF, has no \
indirection cells for a REPL to redefine through, and there is no \
sanitizer pass over hand-written assembly";
let dir = workdir () in
let ll = Filename.concat dir (Filename.basename out ^ ".ll") in
(* The one fork in this function. The x86 backend hands clang an assembly
file where LLVM hands it IR text; clang takes either on its command line,
so everything past this point the runtime objects, the shim, the
package C, the linker arguments is the same build. *)
let ll =
Filename.concat dir
(Filename.basename out ^ if opts.x86 then ".s" else ".ll")
in
write ll
(Emit.program ~checks:opts.checks ~dev:opts.dev ~debug:opts.debug ~pnames
~sanitize:opts.sanitize p);
(if opts.x86 then X86.program p
else
Emit.program ~checks:opts.checks ~dev:opts.dev ~debug:opts.debug ~pnames
~sanitize:opts.sanitize p);
(* [flan_dev.c] is compiled into every build, not only a dev one. Nothing in
a release build calls into it the compiler only emits a registry lookup
for a name the host was not built with, which cannot arise without cells

1095
lib/x86.ml

File diff suppressed because it is too large Load Diff

2
spike/x86/p1-exit.flan Normal file
View File

@ -0,0 +1,2 @@
(defn main [] i32
0)

View File

@ -0,0 +1,5 @@
(defn main [] i32
(dotimes [i 5]
(print i)
(println ""))
0)

11
spike/x86/p3-fizz.flan Normal file
View File

@ -0,0 +1,11 @@
(defn fizz? [n i32] bool
(= 0 (% n 3)))
(defn main [] i32
(dotimes [i 15]
(let [n (+ i 1)]
(if (fizz? n)
(print "fizz")
(print n))
(println "")))
0)