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
29 changes: 29 additions & 0 deletions .dev/checklist-jit-fuel-timeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Checklist: JIT Fuel/Timeout Fix + PR #6 Timeout Merge

## Phase A: Fix JIT fuel bypass (existing bug)

- [ ] A1. Add fuel/deadline check at JIT back-edges (loop headers)
- At OP_BR / OP_BR_IF / OP_BR_IF_NOT / OP_BR_TABLE emission:
detect backward branch (target_pc <= current_pc)
- Emit trampoline: spill → call consumeInstructionBudget → check error → reload
- Or: emit inline decrement + conditional exit (cheaper, no call overhead)
- Both ARM64 and x86_64 codegen
- [ ] A2. Tests: `--fuel 1000000 --invoke loop loop.wasm` terminates with JIT enabled
- [ ] A3. Tests: `zig build test` all pass, no leaks
- [ ] A4. Spec tests: `python3 test/spec/run_spec.py --build --summary` — fail=0, skip=0
- [ ] A5. E2E tests: `bash test/e2e/run_e2e.sh --convert --summary` — fail=0, leak=0
- [ ] A6. Real-world: `bash test/realworld/run_compat.sh` — PASS=50, FAIL=0
- [ ] A7. Benchmarks: `bash bench/run_bench.sh --quick` — no regression
- Record: `bash bench/record.sh --id=jit-fuel-check --reason="back-edge fuel/deadline"`
- [ ] A8. Commit + Merge Gate (Mac + Ubuntu)

## Phase B: Merge timeout support (PR #6 + our additions)

- [ ] B1. Apply PR #6 essential changes (TimeoutExceeded, deadline fields, consumeInstructionBudget, setDeadlineTimeoutMs)
- [ ] B2. Add `--timeout <ms>` CLI option
- [ ] B3. Add tests (expired, infinite loop, API)
- [ ] B4. Verify: `--timeout 50 --invoke loop loop.wasm` terminates (JIT enabled)
- [ ] B5. All gates pass (test, spec, e2e, realworld, bench)
- [ ] B6. Commit + Merge Gate (Mac + Ubuntu)
- [ ] B7. Comment on PR #6 with results, credit DeanoC
- [ ] B8. Close PR #6 (or merge if DeanoC rebases)
4 changes: 4 additions & 0 deletions .dev/checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ Prefix: W## (to distinguish from CW's F## items).
CI pinned to Rust 1.92.0 as workaround. Go not installed on macOS CI.
Reproduce: `rustup run 1.93.1 cargo build` serde_json, run with zwasm ARM64.

- [ ] W36: Flaky real-world compat: go_crypto_sha256 / go_regex intermittent DIFF
Reproduced on base code (no local changes). Non-deterministic — passes ~50% of runs.
Likely related to W35 (ARM64 JIT OOB). Not blocking current work.

## Resolved items (summary, details in git history)

W2 (table.init), W4 (fd_readdir), W5 (sock_*), W7 (Component Model Stage 22),
Expand Down
36 changes: 27 additions & 9 deletions .dev/memo.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,33 @@ Session handover document. Read at session start.

## Current Task

**Phase 11: Allocator Injection + Embedding (D128)**

Design: `@./.dev/references/allocator-injection-plan.md`.

- [x] **11.2** C API config — `zwasm_config_t` + `set_allocator()` + `new_configured()` (3d4db98)
- [x] **11.3** Docs — ARCHITECTURE.md allocator flow, `docs/embedding.md` (d5709f5)
- [x] **11.1** CW finalizer — GC finalizer registry + WasmModule deinit (CW 4a12e1d)

**Done**. v1.5.0 released. CW updated (e41ee70). Next: check roadmap for next phase.
**Fix JIT fuel bypass + PR #6 timeout merge**

Checklist: `@./.dev/checklist-jit-fuel-timeout.md`
PR review: `@./private/pr6-timeout-review.md`

### Phase A: Fix JIT fuel bypass (branch: `fix/jit-fuel-bypass`)
- [x] A1. Add `jitSuppressed()` — suppress JIT when `fuel != null` (6 locations in vm.zig)
- [x] A2. Test: infinite loop with fuel=1M terminates (`30_infinite_loop.wasm`)
- [ ] A3. Commit Gate: `zig build test` pass, spec/e2e/realworld/bench (running)
- [ ] A4. Merge Gate (Mac + Ubuntu)

### Phase B: Merge timeout support (PR #6 + additions)
- [ ] B1. Apply PR #6 changes (TimeoutExceeded, deadline, consumeInstructionBudget)
- [ ] B2. Add `--timeout <ms>` CLI option
- [ ] B3. Tests + verify with JIT enabled
- [ ] B4. Commit + Merge Gate
- [ ] B5. Comment on PR #6, credit DeanoC

## Handover Notes

### JIT fuel/timeout suppression — current fix vs proper solution
- **Current fix**: `jitSuppressed()` disables JIT entirely when `fuel != null`. Simple, correct, zero impact on normal execution.
- **Proper solution**: Emit fuel/deadline checks at JIT loop back-edges (like wasmtime). This preserves JIT performance even with fuel/timeout.
- wasmtime uses negative-accumulation fuel (increment toward 0, sign check) + epoch-based timeout (atomic counter at loop headers).
- zwasm JIT caches `vm_ptr` in x20 (ARM64) — inline `vm->fuel` decrement + conditional trampoline exit is feasible.
- Separate future task. See `@./private/pr6-timeout-review.md` §Fix Options and wasmtime research in `~/Documents/OSS/wasmtime/crates/cranelift/src/func_environ.rs`.
- **Flaky compat tests**: W36 in checklist.md — `go_crypto_sha256`/`go_regex` intermittent DIFF on base code (pre-existing, likely W35-related).

## References

Expand Down
128 changes: 128 additions & 0 deletions bench/history.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2541,3 +2541,131 @@ entries:
rw_cpp_string_cached: {time_ms: 8.5}
rw_cpp_sort: {time_ms: 7.9}
rw_cpp_sort_cached: {time_ms: 7.4}
- id: "jit-fuel-suppress"
date: "2026-03-11"
reason: "Suppress JIT when fuel metering active (no perf impact on normal execution)"
commit: "83d1a33"
build: ReleaseSafe
results:
fib: {time_ms: 49.6}
fib_cached: {time_ms: 48.2}
tak: {time_ms: 6.2}
tak_cached: {time_ms: 5.5}
sieve: {time_ms: 5.2}
sieve_cached: {time_ms: 5.3}
nbody: {time_ms: 23.7}
nbody_cached: {time_ms: 22.1}
nqueens: {time_ms: 2.5}
nqueens_cached: {time_ms: 2.2}
tgo_fib: {time_ms: 34.3}
tgo_fib_cached: {time_ms: 33.5}
tgo_tak: {time_ms: 6.6}
tgo_tak_cached: {time_ms: 6.3}
tgo_arith: {time_ms: 2.3}
tgo_arith_cached: {time_ms: 2.5}
tgo_sieve: {time_ms: 4.2}
tgo_sieve_cached: {time_ms: 4.0}
tgo_fib_loop: {time_ms: 2.0}
tgo_fib_loop_cached: {time_ms: 2.1}
tgo_gcd: {time_ms: 2.0}
tgo_gcd_cached: {time_ms: 3.4}
tgo_nqueens: {time_ms: 41.5}
tgo_nqueens_cached: {time_ms: 41.6}
tgo_mfr: {time_ms: 49.4}
tgo_mfr_cached: {time_ms: 49.7}
tgo_list: {time_ms: 37.7}
tgo_list_cached: {time_ms: 36.1}
tgo_rwork: {time_ms: 6.9}
tgo_rwork_cached: {time_ms: 7.3}
tgo_strops: {time_ms: 33.6}
tgo_strops_cached: {time_ms: 33.1}
st_fib2: {time_ms: 900.0}
st_fib2_cached: {time_ms: 904.3}
st_sieve: {time_ms: 184.2}
st_sieve_cached: {time_ms: 185.4}
st_nestedloop: {time_ms: 3.2}
st_nestedloop_cached: {time_ms: 3.1}
st_ackermann: {time_ms: 6.5}
st_ackermann_cached: {time_ms: 5.2}
st_matrix: {time_ms: 286.2}
st_matrix_cached: {time_ms: 291.0}
gc_alloc: {time_ms: 6.6}
gc_alloc_cached: {time_ms: 3.1}
gc_tree: {time_ms: 23.7}
gc_tree_cached: {time_ms: 23.7}
rw_rust_fib: {time_ms: 36.5}
rw_rust_fib_cached: {time_ms: 37.2}
rw_c_matrix: {time_ms: 20.3}
rw_c_matrix_cached: {time_ms: 17.1}
rw_c_math: {time_ms: 61.1}
rw_c_math_cached: {time_ms: 59.7}
rw_c_string: {time_ms: 41.9}
rw_c_string_cached: {time_ms: 45.3}
rw_cpp_string: {time_ms: 8.9}
rw_cpp_string_cached: {time_ms: 7.5}
rw_cpp_sort: {time_ms: 6.5}
rw_cpp_sort_cached: {time_ms: 7.0}
- id: "timeout-support"
date: "2026-03-11"
reason: "Add timeout support (PR#6 + CLI + JIT deadline suppression)"
commit: "c333873"
build: ReleaseSafe
results:
fib: {time_ms: 50.5}
fib_cached: {time_ms: 48.3}
tak: {time_ms: 5.7}
tak_cached: {time_ms: 7.6}
sieve: {time_ms: 4.8}
sieve_cached: {time_ms: 0.7}
nbody: {time_ms: 24.0}
nbody_cached: {time_ms: 22.9}
nqueens: {time_ms: 3.8}
nqueens_cached: {time_ms: 1.1}
tgo_fib: {time_ms: 38.9}
tgo_fib_cached: {time_ms: 30.8}
tgo_tak: {time_ms: 7.7}
tgo_tak_cached: {time_ms: 7.7}
tgo_arith: {time_ms: 3.0}
tgo_arith_cached: {time_ms: 2.4}
tgo_sieve: {time_ms: 3.7}
tgo_sieve_cached: {time_ms: 4.8}
tgo_fib_loop: {time_ms: 3.0}
tgo_fib_loop_cached: {time_ms: 2.8}
tgo_gcd: {time_ms: 2.0}
tgo_gcd_cached: {time_ms: 3.4}
tgo_nqueens: {time_ms: 41.8}
tgo_nqueens_cached: {time_ms: 44.0}
tgo_mfr: {time_ms: 50.4}
tgo_mfr_cached: {time_ms: 50.5}
tgo_list: {time_ms: 42.6}
tgo_list_cached: {time_ms: 38.2}
tgo_rwork: {time_ms: 6.7}
tgo_rwork_cached: {time_ms: 7.0}
tgo_strops: {time_ms: 33.0}
tgo_strops_cached: {time_ms: 34.9}
st_fib2: {time_ms: 921.5}
st_fib2_cached: {time_ms: 924.6}
st_sieve: {time_ms: 187.3}
st_sieve_cached: {time_ms: 193.1}
st_nestedloop: {time_ms: 1.9}
st_nestedloop_cached: {time_ms: 3.1}
st_ackermann: {time_ms: 4.6}
st_ackermann_cached: {time_ms: 5.7}
st_matrix: {time_ms: 291.3}
st_matrix_cached: {time_ms: 291.8}
gc_alloc: {time_ms: 4.2}
gc_alloc_cached: {time_ms: 5.4}
gc_tree: {time_ms: 21.7}
gc_tree_cached: {time_ms: 24.8}
rw_rust_fib: {time_ms: 35.5}
rw_rust_fib_cached: {time_ms: 35.4}
rw_c_matrix: {time_ms: 21.3}
rw_c_matrix_cached: {time_ms: 20.9}
rw_c_math: {time_ms: 67.9}
rw_c_math_cached: {time_ms: 69.1}
rw_c_string: {time_ms: 48.6}
rw_c_string_cached: {time_ms: 49.9}
rw_cpp_string: {time_ms: 10.1}
rw_cpp_string_cached: {time_ms: 8.7}
rw_cpp_sort: {time_ms: 9.1}
rw_cpp_sort_cached: {time_ms: 7.6}
52 changes: 42 additions & 10 deletions src/cli.zig
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ fn printUsage(w: *std.Io.Writer) void {
\\ --allow-all Grant all WASI capabilities
\\ --max-memory <N> Memory ceiling in bytes (limits memory.grow)
\\ --fuel <N> Instruction fuel limit (traps when exhausted)
\\ --timeout <ms> Execution timeout in milliseconds
\\ --trace=CATS Trace categories: jit,regir,exec,mem,call (comma-separated)
\\ --dump-regir=N Dump RegIR for function index N
\\ --cache Cache predecoded IR to disk for faster startup
Expand Down Expand Up @@ -152,6 +153,7 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer
var caps = types.Capabilities.cli_default;
var max_memory_bytes: ?u64 = null;
var fuel: ?u64 = null;
var timeout_ms: ?u64 = null;

// Parse options
var i: usize = 0;
Expand Down Expand Up @@ -260,6 +262,18 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer
try stderr.flush();
return false;
};
} else if (std.mem.eql(u8, args[i], "--timeout")) {
i += 1;
if (i >= args.len) {
try stderr.print("error: --timeout requires milliseconds\n", .{});
try stderr.flush();
return false;
}
timeout_ms = std.fmt.parseInt(u64, args[i], 10) catch {
try stderr.print("error: --timeout requires a valid number\n", .{});
try stderr.flush();
return false;
};
} else if (std.mem.eql(u8, args[i], "--")) {
// Explicit separator: everything after is function/WASI args
func_args_start = i + 1;
Expand Down Expand Up @@ -334,11 +348,11 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer
types.WasmModule.loadWithImports(allocator, link_bytes, import_entries.items) catch
// Retry without imports if the linked module doesn't need them
types.WasmModule.load(allocator, link_bytes) catch |err| {
allocator.free(link_bytes);
try stderr.print("error: failed to load linked module '{s}': {s}\n", .{ lpath, formatWasmError(err) });
try stderr.flush();
return false;
}
allocator.free(link_bytes);
try stderr.print("error: failed to load linked module '{s}': {s}\n", .{ lpath, formatWasmError(err) });
try stderr.flush();
return false;
}
else
types.WasmModule.load(allocator, link_bytes) catch |err| {
allocator.free(link_bytes);
Expand Down Expand Up @@ -440,6 +454,7 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer
// Apply resource limits
module.vm.max_memory_bytes = max_memory_bytes;
module.vm.fuel = fuel;
if (timeout_ms) |ms| module.vm.setDeadlineTimeoutMs(ms);

// Lookup export info for type-aware parsing and validation
const export_info = module.getExportInfo(func_name);
Expand Down Expand Up @@ -587,6 +602,7 @@ fn cmdRun(allocator: Allocator, args: []const []const u8, stdout: *std.Io.Writer
// Apply resource limits
module.vm.max_memory_bytes = max_memory_bytes;
module.vm.fuel = fuel;
if (timeout_ms) |ms| module.vm.setDeadlineTimeoutMs(ms);

var no_args = [_]u64{};
var no_results = [_]u64{};
Expand Down Expand Up @@ -799,7 +815,10 @@ fn printProfile(profile: *const vm_mod.Profile, w: *std.Io.Writer) void {
// Print misc opcode counts if any
var has_misc = false;
for (0..32) |i| {
if (profile.misc_counts[i] > 0) { has_misc = true; break; }
if (profile.misc_counts[i] > 0) {
has_misc = true;
break;
}
}
if (has_misc) {
w.print("\nMisc opcodes (0xFC prefix):\n", .{}) catch {};
Expand Down Expand Up @@ -1311,7 +1330,10 @@ fn cmdBatch(allocator: Allocator, wasm_bytes: []const u8, imports: []const types
// (after set_main, the original main needs to remain accessible)
var already_tracked = false;
for (dyn_modules.items) |dm| {
if (dm == main_module) { already_tracked = true; break; }
if (dm == main_module) {
already_tracked = true;
break;
}
}
if (!already_tracked) {
dyn_names.append(allocator, reg_name) catch {};
Expand Down Expand Up @@ -1419,11 +1441,17 @@ fn cmdBatch(allocator: Allocator, wasm_bytes: []const u8, imports: []const types
// Find the target module
var t_module: ?*types.WasmModule = null;
for (dyn_names.items, dyn_modules.items) |dn, dm| {
if (std.mem.eql(u8, dn, t_mod_name)) { t_module = dm; break; }
if (std.mem.eql(u8, dn, t_mod_name)) {
t_module = dm;
break;
}
}
if (t_module == null) {
for (link_names, linked_modules) |ln, lm| {
if (std.mem.eql(u8, ln, t_mod_name)) { t_module = lm; break; }
if (std.mem.eql(u8, ln, t_mod_name)) {
t_module = lm;
break;
}
}
}
if (t_module == null) {
Expand Down Expand Up @@ -1726,7 +1754,10 @@ fn cmdBatch(allocator: Allocator, wasm_bytes: []const u8, imports: []const types
break;
};
arg_count += 1;
if (arg_count >= arg_buf.len) { arg_err = true; break; }
if (arg_count >= arg_buf.len) {
arg_err = true;
break;
}
arg_buf[arg_count] = std.fmt.parseInt(u64, v128_data[colon + 1 ..], 10) catch {
arg_err = true;
break;
Expand Down Expand Up @@ -2070,6 +2101,7 @@ fn formatWasmError(err: anyerror) []const u8 {
error.OutOfMemory => "out of memory",
error.MemoryLimitExceeded => "memory grow exceeded maximum",
error.FuelExhausted => "fuel limit exhausted",
error.TimeoutExceeded => "execution timed out",
// File errors
error.FileNotFound => "file not found",
error.WatNotEnabled => "WAT format disabled (build with -Dwat=true)",
Expand Down
Binary file added src/testdata/30_infinite_loop.wasm
Binary file not shown.
13 changes: 13 additions & 0 deletions src/testdata/30_infinite_loop.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
(module
(func (export "loop") (result i32)
(local $i i32)
(local.set $i (i32.const 0))
(block $break
(loop $continue
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $continue)
)
)
(local.get $i)
)
)
Loading
Loading