From 8a175ebec57f1c8b7cc4a30f072819d1b1c46346 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 11 Sep 2026 19:48:04 +0700 Subject: [PATCH] Read wasi-sdk's version instead of guessing it, and pin the one ABI path left MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The wasi-sdk candidate had an LLVM version in it, which moves release to release — so the path advertised as the proper article would have matched only by coincidence, while the emscripten one beside it was derived. Both are derived now. calc-me on wasm32 covers what the other three cases cannot: flan_argv hands Flan an array of flan_slice built in C, so what it pins is the element stride of a ptr+len pair — 16 bytes native, 12 on wasm32 — rather than a field offset. It is also the claim in this file's own header, that the table runs on the second target, honoured for the first time. flan emit refuses --target rather than stripping it. The IR really is target-free, so ignoring it is correct and silence about it is not. --- bin/main.ml | 9 +++++++++ lib/build.ml | 13 ++++++++++++- test/dune | 8 ++++---- test/test_acceptance.ml | 20 +++++++++++++++----- 4 files changed, 40 insertions(+), 10 deletions(-) diff --git a/bin/main.ml b/bin/main.ml index 272c564..07cc208 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -100,6 +100,15 @@ let () = (Flan.Types.to_string f.ret) (Array.length f.slots)) p.fns)) files + (* The IR is target-independent — [Emit] writes no triple and no datalayout, + which is what lets one .ll serve both targets — so there is nothing for a + target to change here. Refused rather than accepted and ignored: silently + swallowing a flag is the shape the house rule exists to prevent. *) + | _ :: "emit" :: args when target_of args <> None -> + prerr_endline + "flan emit: --target is refused — the emitted IR carries no triple and \ + no datalayout, and the target is chosen at build."; + exit 2 | _ :: "emit" :: args when List.exists (fun a -> not (is_flag a)) args -> let checks = not (List.mem no_checks_flag args) in let dev = List.mem dev_flag args in diff --git a/lib/build.ml b/lib/build.ml index 37f5ea1..0f349e8 100644 --- a/lib/build.ml +++ b/lib/build.ml @@ -107,7 +107,18 @@ let wasm_sysroot () = links and runs, and the sand hash matches native byte for byte, but a session reading this should know the joint is glued. *) let wasm_builtins_candidates () = - [ "/opt/wasi-sdk/lib/clang/20/lib/wasm32-unknown-wasi/libclang_rt.builtins.a" ] + (* wasi-sdk's own resource directory, whichever LLVM that release bundled — + the version is in the path and moves release to release, so it is read + rather than guessed. Same rule as [clang_resource_dir]. *) + (let root = "/opt/wasi-sdk/lib/clang" in + match Sys.readdir root with + | vs -> + Array.sort compare vs; + Array.to_list vs + |> List.map (fun v -> + Filename.concat root + (Filename.concat v "lib/wasm32-unknown-wasi/libclang_rt.builtins.a")) + | exception Sys_error _ -> []) @ (match on_path "emcc" with | None -> [] | Some e -> diff --git a/test/dune b/test/dune index 4581ad1..738c770 100644 --- a/test/dune +++ b/test/dune @@ -15,10 +15,10 @@ (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. - (glob_files %{workspace_root}/emacs/*.el))) + (glob_files %{workspace_root}/emacs/*.el) + ; The WASI host the wasm32 case runs its module under, when no wasmtime or + ; wasmer is installed. + (file wasm-run.mjs))) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 7cce392..087ceda 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -356,11 +356,12 @@ let () = ~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 wasm_run ?arg 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) + (Printf.sprintf "%s %s %s > %s 2>&1" runner (Filename.quote wasm) + (match arg with None -> "" | Some a -> Filename.quote a) (Filename.quote out)) in let text = In_channel.with_open_bin out In_channel.input_all in @@ -389,14 +390,14 @@ let () = | Error why -> Printf.printf "acceptance: skipping the wasm32 case (%s)\n" why | Ok () -> - let wasm_case name ?opt path expected = + let wasm_case name ?opt ?arg 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 + let code, text = wasm_run ?arg runner wasm in if text <> expected || code <> 0 then begin incr failures; Printf.printf @@ -418,7 +419,16 @@ let () = 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)); + machine_out; + (* calc-me, for the one host-ABI path the three above do not touch: + [flan_argv] builds an array of flan_slice in C and Flan indexes it + as [string], so what is pinned here is the element *stride* of a + ptr+len pair, which is 16 bytes native and 12 on wasm32 — not a + field offset, and nothing else in the table reaches it. This is + also the file header's own claim, that the table runs on wasm32 + too, honoured for the first time. *) + wasm_case "calc-me, wasm32" "../calc-me.flan" + ~arg:"1 + 2 * (3 - 0.5) / 2" "3.5\n")); if !failures = 0 then print_endline "acceptance: all tests passed" else begin