From fff4f5d985ff88f5d2df8a2b7d12a87c4010771c Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 11:41:24 +0700 Subject: [PATCH] One source, two outcomes: barf is refused on the web and says so MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit programs/web-files.flan is built for both targets from the same text and neither build reads the target anywhere in parse.ml or check.ml. On the desktop it writes the file and says so; in the browser barf signals a FileError the program handles, naming the file and reason 4, file-unsupported. The whole of the difference is one #ifdef in flan_rt.c, which is where the host ABI is already implemented twice. The web case is run under node rather than inspected. An artifact-shape assertion would say nothing about what decision 2 actually bought — that a program on the web is told its write did not happen instead of quietly losing it — so the test asserts the refusal is printed and that the desktop's success line is absent. A silent no-op would have taken that branch, which is the outcome the decision rules out by name. The same program embeds a file and prints it, because that is the half needing no filesystem and no host ABI: the line is identical on both targets and is the answer for assets a web build has to carry. --- test/programs/web-files.flan | 35 ++++++++++++++++++++++++++ test/test_acceptance.ml | 12 +++++++++ test/test_web.ml | 49 ++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 test/programs/web-files.flan diff --git a/test/programs/web-files.flan b/test/programs/web-files.flan new file mode 100644 index 0000000..dcf5263 --- /dev/null +++ b/test/programs/web-files.flan @@ -0,0 +1,35 @@ +;;;; The same source on both targets, which is the whole of decision 2. +;;;; +;;;; Flan has NO conditional compilation — nothing in parse.ml or check.ml +;;;; reads the target — so "isolate this to desktop" is not expressible here, +;;;; and a build-time refusal would therefore be unusable. A silent no-op is +;;;; worse than either, because that is how a save file disappears with nothing +;;;; said. So `barf` on the web signals a condition and the program decides, +;;;; which is the language having something Odin does not: Odin stubs its whole +;;;; file API on js/wasm to .Unsupported so that importing core:os "panics +;;;; cleanly", and a panic is not a decision. +;;;; +;;;; Built for the desktop this prints that the write worked. Built for the +;;;; browser it prints that it was refused, and says which file and why. One +;;;; source, two outcomes, no flag anywhere. + +(defn main [] i32 + ;; Embedding needs no filesystem and no host ABI, so this line is identical + ;; on both targets and is why decision 1 came first. + (print (embed "assets/a.txt" string)) + + (handler-bind + [(FileError [c] + (print "refused: ") + (print (.path c)) + (print " reason ") + (println (.reason c)) + (println (= (.reason c) file-unsupported)) + ;; There is no restart that means "give up and carry on" — spec- + ;; conditions.md §2 makes `error` diverging, and a handler that returns + ;; normally has not answered it. Leaving is the honest way out of a + ;; save that cannot happen. + (exit 0))] + (barf "web-files-out.txt" (bytes "state\n"))) + (println "wrote it") + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 4a748c9..18ddf93 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -488,6 +488,18 @@ let () = outputs ~opt:"-O0" "slurp and barf, -O0" "programs/slurp.flan" slurp_out; clean (); + (* The desktop half of the one program whose behaviour differs by target. + test_web.ml builds this same text for the browser and asserts the other + outcome: there `barf` signals and the program says which file it could + not write, here it writes it. No conditional compilation is involved in + either - nothing in parse.ml or check.ml reads the target, and the whole + of the difference is one #ifdef in flan_rt.c. Seeing both halves is what + makes the claim a test rather than an assertion. *) + (try Sys.remove "web-files-out.txt" with Sys_error _ -> ()); + outputs "files, the desktop half of the web case" "programs/web-files.flan" + "hello from a\nwrote it\n"; + (try Sys.remove "web-files-out.txt" with Sys_error _ -> ()); + (* A missing file with nothing handling it. The same rule StorageExhausted follows: [error] is the diverging variant, so the program stops on the frame that erred rather than carrying on with a Vec that was never diff --git a/test/test_web.ml b/test/test_web.ml index c1dea12..3f6b0e9 100644 --- a/test/test_web.ml +++ b/test/test_web.ml @@ -131,6 +131,55 @@ let () = end; cleanup probe; + (* ── Files on the web, NEXT.md decision 2 ───────────────────────── + The one case here whose *behaviour* differs from the desktop's, and it + differs with no conditional compilation anywhere: programs/web-files.flan + is built for both targets from the same text, and nothing in parse.ml or + check.ml has read the target. On the desktop it writes the file and says + so; here `barf` signals a FileError the program handles, naming the file + and the reason. The refusal lives in one #ifdef in flan_rt.c, which is + where the host ABI is already implemented twice. + + This is run rather than inspected. An artifact-shape assertion would say + nothing about the thing decision 2 actually bought — that a program on + the web is *told* its write did not happen instead of quietly losing it. + + `embed` is in the same program on purpose: it is the half that needs no + filesystem and no host ABI, so the same line works on both targets and + is the answer for assets a web build has to carry. *) + if not (have "node") then + print_endline "web: skipping the barf case (no node)" + else begin + let out = Filename.concat scratch "flan-web-files.html" in + (match web_build "programs/web-files.flan" out with + | exception Failure m -> fail "barf for the browser: %s" m + | () -> + let _, js, _ = parts out in + let log = Filename.concat scratch "flan-web-files.out" in + let code = + Sys.command + (Printf.sprintf "node %s > %s 2>&1" (Filename.quote js) + (Filename.quote log)) + in + let text = In_channel.with_open_bin log In_channel.input_all in + (try Sys.remove log with Sys_error _ -> ()); + (* The embed, byte for byte, out of the module's own data. *) + if not (contains text "hello from a") then + fail "the embedded file did not reach the web build: %S" text; + (* The refusal, naming the file and the reason, and reason 4 is + file-unsupported rather than a missing file or a denied one. *) + if not (contains text "refused: web-files-out.txt reason 4") + || not (contains text "true") then + fail "barf did not signal on the web: %S" text; + (* And the desktop's line is absent: a silent no-op would have taken + this branch, which is the outcome decision 2 rules out by name. *) + if contains text "wrote it" then + fail "barf reported success on the web: %S" text; + if code <> 0 then + fail "the web barf case exited %d: %S" code text; + cleanup out) + end; + (* ── raylib in the browser ──────────────────────────────────────── The claim BUILT.md left open. core-basic-window.flan is built for the web unchanged — no edit to its `until` loop, which is the whole point