Summary
Running the tree-shaken shen-cas slice through shenffi::CasEngine, evaluation is nondeterministic relative to the reference Shen kernel. The same input yields one of three outcomes — correct, unreduced (inert leftover), or silently wrong — selected by process/boot state. The same machinery also degrades in performance the longer a session runs. The reference evaluator (the identical CAS program under ShenScript) is correct and deterministic, so the divergence is introduced by shen-rust's execution, not by the shen-cas rules.
This is the root cause of the now-closed shen-cas issues pyrex41/shen-cas#5 (wrong/unreduced D[ArcTan[x],x]) and pyrex41/shen-cas#6 (Integrate latency), both retargeted here.
Decisive repro — two fresh boots in one process disagree
crates/shenffi/examples/cas_bench.rs (run on a large-stack thread, as the README requires):
use shenffi::CasEngine;
fn main() {
std::thread::Builder::new().stack_size(512*1024*1024)
.spawn(|| {
let mut e1 = CasEngine::boot().unwrap();
println!("e1 D[x,x] = {}", e1.reduce("D[x,x]"));
println!("e1 D[ArcTan[x],x] = {}", e1.reduce("D[ArcTan[x],x]"));
let mut e2 = CasEngine::boot().unwrap();
println!("e2 D[x,x] = {}", e2.reduce("D[x,x]"));
println!("e2 D[ArcTan[x],x] = {}", e2.reduce("D[ArcTan[x],x]"));
}).unwrap().join().unwrap();
}
Observed:
e1 D[x,x] = [D x x] <- inert (rule didn't fire)
e1 D[ArcTan[x],x] = [Times [D x x] [Power [Plus [Power x 2] 1] -1]] <- unreduced
e2 D[x,x] = 1
e2 D[ArcTan[x],x] = 1 <- WRONG (correct: (1+x^2)^-1)
|
engine 1 (1st boot) |
engine 2 (2nd boot) |
reference (ShenScript) |
D[x,x] |
[D x x] (inert) |
1 |
1 ✓ |
D[ArcTan[x],x] |
[Times [D x x] [Power…-1]] |
1 (wrong) |
(1+x^2)^-1 ✓ |
Each CasEngine is freshly booted (own Interp), so it is not memo-priming/history. Both share the process, and 8 separate process launches all agreed — so it is not a per-process RandomState seed either. The only discriminating variable is boot order within the process, i.e. process-global state that survives across Interp::new() / boot_from_kl_source.
Why this points at intern/hash state
The guarded calculus rules both modes hinge on (D[x,x] → 1 guarded by SameQ, D[c,x] → 0 by FreeQ) evaluate through the CAS's content-eq → content-hash, and the CAS's normal-form memo is content-hash-keyed. shen-cas deliberately uses a self-contained polynomial rolling hash (base 31, large-prime modulus) precisely because host hash is unsafe across ports (see shen-cas HANDOFF.md "Portable content hash"). If shen-rust computes that rolling hash with fixed-width integer wraparound (vs ShenScript's number type) the modular arithmetic diverges → collisions; and if symbol interning / a global symbol table is not reset per boot, the collision set shifts by boot order — exactly the observed behavior.
Second symptom — monotonic in-session perf degradation
The identical Integrate[x^4,x] call, repeated 8× in one process:
#0 4.08s #1 5.08s #2 5.23s #3 16.28s #4 14.81s #5 14.49s #6 13.64s #7 14.00s
Same input, same process, climbing 4s → 16s and plateauing — a state-accumulation cost (content-hash-keyed nf-memo / store growth), the same machinery as above. The reference (ShenScript, a slower interpreter) does these in <1.2s, so this is a native-vs-interpreter performance inversion. On the app's single long-lived worker thread this reads as a progressive slowdown / "hang."
Suggested investigation
- Is shen-rust's number/integer arithmetic for the CAS's rolling hash arbitrary-precision, or does it wrap at i64? Wraparound would produce host-specific collisions.
- Is the symbol intern table (and any global hash/symbol state) reset on each
Interp::new() / boot_from_kl_source? The two-boot divergence says some global state leaks.
- Profile the nf-memo / store under repeated reduction in one boot (the 4s→16s climb) — is
content-hash recomputed over growing state, or are map lookups degrading?
Expected
CasEngine::reduce should match the reference byte-for-byte and be boot-order- and history-independent: D[x,x] → 1, D[ArcTan[x],x] → (1+x^2)^-1, every boot, every call.
Provenance
Found while evaluating shen-cas #5/#6 against the shipped slice; cross-checked against the pure-Shen reference via shen-cas/scripts/shenscript-run.js.
Summary
Running the tree-shaken shen-cas slice through
shenffi::CasEngine, evaluation is nondeterministic relative to the reference Shen kernel. The same input yields one of three outcomes — correct, unreduced (inert leftover), or silently wrong — selected by process/boot state. The same machinery also degrades in performance the longer a session runs. The reference evaluator (the identical CAS program under ShenScript) is correct and deterministic, so the divergence is introduced by shen-rust's execution, not by the shen-cas rules.This is the root cause of the now-closed shen-cas issues pyrex41/shen-cas#5 (wrong/unreduced
D[ArcTan[x],x]) and pyrex41/shen-cas#6 (Integratelatency), both retargeted here.Decisive repro — two fresh boots in one process disagree
crates/shenffi/examples/cas_bench.rs(run on a large-stack thread, as the README requires):Observed:
D[x,x][D x x](inert)11✓D[ArcTan[x],x][Times [D x x] [Power…-1]]1(wrong)(1+x^2)^-1✓Each
CasEngineis freshly booted (ownInterp), so it is not memo-priming/history. Both share the process, and 8 separate process launches all agreed — so it is not a per-processRandomStateseed either. The only discriminating variable is boot order within the process, i.e. process-global state that survives acrossInterp::new()/boot_from_kl_source.Why this points at intern/hash state
The guarded calculus rules both modes hinge on (
D[x,x] → 1guarded bySameQ,D[c,x] → 0byFreeQ) evaluate through the CAS'scontent-eq → content-hash, and the CAS's normal-form memo is content-hash-keyed. shen-cas deliberately uses a self-contained polynomial rolling hash (base 31, large-prime modulus) precisely because hosthashis unsafe across ports (see shen-casHANDOFF.md"Portable content hash"). If shen-rust computes that rolling hash with fixed-width integer wraparound (vs ShenScript's number type) the modular arithmetic diverges → collisions; and if symbol interning / a global symbol table is not reset per boot, the collision set shifts by boot order — exactly the observed behavior.Second symptom — monotonic in-session perf degradation
The identical
Integrate[x^4,x]call, repeated 8× in one process:Same input, same process, climbing 4s → 16s and plateauing — a state-accumulation cost (content-hash-keyed nf-memo / store growth), the same machinery as above. The reference (ShenScript, a slower interpreter) does these in <1.2s, so this is a native-vs-interpreter performance inversion. On the app's single long-lived worker thread this reads as a progressive slowdown / "hang."
Suggested investigation
Interp::new()/boot_from_kl_source? The two-boot divergence says some global state leaks.content-hashrecomputed over growing state, or are map lookups degrading?Expected
CasEngine::reduceshould match the reference byte-for-byte and be boot-order- and history-independent:D[x,x] → 1,D[ArcTan[x],x] → (1+x^2)^-1, every boot, every call.Provenance
Found while evaluating shen-cas #5/#6 against the shipped slice; cross-checked against the pure-Shen reference via
shen-cas/scripts/shenscript-run.js.