Skip to content

QuickJS in Wasm in Nix#6

Merged
edolstra merged 9 commits intomainfrom
quickjs
Feb 28, 2026
Merged

QuickJS in Wasm in Nix#6
edolstra merged 9 commits intomainfrom
quickjs

Conversation

@edolstra
Copy link
Collaborator

@edolstra edolstra commented Feb 28, 2026

Includes #4. Merges the wasi subflake into the quickjs derivation to simplify the build.

Summary by CodeRabbit

  • New Features

    • QuickJS plugin: evaluate JavaScript in WASM with broad type support.
    • Fibonacci WASI plugin: Fibonacci computation available as a WASM plugin.
    • Workspace-wide build/test improvements and expanded WASI toolchain for plugin development and packaging.
  • Tests

    • Added plugin tests and expected outputs exercising type conversions and Fibonacci output.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

The PR restructures WASI plugin support by integrating two new plugins (nix-wasm-plugin-fib-wasi and nix-wasm-plugin-quickjs) into the main workspace, refactors the nix-wasm-rust API (renames Value::from_id()Value::from_raw() and adds wasi_arg()), updates flake.nix to add a nix-wasi-plugins output and WASI toolchain/vendoring, and removes the legacy wasi/ directory and its flake.

Changes

Cohort / File(s) Summary
Build tasks & workspace members
/.vscode/tasks.json, Cargo.toml
Updated VSCode cargo build task to --workspace and added --exclude entries; added new workspace members nix-wasm-plugin-fib-wasi and nix-wasm-plugin-quickjs.
Top-level flake & WASI packaging
flake.nix
Added nix-wasi-plugins package output, full WASI toolchain/sysroot setup, vendoring workflows, conditional copying of wasm artifacts, and adjustments to build/test/install phases and devShell overrides.
New WASI Fibonacci plugin
nix-wasm-plugin-fib-wasi/Cargo.toml, nix-wasm-plugin-fib-wasi/src/main.rs, nix-wasm-plugin-fib-wasi/tests/*
Introduced WASI binary crate that depends on nix-wasm-rust and nix-wasm-plugin-fib; updated call site to Value::from_raw(); added test fixtures (fib.exp, fib.nix).
New QuickJS plugin (cdylib)
nix-wasm-plugin-quickjs/Cargo.toml, nix-wasm-plugin-quickjs/src/lib.rs, nix-wasm-plugin-quickjs/tests/*
Added cdylib crate exposing pub extern "C" fn eval(arg: Value) -> Value and _start(), implements QuickJS→Nix Value conversions, and adds tests (eval.exp, eval.nix).
nix-wasm-rust API changes
nix-wasm-rust/src/lib.rs
Renamed Value::from_id()Value::from_raw() and added pub fn wasi_arg() -> Value to parse WASI argv into a Value.
Removed legacy WASI layout
wasi/flake.nix, wasi/Cargo.toml, wasi/nix-wasi-plugin-fib/Cargo.toml
Deleted the old wasi/ flake, workspace config, and the legacy nix-wasi-plugin-fib crate manifest; CI removed a ./wasi flake check invocation.
CI change
.github/workflows/ci.yml
Removed the separate ./wasi flake check step from CI.

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

Suggested Reviewers

  • cole-h
  • lucperkins

Poem

🐇 I hopped into code tonight,

wasm and wasi in delight,
QuickJS speaks, fib numbers climb,
flakes rearranged, new builds in time,
the workspace hums — a rabbit's rhyme.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.22% 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 reflects the primary change: integrating QuickJS into a WASM-based Nix environment by merging wasi functionality into the quickjs derivation.

✏️ 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 quickjs

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

Copy link
Contributor

@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: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@flake.nix`:
- Around line 51-59: The flake currently hard-codes the Clang major version
("clang/19") in install paths; extract the major version from wasiCc.version
(e.g., parse wasiCc.version to get the major number into a variable like
clangMajor or clang_version_major) and use that variable instead of the literal
"19" wherever paths reference $out/lib/clang/19 (and the other occurrences
mentioned) so the links created (e.g., in the ln -s lines referencing
$out/lib/clang/<major>/include) and any other install targets automatically
follow the wasiCc version; ensure the parsing handles common version formats and
reuse the single variable in all place(s) that previously used "19".

In `@nix-wasm-plugin-quickjs/src/lib.rs`:
- Around line 10-55: The js_value_to_nix function is unbounded-recursive for
arrays/objects; add a recursion depth guard by changing js_value_to_nix to
accept a depth parameter (e.g., depth: usize) and a MAX_DEPTH constant, check at
the start and return a safe fallback (e.g., Value::make_null() or a controlled
error via fail) when depth == 0, and decrement depth on each recursive call into
arrays (Array::into_iter -> js_value_to_nix(entry, depth-1)) and objects
(object.props -> js_value_to_nix(value, depth-1)); update all call sites to pass
an initial MAX_DEPTH and ensure the conversion for lists (Value::make_list) and
attrsets (Value::make_attrset) uses the guarded recursive calls so deeply nested
or cyclic JS values stop at the configured depth.
- Line 2: The rquickjs types Value, Array, and Object now require the context
lifetime parameter; update the import and all usages to include the `'js`
lifetime (e.g., use rquickjs::{Array as Array<'js>, Object as Object<'js>, Value
as JsValue<'js>} or adjust type aliases) and then update any function
signatures, struct fields, and variable annotations that reference JsValue,
Array, or Object to JsValue<'js>, Array<'js>, and Object<'js> respectively
(ensure functions that introduce these types are generic over or bound to the
`'js` lifetime via fn foo<'js>(ctx: Ctx<'js>, ...) or appropriate context
types). Ensure no JS-tied value escapes its context by aligning lifetimes across
Context/Runtime usages (look for identifiers like JsValue, Array, Object,
Context in the diff and update them accordingly).

In `@nix-wasm-rust/src/lib.rs`:
- Around line 43-47: The parsed ValueId returned by wasi_arg() must be checked
for the sentinel zero before constructing a Value: after parsing into ValueId
(the variable value_id), add a guard that rejects 0 (e.g., panic or return an
error) with a clear message (similar style to the existing parse error) and only
call Value::from_raw(value_id) for non-zero ids; this prevents propagating an
invalid handle used elsewhere (see get_attr and Value::from_raw).

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 17b3ab2 and 09451cb.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • wasi/Cargo.lock is excluded by !**/*.lock
  • wasi/flake.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • .vscode/tasks.json
  • Cargo.toml
  • flake.nix
  • nix-wasm-plugin-fib-wasi/Cargo.toml
  • nix-wasm-plugin-fib-wasi/src/main.rs
  • nix-wasm-plugin-fib-wasi/tests/fib.exp
  • nix-wasm-plugin-fib-wasi/tests/fib.nix
  • nix-wasm-plugin-quickjs/Cargo.toml
  • nix-wasm-plugin-quickjs/src/lib.rs
  • nix-wasm-plugin-quickjs/tests/eval.exp
  • nix-wasm-plugin-quickjs/tests/eval.nix
  • nix-wasm-rust/src/lib.rs
  • wasi/Cargo.toml
  • wasi/flake.nix
  • wasi/nix-wasi-plugin-fib/Cargo.toml
💤 Files with no reviewable changes (3)
  • wasi/nix-wasi-plugin-fib/Cargo.toml
  • wasi/flake.nix
  • wasi/Cargo.toml

Comment on lines +51 to +59
mkdir -p $out/bin $out/lib/clang/19 $out/share

ln -s ${wasiCc}/bin/wasm32-unknown-wasi-clang $out/bin/clang
ln -s ${wasiCc}/bin/wasm32-unknown-wasi-clang++ $out/bin/clang++
ln -s ${wasiCc}/bin/wasm32-unknown-wasi-ar $out/bin/ar
ln -s ${wasiCc}/bin/wasm32-unknown-wasi-ld.lld $out/bin/ld.lld

ln -s ${wasiCc}/resource-root/include $out/lib/clang/19/include
ln -s ${wasiSysroot} $out/share/wasi-sysroot
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# First, find the flake.nix file
echo "=== Finding flake.nix ==="
fd -name "flake.nix" -type f

# Read the file to see its content
echo
echo "=== Reading flake.nix ==="
wc -l flake.nix && echo

# Show lines 40-70 to see context around lines 51-59
echo "=== Lines 40-70 (context for lines 51-59) ==="
sed -n '40,70p' flake.nix

# Show lines 105-125 to see context around lines 115-117
echo
echo "=== Lines 105-125 (context for lines 115-117) ==="
sed -n '105,125p' flake.nix

# Search for all hard-coded /lib/clang/19
echo
echo "=== All occurrences of /lib/clang/19 ==="
rg -n "clang/19" flake.nix || echo "No matches found"

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 349


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# First, find the flake.nix file
echo "=== Finding flake.nix ==="
fd "flake.nix" -type f

# Read the file to see its content
echo
echo "=== File line count ==="
wc -l flake.nix

# Show lines 40-70 to see context around lines 51-59
echo
echo "=== Lines 40-70 (context for lines 51-59) ==="
sed -n '40,70p' flake.nix

# Show lines 105-125 to see context around lines 115-117
echo
echo "=== Lines 105-125 (context for lines 115-117) ==="
sed -n '105,125p' flake.nix

# Search for all hard-coded /lib/clang/19
echo
echo "=== All occurrences of clang/19 ==="
rg "clang/19" flake.nix || echo "No matches found"

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 289


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check if flake.nix exists in current directory
echo "=== Checking for flake.nix ==="
ls -la flake.nix

# Get line count
echo
echo "=== File line count ==="
wc -l flake.nix

# Show lines 40-70 to see context around lines 51-59
echo
echo "=== Lines 40-70 (context for lines 51-59) ==="
sed -n '40,70p' flake.nix

# Show lines 105-125 to see context around lines 115-117
echo
echo "=== Lines 105-125 (context for lines 115-117) ==="
sed -n '105,125p' flake.nix

# Search for all hard-coded /lib/clang/19
echo
echo "=== All occurrences of clang/19 ==="
rg "clang/19" flake.nix || echo "No matches found"

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 3305


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show the beginning of flake.nix to see imports
echo "=== Lines 1-40 (imports and setup) ==="
sed -n '1,40p' flake.nix

# Check if lib is imported and what's available
echo
echo "=== Search for 'lib' usage in file ==="
rg "lib\." flake.nix | head -20

# Search specifically for versions function
echo
echo "=== Search for 'versions' in file ==="
rg "versions" flake.nix || echo "No matches found"

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 1809


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check full context around the wasiCc definition and the 'with pkgs;' scope
echo "=== Lines 24-45 (context showing 'with pkgs;' and wasiCc definition) ==="
sed -n '24,45p' flake.nix

# Check what happens with lib in context of nix-wasi-plugins derivation
echo
echo "=== Lines 128-132 (nix-wasm-plugins with pkgs; scope) ==="
sed -n '128,132p' flake.nix

# Search for any version extraction patterns in the file
echo
echo "=== Search for .version usage ==="
rg "\.version" flake.nix

# Check if the proposed lib.versions.major could work by looking at pkgs imports
echo
echo "=== Full grep for 'with pkgs' ==="
rg -A2 "with pkgs" flake.nix | head -30

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 1972


🏁 Script executed:

#!/bin/bash
set -eulo pipefail

# Check if wasiCc is actually accessible with .version attribute
# by looking at how stdenv.cc typically has version
echo "=== Check nixpkgs documentation or common patterns ==="
echo "In nixpkgs, stdenv.cc typically has .version attribute."
echo "lib.versions.major is a standard function in nixpkgs nixpkgs.lib"
echo

# Let's verify the exact structure by checking if there are similar patterns
echo "=== Search for any version string patterns in flake ==="
rg "version\s*=" flake.nix

# Check the full scope of nix-wasi-plugins to understand where lib would come from
echo
echo "=== Lines 24-26 (context of with pkgs scope) ==="
sed -n '24,26p' flake.nix

# Let's see the exact line numbers of the problem lines
echo
echo "=== Show exact problem lines with line numbers ==="
sed -n '51p; 57p; 115p; 116p' flake.nix | cat -n

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 1165


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Let's verify the proposed solution more carefully
# Check what's available in scope at line 42 where clangMajor would be added

echo "=== Lines 24-65 (full nix-wasi-plugins derivation context) ==="
sed -n '24,65p' flake.nix

# Also check if inputs.nixpkgs.lib is available at the top level
echo
echo "=== Lines 9-15 (outputs function and available inputs) ==="
sed -n '9,15p' flake.nix

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 2549


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify that lib.versions.major is a standard nix function
# by checking if it's a widely recognized pattern

echo "=== Verify lib.versions.major exists in nixpkgs ==="
echo "lib.versions.major is a standard function in nixpkgs.lib"
echo "It takes a version string and returns the major version number"
echo

# Let's also check what the actual clang version is in the nixpkgs being used
echo "=== Check the nix input for nixpkgs ==="
sed -n '1,10p' flake.nix | grep -A5 "inputs"

# Verify that within 'with pkgs;' scope, lib is indeed available
echo
echo "=== Confirm lib is available in 'with pkgs;' scope ==="
echo "Within 'with pkgs;' scope, all attributes of pkgs become available"
echo "lib is a standard attribute in pkgs, so lib.versions.major should work"
echo

# Count total occurrences to ensure we haven't missed any
echo "=== Final verification: count all clang/19 occurrences ==="
rg -c "clang/19" flake.nix || echo "0"

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 777


Avoid hard-coding the Clang major version in WASI SDK paths.

Lines 51 and 57 hard-code clang/19, as do lines 115–116, creating fragility when nixpkgs updates Clang. Derive the major version from wasiCc.version once and reuse it.

♻️ Proposed refactor
             wasiCc = pkgs.pkgsCross.wasi32.stdenv.cc;
             wasiLibc = pkgs.pkgsCross.wasi32.wasilibc;
             wasiLibcDev = wasiLibc.dev;
+            clangMajor = lib.versions.major wasiCc.version;
             wasiSysroot = runCommand "wasi-sysroot" { } ''
               mkdir -p $out/include $out/lib/wasm32-wasip1
               cp -R ${wasiLibcDev}/include/* $out/include/
               cp -R ${wasiLibc}/lib/* $out/lib/
               cp -R ${wasiLibc}/lib/* $out/lib/wasm32-wasip1/
             '';
             wasiSdk = runCommand "wasi-sdk-compat" { } ''
-              mkdir -p $out/bin $out/lib/clang/19 $out/share
+              mkdir -p $out/bin $out/lib/clang/${clangMajor} $out/share

               ln -s ${wasiCc}/bin/wasm32-unknown-wasi-clang $out/bin/clang
               ln -s ${wasiCc}/bin/wasm32-unknown-wasi-clang++ $out/bin/clang++
               ln -s ${wasiCc}/bin/wasm32-unknown-wasi-ar $out/bin/ar
               ln -s ${wasiCc}/bin/wasm32-unknown-wasi-ld.lld $out/bin/ld.lld

-              ln -s ${wasiCc}/resource-root/include $out/lib/clang/19/include
+              ln -s ${wasiCc}/resource-root/include $out/lib/clang/${clangMajor}/include
               ln -s ${wasiSysroot} $out/share/wasi-sysroot
             '';
@@
-            CFLAGS_wasm32_wasip1 = "--sysroot=${wasiSdk}/share/wasi-sysroot -isystem ${wasiSdk}/lib/clang/19/include";
-            BINDGEN_EXTRA_CLANG_ARGS_wasm32_wasip1 = "-fvisibility=default --sysroot=${wasiSdk}/share/wasi-sysroot -isystem ${wasiSdk}/lib/clang/19/include -resource-dir ${wasiSdk}/lib/clang/19";
+            CFLAGS_wasm32_wasip1 = "--sysroot=${wasiSdk}/share/wasi-sysroot -isystem ${wasiSdk}/lib/clang/${clangMajor}/include";
+            BINDGEN_EXTRA_CLANG_ARGS_wasm32_wasip1 = "-fvisibility=default --sysroot=${wasiSdk}/share/wasi-sysroot -isystem ${wasiSdk}/lib/clang/${clangMajor}/include -resource-dir ${wasiSdk}/lib/clang/${clangMajor}";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@flake.nix` around lines 51 - 59, The flake currently hard-codes the Clang
major version ("clang/19") in install paths; extract the major version from
wasiCc.version (e.g., parse wasiCc.version to get the major number into a
variable like clangMajor or clang_version_major) and use that variable instead
of the literal "19" wherever paths reference $out/lib/clang/19 (and the other
occurrences mentioned) so the links created (e.g., in the ln -s lines
referencing $out/lib/clang/<major>/include) and any other install targets
automatically follow the wasiCc version; ensure the parsing handles common
version formats and reuse the single variable in all place(s) that previously
used "19".

@@ -0,0 +1,84 @@
use nix_wasm_rust::{nix_wasm_init_v1, warn, wasi_arg, Value};
use rquickjs::{Array, Context, Object, Runtime, Value as JsValue};
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -name "Cargo.toml" -path "*nix-wasm-plugin-quickjs*" | head -5

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 112


🏁 Script executed:

cat nix-wasm-plugin-quickjs/Cargo.toml | grep -A 5 -B 5 rquickjs

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 225


🏁 Script executed:

head -80 nix-wasm-plugin-quickjs/src/lib.rs

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 2782


🌐 Web query:

rquickjs 0.11.0 type signatures Value Array Object lifetime parameters

💡 Result:

In rquickjs 0.11.0, the core JS handle types all carry the context-tied lifetime parameter 'js:

  • Value<'js>: pub struct Value<'js> { /* private fields */ } (any JS value) and constructors take a Ctx<'js> and return Value<'js> (e.g. new_undefined(ctx: Ctx<'js>) -> Value<'js>). [1]
  • Object<'js>: pub struct Object<'js>(/* private fields */); [2]
  • Array<'js>: pub struct Array<'js>(/* private fields */); and e.g. Array::new(ctx: Ctx<'js>) -> Result<Array<'js>, Error>. [3]

The lifetime plumbing is formalized by JsLifetime:

pub unsafe trait JsLifetime<'js> {
    type Changed<'to>: 'to;
}

This trait is described as the “'js lifetime trick” used to keep JS values from outliving the JS context they come from. [4]

For these types specifically, JsLifetime is implemented so you can “retag” the lifetime at the type level:

  • Array<'js>Changed<'to> = Array<'to> [5]
  • Object<'js>Changed<'to> = Object<'to> [5]
  • Value<'js>Changed<'to> = Value<'to> [5]

Meaning of 'js in practice: you generally can’t store/return a Value<'js> / Object<'js> / Array<'js> in a way that outlives the Ctx<'js> it’s associated with (the compiler enforces this via 'js). [1][4]

Sources: [1] Value docs, [2] Object docs, [3] Array docs, [4] JsLifetime docs, [5] JsLifetime implementors for Array/Object/Value.


rquickjs type annotations require explicit 'js lifetime parameters in version 0.11.0.

In rquickjs 0.11.0, Value<'js>, Array<'js>, and Object<'js> all require the context-tied lifetime parameter. The current code uses these types without lifetimes (JsValue, Array, Object), which will fail to compile. The lifetime parameter enforces that values cannot outlive their associated context.

Required fixes
-fn js_value_to_nix(value: JsValue) -> Value {
+fn js_value_to_nix<'js>(value: JsValue<'js>) -> Value {
    if value.is_null() || value.is_undefined() {
        return Value::make_null();
    }
    ...
    if value.is_array() {
-        let array: Array = value
+        let array: Array<'js> = value
             .clone()
             .into_array()
             .unwrap_or_else(|| fail("array conversion", "value is not an array"));
    ...
    if value.is_object() {
-        let object: Object = value
+        let object: Object<'js> = value
             .into_object()
             .unwrap_or_else(|| fail("object conversion", "value is not an object"));
        let mut entries: Vec<(StdString, Value)> = Vec::new();
-        for entry in object.props::<StdString, JsValue>() {
+        for entry in object.props::<StdString, JsValue<'js>>() {
    ...
    let value: JsValue = ctx.eval(code)
+        let value: JsValue<'_> = ctx.eval(code)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nix-wasm-plugin-quickjs/src/lib.rs` at line 2, The rquickjs types Value,
Array, and Object now require the context lifetime parameter; update the import
and all usages to include the `'js` lifetime (e.g., use rquickjs::{Array as
Array<'js>, Object as Object<'js>, Value as JsValue<'js>} or adjust type
aliases) and then update any function signatures, struct fields, and variable
annotations that reference JsValue, Array, or Object to JsValue<'js>,
Array<'js>, and Object<'js> respectively (ensure functions that introduce these
types are generic over or bound to the `'js` lifetime via fn foo<'js>(ctx:
Ctx<'js>, ...) or appropriate context types). Ensure no JS-tied value escapes
its context by aligning lifetimes across Context/Runtime usages (look for
identifiers like JsValue, Array, Object, Context in the diff and update them
accordingly).

Comment on lines +10 to +55
fn js_value_to_nix(value: JsValue) -> Value {
if value.is_null() || value.is_undefined() {
return Value::make_null();
}
if let Some(b) = value.as_bool() {
return Value::make_bool(b);
}
if let Some(i) = value.as_int() {
return Value::make_int(i as i64);
}
if let Some(f) = value.as_float() {
return Value::make_float(f);
}
if let Some(js_str) = value.as_string() {
let s = js_str
.to_string()
.unwrap_or_else(|err| fail("string conversion", err));
return Value::make_string(&s);
}
if value.is_array() {
let array: Array = value
.clone()
.into_array()
.unwrap_or_else(|| fail("array conversion", "value is not an array"));
let mut items = Vec::new();
for entry in array.into_iter() {
let entry = entry.unwrap_or_else(|err| fail("array iteration", err));
items.push(js_value_to_nix(entry));
}
return Value::make_list(&items);
}
if value.is_object() {
let object: Object = value
.into_object()
.unwrap_or_else(|| fail("object conversion", "value is not an object"));
let mut entries: Vec<(StdString, Value)> = Vec::new();
for entry in object.props::<StdString, JsValue>() {
let (key, value) = entry.unwrap_or_else(|err| fail("object iteration", err));
entries.push((key, js_value_to_nix(value)));
}
let attrs: Vec<(&str, Value)> = entries
.iter()
.map(|(key, value)| (key.as_str(), *value))
.collect();
return Value::make_attrset(&attrs);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add a recursion guard for JS→Nix conversion.

js_value_to_nix is unbounded recursive for arrays/objects. Deeply nested or cyclic JS values can cause stack overflow and hard-fail the plugin.

Suggested patch (depth guard)
+const MAX_CONVERSION_DEPTH: usize = 256;
+
 fn js_value_to_nix(value: JsValue) -> Value {
+    js_value_to_nix_with_depth(value, 0)
+}
+
+fn js_value_to_nix_with_depth(value: JsValue, depth: usize) -> Value {
+    if depth > MAX_CONVERSION_DEPTH {
+        fail("value conversion", "maximum conversion depth exceeded");
+    }
+
     if value.is_null() || value.is_undefined() {
         return Value::make_null();
     }
@@
         for entry in array.into_iter() {
             let entry = entry.unwrap_or_else(|err| fail("array iteration", err));
-            items.push(js_value_to_nix(entry));
+            items.push(js_value_to_nix_with_depth(entry, depth + 1));
         }
         return Value::make_list(&items);
     }
@@
         for entry in object.props::<StdString, JsValue>() {
             let (key, value) = entry.unwrap_or_else(|err| fail("object iteration", err));
-            entries.push((key, js_value_to_nix(value)));
+            entries.push((key, js_value_to_nix_with_depth(value, depth + 1)));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nix-wasm-plugin-quickjs/src/lib.rs` around lines 10 - 55, The js_value_to_nix
function is unbounded-recursive for arrays/objects; add a recursion depth guard
by changing js_value_to_nix to accept a depth parameter (e.g., depth: usize) and
a MAX_DEPTH constant, check at the start and return a safe fallback (e.g.,
Value::make_null() or a controlled error via fail) when depth == 0, and
decrement depth on each recursive call into arrays (Array::into_iter ->
js_value_to_nix(entry, depth-1)) and objects (object.props ->
js_value_to_nix(value, depth-1)); update all call sites to pass an initial
MAX_DEPTH and ensure the conversion for lists (Value::make_list) and attrsets
(Value::make_attrset) uses the guarded recursive calls so deeply nested or
cyclic JS values stop at the configured depth.

Comment on lines +43 to +47
let value_id = arg
.parse::<ValueId>()
.unwrap_or_else(|err| panic(&format!("invalid WASI argument '{arg}': {err}")));
Value::from_raw(value_id)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate ValueId is non-zero before constructing Value.

wasi_arg() currently accepts 0, but 0 is treated as a sentinel invalid/missing value elsewhere (e.g., get_attr). Rejecting 0 here avoids propagating invalid handles.

Suggested patch
 pub fn wasi_arg() -> Value {
     let arg = std::env::args()
         .nth(1)
         .unwrap_or_else(|| panic("missing WASI argument"));
     let value_id = arg
         .parse::<ValueId>()
         .unwrap_or_else(|err| panic(&format!("invalid WASI argument '{arg}': {err}")));
+    if value_id == 0 {
+        panic("invalid WASI argument: ValueId must be non-zero");
+    }
     Value::from_raw(value_id)
 }
📝 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
let value_id = arg
.parse::<ValueId>()
.unwrap_or_else(|err| panic(&format!("invalid WASI argument '{arg}': {err}")));
Value::from_raw(value_id)
}
let value_id = arg
.parse::<ValueId>()
.unwrap_or_else(|err| panic(&format!("invalid WASI argument '{arg}': {err}")));
if value_id == 0 {
panic("invalid WASI argument: ValueId must be non-zero");
}
Value::from_raw(value_id)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nix-wasm-rust/src/lib.rs` around lines 43 - 47, The parsed ValueId returned
by wasi_arg() must be checked for the sentinel zero before constructing a Value:
after parsing into ValueId (the variable value_id), add a guard that rejects 0
(e.g., panic or return an error) with a clear message (similar style to the
existing parse error) and only call Value::from_raw(value_id) for non-zero ids;
this prevents propagating an invalid handle used elsewhere (see get_attr and
Value::from_raw).

Copy link
Contributor

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
nix-wasm-plugin-fib-wasi/src/main.rs (1)

25-30: ⚠️ Potential issue | 🔴 Critical

Guard argv[1] before indexing.

Line 27 accesses args[1] without a bounds check; invoking the binary without that argument will panic.

Proposed fix
 use nix_wasm_plugin_fib::fib;
 use nix_wasm_rust::Value;
 use std::env;
+use std::process;

 fn main() {
@@
-    let args: Vec<String> = env::args().collect();
-    let arg = Value::from_raw(
-        args[1]
-            .parse::<u32>()
-            .expect("argv[1] should be a valid ValueId"),
-    );
+    let arg_raw = match env::args().nth(1) {
+        Some(v) => v,
+        None => {
+            eprintln!("missing argv[1]: expected ValueId");
+            process::exit(2);
+        }
+    };
+    let arg = Value::from_raw(
+        arg_raw
+            .parse::<u32>()
+            .expect("argv[1] should be a valid ValueId"),
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nix-wasm-plugin-fib-wasi/src/main.rs` around lines 25 - 30, The code indexes
args[1] without checking args.len(), which will panic if the program is invoked
without that argument; modify the main function to guard access by checking
args.len() or using args.get(1) and handling the None case (e.g., print a usage
error and exit or return a Result). Ensure the guarded value is then parsed as
before and passed into Value::from_raw, keeping the existing
parse::<u32>().expect only for parse failures, and refer to the symbols
env::args(), args, args.get(1), and Value::from_raw when making the change.
🧹 Nitpick comments (2)
flake.nix (2)

96-101: Consider documenting WASM feature requirements.

The wasm-opt flags (--enable-bulk-memory, --enable-nontrapping-float-to-int, --enable-simd) enable WASM features that require runtime support. If downstream consumers need to know the minimum WASM runtime version, consider documenting these requirements.

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

In `@flake.nix` around lines 96 - 101, The installPhase currently invokes wasm-opt
with feature flags (--enable-bulk-memory, --enable-nontrapping-float-to-int,
--enable-simd) that require runtime support; add documentation describing these
required WASM features and minimum runtime versions (or engine flags) so
downstream consumers know compatibility constraints. Update the flake.nix by
adding a brief comment above the installPhase referencing the exact flags and
recommended minimum engine versions (e.g., wasmtime/node/chrome versions) and
also add a short entry to the repository README (or a new WASM.md) listing the
same flags, the reason they're used, and links to relevant runtime/feature docs
so consumers can verify their environment supports installPhase's outputs.

87-88: Address the FIXME: Filter workspace to include only WASI plugins.

The comment indicates that non-WASI plugins should be filtered from the source. Currently, the entire workspace is passed as src, which may include unnecessary files and could affect build reproducibility.

Would you like me to help create a source filter that includes only the WASI plugin crates (nix-wasm-plugin-quickjs, nix-wasm-plugin-fib-wasi) and their dependencies? Alternatively, I can open an issue to track this cleanup task.

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

In `@flake.nix` around lines 87 - 88, Replace the blanket workspace source
assignment "src = self" with a filtered source that includes only the WASI
plugin crates and their dependencies: use Nix's filterSource (or
lib.filterSource) to match crate directories named "nix-wasm-plugin-quickjs" and
"nix-wasm-plugin-fib-wasi" and exclude other workspace members; ensure the
filter preserves Cargo.toml/Cargo.lock and any shared crates those plugins
depend on so builds remain reproducible. Locate the line "src = self" in
flake.nix and replace it by constructing a filtered source using filterSource
with a predicate that returns true for the two plugin directories and for any
path under their dependency subtrees.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@flake.nix`:
- Around line 25-26: The flake references a non-existent attribute
rustPackages_1_89 causing build failures; replace it by either updating the
pinned nixpkgs in flake.lock to a revision that provides rustPackages_1_89, or
change the flake to use the standard pkg attributes (e.g. pkgs.rustc and
pkgs.cargo) and derive rustPlatform from pkgs (replace rustPackages =
pkgs.rustPackages_1_89; rustPlatform = rustPackages.rustPlatform; with an
approach that uses the updated nixpkgs or pkgs.rustc/pkgs.cargo to construct the
rustPlatform).

---

Outside diff comments:
In `@nix-wasm-plugin-fib-wasi/src/main.rs`:
- Around line 25-30: The code indexes args[1] without checking args.len(), which
will panic if the program is invoked without that argument; modify the main
function to guard access by checking args.len() or using args.get(1) and
handling the None case (e.g., print a usage error and exit or return a Result).
Ensure the guarded value is then parsed as before and passed into
Value::from_raw, keeping the existing parse::<u32>().expect only for parse
failures, and refer to the symbols env::args(), args, args.get(1), and
Value::from_raw when making the change.

---

Nitpick comments:
In `@flake.nix`:
- Around line 96-101: The installPhase currently invokes wasm-opt with feature
flags (--enable-bulk-memory, --enable-nontrapping-float-to-int, --enable-simd)
that require runtime support; add documentation describing these required WASM
features and minimum runtime versions (or engine flags) so downstream consumers
know compatibility constraints. Update the flake.nix by adding a brief comment
above the installPhase referencing the exact flags and recommended minimum
engine versions (e.g., wasmtime/node/chrome versions) and also add a short entry
to the repository README (or a new WASM.md) listing the same flags, the reason
they're used, and links to relevant runtime/feature docs so consumers can verify
their environment supports installPhase's outputs.
- Around line 87-88: Replace the blanket workspace source assignment "src =
self" with a filtered source that includes only the WASI plugin crates and their
dependencies: use Nix's filterSource (or lib.filterSource) to match crate
directories named "nix-wasm-plugin-quickjs" and "nix-wasm-plugin-fib-wasi" and
exclude other workspace members; ensure the filter preserves
Cargo.toml/Cargo.lock and any shared crates those plugins depend on so builds
remain reproducible. Locate the line "src = self" in flake.nix and replace it by
constructing a filtered source using filterSource with a predicate that returns
true for the two plugin directories and for any path under their dependency
subtrees.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 09451cb and 87a96c9.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock
  • wasi/Cargo.lock is excluded by !**/*.lock
  • wasi/flake.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • .github/workflows/ci.yml
  • .vscode/tasks.json
  • Cargo.toml
  • flake.nix
  • nix-wasm-plugin-fib-wasi/Cargo.toml
  • nix-wasm-plugin-fib-wasi/src/main.rs
  • nix-wasm-plugin-fib-wasi/tests/fib.exp
  • nix-wasm-plugin-fib-wasi/tests/fib.nix
  • wasi/Cargo.toml
  • wasi/flake.nix
  • wasi/nix-wasi-plugin-fib/Cargo.toml
💤 Files with no reviewable changes (4)
  • wasi/Cargo.toml
  • wasi/nix-wasi-plugin-fib/Cargo.toml
  • .github/workflows/ci.yml
  • wasi/flake.nix
✅ Files skipped from review due to trivial changes (1)
  • nix-wasm-plugin-fib-wasi/tests/fib.exp
🚧 Files skipped from review as they are similar to previous changes (3)
  • Cargo.toml
  • nix-wasm-plugin-fib-wasi/tests/fib.nix
  • .vscode/tasks.json

Comment on lines +25 to +26
rustPackages = pkgs.rustPackages_1_89;
rustPlatform = rustPackages.rustPlatform;
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify rustPackages_1_89 exists in nixpkgs

# First, check available rust versions in nixpkgs
echo "=== Checking rustPackages attributes in nixpkgs ==="
nix eval --impure --expr 'let pkgs = import <nixpkgs> {}; in builtins.filter (x: builtins.match "rustPackages_[0-9]+_[0-9]+" x != null) (builtins.attrNames pkgs)' 2>&1 | head -50

echo ""
echo "=== Attempting direct access to rustPackages_1_89 ==="
nix eval --impure --expr '(import <nixpkgs> {}).rustPackages_1_89 or "NOT_FOUND"' 2>&1

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 269


🏁 Script executed:

#!/bin/bash
# Read flake.nix to understand the context around lines 25-26
echo "=== Examining flake.nix around the rustPackages_1_89 reference ==="
head -35 flake.nix | tail -20

echo ""
echo "=== Checking flake.lock for nixpkgs input ==="
if [ -f flake.lock ]; then
  cat flake.lock | grep -A 10 '"nixpkgs"'
fi

echo ""
echo "=== Searching for rustPackages references in the repo ==="
rg "rustPackages" --type nix -A 2 -B 2

Repository: DeterminateSystems/nix-wasm-rust

Length of output: 4875


🌐 Web query:

Rust 1.89 release date nixpkgs rustPackages_1_89

💡 Result:

  • Rust 1.89.0 release date: August 7, 2025. [1] (also confirmed by the 1.89.0 pre-release announcement stating the release was scheduled for Aug 7) [2]

  • nixpkgs rustPackages_1_89: nixpkgs does not generally provide a rustPackages_1_XX scope for every Rust version; older pinned scopes exist for some versions (example list includes rustPackages, rustPackages_1_45, rustPackages_1_48, etc.), but rustPackages_1_89 is not a standard top-level attribute you can rely on. [3]

  • How to get Rust 1.89 from nixpkgs anyway (pin a revision that packaged it):

    • Rust 1.89.0 is available in nixpkgs at revision 01b6809f7f9d1183a2b3e081f0a1e6f8f415cb09 (as #rustc). [4]
    • Practically: pin nixpkgs to that commit and use pkgs.rustc / pkgs.cargo (or that revision’s pkgs.rustPlatform) rather than expecting rustPackages_1_89. [4]

Sources: [1] [2] [3] [4]


rustPackages_1_89 does not exist in the pinned nixpkgs version and will cause build failures.

The flake.lock pins nixpkgs to revision daf6dc47... (September 2024), which predates Rust 1.89's release (August 2025). Additionally, nixpkgs does not maintain rustPackages_1_XX attributes for every Rust version. Either update the nixpkgs pin to a recent revision that includes Rust 1.89, or use pkgs.rustc and pkgs.cargo directly instead of the non-existent rustPackages_1_89 scope.

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

In `@flake.nix` around lines 25 - 26, The flake references a non-existent
attribute rustPackages_1_89 causing build failures; replace it by either
updating the pinned nixpkgs in flake.lock to a revision that provides
rustPackages_1_89, or change the flake to use the standard pkg attributes (e.g.
pkgs.rustc and pkgs.cargo) and derive rustPlatform from pkgs (replace
rustPackages = pkgs.rustPackages_1_89; rustPlatform = rustPackages.rustPlatform;
with an approach that uses the updated nixpkgs or pkgs.rustc/pkgs.cargo to
construct the rustPlatform).

@edolstra edolstra merged commit 5e87518 into main Feb 28, 2026
3 checks passed
@edolstra edolstra deleted the quickjs branch February 28, 2026 20:32
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.

4 participants