Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CPPVARIABLES.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
13 changes: 11 additions & 2 deletions docs/wasm_build.md
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Comment thread
Copilot marked this conversation as resolved.
| `-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`).
Expand Down
13 changes: 8 additions & 5 deletions specs/build-system.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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 |
Expand Down
10 changes: 9 additions & 1 deletion specs/wasm-emscripten.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
capability: wasm-emscripten
owners: [wasm]
last-updated: 2026-07-18
last-updated: 2026-07-19
---

# WASM (Emscripten) Build
Expand Down Expand Up @@ -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.
15 changes: 13 additions & 2 deletions specs/web-mvp.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
capability: web-mvp
owners: [web]
last-updated: 2026-07-18
last-updated: 2026-07-19
---

# Web MVP
Expand Down Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion wasm_compat.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
16 changes: 14 additions & 2 deletions web/dds_mvp_wasm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,22 @@

#include <cstring>

#include <api/dll.h>
#include <api/calc_dd_table.hpp>

#if defined(__EMSCRIPTEN__)
#include <emscripten.h>
#else
#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).
Expand All @@ -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;
}
Expand Down
29 changes: 29 additions & 0 deletions web/dds_mvp_wasm_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
}
Loading