From b14793517b2b8f3feb77092ededa9e043b809827 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 11 Sep 2026 19:43:35 +0700 Subject: [PATCH] The hash, asked of the second target and compared to the first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit sand-headless imports no raylib so that it can run here, and the point of the case is not that a module exists — it is that the number matches native byte for byte, which is only possible because rand-f32 is Flan's rather than libc's. It does, at -O2 and at -O0; -O0 is the cheap way to say the agreement is not a coincidence of how LLVM folded the float arithmetic. values and machine run there too, which is where a 32-bit pointer would have shown. Four separate things can be missing — clang's wasm target, the sysroot, the builtins, a WASI runtime — so the skip is a probe rather than a lookup: build the smallest program and run it, and print what went wrong. A which(1) would go red on the machine where Node is too old, with a reason nobody could read. No wasmtime and no wasmer here, so the runner is node:wasi, with wasmtime and wasmer preferred if either appears. --no-warnings because node:wasi writes to stderr on every run and this harness compares combined output. --- test/dune | 3 ++ test/test_acceptance.ml | 95 +++++++++++++++++++++++++++++++++++++++++ test/wasm-run.mjs | 23 ++++++++++ 3 files changed, 121 insertions(+) create mode 100644 test/wasm-run.mjs diff --git a/test/dune b/test/dune index 6830b44..4581ad1 100644 --- a/test/dune +++ b/test/dune @@ -15,6 +15,9 @@ (glob_files programs/*.flan) ; The reload primitive's host: a C main that dlopens what Build.shared made. (file reload_host.c) + ; The WASI host the wasm32 case runs its module under, when no wasmtime or + ; wasmer is installed. + (file wasm-run.mjs) ; test_dev runs the compiler itself: flan dev launches and owns a program. (file %{workspace_root}/bin/main.exe) ; The Emacs client, which test_emacs drives against a real daemon. diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index a3c5e84..7cce392 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -325,6 +325,101 @@ let () = print_endline "FAIL --no-bounds-checks: a check survived" end; + (* ── wasm32 (NEXT.md, deferred item 6) ────────────────────────────── + The second target, and the reason sand-headless imports no raylib. What + is asserted is not that a wasm module exists — it is that it prints the + *same hash* as the native build, byte for byte. That is only possible + because rand-f32 is written in Flan rather than bound to libc, so the + case is the regression test for that decision as much as for the port. + + Four independent things can be absent — clang's wasm target, the + wasi-libc sysroot, a builtins archive, and a runtime that speaks WASI — + so the skip is a *probe*: build the smallest program and run it. A + [which] would go red on the machine where Node is too old, with a + reason nobody could read. *) + let wasm_runner = + if Sys.command "command -v wasmtime > /dev/null 2>&1" = 0 then + Some "wasmtime" + else if Sys.command "command -v wasmer > /dev/null 2>&1" = 0 then + Some "wasmer run" + else if Sys.command "command -v node > /dev/null 2>&1" = 0 then + (* --no-warnings because node:wasi prints an ExperimentalWarning to + stderr on every run, and this harness compares combined output. *) + Some "node --no-warnings wasm-run.mjs" + else None + in + let wasm_build ?(opt = "-O2") path out = + let l = Load.program ~file:path (Parse.program (Reader.read_file path)) in + let p = Check.program l.Load.decls in + ignore + (Build.executable + ~opts:{ Build.default with opt; target = Some "wasm32-wasi" } + ~csrcs:l.Load.csrcs ~lflags:l.Load.lflags p ~out) + in + let wasm_run runner wasm = + let out = Filename.concat scratch "flan-acceptance-wasm.out" in + let code = + Sys.command + (Printf.sprintf "%s %s > %s 2>&1" runner (Filename.quote wasm) + (Filename.quote out)) + in + let text = In_channel.with_open_bin out In_channel.input_all in + (try Sys.remove out with Sys_error _ -> ()); + (code, text) + in + (match wasm_runner with + | None -> + print_endline + "acceptance: skipping the wasm32 case (no wasmtime, wasmer or node)" + | Some runner -> + let probe = Filename.concat scratch "flan-wasm-probe.wasm" in + let outcome = + match wasm_build "programs/unit-main.flan" probe with + | () -> + let code, text = wasm_run runner probe in + if code = 0 && text = "ok\n" then Ok () + else + Error + (Printf.sprintf "%s could not run it: %S (exit %d)" runner text + code) + | exception Failure m -> Error m + in + (try Sys.remove probe with Sys_error _ -> ()); + (match outcome with + | Error why -> + Printf.printf "acceptance: skipping the wasm32 case (%s)\n" why + | Ok () -> + let wasm_case name ?opt path expected = + let wasm = + Filename.concat scratch + ("flan-w-" ^ Filename.remove_extension (Filename.basename path) + ^ ".wasm") + in + wasm_build ?opt path wasm; + let code, text = wasm_run runner wasm in + if text <> expected || code <> 0 then begin + incr failures; + Printf.printf + "FAIL %s\n got: %S (exit %d)\n wanted: %S (exit 0)\n" + name text code expected + end; + (try Sys.remove wasm with Sys_error _ -> ()) + in + (* The hash, which must equal the native one above. At both levels: + agreement at -O2 alone could be a coincidence of how LLVM folded + the float arithmetic, and -O0 is the cheap way to say it is not. *) + wasm_case "sand, headless, wasm32" "programs/sand-headless.flan" + sand_out; + wasm_case "sand, headless, wasm32, -O0" ~opt:"-O0" + "programs/sand-headless.flan" sand_out; + (* And the two fixed-output programs, which between them cover the + milestone-2 surface: globals, 2-D arrays, places through a + pointer, casts and match. A 32-bit pointer is the thing most + likely to go wrong and these are where it would show. *) + wasm_case "value semantics, wasm32" "programs/values.flan" values_out; + wasm_case "machine surface, wasm32" "programs/machine.flan" + machine_out)); + if !failures = 0 then print_endline "acceptance: all tests passed" else begin Printf.printf "\n%d failure(s)\n" !failures; diff --git a/test/wasm-run.mjs b/test/wasm-run.mjs new file mode 100644 index 0000000..5d4d5d4 --- /dev/null +++ b/test/wasm-run.mjs @@ -0,0 +1,23 @@ +// A WASI host, so the acceptance table can run a wasm32 build without a +// wasmtime or wasmer being installed. Node has had node:wasi since 16; it is +// still flagged experimental, hence --no-warnings at the call site, and before +// Node 20 it needs --experimental-wasi-unstable-preview1 as well. The +// acceptance test probes rather than assumes: if this file cannot run the +// module, the wasm case is skipped with what went wrong. +// +// preopens is empty on purpose. sand-headless reads nothing and writes one +// line to stdout, which is the whole point of it being the cross-target case. +import { WASI } from 'node:wasi'; +import { readFile } from 'node:fs/promises'; + +const [, , file, ...rest] = process.argv; +const wasi = new WASI({ + version: 'preview1', + args: [file, ...rest], + env: {}, + preopens: {}, + returnOnExit: true, +}); +const mod = await WebAssembly.compile(await readFile(file)); +const inst = await WebAssembly.instantiate(mod, wasi.getImportObject()); +process.exitCode = wasi.start(inst);