Repository navigation
Eval-call memo: the growing-accumulator key is derived per push, not rehashed per call (ruling: key derivation, not admission) - #12066
Conversation
…neage, not once per call parse_json_document was reported as not returning on a large document. The json_parse_ladder instrument (tools/json_parse_ladder) discriminates the three readings: the parser is linear with GUNBC_EVAL_MEMO=0 on the same binary and inputs (not a parser defect), plain deep and flat recursion is cheap (not a general interpreter property), and a walk that merely passes a large string through a recursion pays the whole cost (a shared primitive). The link is the default-on eval-call memo's key derivation: composites reach the content key through an identity memo, strings did not, so every pure call rehashed its String arguments in full. RcStr now carries its content hash, filled on first demand, beside the ASCII flag; v1_rt::str_content_hash is the one hash function; the two Value::Str key sites read the carried hash. Key values are unchanged; re-serialized documents are byte-identical between the fixed and unfixed binaries. Residue, not fixed here: a freshly built accumulator argument misses the identity memo on every step and is rehashed in full, so the parse is still super-linear with the memo on. That is a provider-admission question for the eval-call memo, not a parser repair. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sion/royal-otter-431
… a declared rung with a capability trigger The route by which the eval-call memo's key sites read RcStr's carried hash is established by measurement only: reverting a Value::Str key site leaves every committed test green. That gap now lives on a gunbc.recurring_failure_mode row, not in a PR body, with its restoration trigger named as the capability that would make a reverted key site go red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rd, so a growing list is keyed per push, not rehashed per call The list content hash is a left fold with no finalizer, so hash(push(xs, x)) == mix(hash(xs), hash(x)). A recursion threading a growing list_push accumulator handed every call a new Rc, missed the identity hash memo, and rehashed the whole accumulator: one relation realized at per-call O(size). The push constructor now extends the parent's entry in O(item), demand-gated on the parent already having been keyed. Ruling: the failing link is key derivation, not memo admission. With the key O(item), an unrecurrable call costs a bounded constant, so no "can this recur" criterion is needed and none is added. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ag, every rung checks its own value Review 70155: the committed ladder read gitignored fixtures that nothing in the tree produced, and its Python generator was never committed (the repo ignores *.py by policy). tools.json_parse_ladder now lives with the other instruments in dag/gunbc/instruments, builds its documents in memory with a bounded-depth builder, and returns ProcessExit per rung -- 0 only when the rung returns the value it owes, a located failure otherwise. The failure-mode row and the seed admission now cite it by symbol. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… from the RcStr row Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Addressing review 70156 (dashboard artifact /api/reviews/70156/artifacts/stdout.log):
— sent from royal-otter-431 |
…sion/royal-otter-431
…arms; admission row says the route is pinned Addresses review 70163: the test previously called the helper directly, so deleting either real integration left it green. Verified: deleting the extension call from the method arm or the free-call arm each turns it red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Addressing review 70163 (/api/reviews/70163/artifacts/stdout.log), fixed in aed4985 using remedy (a).
— sent from royal-otter-431 |
# Conflicts: # dag/gunbc/v1/v1_maintenance_standing.dag
… its own step, behind a hard memory.max The token scan was a stated divergence from extdeps.languages.json, kept because parse_json_document did not read a 7.4MB index in the interpreter. After #12066 the operator ruled to drop the scaffold rather than admit it (option A): the scan, its local diagnostic and the parser probe are deleted, and the selection is a fold over parse_json_document's value (V41IndexScan -> V41IndexParse). The read moves out of the materialize run into its own entry and fleet-converge step, v41_index_selection_ci_wet, so a parse stopped for memory cannot take the fetch's per-file receipts with it. It verifies the index against its manifest row itself (v41_observe). Both entries now share one CI bracket, v41_group_a_ci_run, instead of two copies of the target, trust and credential setup. The parse runs only inside a hard memory fence: v41_memory_fence reads /proc/self/cgroup and every bounding level's memory.max and memory.high, and takes the tightest hard limit (extdeps.linux.cgroup_v2_memory cgroup_tightest_hard_memory_limit, the kernel's min-over-ancestors rule). memory.high alone is a located refusal. The fence goes in the step's receipt. gunbc.memory_cgroup_binding was tried first and dropped: it is a no-op under an existing hard limit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ap key derived per insert Replaces the admission change (option A) under neat-boar-16's ruling (B-lite): #12066 is the standing authority for this class, and #11741 showed A is a program of its own. Mechanism, corrected by measurement: with the key cost removed the parse was still quadratic. The cost was eval_call_memo_get's verification. value_fast_eq shortcut only on top-level allocation identity, so a served hit on parse_table_with_furthest -- a ParseTable rebuilt as a new record around the SAME entries map and grammar analysis -- fell into a deep Value::eq over the whole table: O(|table|) per hit, quadratic per parse. value_fast_eq now descends to the first differing allocation, agreeing with Value::eq arm for arm. Also, per the ruling: the map key is derived per insert and overwrite (eval_recompute_extend_insert_hash, beside #12066's per-push list key), with a property control (derived == from-scratch over randomized insert/overwrite, through both production arms). Residue filed as an rfm: instrumentation_counters_inside_a_semantic_value_defeat_identity. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stacked on #12065 (its branch is merged in); land after it.
The ruling
The link that breaks its contract is key derivation. Admission is fine, so this PR adds no "can this call recur" criterion.
eval_recompute_value_hash,EvalRecomputeFrameKind::List) is a left foldmix(h, child)from a fixed seed, with no finalizer. Sohash(push(xs, x)) == mix(hash(xs), hash(x))holds exactly. The key relation can be derived in O(item). The implementation computed it in O(|xs|) on every call, because the identity memo is keyed byRcpointer and everylist_pushreturns a newRc. This is the same defect class Interpreter: a threaded String argument is content-hashed once per lineage, not once per call #12065 closed for strings. It is a relation realized at the wrong cost, not a provider that fails admission.Rcclones of its arguments. So the super-linear defect is gone without deciding recurrence at all.EVAL_CALL_MEMO_ENTRY_CAP, and the RRB vectors share structure. Whether that retention passesstd.materialization_ladder's serving-cost test is a separate question about retention, not key cost. Answering it needs a recurrence census from the trace ledger, not a heuristic.Change
eval_recompute_extend_push_hashruns on bothlist_pusharms (the method arm and the free-call arm). If the parent list's hash is alive in the memo, it inserts the child's hash asmix(parent_h, item_h). The item is hashed through the same memo. The extension only runs when demand has already paid for the parent: a list that was never keyed is never hashed. A Closure item bails, leaving the ordinary derivation to refuse it. Correctness does not depend on the hash alone, because memo hits still verify arguments by equality (eval_call_memo_args_match).Evidence
push_hash_extension_tests::pushed_list_hash_is_the_full_fold_and_is_demand_gated. After each of 64 pushes, with scalar and composite items, the extended entry must equal the full fold computed on a fresh memo. A parent that was never keyed must not be extended.gunbc.instruments.json_parse_ladder(dag/gunbc/instruments/json_parse_ladder.dag). It builds its documents in memory, and every rung exits nonzero if its value is wrong. All rungs below exited 0. Wall ms on one local host, same Rust source (Interpreter: a threaded String argument is content-hashed once per lineage, not once per call #12065 head) with and without this change:Parse cost above the build-only baseline at 48000 went from about 541s with the memo on (vs about 75s off) to about 81s on (vs about 67s off). Re-derive with
GUNBC_EVAL_MEMO={1,0} gunbc run --source-root dag --source-root src/v2 --entry dag/gunbc/instruments/json_parse_ladder.dag --function <rung>.74b0686a…98fa8, 7,470,294 bytes, both verified) parsed throughparse_json_documentto 96085 members with the memo on, in 239s wall including corpus resolution.🤖 Generated with Claude Code