Skip to content

wam_rust: R−1 baseline fixes, align nested execution, cut and ITE with SWI (D129–D134) - #4356

Merged
s243a merged 6 commits into
mainfrom
claude/peerhailer-exploratory-docs-aodas5
Oct 9, 2026
Merged

s243a merged 6 commits into
mainfrom
claude/peerhailer-exploratory-docs-aodas5

Conversation

@s243a

@s243a s243a commented Oct 6, 2026

Copy link
Copy Markdown
Owner

Phase R−1 of the heap-cell rewrite design (docs/proposals/wam_rust_heap_cell_rewrite_design.md §12.1): fix, on today's runtime, the places where the Rust WAM target differs from SWI-Prolog, so the rewrite targets correct behaviour instead of emulating bugs. One commit per item, each with its own ledger row. A shared report is in docs/reports/wam_rust_r_minus_1_baseline_fixes.md.

item row fix example: SWI vs Rust before → after
R−1f D129 the lowered emitter's atom and fresh-variable literals now compile under the default intern feature lowered crate didn't build (type errors) → builds and matches SWI under both feature sets
R−1a D130 backtrack floor around the catch/3 goal and recovery, lowered call/execute/lowered_dispatch, and the dynamic rule-body solver catch_floor: [ok] vs [] → [ok] (also lower_floor, dyn_floor)
R−1b D131 one accumulator per active aggregate frame nested: [[a,b]] vs [[b]] → [[a,b]]
R−1c D132 Rust-only WAM rewrite (shared wam_target.pl unchanged): permanent variables first bound inside an ITE are initialized before the guard, limited to the code the guard reaches ite_map(no,R): [a,b] vs [] → [a,b]; lowered and interpreter now agree on rundoite
R−1d D133 B0 saved in the environment frame at Allocate, restored at Deallocate (standard WAM) cut_after_call: [first] vs [first,fallback] → [first]
R−1e D134 the catch/3 goal is opaque to cut catch_cut was broken → [a,c]

catch/3 stays first-solution by design, so catch_cut gives [a,c] where SWI gives [a,b,c]. The harness checks it against SWI's catch(once(G),C,R) and asserts the difference explicitly.

R−1a and the hybrid wrappers: the rust_target.pl hybrid wrappers were left unchanged on purpose. Each starts with vm.reset_query(), so no older caller choice point can exist and a floor would be a no-op.

Tests

  • New tests/test_wam_rust_baseline_fixes.pl (92 tests):
    • compiles each program through the Rust WAM target and compares with SWI in interpreter mode, functions mode, direct lowered calls, and a --no-default-features build;
    • every case was confirmed failing before its fix.
  • Lowered exec tests and two expected-text assertions were updated for the .into() literal form.
  • 6 previously failing Rust WAM plunit files now pass (cut_semantics, lowered_dispatch, lowered_ite_exec, lowered_t4/5/6). The set of failing tests is a strict subset of the base's.

Verification

  • Agent, on every commit (clean export):
    • term differential 2600/0/0, corpus 51/51;
    • store differential 503/0/0, store corpus 51/51 and identical to the term corpus;
    • crate lib 260/260, CI rust conformance smoke.
  • Byte identity: all resolver output JSONLs and scale --bench stdout at N=40 and N=5000 are cmp-identical to the base after every commit. The re-freeze records an unchanged baseline.
  • Perf: N=40 callgrind −0.5% overall; no item above +0.05%.
  • Independently re-run on the final head after a fresh build: term differential 0 divergences, corpus 51/51, crate lib 260/260, and the harness's 92/92.

Found, not fixed (documented in the report; outside R−1)

  • the lowered emitter drops aggregates;
  • a lowered call/execute of a runtime builtin with no label (e.g. catch/3) returns false;
  • the lowered emitter emits the ','/2 goal functor as "/2";
  • the \+/1 builtin arm uses a sentinel, not a floor (not reached under default options);
  • catch/3 recovery is still transparent to cut;
  • an aggregate frame abandoned by an exception leaves its inner list behind.

🤖 Generated with Claude Code

https://claude.ai/code/session_013PJqq7jMzces6bMwTnPbpd


Generated by Claude Code

claude added 6 commits October 6, 2026 03:43
The lowered emitter wrote Value::Atom("x".to_string()) and
Value::Unbound(format!(..)), which do not compile since D96 made the
payload a Sym (an interner id under the default intern feature). Emit
"x".into() and WamState::fresh_var_sym instead; same names, no behavior
change.

Adds tests/test_wam_rust_baseline_fixes.pl, the R-1 SWI-comparison
harness, with the R-1f programs (lowered atom constants, get_nil, fresh
variables) built under default features and under
--no-default-features --features decorate_sort. The lowered exec tests
and the cut-semantics driver build their own inputs with .into() now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PJqq7jMzces6bMwTnPbpd
catch/3 (goal and recovery), lowered call/execute, lowered_dispatch and
the dynamic rule-body solver ran nested code without a backtrack floor,
so a goal failing inside the nested run resumed the caller's choice
points. Each now saves backtrack_floor, sets it to its entry depth (the
mechanism call_goal_once already uses) and restores it on every exit.

The hybrid-wrapper backtrack loops in rust_target.pl are unchanged: each
wrapper starts with reset_query(), so no older caller choice point exists.

Harness cases catch_floor, catch_rec_floor, lower_floor, lower_floor_ex
and dyn_floor now match SWI in interpreter and functions mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PJqq7jMzces6bMwTnPbpd
BeginAggregate cleared the single aggregate_acc and finalisation cleared
it again, so an aggregate nested in another aggregate's goal wiped the
outer one's solutions (findall over a findall gave [b] for [a,b]).
BeginAggregate now parks the enclosing accumulator in its frame's data
after the continuation pc, and finalisation swaps it back before any
exit and finalises from the frame's own list.

Harness cases nested, nested_bag and nested_fail match SWI in
interpreter and functions mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PJqq7jMzces6bMwTnPbpd
After an if-then-else (or disjunction) the shared compiler keeps one
branch's variable map, so a Y variable first seen inside the construct
was initialised on one path only: a failed condition left the slot
absent for the read after the ITE (ite_map(no,R) gave [] for [a,b];
rundoite failed interpreted but succeeded lowered), and a variable first
seen in the else branch was re-created fresh after the ITE.

Rust-only fix, shared compiler unchanged: the Rust target rewrites each
predicate's WAM text so such a Yn gets put_variable Yn, Yn before the
outermost guard and its later first-occurrence instructions become value
forms. Both the instruction table and the lowered emitter read the
rewritten text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PJqq7jMzces6bMwTnPbpd
The cut barrier B0 was a machine register that every Allocate overwrote
and nothing restored, so after a call whose callee allocated, a cut in
the caller used the callee's barrier (cut_after_call printed first and
fallback in the interpreter). As in the standard WAM, the environment
frame now keeps the caller's B0: Allocate saves it, Deallocate and the
fused deallocate-and-proceed arms restore it.

Harness cases cut_after_call and cut_in_agg match SWI in interpreter and
functions mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PJqq7jMzces6bMwTnPbpd
catch/3 meta-called its goal with the cut barrier left at the enclosing
clause's B0, so a cut inside the goal removed the caller's choice
points. Set the barrier to the entry depth around the goal, as
call_goal_once does; catch/3 stays first-solution.

Harness cases catch_cut (oracled with the first-solution reading of
catch/3) and catch_cut_local match in interpreter and functions mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013PJqq7jMzces6bMwTnPbpd
@s243a
s243a merged commit 5f08375 into main Oct 9, 2026
16 checks passed
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