The hash, asked of the second target and compared to the first
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.
This commit is contained in:
parent
48a7186c58
commit
b14793517b
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
23
test/wasm-run.mjs
Normal file
23
test/wasm-run.mjs
Normal file
@ -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);
|
||||
Loading…
x
Reference in New Issue
Block a user