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