diff --git a/CPPVARIABLES.bzl b/CPPVARIABLES.bzl index 432cfb57..6325f45a 100644 --- a/CPPVARIABLES.bzl +++ b/CPPVARIABLES.bzl @@ -59,7 +59,12 @@ DDS_CPPOPTS = select({ "-Wpedantic", "-Wall", "-Werror", + # -fexceptions must precede -fwasm-exceptions: the toolchain's default + # -fno-exceptions is otherwise not overridden at the clang frontend + # level by -fwasm-exceptions alone (it only selects the EH lowering + # mechanism, not the "exceptions enabled" toggle, in this LLVM build). "-fexceptions", + "-fwasm-exceptions", ], "//conditions:default": [ "-std=c++20" diff --git a/docs/wasm_build.md b/docs/wasm_build.md index 561192fa..289faa65 100644 --- a/docs/wasm_build.md +++ b/docs/wasm_build.md @@ -96,13 +96,22 @@ For other experiments, copy built `.js` / `.wasm` files from `bazel-bin/wasm/` t | Flag | Purpose | |------|---------| | `-O3` | Aggressive optimization | -| `-flto` | Link-time optimization | -| `-fexceptions` | Enable C++ exceptions | +| `-flto` | Link-time optimization at compile time only (see note below) | +| `-fwasm-exceptions` | Native WebAssembly exception handling (use together with `-fexceptions`; replaces the slower JS-trampoline EH lowering used when linking with `-fexceptions` alone) | | `-sWASM=1` | Emscripten WASM output (link flag) | | `-sALLOW_MEMORY_GROWTH=1` | Allow heap growth at runtime | | `-sINITIAL_MEMORY=268435456` | 256MB initial memory | | `-sSTACK_SIZE=8388608` | 8MB stack (default 64KB is too small for DDS search) | +`-flto` is applied at compile time (`DDS_CPPOPTS`) but deliberately **not** at +link time. Passing `-flto` to the wasm link step was tried and reverted: the +`emsdk 5.0.7` package pinned in `MODULE.bazel` ships a frozen, pre-built cache +containing only non-LTO system libraries, and requesting link-time LTO makes +Emscripten require LTO-bitcode variants of core sysroot libraries (e.g. +`libprintf_long_double`) that the frozen cache can't build on demand inside +Bazel's hermetic sandbox. Revisit only if the emsdk packaging changes to ship +a populated LTO cache. + ## C++ standard WASM example binaries are built as C++20. The Emscripten toolchain (via `wasm_cc_binary`) compiles the transitioned `cc_target`; project flags for that configuration come from `CPPVARIABLES.bzl` when `//:build_wasm` matches (the Emscripten platform transition sets `wasm32`). diff --git a/specs/build-system.md b/specs/build-system.md index 76105081..3417e4a5 100644 --- a/specs/build-system.md +++ b/specs/build-system.md @@ -1,7 +1,7 @@ --- capability: build-system owners: [//, CPPVARIABLES.bzl, wasm_compat.bzl] -last-updated: 2026-07-18 +last-updated: 2026-07-19 --- # Build System @@ -37,10 +37,13 @@ than re-encoding toolchain knowledge. the host OS). Any target that wants per-platform flags does so through the `select()`s in `CPPVARIABLES.bzl` — it should not hand-roll `-O3`/`/O2`. - **Optimised builds are strict.** Non-debug macOS/Linux/WASM use `-O3` with - `-Wall -Wpedantic -Werror`; Windows uses `/O2 /W4 /WX /permissive-`. macOS and - WASM add LTO. C++20 is the baseline (`--cxxopt=-std=c++20` in `.bazelrc` for - macOS/Linux; Windows `/std:c++20` in `DDS_CPPOPTS`). Treat `-Werror` as a - standing invariant: warnings break the build. + `-Wall -Wpedantic -Werror`; Windows uses `/O2 /W4 /WX /permissive-`. macOS adds + LTO at both compile and link (`-flto=thin`); WASM adds `-flto` at compile time + only — link-time LTO is not currently possible under the pinned hermetic emsdk + toolchain (see [wasm-emscripten](wasm-emscripten.md)). C++20 is the baseline + (`--cxxopt=-std=c++20` in `.bazelrc` for macOS/Linux; Windows `/std:c++20` in + `DDS_CPPOPTS`). Treat `-Werror` as a standing invariant: warnings break the + build. - **Feature flags are `--define`-driven `config_setting`s**, surfaced through `DDS_LOCAL_DEFINES` / `DDS_SCHEDULER_DEFINE`: | config setting | `--define` | preprocessor define | capability | diff --git a/specs/wasm-emscripten.md b/specs/wasm-emscripten.md index 697fba99..67b5f357 100644 --- a/specs/wasm-emscripten.md +++ b/specs/wasm-emscripten.md @@ -1,7 +1,7 @@ --- capability: wasm-emscripten owners: [wasm] -last-updated: 2026-07-18 +last-updated: 2026-07-19 --- # WASM (Emscripten) Build @@ -56,6 +56,14 @@ core solver builds and runs correctly under Emscripten. - **Only three examples are ported**, not the full [examples-cli](examples-cli.md) set. - **No threaded WASM** — single-thread only. +- **No link-time LTO.** `-flto` applies at WASM compile time only + ([build-system](build-system.md)); enabling it at link time was tried and + reverted because the `emsdk 5.0.7` toolchain pinned in `MODULE.bazel` ships a + frozen, pre-built cache containing only non-LTO system libraries. Link-time + LTO requires LTO-bitcode variants of core sysroot libraries (e.g. + `libprintf_long_double`) that the frozen cache cannot build on demand inside + Bazel's hermetic sandbox. Revisit only if the emsdk packaging ships a + populated LTO cache. - Browser wiring, the site, MVP post-build JS patches (`web/patch_mvp_wasm.py`), and JS/e2e tests belong to [web-mvp](web-mvp.md); this capability provides the example modules, not the page. diff --git a/specs/web-mvp.md b/specs/web-mvp.md index 760e9db7..653adae7 100644 --- a/specs/web-mvp.md +++ b/specs/web-mvp.md @@ -1,7 +1,7 @@ --- capability: web-mvp owners: [web] -last-updated: 2026-07-18 +last-updated: 2026-07-19 --- # Web MVP @@ -53,8 +53,19 @@ a full application. browser/`file://` safety on `//web:dds_mvp_wasm` only — not the example WASM CLIs. If an emsdk upgrade moves that line, update the regex and the note in `docs/wasm_build.md`. +- **The module holds one session-scoped `SolverContext` for its lifetime.** + `dds_mvp_calc_table` (`web/dds_mvp_wasm.cpp`) constructs a function-local + `static SolverContext` lazily on first call and reuses it — including its + transposition table — for every subsequent call in that module instance, + calling `reset_for_solve()` between deals to recycle the TT memory pool + without freeing the underlying allocation. This mirrors a WASM module + instance's own lifetime (one `WebAssembly.Memory`, static constructors run + once) instead of allocating a fresh [solver-context](solver-context.md) per + call. Because the context is shared, it is **not safe for concurrent + solves** — a future move to Web Workers would need one context per worker. - **The MVP's WASM inherits the single-thread assumption** of - [wasm-emscripten](wasm-emscripten.md); results come from the same [dds-public-api](dds-public-api.md) core. + [wasm-emscripten](wasm-emscripten.md) — which is what makes sharing that + single context safe today; results come from the same [dds-public-api](dds-public-api.md) core. ## Key entry points diff --git a/wasm_compat.bzl b/wasm_compat.bzl index c3fea9b7..0e651073 100644 --- a/wasm_compat.bzl +++ b/wasm_compat.bzl @@ -3,7 +3,7 @@ # Emscripten link flags for cc_binary targets wrapped by wasm_cc_binary. WASM_LINKOPTS = [ "-sWASM=1", - "-fexceptions", + "-fwasm-exceptions", "-sALLOW_MEMORY_GROWTH=1", "-sINITIAL_MEMORY=268435456", # DDS search recursion needs more than Emscripten's 64KB default stack. diff --git a/web/dds_mvp_wasm.cpp b/web/dds_mvp_wasm.cpp index 34634509..4c99fa19 100644 --- a/web/dds_mvp_wasm.cpp +++ b/web/dds_mvp_wasm.cpp @@ -7,7 +7,7 @@ #include -#include +#include #if defined(__EMSCRIPTEN__) #include @@ -15,6 +15,14 @@ #define EMSCRIPTEN_KEEPALIVE #endif +namespace { +auto mvp_context() -> SolverContext& { + static SolverContext ctx; // lazily constructed on first call, lives for + // the module instance's lifetime (the session) + return ctx; +} +} // namespace + extern "C" { // Fills out_table[20] with res_table[strain][hand] (strain 0..4 = S,H,D,C,N). @@ -33,8 +41,12 @@ auto dds_mvp_calc_table(const char* pbn, int* out_table) -> int } std::memcpy(deal.cards, pbn, pbn_len + 1); + SolverContext& ctx = mvp_context(); + ctx.reset_for_solve(); // recycle TT memory pool + search bookkeeping + // between deals; keeps the underlying allocation + DdTableResults table{}; - const int res = CalcDDtablePBN(deal, &table); + const int res = calc_dd_table_pbn(ctx, deal, &table); if (res != RETURN_NO_FAULT) { return res; } diff --git a/web/dds_mvp_wasm_test.cpp b/web/dds_mvp_wasm_test.cpp index 35bc17ce..a4c895a0 100644 --- a/web/dds_mvp_wasm_test.cpp +++ b/web/dds_mvp_wasm_test.cpp @@ -23,6 +23,13 @@ constexpr int kExpectedHand0[20] = { constexpr char kPbnHand0[] = "N:QJ6.K652.J85.T98 873.J97.AT764.Q4 K5.T83.KQ9.A7652 AT942.AQ4.32.KJ3"; +constexpr int kExpectedHand1[20] = { + 3, 10, 3, 10, 9, 4, 9, 4, 8, 4, 8, 4, 3, 9, 3, 9, 4, 8, 4, 8, +}; + +constexpr char kPbnHand1[] = + "N:73.QJT.AQ54.T752 QT6.876.KJ9.AQ84 5.A95432.7632.K6 AKJ9842.K.T8.J93"; + } // namespace TEST(DdsMvpWasmTest, RejectsNullPointers) { @@ -51,3 +58,25 @@ TEST(DdsMvpWasmTest, FillsFlatStrainHandTable) { EXPECT_EQ(out[i], kExpectedHand0[i]) << "index " << i; } } + +TEST(DdsMvpWasmTest, FillsFlatStrainHandTableAcrossReuse) { + int out0[20]{}; + ASSERT_EQ(dds_mvp_calc_table(kPbnHand0, out0), RETURN_NO_FAULT); + for (int i = 0; i < 20; ++i) { + EXPECT_EQ(out0[i], kExpectedHand0[i]) << "hand0 index " << i; + } + + int out1[20]{}; + ASSERT_EQ(dds_mvp_calc_table(kPbnHand1, out1), RETURN_NO_FAULT); + for (int i = 0; i < 20; ++i) { + EXPECT_EQ(out1[i], kExpectedHand1[i]) << "hand1 index " << i; + } + + // Solve hand0 again through the same reused context to confirm the + // intervening different-deal solve didn't leave stale state behind. + int out0_again[20]{}; + ASSERT_EQ(dds_mvp_calc_table(kPbnHand0, out0_again), RETURN_NO_FAULT); + for (int i = 0; i < 20; ++i) { + EXPECT_EQ(out0_again[i], kExpectedHand0[i]) << "hand0 repeat index " << i; + } +}