feat: feature-detected browser-wasm variants (relaxed-SIMD) + variant-aware runtime
Some checks failed
CI / build-test-release (pull_request) Failing after 7m19s
Some checks failed
CI / build-test-release (pull_request) Failing after 7m19s
The browser wasm pipeline hardcoded -msimd128 for every wasm32 target and baked a single wasm URL into index.html, so newer codegen features that aren't yet baseline across engines (relaxed SIMD today; threads, future SIMD revisions later) couldn't be adopted without dropping the browsers that lack them. Add a general, feature-parameterized mechanism owned entirely by Crafter.Build: - Configuration::wasmVariants declares N codegen variants (label, extra -m flags, runtime probes). Build() compiles the baseline plus one outputName.<label>.wasm per variant, recompiling the whole graph (incl. dep libs + std PCM) with the variant's flags — relaxed-SIMD is per-TU codegen, not a link switch. wasmVariantFlags folds into VariantId so each variant's objects/PCMs land in their own build+bin dir. - EnableWasiBrowserRuntime emits a variants.json manifest (label -> url + probes), preferred-first with the baseline as the universal fallback. - The shipped runtime.js runs inlined wasm-feature-detect probes (relaxed-simd, simd, tail-call, bulk-memory, exception-handling, threads), picks the first variant whose probes all pass, and falls back to the single baked CRAFTER_WASM_URL when no manifest is present (backward compatible). - EnableWasiRelaxedSimdVariant registers the relaxed-SIMD variant — the motivating case (Chrome 114+/Firefox 120+ enable it by default; Safari still flag-gates it as of mid-2026). Verified end to end: a wasm32-wasip1 build emits both wasi-hello.wasm and wasi-hello.relaxed-simd.wasm + variants.json; Firefox selects the relaxed-simd variant and runs it. Resolves #24 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
dbee23c564
commit
022ada70a6
7 changed files with 395 additions and 5 deletions
|
|
@ -47,8 +47,8 @@ int main() {
|
|||
EnableWasiBrowserRuntime(cfg);
|
||||
|
||||
// Three files registered: runtime.js, generated index.html, files.json.
|
||||
if (cfg.files.size() != 3) {
|
||||
std::println(std::cerr, "expected 3 cfg.files entries, got {}", cfg.files.size());
|
||||
if (cfg.files.size() != 4) {
|
||||
std::println(std::cerr, "expected 4 cfg.files entries, got {}", cfg.files.size());
|
||||
for (const auto& f : cfg.files) std::println(std::cerr, " - {}", f.string());
|
||||
return 1;
|
||||
}
|
||||
|
|
@ -63,10 +63,12 @@ int main() {
|
|||
const fs::path* runtimeJs = findByName("runtime.js");
|
||||
const fs::path* indexHtml = findByName("index.html");
|
||||
const fs::path* manifest = findByName("files.json");
|
||||
const fs::path* variants = findByName("variants.json");
|
||||
|
||||
if (!runtimeJs) { std::println(std::cerr, "runtime.js not registered"); return 1; }
|
||||
if (!indexHtml) { std::println(std::cerr, "index.html not registered"); return 1; }
|
||||
if (!manifest) { std::println(std::cerr, "files.json not registered"); return 1; }
|
||||
if (!variants) { std::println(std::cerr, "variants.json not registered"); return 1; }
|
||||
|
||||
// The generated index.html and manifest live under the project's build/
|
||||
// wasi-runtime/<name>/ directory and must actually exist on disk.
|
||||
|
|
@ -78,6 +80,25 @@ int main() {
|
|||
std::println(std::cerr, "files.json not produced at {}", manifest->string());
|
||||
return 1;
|
||||
}
|
||||
if (!fs::exists(*variants)) {
|
||||
std::println(std::cerr, "variants.json not produced at {}", variants->string());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// With no declared variants, variants.json is a single baseline entry
|
||||
// pointing at outputName.wasm with no probes.
|
||||
{
|
||||
std::ifstream vin(*variants);
|
||||
std::string vjson{std::istreambuf_iterator<char>(vin), std::istreambuf_iterator<char>()};
|
||||
if (vjson.find("\"wasi-helper.wasm\"") == std::string::npos) {
|
||||
std::println(std::cerr, "variants.json missing baseline url: {}", vjson);
|
||||
return 1;
|
||||
}
|
||||
if (vjson.find("relaxed-simd") != std::string::npos) {
|
||||
std::println(std::cerr, "variants.json unexpectedly mentions relaxed-simd: {}", vjson);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// The template's {{WASM}} placeholder should have been replaced with
|
||||
// the configured outputName.
|
||||
|
|
|
|||
115
tests/WasmVariants/main.cpp
Normal file
115
tests/WasmVariants/main.cpp
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
import std;
|
||||
import Crafter.Build;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter;
|
||||
|
||||
extern "C" int setenv(const char* name, const char* value, int overwrite);
|
||||
|
||||
// Covers the browser-wasm variant plumbing that does NOT require a wasm
|
||||
// toolchain run: EnableWasiRelaxedSimdVariant's registration, the
|
||||
// variants.json manifest EnableWasiBrowserRuntime emits, and the VariantId
|
||||
// perturbation that keeps each variant's objects in their own build dir. The
|
||||
// actual multi-.wasm production is exercised by a real wasm build (manual /
|
||||
// integration), not here — same boundary the WasiBrowserRuntime test draws.
|
||||
namespace {
|
||||
int failures = 0;
|
||||
void Check(bool cond, std::string_view msg) {
|
||||
if (!cond) { std::println(std::cerr, "FAIL: {}", msg); ++failures; }
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
// ── EnableWasiRelaxedSimdVariant: registration + idempotency ──────────
|
||||
{
|
||||
Configuration cfg;
|
||||
cfg.target = "wasm32-wasip1";
|
||||
EnableWasiRelaxedSimdVariant(cfg);
|
||||
Check(cfg.wasmVariants.size() == 1, "one variant registered");
|
||||
if (!cfg.wasmVariants.empty()) {
|
||||
const WasmVariant& v = cfg.wasmVariants.front();
|
||||
Check(v.label == "relaxed-simd", "label is relaxed-simd");
|
||||
Check(v.flags.size() == 1 && v.flags.front() == "-mrelaxed-simd", "flag is -mrelaxed-simd");
|
||||
Check(v.probes.size() == 1 && v.probes.front() == "relaxed-simd", "probe is relaxed-simd");
|
||||
}
|
||||
EnableWasiRelaxedSimdVariant(cfg);
|
||||
Check(cfg.wasmVariants.size() == 1, "second call is idempotent");
|
||||
}
|
||||
|
||||
// ── wasmVariantFlags perturbs VariantId / BuildDir / BinDir ──────────
|
||||
// Configuration owns move-only members (unique_ptr<Module>), so build the
|
||||
// two cases independently rather than copying.
|
||||
{
|
||||
auto make = [] {
|
||||
Configuration c;
|
||||
c.path = "/tmp/wasm-variant-id-test";
|
||||
c.name = "wv"; c.outputName = "wv";
|
||||
c.target = "wasm32-wasip1";
|
||||
c.type = ConfigurationType::Executable;
|
||||
return c;
|
||||
};
|
||||
Configuration a = make();
|
||||
Configuration b = make();
|
||||
b.wasmVariantFlags = {"-mrelaxed-simd"};
|
||||
Check(a.VariantId() != b.VariantId(), "wasmVariantFlags perturbs VariantId");
|
||||
Check(a.BuildDir() != b.BuildDir(), "wasmVariantFlags perturbs BuildDir");
|
||||
Check(a.BinDir() != b.BinDir(), "wasmVariantFlags perturbs BinDir");
|
||||
}
|
||||
|
||||
// ── variants.json manifest contents ──────────────────────────────────
|
||||
{
|
||||
fs::path repoWasi = fs::current_path() / "wasi-runtime";
|
||||
fs::path systemHome = "/usr/local/share/crafter-build";
|
||||
if (fs::exists(repoWasi / "runtime.js")) {
|
||||
setenv("CRAFTER_BUILD_HOME", fs::current_path().c_str(), 1);
|
||||
} else if (fs::exists(systemHome / "wasi-runtime" / "runtime.js")) {
|
||||
setenv("CRAFTER_BUILD_HOME", systemHome.c_str(), 1);
|
||||
} else {
|
||||
std::println(std::cerr, "wasi-runtime assets not found; skipping manifest check");
|
||||
return failures ? 1 : 77;
|
||||
}
|
||||
|
||||
Configuration cfg;
|
||||
cfg.path = fs::temp_directory_path() / "crafter-build-wasm-variants-test";
|
||||
cfg.name = "wvhelper";
|
||||
cfg.outputName = "wvhelper";
|
||||
cfg.target = "wasm32-wasip1";
|
||||
cfg.type = ConfigurationType::Executable;
|
||||
|
||||
std::error_code ec;
|
||||
fs::remove_all(cfg.path, ec);
|
||||
fs::create_directories(cfg.path);
|
||||
|
||||
EnableWasiRelaxedSimdVariant(cfg);
|
||||
EnableWasiBrowserRuntime(cfg);
|
||||
|
||||
const fs::path* variants = nullptr;
|
||||
for (const fs::path& f : cfg.files) {
|
||||
if (f.filename() == "variants.json") variants = &f;
|
||||
}
|
||||
Check(variants != nullptr, "variants.json registered");
|
||||
if (variants) {
|
||||
std::ifstream vin(*variants);
|
||||
std::string j{std::istreambuf_iterator<char>(vin), std::istreambuf_iterator<char>()};
|
||||
// relaxed-simd entry, with its labelled url + probe.
|
||||
Check(j.find("\"label\":\"relaxed-simd\"") != std::string::npos, "manifest has relaxed-simd label");
|
||||
Check(j.find("\"wvhelper.relaxed-simd.wasm\"") != std::string::npos, "manifest has labelled url");
|
||||
Check(j.find("\"relaxed-simd\"") != std::string::npos, "manifest has relaxed-simd probe");
|
||||
// baseline fallback entry.
|
||||
Check(j.find("\"wvhelper.wasm\"") != std::string::npos, "manifest has baseline url");
|
||||
// The relaxed-simd variant must precede the baseline so the runtime
|
||||
// prefers it when probes pass.
|
||||
auto relaxedPos = j.find("relaxed-simd");
|
||||
auto baselinePos = j.find("\"wvhelper.wasm\"");
|
||||
Check(relaxedPos != std::string::npos && baselinePos != std::string::npos && relaxedPos < baselinePos,
|
||||
"relaxed-simd entry precedes baseline");
|
||||
}
|
||||
|
||||
fs::remove_all(cfg.path, ec);
|
||||
}
|
||||
|
||||
if (failures > 0) {
|
||||
std::println(std::cerr, "{} assertions failed", failures);
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue