Skip to content

Merge builtins.wasm and builtins.wasi into one function#370

Open
edolstra wants to merge 3 commits intomainfrom
merge-wasm-wasi
Open

Merge builtins.wasm and builtins.wasi into one function#370
edolstra wants to merge 3 commits intomainfrom
merge-wasm-wasi

Conversation

@edolstra
Copy link
Collaborator

@edolstra edolstra commented Feb 26, 2026

Motivation

The new interface is

builtins.wasm {
  path = ./path/to/file.wasm;
  function = "fib";
} 33

This will allow other Wasm-related configuration to be added in the future (e.g. WASI features, memory limits, caching info, etc).

The argument is separate from the configuration because typically you want to write something like

let
  fib = builtins.wasm {
    path = ./path/to/file.wasm;
    function = "fib";
  };
in fib 33

It now also auto-detects whether a module uses WASI.

Context

Summary by CodeRabbit

  • Documentation

    • Reworked Wasm host docs into a single unified interface describing WASI vs non‑WASI modes, automatic WASI detection, updated examples, and clarified return/error semantics (WASI-only return path). Host imports now documented under the env module; removed prior dual‑interface narrative.
  • Refactor

    • Unified Wasm invocation into one configurable call accepting a config set (path + function) and an argument. Non‑WASI modules require an init export invoked on instantiation; WASI modules use standard WASI entry/argv semantics. Module caching simplified to per‑path; WASI IO routed through host logging.

The new interface is

  builtins.wasm {
    path = ./path/to/file.wasm;
    wasi = false;
    function = "fib";
  } 33
@coderabbitai
Copy link

coderabbitai bot commented Feb 26, 2026

📝 Walkthrough

Walkthrough

Consolidates Wasm handling into a single builtins.wasm primop (__wasm) with automatic WASI detection (based on _start), splitting execution into WASI and non‑WASI modes, adds nix_wasm_init_v1() requirement for non‑WASI modules, updates host imports to env, and simplifies instance caching by wasm path.

Changes

Cohort / File(s) Summary
Documentation
doc/manual/source/protocols/wasm.md
Restructured docs to present a single builtins.wasm entry with WASI mode vs non‑WASI, removed builtins.wasi narrative, standardized terminology, updated usage examples, and documented nix_wasm_init_v1() for non‑WASI and WASI return semantics.
Wasm primop implementation
src/libexpr/primops/wasm.cc
Replaced previous __wasi flow with unified __wasm primop that accepts a config attribute-set (path, optional function, optional wasi), auto-detects WASI by inspecting imports, builds instantiation/linker per-module, routes WASI IO via WasiLogger, requires nix_wasm_init_v1 for non‑WASI, uses _start/argv return path for WASI, and changed instance caching key to wasmPath.

Sequence Diagram(s)

sequenceDiagram
    participant Evaluator as Nix Evaluator
    participant Parser as Config Parser
    participant Instantiator as Wasm Instantiator
    participant WasiRuntime as WASI Runtime
    participant NonWasiRuntime as Non‑WASI Runtime
    participant Module as Wasm Module

    Evaluator->>Parser: __wasm(config, arg)
    Parser->>Instantiator: validate config (path, function?)
    Instantiator->>Module: load & compile module
    Instantiator->>Instantiator: inspect imports -> detect `_start` => useWasi?

    alt useWasi == true
        Instantiator->>WasiRuntime: instantiate with WASI, wire return_to_nix
        WasiRuntime->>Module: call _start(argv[1])
        Module-->>WasiRuntime: invoke return_to_nix(result)
        WasiRuntime-->>Evaluator: deliver result
    else useWasi == false
        Instantiator->>NonWasiRuntime: instantiate without WASI
        NonWasiRuntime->>Module: call nix_wasm_init_v1()
        NonWasiRuntime->>Module: call configured function(valueId)
        Module-->>NonWasiRuntime: return i32 valueId
        NonWasiRuntime-->>Evaluator: deliver result
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • cole-h

Poem

🐰 I hopped from two to one, a tidy, nimble trick,
_start tells me WASI, else nix_wasm_init_v1 does the trick.
One primop sings the tune, one path to fetch the prize,
I nibble bits and bounce — the result returns, surprise! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: consolidating two separate builtins (builtins.wasm and builtins.wasi) into a single unified builtins.wasm function.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch merge-wasm-wasi

Comment @coderabbitai help to get the list of available commands and usage tips.

@grahamc
Copy link
Member

grahamc commented Feb 26, 2026

Maybe we should make the toggle not a boolean but an enum, like mode = "wasi" | "wasm" for a potential future where they rev the spec, or some secret third "was*" option come about? Also, maybe changing it from builtins.wasm to builtins.webassembly?

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
doc/manual/source/protocols/wasm.md (1)

38-41: Clarify nix_wasm_init_v1() call frequency wording.

“called once when the module is instantiated” can read as global/per-module caching behavior. Consider clarifying it is once per instance (i.e., per builtins.wasm invocation).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@doc/manual/source/protocols/wasm.md` around lines 38 - 41, Clarify the
wording for nix_wasm_init_v1() to state it is invoked once per module instance,
not globally; update the sentence mentioning `nix_wasm_init_v1()` to read that
the function is called once for each instantiation of the module (i.e., once per
`builtins.wasm` invocation / per instance), so readers understand it’s
per-instance initialization rather than a single global call across the process.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/libexpr/primops/wasm.cc`:
- Around line 614-635: The WASI configuration is being applied after
instantiateWasm(), but Wasmtime requires setting the WASI context on the store
before module instantiation; move the WasiConfig creation and calls to
wasi_config_set_stdout_custom/wasi_config_set_stderr_custom,
WasiConfig::argv(...) and the call to instance.wasmStore.context().set_wasi(...)
to occur before calling instantiateWasm(), then call instantiateWasm(state,
wasmPath, useWasi) (and only afterward use instance.addValue and
instance.getExport<Func>().call(...)); ensure the WasiLogger, its trampoline,
and argv are created and passed into the store via set_wasi prior to
instantiation so the module start can safely use WASI.

---

Nitpick comments:
In `@doc/manual/source/protocols/wasm.md`:
- Around line 38-41: Clarify the wording for nix_wasm_init_v1() to state it is
invoked once per module instance, not globally; update the sentence mentioning
`nix_wasm_init_v1()` to read that the function is called once for each
instantiation of the module (i.e., once per `builtins.wasm` invocation / per
instance), so readers understand it’s per-instance initialization rather than a
single global call across the process.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d1c3725 and 8e0ba58.

📒 Files selected for processing (2)
  • doc/manual/source/protocols/wasm.md
  • src/libexpr/primops/wasm.cc

@github-actions
Copy link

github-actions bot commented Feb 26, 2026

@github-actions github-actions bot temporarily deployed to pull request February 26, 2026 14:46 Inactive
@edolstra
Copy link
Collaborator Author

@grahamc It now auto-detects whether to use WASI.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/libexpr/primops/wasm.cc (1)

607-642: ⚠️ Potential issue | 🔴 Critical

Configure WASI before instance instantiation.

Line 607 instantiates the module before Line 641 calls set_wasi. If the module executes start-time code during instantiation, this ordering can fail before WASI is available. This appears to be the same unresolved issue raised earlier.

In Wasmtime C++/C APIs, should `Store::Context::set_wasi(...)` be called before `instantiate` when start-time code may run?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libexpr/primops/wasm.cc` around lines 607 - 642, The code currently calls
instantiateWasm before configuring WASI which can run module start-time code
without WASI available; move the WASI setup so that when instance.pre->useWasi
is true you create a WasiConfig, set the custom stdout/stderr trampolines and
argv, call instance.wasmStore.context().set_wasi(std::move(wasiConfig)) before
calling instantiateWasm, and then call instantiateWasm (and remove the later
set_wasi call); look for instantiateWasm, instance.pre->useWasi, WasiConfig,
wasi_config_set_stdout_custom/wasi_config_set_stderr_custom, and
instance.wasmStore.context().set_wasi to make these changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@doc/manual/source/protocols/wasm.md`:
- Line 355: Update the stale wording that says "wasi = true" to instead
reference WASI mode detection: change the sentence "This function is only
available when `wasi = true`" to something like "This function is only available
in WASI mode (module exports `_start`)" so it matches the document's
auto-detection behavior and references the `_start` export.

In `@src/libexpr/primops/wasm.cc`:
- Around line 98-103: The current heuristic that marks useWasi = true by
scanning module.exports() for the "_start" export is incorrect; instead detect
WASI by checking module.imports() for a WASI import module (e.g.,
"wasi_snapshot_preview1", "wasi_unstable", or imports whose module name starts
with "wasi_") and set useWasi based on that. Replace the export-based loop that
inspects module.exports() and the "_start" name with a loop over
module.imports() (or equivalent API) that looks at import.module() (or
import.module_name()) for known WASI module names and sets useWasi = true if
found, leaving all other logic (including the code that validates the "function"
attribute) untouched. Ensure you reference the same variables (useWasi, module)
and remove reliance on "_start" so WASI reactors and command modules are both
detected correctly.

---

Duplicate comments:
In `@src/libexpr/primops/wasm.cc`:
- Around line 607-642: The code currently calls instantiateWasm before
configuring WASI which can run module start-time code without WASI available;
move the WASI setup so that when instance.pre->useWasi is true you create a
WasiConfig, set the custom stdout/stderr trampolines and argv, call
instance.wasmStore.context().set_wasi(std::move(wasiConfig)) before calling
instantiateWasm, and then call instantiateWasm (and remove the later set_wasi
call); look for instantiateWasm, instance.pre->useWasi, WasiConfig,
wasi_config_set_stdout_custom/wasi_config_set_stderr_custom, and
instance.wasmStore.context().set_wasi to make these changes.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e0ba58 and c9559dc.

📒 Files selected for processing (2)
  • doc/manual/source/protocols/wasm.md
  • src/libexpr/primops/wasm.cc

@github-actions github-actions bot temporarily deployed to pull request February 26, 2026 15:45 Inactive
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/libexpr/primops/wasm.cc (1)

135-144: ⚠️ Potential issue | 🟠 Major

WASI context is still configured after module instantiation.

The NixWasmInstance constructor instantiates the module at line 140 via pre->instancePre.instantiate(wasmCtx), but set_wasi() is called much later at line 641 in prim_wasm. While using instantiate_pre pre-compiles the module, the actual instantiation still happens in the constructor before WASI is configured. If the Wasm module has a start section (distinct from the _start export), it will execute during instantiation and may call WASI functions before the context is set, causing an abort.

Consider restructuring so that set_wasi() is called on the store context before NixWasmInstance is constructed, or defer instantiation until after WASI configuration.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libexpr/primops/wasm.cc` around lines 135 - 144, The constructor
NixWasmInstance currently calls pre->instancePre.instantiate(wasmCtx) (in
NixWasmInstance(...)) before WASI is configured via set_wasi (done later in
prim_wasm), which allows a module start function to run without a proper WASI
context; fix by moving instantiation to after WASI is set: either (A) call
set_wasi on the store/context before constructing NixWasmInstance so the wasmCtx
passed into NixWasmInstance already has WASI, or (B) change NixWasmInstance to
defer instantiate (remove/replace instance =
unwrap(pre->instancePre.instantiate(wasmCtx)) from the constructor and add an
explicit instantiate() method invoked from prim_wasm after set_wasi completes);
update all call sites in prim_wasm to construct NixWasmInstance first only when
using approach A or to call the new instantiate() method after set_wasi when
using approach B, ensuring wasmCtx.set_data(this) is retained on the correct
store/context.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@doc/manual/source/protocols/wasm.md`:
- Around line 43-52: The "WASI Mode" section currently says WASI is detected by
the module exporting `_start`, which contradicts the overview and non-WASI
section; update the "WASI Mode" text to state that WASI is detected by the
presence of `wasi_snapshot_preview1` imports, and optionally note that an
exported `_start` is just the conventional entrypoint used by WASI runtimes (but
not the detection mechanism). Adjust any example or wording in the "WASI Mode"
heading to reference `wasi_snapshot_preview1` as the detection mechanism and
keep `_start` described only as the entry point.

In `@src/libexpr/primops/wasm.cc`:
- Around line 671-701: The docstring for the "__wasm" primop (registered via
primop_wasm with implementation prim_wasm) inaccurately says WASI mode is
detected by checking for an exported "_start"; update the documentation to match
the implementation: state that WASI mode is detected by the presence of wasi
imports (e.g. "wasi_snapshot_preview1") rather than an exported "_start", and
clarify the rule about when the `function` attribute is required/forbidden
accordingly so the text aligns with how prim_wasm actually detects WASI.
- Around line 612-622: The error text thrown when instance.pre->useWasi is true
is misleading because WASI is detected via imports (wasi_snapshot_preview1) not
the presence of a '_start' export; update the Exception message thrown in the
builtins.wasm handling (the throw inside the if (instance.pre->useWasi) path
where functionAttr is provided) to reference WASI imports (e.g., "WASI modules
(modules that import 'wasi_snapshot_preview1') do not accept a 'function'
attribute") or similar wording that mentions imports rather than a '_start'
export; keep the same throw location and type (throw Error(...)) and ensure any
other related messages around functionAttr handling in this block (and the
non-WASI branch) remain consistent.

---

Duplicate comments:
In `@src/libexpr/primops/wasm.cc`:
- Around line 135-144: The constructor NixWasmInstance currently calls
pre->instancePre.instantiate(wasmCtx) (in NixWasmInstance(...)) before WASI is
configured via set_wasi (done later in prim_wasm), which allows a module start
function to run without a proper WASI context; fix by moving instantiation to
after WASI is set: either (A) call set_wasi on the store/context before
constructing NixWasmInstance so the wasmCtx passed into NixWasmInstance already
has WASI, or (B) change NixWasmInstance to defer instantiate (remove/replace
instance = unwrap(pre->instancePre.instantiate(wasmCtx)) from the constructor
and add an explicit instantiate() method invoked from prim_wasm after set_wasi
completes); update all call sites in prim_wasm to construct NixWasmInstance
first only when using approach A or to call the new instantiate() method after
set_wasi when using approach B, ensuring wasmCtx.set_data(this) is retained on
the correct store/context.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9559dc and 9a660b9.

📒 Files selected for processing (2)
  • doc/manual/source/protocols/wasm.md
  • src/libexpr/primops/wasm.cc

Comment on lines +43 to +52
### WASI Mode

Usage: `builtins.wasi <module> <arg>`
WASI mode is automatically used when the module exports a `_start` function.

Usage:
```nix
builtins.wasm {
path = <module>;
} <arg>
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

WASI mode section contradicts the overview regarding detection mechanism.

Line 45 states "WASI mode is automatically used when the module exports a _start function" but the overview (line 15) and non-WASI section (line 28) correctly state that WASI is detected by checking for wasi_snapshot_preview1 imports. This inconsistency could confuse users.

✏️ Suggested fix
-WASI mode is automatically used when the module exports a `_start` function.
+WASI mode is automatically used when the module imports from `wasi_snapshot_preview1`.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### WASI Mode
Usage: `builtins.wasi <module> <arg>`
WASI mode is automatically used when the module exports a `_start` function.
Usage:
```nix
builtins.wasm {
path = <module>;
} <arg>
```
### WASI Mode
WASI mode is automatically used when the module imports from `wasi_snapshot_preview1`.
Usage:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@doc/manual/source/protocols/wasm.md` around lines 43 - 52, The "WASI Mode"
section currently says WASI is detected by the module exporting `_start`, which
contradicts the overview and non-WASI section; update the "WASI Mode" text to
state that WASI is detected by the presence of `wasi_snapshot_preview1` imports,
and optionally note that an exported `_start` is just the conventional
entrypoint used by WASI runtimes (but not the detection mechanism). Adjust any
example or wording in the "WASI Mode" heading to reference
`wasi_snapshot_preview1` as the detection mechanism and keep `_start` described
only as the entry point.

Comment on lines +612 to +622
if (instance.pre->useWasi) {
functionName = "_start";
if (functionAttr)
throw Error("'function' attribute is not allowed for WASI modules (modules with a '_start' export)");
} else {
if (!functionAttr)
throw Error(
"missing required 'function' attribute in first argument to `builtins.wasm` for non-WASI modules");
functionName = std::string(
state.forceStringNoCtx(*functionAttr->value, pos, "while evaluating the 'function' attribute"));
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error message references _start export but WASI is detected via imports.

The error message at line 615 states "modules with a '_start' export" but WASI detection actually checks for wasi_snapshot_preview1 imports (lines 98-103). A WASI reactor module (which imports WASI but doesn't export _start) would trigger useWasi = true but this error message would be misleading.

✏️ Suggested fix
         if (functionAttr)
-                throw Error("'function' attribute is not allowed for WASI modules (modules with a '_start' export)");
+                throw Error("'function' attribute is not allowed for WASI modules (modules that import from wasi_snapshot_preview1)");
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/libexpr/primops/wasm.cc` around lines 612 - 622, The error text thrown
when instance.pre->useWasi is true is misleading because WASI is detected via
imports (wasi_snapshot_preview1) not the presence of a '_start' export; update
the Exception message thrown in the builtins.wasm handling (the throw inside the
if (instance.pre->useWasi) path where functionAttr is provided) to reference
WASI imports (e.g., "WASI modules (modules that import 'wasi_snapshot_preview1')
do not accept a 'function' attribute") or similar wording that mentions imports
rather than a '_start' export; keep the same throw location and type (throw
Error(...)) and ensure any other related messages around functionAttr handling
in this block (and the non-WASI branch) remain consistent.

@github-actions github-actions bot temporarily deployed to pull request February 26, 2026 16:30 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants