From 571c1d7daeca26de3d4136232926a1b50f308f07 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 22 Sep 2026 15:00:49 +0000 Subject: [PATCH 1/3] Interpreter: a threaded String argument is content-hashed once per lineage, 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) --- dag/gunbc/v1/v1_maintenance_standing.dag | 40 ++++++++ src/v1/runtime_rust.dag | 86 ++++++++++++++++- src/v1/stage0/src/v1_compiler_runtime_rust.rs | 2 +- src/v1/stage0/src/v1_interpreter.rs | 14 +-- src/v1/stage0/src/v1_rt.rs | 92 ++++++++++++++++++- tools/json_parse_ladder/.gitignore | 1 + tools/json_parse_ladder/acc.dag | 8 ++ tools/json_parse_ladder/depth.dag | 14 +++ tools/json_parse_ladder/json_ladder.dag | 62 +++++++++++++ tools/json_parse_ladder/split.dag | 16 ++++ 10 files changed, 322 insertions(+), 13 deletions(-) create mode 100644 tools/json_parse_ladder/.gitignore create mode 100644 tools/json_parse_ladder/acc.dag create mode 100644 tools/json_parse_ladder/depth.dag create mode 100644 tools/json_parse_ladder/json_ladder.dag create mode 100644 tools/json_parse_ladder/split.dag diff --git a/dag/gunbc/v1/v1_maintenance_standing.dag b/dag/gunbc/v1/v1_maintenance_standing.dag index e6c10ff4396..9bdc5a758c8 100644 --- a/dag/gunbc/v1/v1_maintenance_standing.dag +++ b/dag/gunbc/v1/v1_maintenance_standing.dag @@ -537,6 +537,46 @@ data v1_seed_standing: SeedLifecycleStanding = SemanticsFrozen { maintenance: Ma // The obligation was real and the named authority was not, which is why the receipt lands HERE, in // the carrier that actually governs v1 admissions. +// THE SECOND CARRIED FACT ON THE SAME CARRIER, ADMITTED ON THE SAME ARGUMENT AND NO WIDER. THE +// CHANGE: `v1_rt::RcStr` carries its content hash, filled on first demand, beside the ASCII flag; +// `v1_rt::str_content_hash` becomes the one hash function (the interpreter's +// eval_recompute_str_hash delegates to it); and the eval-call memo's two `Value::Str` key sites +// read the carried hash instead of rehashing the text. +// +// THE DEFECT, located rather than assumed. extdeps.languages.json parse_json_document was reported +// as not returning on a large document. A size ladder over synthetic documents of that shape bent +// super-linearly with the memo on and ran linear with GUNBC_EVAL_MEMO=0 (the memo's declared +// diagnostic realization switch, identical values), and a walk that merely PASSES a large string +// through a recursion -- never reading it -- paid the whole cost, while the same walk with the +// string held in scope and not passed paid none. The eval-call memo is default-on and keys every +// pure call by argument content; composites reach that key through an identity memo, strings did +// not, so a recursion threading one text through its steps rehashed all of it per step. The parser +// is a victim; the link that violated its own contract is the key derivation, which states one +// relation (content hash) and realized it at O(|text|) per call for exactly one value kind. The +// instrument is the ladder named in the PR that lands this row. +// +// THE PURPOSE TEST IS SATISFIED FOR THE SAME REASON AS THE ASCII FLAG: the interpreter is the engine +// the v2 self-host program runs on, and a pure recursion over source text is that program's +// ordinary shape. It is a cost-shape defect, fixed under DESIGN section 6's bare-minimum-cost rule +// regardless of n. BehaviorPreservingRedundancyRemoval: the key relation and every key VALUE are +// unchanged -- the carried hash is produced by the same function the interpreter computed before -- +// and the carrier's tests pin carried against free at ASCII and non-ASCII text. +// +// PUBLICSURFACEGROWTH, answered rather than skipped, and NOT settled by citing gunbc#9256: two `pub` +// items join the seed's exported surface (`RcStr::content_hash`, `str_content_hash`). The argument +// is the one recorded above -- the export is the mechanism of a removal, and nothing is expressible +// after this change that was not before -- and it inherits that argument's stated weakness: the +// line between capability and mechanism is authored, not derived. A reviewer or the operator may +// reject it on that ground, and the rejection would not need to overturn the earlier admission. +// +// WHAT WAS NOT DONE, because each was the tempting move at the site where the number was read. No +// fast path or size threshold in the parser. No switch flipped: GUNBC_EVAL_MEMO=0 was the +// discriminating instrument, not the repair, and disabling a provider to escape its key cost would +// have hidden the defect. No identity memo for strings: a Weak retains the inline bytes, so +// it would have traded CPU for retaining every hashed argument. WHETHER THE EVAL-CALL MEMO SHOULD +// KEY CALLS THAT CAN NEVER HIT -- a step whose loop index differs each time -- is a demand-graph +// question under std.materialization_ladder that this change deliberately leaves open. + data v1_maintenance_typecheck_progress_admission_note: String = "AN ADMISSION UNDER THE 2026-08-20 PURPOSE TEST, recorded in the same PR as the change it admits, because an admission written afterwards is indistinguishable from an author's own classification. THE RULING, verbatim and relayed through the requesting session's parent (snappy-dove-250, import/namespace manager) on 2026-08-25: 'ah yeah - we need to fix the typecheckmoduleresult issue'. There is no PR review or transcript to point at; the relay is the record and this row is the citation, stated plainly rather than dressed up as pointing at a document that does not exist -- the same shape as v1_maintenance_purpose_ruling_note above.\n\nTHE DEFECT, at the grain that makes it a defect rather than an improvement. v1.compiler.infer typecheck_module has an early return: when build_type_env yields error diagnostics it hands back a TypedModule whose items list is EMPTY and never reaches item analysis. The seed therefore renders two different facts as one value -- a module that COMPLETED item checking and legitimately has no items, and a module that was ABANDONED before item checking started. Every downstream reader of TypedModule.items receives the same empty list for both, so a consumer wanting the distinction must infer it from 'items is empty AND there are error diagnostics', and that inference is unreliable in the direction that matters: a module that completed and has item-level errors presents identically. This is the state-space conflation DESIGN names, in the seed, on a carrier read by the whole back end.\n\nWHY THE FIELD LANDS ON TypedModule AND NOT ON TypecheckModuleResult, since the ruling names the result type and the change does not edit it. realize_module destructures the result and inserts only .typed into the module index; a progress field on the result alone would be discarded one line after it was set, which is this same defect one level out. The result type is where the issue was NOTICED; the carrier that survives the destructure is where the fact has to live.\n\nTHE EXHIBITED COST, because a defect with no demonstrated victim is a preference. A demand oracle built in this lane to score resolution producers read TypedModule.items to derive per-module demand, and was found DEAD as a scoring instrument for exactly this reason: an abandoned module reports total_arm1=1, which is indistinguishable from a genuine single-ask module, so the instrument's error is ANTI-CORRELATED with the defect it was built to find -- the modules most likely to abandon are the ones whose demand it most understates. That oracle was discarded rather than repaired, which is the measured price of the conflation and is why this is not speculative hardening.\n\nWHY IT SERVES THE V2 SELF-HOST PROGRAM, which is the whole admission test. The v2 resolution work this lane exists to do reads seed-produced TypedModule values to measure and compare producers. An instrument that cannot tell 'not checked' from 'nothing to check' cannot score a producer, and the program's next steps (one closure authority, exact denotation) are measurement-gated. This repairs the seed's report of its own progress; it adds no language behavior, no compatibility obligation, no escape hatch, and completes no seed feature.\n\nTHE SCOPE LIMIT, stated because this row will be cited and its limit is the part most likely to be dropped in the citing. THE RULING ADMITS THIS CHANGE ONLY. It does NOT settle whether PublicSurfaceGrowth means a NEW EXPORTED DECLARATION or ANY CHANGE TO EXPORTED SURFACE -- this change adds a coproduct and a record field to a type the seed exports, so it sits squarely on that unresolved boundary and was ruled admissible without the boundary being drawn. Nobody may cite this row as the answer to that question. A later proposal that needs the boundary decided must route it for its own ruling; treating this admission as precedent for a general reading of PublicSurfaceGrowth would be the authority-substitution failure DESIGN names -- borrowing one authority's answer to settle a question it never addressed. The five refused classes are untouched and still dominate every admission, including this one." // gunbc#10727 MERGED WHILE ITS OBJECTION STOOD UNANSWERED, filed as the row below. The mechanical diff --git a/src/v1/runtime_rust.dag b/src/v1/runtime_rust.dag index 1144ea782db..ae398434607 100644 --- a/src/v1/runtime_rust.dag +++ b/src/v1/runtime_rust.dag @@ -295,15 +295,60 @@ fn rt_string_carrier() -> String { "/// free functions' semantics: each method falls back to the function it shadows\n", "/// whenever the flag is false, and takes the byte path only under the same\n", "/// condition that path is already taken there (byte index == code-point index).\n", + "///\n", + "/// The content hash is the second carried fact, carried for the same reason as the\n", + "/// flag: a pure function of immutable content that a hot consumer re-derived per\n", + "/// access. The interpreter's eval-call memo keys every pure call by the content hash\n", + "/// of its arguments and rehashed a `Value::Str` in full on EVERY call, so a recursion\n", + "/// threading one large text through its steps paid O(|text|) per step and O(|text|^2)\n", + "/// overall. It is filled lazily -- most strings are never a call argument -- and it is\n", + "/// carried beside the Rc rather than behind it: a clone taken after the first hash\n", + "/// inherits it, which is the route a threaded argument takes (the key is read off the\n", + "/// argument before it becomes the callee's binding, and the next step's argument is a\n", + "/// clone of that binding). An identity memo keyed on the allocation was rejected: a\n", + "/// `Weak` keeps the string's inline bytes allocated, so every hashed argument\n", + "/// would be retained for the context's lifetime.\n", "#[derive(Debug, Clone)]\n", "pub struct RcStr \{\n", " rc: Rc,\n", " is_ascii: bool,\n", - "\}\n\n", + " content_hash: Cell>,\n", + "\}\n", + "\n", + "/// The one content hash of interpreted string text. `RcStr::content_hash` carries it and\n", + "/// a consumer holding a bare `&str` calls it directly, so a carried hash and a computed\n", + "/// one cannot disagree.\n", + "pub fn str_content_hash(s: &str) -> u64 \{\n", + " use std::hash::\{Hash, Hasher\};\n", + " let mut h = std::collections::hash_map::DefaultHasher::new();\n", + " s.hash(&mut h);\n", + " h.finish()\n", + "\}\n", + "\n", + "#[cfg(test)]\n", + "thread_local! \{\n", + " static STR_CONTENT_HASH_COMPUTED: Cell = const \{ Cell::new(0) \};\n", + "\}\n", + "\n", "impl RcStr \{\n", " pub fn new(rc: Rc) -> Self \{\n", " let is_ascii = rc.is_ascii();\n", - " RcStr \{ rc, is_ascii \}\n", + " RcStr \{\n", + " rc,\n", + " is_ascii,\n", + " content_hash: Cell::new(None),\n", + " \}\n", + " \}\n\n", + " /// `str_content_hash` of this text, computed at most once per carrier lineage.\n", + " pub fn content_hash(&self) -> u64 \{\n", + " if let Some(h) = self.content_hash.get() \{\n", + " return h;\n", + " \}\n", + " #[cfg(test)]\n", + " STR_CONTENT_HASH_COMPUTED.with(|c| c.set(c.get() + 1));\n", + " let h = str_content_hash(&self.rc);\n", + " self.content_hash.set(Some(h));\n", + " h\n", " \}\n\n", " #[inline]\n", " pub fn as_str(&self) -> &str \{\n", @@ -379,6 +424,43 @@ fn rt_string_carrier() -> String { " std::fmt::Display::fmt(&*self.rc, f)\n", " \}\n", "\}\n\n", + "#[cfg(test)]\n", + "mod rc_str_content_hash_tests \{\n", + " use super::*;\n", + "\n", + " fn computed() -> u64 \{\n", + " STR_CONTENT_HASH_COMPUTED.with(|c| c.get())\n", + " \}\n", + "\n", + " #[test]\n", + " fn carried_hash_equals_the_free_authority() \{\n", + " for text in [\"\", \"a\", \"\{\\\"weight_map\\\": \{\}\}\", \"caf\\u\{e9\} \\u\{1f600\}\"] \{\n", + " assert_eq!(RcStr::new(Rc::from(text)).content_hash(), str_content_hash(text));\n", + " \}\n", + " \}\n", + "\n", + " #[test]\n", + " fn a_threaded_clone_inherits_the_hash_instead_of_rehashing() \{\n", + " let first = RcStr::new(Rc::from(\"x\".repeat(4096).as_str()));\n", + " let before = computed();\n", + " let h = first.content_hash();\n", + " let mut carried = first.clone();\n", + " for _ in 0..1000 \{\n", + " assert_eq!(carried.content_hash(), h);\n", + " carried = carried.clone();\n", + " \}\n", + " assert_eq!(computed() - before, 1, \"one hash per lineage, not one per step\");\n", + " \}\n", + "\n", + " #[test]\n", + " fn a_clone_taken_before_the_first_hash_is_an_independent_lineage() \{\n", + " let a = RcStr::new(Rc::from(\"abc\"));\n", + " let b = a.clone();\n", + " let before = computed();\n", + " assert_eq!(a.content_hash(), b.content_hash());\n", + " assert_eq!(computed() - before, 2);\n", + " \}\n", + "\}\n\n", "impl PartialEq for RcStr \{\n", " fn eq(&self, other: &Self) -> bool \{\n", " self.rc == other.rc\n", diff --git a/src/v1/stage0/src/v1_compiler_runtime_rust.rs b/src/v1/stage0/src/v1_compiler_runtime_rust.rs index 7a0ce96915e..387417b98ac 100644 --- a/src/v1/stage0/src/v1_compiler_runtime_rust.rs +++ b/src/v1/stage0/src/v1_compiler_runtime_rust.rs @@ -77,7 +77,7 @@ pub fn rt_collection_ops() -> String { } pub fn rt_string_carrier() -> String { - v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("/// The string carrier for interpreted values: an `Rc` plus the ASCII fact,\n".to_string(), "/// computed ONCE at construction (`RcStr::new`) instead of rescanned on every\n".to_string()), "/// index. `char_at`/`substring`/`string_length` above are the no-carrier entry\n".to_string()), "/// points for emitted code holding a bare `&str`; they bound their ASCII test by\n".to_string()), "/// the requested position, which is O(min(pos, n)) and makes a left-to-right walk\n".to_string()), "/// O(n^2). The methods here read the carried flag instead, so a single call is\n".to_string()), "/// O(1) on ASCII text and the walk is linear (STRING-INDEX-0).\n".to_string()), "///\n".to_string()), "/// The flag has exactly one producer -- `RcStr::new` -- so it is the carrier's own\n".to_string()), "/// fact rather than a parameter a caller could supply wrongly. Semantics are the\n".to_string()), "/// free functions' semantics: each method falls back to the function it shadows\n".to_string()), "/// whenever the flag is false, and takes the byte path only under the same\n".to_string()), "/// condition that path is already taken there (byte index == code-point index).\n".to_string()), "#[derive(Debug, Clone)]\n".to_string()), "pub struct RcStr {\n".to_string()), " rc: Rc,\n".to_string()), " is_ascii: bool,\n".to_string()), "}\n\n".to_string()), "impl RcStr {\n".to_string()), " pub fn new(rc: Rc) -> Self {\n".to_string()), " let is_ascii = rc.is_ascii();\n".to_string()), " RcStr { rc, is_ascii }\n".to_string()), " }\n\n".to_string()), " #[inline]\n".to_string()), " pub fn as_str(&self) -> &str {\n".to_string()), " &self.rc\n".to_string()), " }\n\n".to_string()), " #[inline]\n".to_string()), " pub fn is_ascii(&self) -> bool {\n".to_string()), " self.is_ascii\n".to_string()), " }\n\n".to_string()), " /// Owned handle on the underlying allocation, for sites that need a bare `Rc`.\n".to_string()), " pub fn rc(&self) -> Rc {\n".to_string()), " Rc::clone(&self.rc)\n".to_string()), " }\n\n".to_string()), " /// Pointer identity of the underlying allocation, for consumers whose subject is\n".to_string()), " /// sharing rather than content.\n".to_string()), " pub fn ptr_eq(a: &RcStr, b: &RcStr) -> bool {\n".to_string()), " Rc::ptr_eq(&a.rc, &b.rc)\n".to_string()), " }\n\n".to_string()), " pub fn char_at(&self, pos: i64) -> String {\n".to_string()), " if !self.is_ascii {\n".to_string()), " return char_at(&self.rc, pos);\n".to_string()), " }\n".to_string()), " let pos = pos.max(0) as usize;\n".to_string()), " let bytes = self.rc.as_bytes();\n".to_string()), " if pos >= bytes.len() {\n".to_string()), " return String::new();\n".to_string()), " }\n".to_string()), " String::from(bytes[pos] as char)\n".to_string()), " }\n\n".to_string()), " pub fn string_length(&self) -> i64 {\n".to_string()), " if self.is_ascii {\n".to_string()), " self.rc.len() as i64\n".to_string()), " } else {\n".to_string()), " self.rc.chars().count() as i64\n".to_string()), " }\n".to_string()), " }\n\n".to_string()), " pub fn substring(&self, start: i64, end: i64) -> String {\n".to_string()), " if !self.is_ascii {\n".to_string()), " return substring(&self.rc, start, end);\n".to_string()), " }\n".to_string()), " let start = start.max(0) as usize;\n".to_string()), " let end = end.max(0) as usize;\n".to_string()), " if end <= start {\n".to_string()), " return String::new();\n".to_string()), " }\n".to_string()), " let bytes = self.rc.as_bytes();\n".to_string()), " if start >= bytes.len() {\n".to_string()), " return String::new();\n".to_string()), " }\n".to_string()), " let out_end = end.min(bytes.len());\n".to_string()), " record_substring_chars_walked(&self.rc, start, out_end.saturating_sub(start));\n".to_string()), " self.rc[start..out_end].to_string()\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl std::ops::Deref for RcStr {\n".to_string()), " type Target = str;\n".to_string()), " fn deref(&self) -> &str {\n".to_string()), " &self.rc\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl AsRef for RcStr {\n".to_string()), " fn as_ref(&self) -> &str {\n".to_string()), " &self.rc\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl std::borrow::Borrow for RcStr {\n".to_string()), " fn borrow(&self) -> &str {\n".to_string()), " &self.rc\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl std::fmt::Display for RcStr {\n".to_string()), " fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n".to_string()), " std::fmt::Display::fmt(&*self.rc, f)\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl PartialEq for RcStr {\n".to_string()), " fn eq(&self, other: &Self) -> bool {\n".to_string()), " self.rc == other.rc\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl Eq for RcStr {}\n\n".to_string()), "impl PartialOrd for RcStr {\n".to_string()), " fn partial_cmp(&self, other: &Self) -> Option {\n".to_string()), " Some(self.cmp(other))\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl Ord for RcStr {\n".to_string()), " fn cmp(&self, other: &Self) -> std::cmp::Ordering {\n".to_string()), " self.rc.as_ref().cmp(other.rc.as_ref())\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl std::hash::Hash for RcStr {\n".to_string()), " fn hash(&self, state: &mut H) {\n".to_string()), " self.rc.as_ref().hash(state)\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl From> for RcStr {\n".to_string()), " fn from(rc: Rc) -> Self {\n".to_string()), " RcStr::new(rc)\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl From<&str> for RcStr {\n".to_string()), " fn from(s: &str) -> Self {\n".to_string()), " RcStr::new(Rc::from(s))\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl From for RcStr {\n".to_string()), " fn from(s: String) -> Self {\n".to_string()), " RcStr::new(Rc::from(s.as_str()))\n".to_string()), " }\n".to_string()), "}\n\n".to_string()) + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat("/// The string carrier for interpreted values: an `Rc` plus the ASCII fact,\n".to_string(), "/// computed ONCE at construction (`RcStr::new`) instead of rescanned on every\n".to_string()), "/// index. `char_at`/`substring`/`string_length` above are the no-carrier entry\n".to_string()), "/// points for emitted code holding a bare `&str`; they bound their ASCII test by\n".to_string()), "/// the requested position, which is O(min(pos, n)) and makes a left-to-right walk\n".to_string()), "/// O(n^2). The methods here read the carried flag instead, so a single call is\n".to_string()), "/// O(1) on ASCII text and the walk is linear (STRING-INDEX-0).\n".to_string()), "///\n".to_string()), "/// The flag has exactly one producer -- `RcStr::new` -- so it is the carrier's own\n".to_string()), "/// fact rather than a parameter a caller could supply wrongly. Semantics are the\n".to_string()), "/// free functions' semantics: each method falls back to the function it shadows\n".to_string()), "/// whenever the flag is false, and takes the byte path only under the same\n".to_string()), "/// condition that path is already taken there (byte index == code-point index).\n".to_string()), "///\n".to_string()), "/// The content hash is the second carried fact, carried for the same reason as the\n".to_string()), "/// flag: a pure function of immutable content that a hot consumer re-derived per\n".to_string()), "/// access. The interpreter's eval-call memo keys every pure call by the content hash\n".to_string()), "/// of its arguments and rehashed a `Value::Str` in full on EVERY call, so a recursion\n".to_string()), "/// threading one large text through its steps paid O(|text|) per step and O(|text|^2)\n".to_string()), "/// overall. It is filled lazily -- most strings are never a call argument -- and it is\n".to_string()), "/// carried beside the Rc rather than behind it: a clone taken after the first hash\n".to_string()), "/// inherits it, which is the route a threaded argument takes (the key is read off the\n".to_string()), "/// argument before it becomes the callee's binding, and the next step's argument is a\n".to_string()), "/// clone of that binding). An identity memo keyed on the allocation was rejected: a\n".to_string()), "/// `Weak` keeps the string's inline bytes allocated, so every hashed argument\n".to_string()), "/// would be retained for the context's lifetime.\n".to_string()), "#[derive(Debug, Clone)]\n".to_string()), "pub struct RcStr {\n".to_string()), " rc: Rc,\n".to_string()), " is_ascii: bool,\n".to_string()), " content_hash: Cell>,\n".to_string()), "}\n".to_string()), "\n".to_string()), "/// The one content hash of interpreted string text. `RcStr::content_hash` carries it and\n".to_string()), "/// a consumer holding a bare `&str` calls it directly, so a carried hash and a computed\n".to_string()), "/// one cannot disagree.\n".to_string()), "pub fn str_content_hash(s: &str) -> u64 {\n".to_string()), " use std::hash::{Hash, Hasher};\n".to_string()), " let mut h = std::collections::hash_map::DefaultHasher::new();\n".to_string()), " s.hash(&mut h);\n".to_string()), " h.finish()\n".to_string()), "}\n".to_string()), "\n".to_string()), "#[cfg(test)]\n".to_string()), "thread_local! {\n".to_string()), " static STR_CONTENT_HASH_COMPUTED: Cell = const { Cell::new(0) };\n".to_string()), "}\n".to_string()), "\n".to_string()), "impl RcStr {\n".to_string()), " pub fn new(rc: Rc) -> Self {\n".to_string()), " let is_ascii = rc.is_ascii();\n".to_string()), " RcStr {\n".to_string()), " rc,\n".to_string()), " is_ascii,\n".to_string()), " content_hash: Cell::new(None),\n".to_string()), " }\n".to_string()), " }\n\n".to_string()), " /// `str_content_hash` of this text, computed at most once per carrier lineage.\n".to_string()), " pub fn content_hash(&self) -> u64 {\n".to_string()), " if let Some(h) = self.content_hash.get() {\n".to_string()), " return h;\n".to_string()), " }\n".to_string()), " #[cfg(test)]\n".to_string()), " STR_CONTENT_HASH_COMPUTED.with(|c| c.set(c.get() + 1));\n".to_string()), " let h = str_content_hash(&self.rc);\n".to_string()), " self.content_hash.set(Some(h));\n".to_string()), " h\n".to_string()), " }\n\n".to_string()), " #[inline]\n".to_string()), " pub fn as_str(&self) -> &str {\n".to_string()), " &self.rc\n".to_string()), " }\n\n".to_string()), " #[inline]\n".to_string()), " pub fn is_ascii(&self) -> bool {\n".to_string()), " self.is_ascii\n".to_string()), " }\n\n".to_string()), " /// Owned handle on the underlying allocation, for sites that need a bare `Rc`.\n".to_string()), " pub fn rc(&self) -> Rc {\n".to_string()), " Rc::clone(&self.rc)\n".to_string()), " }\n\n".to_string()), " /// Pointer identity of the underlying allocation, for consumers whose subject is\n".to_string()), " /// sharing rather than content.\n".to_string()), " pub fn ptr_eq(a: &RcStr, b: &RcStr) -> bool {\n".to_string()), " Rc::ptr_eq(&a.rc, &b.rc)\n".to_string()), " }\n\n".to_string()), " pub fn char_at(&self, pos: i64) -> String {\n".to_string()), " if !self.is_ascii {\n".to_string()), " return char_at(&self.rc, pos);\n".to_string()), " }\n".to_string()), " let pos = pos.max(0) as usize;\n".to_string()), " let bytes = self.rc.as_bytes();\n".to_string()), " if pos >= bytes.len() {\n".to_string()), " return String::new();\n".to_string()), " }\n".to_string()), " String::from(bytes[pos] as char)\n".to_string()), " }\n\n".to_string()), " pub fn string_length(&self) -> i64 {\n".to_string()), " if self.is_ascii {\n".to_string()), " self.rc.len() as i64\n".to_string()), " } else {\n".to_string()), " self.rc.chars().count() as i64\n".to_string()), " }\n".to_string()), " }\n\n".to_string()), " pub fn substring(&self, start: i64, end: i64) -> String {\n".to_string()), " if !self.is_ascii {\n".to_string()), " return substring(&self.rc, start, end);\n".to_string()), " }\n".to_string()), " let start = start.max(0) as usize;\n".to_string()), " let end = end.max(0) as usize;\n".to_string()), " if end <= start {\n".to_string()), " return String::new();\n".to_string()), " }\n".to_string()), " let bytes = self.rc.as_bytes();\n".to_string()), " if start >= bytes.len() {\n".to_string()), " return String::new();\n".to_string()), " }\n".to_string()), " let out_end = end.min(bytes.len());\n".to_string()), " record_substring_chars_walked(&self.rc, start, out_end.saturating_sub(start));\n".to_string()), " self.rc[start..out_end].to_string()\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl std::ops::Deref for RcStr {\n".to_string()), " type Target = str;\n".to_string()), " fn deref(&self) -> &str {\n".to_string()), " &self.rc\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl AsRef for RcStr {\n".to_string()), " fn as_ref(&self) -> &str {\n".to_string()), " &self.rc\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl std::borrow::Borrow for RcStr {\n".to_string()), " fn borrow(&self) -> &str {\n".to_string()), " &self.rc\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl std::fmt::Display for RcStr {\n".to_string()), " fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {\n".to_string()), " std::fmt::Display::fmt(&*self.rc, f)\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "#[cfg(test)]\n".to_string()), "mod rc_str_content_hash_tests {\n".to_string()), " use super::*;\n".to_string()), "\n".to_string()), " fn computed() -> u64 {\n".to_string()), " STR_CONTENT_HASH_COMPUTED.with(|c| c.get())\n".to_string()), " }\n".to_string()), "\n".to_string()), " #[test]\n".to_string()), " fn carried_hash_equals_the_free_authority() {\n".to_string()), " for text in [\"\", \"a\", \"{\\\"weight_map\\\": {}}\", \"caf\\u{e9} \\u{1f600}\"] {\n".to_string()), " assert_eq!(RcStr::new(Rc::from(text)).content_hash(), str_content_hash(text));\n".to_string()), " }\n".to_string()), " }\n".to_string()), "\n".to_string()), " #[test]\n".to_string()), " fn a_threaded_clone_inherits_the_hash_instead_of_rehashing() {\n".to_string()), " let first = RcStr::new(Rc::from(\"x\".repeat(4096).as_str()));\n".to_string()), " let before = computed();\n".to_string()), " let h = first.content_hash();\n".to_string()), " let mut carried = first.clone();\n".to_string()), " for _ in 0..1000 {\n".to_string()), " assert_eq!(carried.content_hash(), h);\n".to_string()), " carried = carried.clone();\n".to_string()), " }\n".to_string()), " assert_eq!(computed() - before, 1, \"one hash per lineage, not one per step\");\n".to_string()), " }\n".to_string()), "\n".to_string()), " #[test]\n".to_string()), " fn a_clone_taken_before_the_first_hash_is_an_independent_lineage() {\n".to_string()), " let a = RcStr::new(Rc::from(\"abc\"));\n".to_string()), " let b = a.clone();\n".to_string()), " let before = computed();\n".to_string()), " assert_eq!(a.content_hash(), b.content_hash());\n".to_string()), " assert_eq!(computed() - before, 2);\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl PartialEq for RcStr {\n".to_string()), " fn eq(&self, other: &Self) -> bool {\n".to_string()), " self.rc == other.rc\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl Eq for RcStr {}\n\n".to_string()), "impl PartialOrd for RcStr {\n".to_string()), " fn partial_cmp(&self, other: &Self) -> Option {\n".to_string()), " Some(self.cmp(other))\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl Ord for RcStr {\n".to_string()), " fn cmp(&self, other: &Self) -> std::cmp::Ordering {\n".to_string()), " self.rc.as_ref().cmp(other.rc.as_ref())\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl std::hash::Hash for RcStr {\n".to_string()), " fn hash(&self, state: &mut H) {\n".to_string()), " self.rc.as_ref().hash(state)\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl From> for RcStr {\n".to_string()), " fn from(rc: Rc) -> Self {\n".to_string()), " RcStr::new(rc)\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl From<&str> for RcStr {\n".to_string()), " fn from(s: &str) -> Self {\n".to_string()), " RcStr::new(Rc::from(s))\n".to_string()), " }\n".to_string()), "}\n\n".to_string()), "impl From for RcStr {\n".to_string()), " fn from(s: String) -> Self {\n".to_string()), " RcStr::new(Rc::from(s.as_str()))\n".to_string()), " }\n".to_string()), "}\n\n".to_string()) } pub fn rt_scanner_ops() -> String { diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index 300aa04cf26..a58b04620d6 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -8774,10 +8774,7 @@ fn is_structural_pure_fn(name: &str) -> bool { } fn eval_recompute_str_hash(s: &str) -> u64 { - use std::hash::{Hash, Hasher}; - let mut h = std::collections::hash_map::DefaultHasher::new(); - s.hash(&mut h); - h.finish() + v1_rt::str_content_hash(s) } fn eval_recompute_mix(seed: u64, x: u64) -> u64 { @@ -8927,10 +8924,9 @@ fn eval_recompute_value_hash( Value::Float(f) => { EvalRecomputeStep::Have(eval_recompute_mix(0xA5A5_0030, f.to_bits())) } - Value::Str(s) => EvalRecomputeStep::Have(eval_recompute_mix( - 0xA5A5_0040, - eval_recompute_str_hash(s), - )), + Value::Str(s) => { + EvalRecomputeStep::Have(eval_recompute_mix(0xA5A5_0040, s.content_hash())) + } Value::Fn { node } => EvalRecomputeStep::Have(eval_recompute_mix( 0xA5A5_0050, Rc::as_ptr(node) as u64, @@ -9102,7 +9098,7 @@ fn eval_recompute_arg_key( Value::Bool(b) => Some(EvalRecomputeArgKey::Bool(*b)), Value::Int(i) => Some(EvalRecomputeArgKey::Int(*i)), Value::Float(f) => Some(EvalRecomputeArgKey::FloatBits(f.to_bits())), - Value::Str(s) => Some(EvalRecomputeArgKey::StrHash(eval_recompute_str_hash(s))), + Value::Str(s) => Some(EvalRecomputeArgKey::StrHash(s.content_hash())), Value::Variant { type_name, variant_name, diff --git a/src/v1/stage0/src/v1_rt.rs b/src/v1/stage0/src/v1_rt.rs index af8b0738ec0..a2645a7390c 100644 --- a/src/v1/stage0/src/v1_rt.rs +++ b/src/v1/stage0/src/v1_rt.rs @@ -399,16 +399,61 @@ pub fn clamp(val: i64, min_val: i64, max_val: i64) -> i64 { /// free functions' semantics: each method falls back to the function it shadows /// whenever the flag is false, and takes the byte path only under the same /// condition that path is already taken there (byte index == code-point index). +/// +/// The content hash is the second carried fact, carried for the same reason as the +/// flag: a pure function of immutable content that a hot consumer re-derived per +/// access. The interpreter's eval-call memo keys every pure call by the content hash +/// of its arguments and rehashed a `Value::Str` in full on EVERY call, so a recursion +/// threading one large text through its steps paid O(|text|) per step and O(|text|^2) +/// overall. It is filled lazily -- most strings are never a call argument -- and it is +/// carried beside the Rc rather than behind it: a clone taken after the first hash +/// inherits it, which is the route a threaded argument takes (the key is read off the +/// argument before it becomes the callee's binding, and the next step's argument is a +/// clone of that binding). An identity memo keyed on the allocation was rejected: a +/// `Weak` keeps the string's inline bytes allocated, so every hashed argument +/// would be retained for the context's lifetime. #[derive(Debug, Clone)] pub struct RcStr { rc: Rc, is_ascii: bool, + content_hash: Cell>, +} + +/// The one content hash of interpreted string text. `RcStr::content_hash` carries it and +/// a consumer holding a bare `&str` calls it directly, so a carried hash and a computed +/// one cannot disagree. +pub fn str_content_hash(s: &str) -> u64 { + use std::hash::{Hash, Hasher}; + let mut h = std::collections::hash_map::DefaultHasher::new(); + s.hash(&mut h); + h.finish() +} + +#[cfg(test)] +thread_local! { + static STR_CONTENT_HASH_COMPUTED: Cell = const { Cell::new(0) }; } impl RcStr { pub fn new(rc: Rc) -> Self { let is_ascii = rc.is_ascii(); - RcStr { rc, is_ascii } + RcStr { + rc, + is_ascii, + content_hash: Cell::new(None), + } + } + + /// `str_content_hash` of this text, computed at most once per carrier lineage. + pub fn content_hash(&self) -> u64 { + if let Some(h) = self.content_hash.get() { + return h; + } + #[cfg(test)] + STR_CONTENT_HASH_COMPUTED.with(|c| c.set(c.get() + 1)); + let h = str_content_hash(&self.rc); + self.content_hash.set(Some(h)); + h } #[inline] @@ -496,6 +541,51 @@ impl std::fmt::Display for RcStr { } } +#[cfg(test)] +mod rc_str_content_hash_tests { + use super::*; + + fn computed() -> u64 { + STR_CONTENT_HASH_COMPUTED.with(|c| c.get()) + } + + #[test] + fn carried_hash_equals_the_free_authority() { + for text in ["", "a", "{\"weight_map\": {}}", "caf\u{e9} \u{1f600}"] { + assert_eq!( + RcStr::new(Rc::from(text)).content_hash(), + str_content_hash(text) + ); + } + } + + #[test] + fn a_threaded_clone_inherits_the_hash_instead_of_rehashing() { + let first = RcStr::new(Rc::from("x".repeat(4096).as_str())); + let before = computed(); + let h = first.content_hash(); + let mut carried = first.clone(); + for _ in 0..1000 { + assert_eq!(carried.content_hash(), h); + carried = carried.clone(); + } + assert_eq!( + computed() - before, + 1, + "one hash per lineage, not one per step" + ); + } + + #[test] + fn a_clone_taken_before_the_first_hash_is_an_independent_lineage() { + let a = RcStr::new(Rc::from("abc")); + let b = a.clone(); + let before = computed(); + assert_eq!(a.content_hash(), b.content_hash()); + assert_eq!(computed() - before, 2); + } +} + impl PartialEq for RcStr { fn eq(&self, other: &Self) -> bool { self.rc == other.rc diff --git a/tools/json_parse_ladder/.gitignore b/tools/json_parse_ladder/.gitignore new file mode 100644 index 00000000000..d0c2b43c6a0 --- /dev/null +++ b/tools/json_parse_ladder/.gitignore @@ -0,0 +1 @@ +doc_*.json diff --git a/tools/json_parse_ladder/acc.dag b/tools/json_parse_ladder/acc.dag new file mode 100644 index 00000000000..d1af5590f69 --- /dev/null +++ b/tools/json_parse_ladder/acc.dag @@ -0,0 +1,8 @@ +module tools.json_parse_ladder.acc + +fn grow(i: Int, n: Int, acc: List) -> Int { + if i >= n { count(acc) } else { grow(i: i + 1, n: n, acc: acc |> list_push(i)) } +} +fn acc_10k() -> Int { grow(i: 0, n: 10000, acc: []) } +fn acc_20k() -> Int { grow(i: 0, n: 20000, acc: []) } +fn acc_40k() -> Int { grow(i: 0, n: 40000, acc: []) } diff --git a/tools/json_parse_ladder/depth.dag b/tools/json_parse_ladder/depth.dag new file mode 100644 index 00000000000..1dec59986d2 --- /dev/null +++ b/tools/json_parse_ladder/depth.dag @@ -0,0 +1,14 @@ +module tools.json_parse_ladder.depth + +fn deep(i: Int, n: Int, acc: Int) -> Int { + if i >= n { acc } else { deep(i: i + 1, n: n, acc: acc + 1) } +} + +fn chunks(k: Int, per: Int, acc: Int) -> Int { + if k <= 0 { acc } else { chunks(k: k - 1, per: per, acc: acc + deep(i: 0, n: per, acc: 0)) } +} + +fn deep_20k() -> Int { deep(i: 0, n: 20000, acc: 0) + deep(i: 0, n: 20000, acc: 0) + deep(i: 0, n: 20000, acc: 0) + deep(i: 0, n: 20000, acc: 0) } +fn deep_80k() -> Int { deep(i: 0, n: 80000, acc: 0) } +fn flat_80k() -> Int { chunks(k: 400, per: 200, acc: 0) } +fn flat_320k() -> Int { chunks(k: 1600, per: 200, acc: 0) } diff --git a/tools/json_parse_ladder/json_ladder.dag b/tools/json_parse_ladder/json_ladder.dag new file mode 100644 index 00000000000..62a36d4b46c --- /dev/null +++ b/tools/json_parse_ladder/json_ladder.dag @@ -0,0 +1,62 @@ +module tools.json_parse_ladder.json_ladder + +import extdeps.languages.json.parse { parse_json_document, JsonDocumentParsed, JsonDocumentUnreadable, json_object_unique_member, JsonMemberFound } +import extdeps.filesystem.filesystem_io +import extdeps.languages.json.emit { JsonObject, serialize_json } + +fn members_of(path: String) -> Int { + let r = filesystem_read(path: path) + match parse_json_document(s: r.content) { + JsonDocumentUnreadable { gap: _ } => 0 - 1 + JsonDocumentParsed { value: v } => + match json_object_unique_member(v: v, key: "weight_map") { + JsonMemberFound { value: JsonObject { members: ms } } => count(ms) + _ => 0 - 2 + } + } +} + +fn n1000() -> Int { members_of(path: "tools/json_parse_ladder/doc_1000.json") } +fn n4000() -> Int { members_of(path: "tools/json_parse_ladder/doc_4000.json") } +fn n16000() -> Int { members_of(path: "tools/json_parse_ladder/doc_16000.json") } +fn n48000() -> Int { members_of(path: "tools/json_parse_ladder/doc_48000.json") } +fn n96000() -> Int { members_of(path: "tools/json_parse_ladder/doc_96000.json") } +fn read_only_96000() -> Int { filesystem_read(path: "tools/json_parse_ladder/doc_96000.json").content.length() } + +fn walk_from(s: String, i: Int, n: Int, acc: Int) -> Int { + if i >= n { acc } else if char_at(s: s, pos: i) == "\"" { walk_from(s: s, i: i + 1, n: n, acc: acc + 1) } else { walk_from(s: s, i: i + 1, n: n, acc: acc) } +} +fn walk_of(path: String) -> Int { + let t = filesystem_read(path: path).content + walk_from(s: t, i: 0, n: t.length(), acc: 0) +} +fn w1000() -> Int { walk_of(path: "tools/json_parse_ladder/doc_1000.json") } +fn w4000() -> Int { walk_of(path: "tools/json_parse_ladder/doc_4000.json") } +fn w16000() -> Int { walk_of(path: "tools/json_parse_ladder/doc_16000.json") } +fn w48000() -> Int { walk_of(path: "tools/json_parse_ladder/doc_48000.json") } + +fn big_walk_50k() -> Int { + let t = filesystem_read(path: "tools/json_parse_ladder/doc_96000.json").content + walk_from(s: t, i: 0, n: 50000, acc: 0) +} +fn small_walk_50k() -> Int { + let t = substring(s: filesystem_read(path: "tools/json_parse_ladder/doc_96000.json").content, start: 0, end: 50001) + walk_from(s: t, i: 0, n: 50000, acc: 0) +} +fn big_len_walk(s: String, i: Int, acc: Int) -> Int { + if i >= 50000 || i >= s.length() { acc } else { big_len_walk(s: s, i: i + 1, acc: acc + 1) } +} +fn big_len_50k() -> Int { big_len_walk(s: filesystem_read(path: "tools/json_parse_ladder/doc_96000.json").content, i: 0, acc: 0) } +fn big_substr_walk(s: String, i: Int, acc: Int) -> Int { + if i >= 50000 { acc } else { big_substr_walk(s: s, i: i + 1, acc: acc + substring(s: s, start: i, end: i + 1).length()) } +} +fn big_substr_50k() -> Int { big_substr_walk(s: filesystem_read(path: "tools/json_parse_ladder/doc_96000.json").content, i: 0, acc: 0) } + +fn reserialized(path: String) -> String { + match parse_json_document(s: filesystem_read(path: path).content) { + JsonDocumentUnreadable { gap: _ } => "UNREADABLE" + JsonDocumentParsed { value: v } => serialize_json(v: v) + } +} +fn rs1000() -> String { reserialized(path: "tools/json_parse_ladder/doc_1000.json") } +fn rs16000() -> String { reserialized(path: "tools/json_parse_ladder/doc_16000.json") } diff --git a/tools/json_parse_ladder/split.dag b/tools/json_parse_ladder/split.dag new file mode 100644 index 00000000000..a9b92b41e45 --- /dev/null +++ b/tools/json_parse_ladder/split.dag @@ -0,0 +1,16 @@ +module tools.json_parse_ladder.split + +import extdeps.filesystem.filesystem_io + +fn big() -> String { filesystem_read(path: "tools/json_parse_ladder/doc_96000.json").content } + +fn pass_only(s: String, i: Int) -> Int { if i >= 50000 { i } else { pass_only(s: s, i: i + 1) } } +fn a_pass_only() -> Int { pass_only(s: big(), i: 0) } + +fn charat_closed(i: Int, acc: Int, t: String) -> Int { if i >= 50000 { acc } else { charat_closed(i: i + 1, acc: acc + char_at(s: t, pos: 3).length(), t: "x") } } +fn b_charat_small() -> Int { charat_closed(i: 0, acc: 0, t: "x") } + +fn let_bound(i: Int) -> Int { + if i >= 50000 { i } else { let_bound(i: i + 1) } +} +fn c_big_in_scope() -> Int { let t = big() let_bound(i: 0) + t.length() } From 3d3ba661ce4cc308dd2fc8ad5313c1c9d8865778 Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 22 Sep 2026 15:18:49 +0000 Subject: [PATCH 2/3] File the memo key-cost class, carrying the unpinned key-site route as 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) --- ...key_cost_proportional_to_argument_size.dag | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 dag/gunbc/recurring_failure_mode/memo_key_cost_proportional_to_argument_size.dag diff --git a/dag/gunbc/recurring_failure_mode/memo_key_cost_proportional_to_argument_size.dag b/dag/gunbc/recurring_failure_mode/memo_key_cost_proportional_to_argument_size.dag new file mode 100644 index 00000000000..2ae0e1fb022 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/memo_key_cost_proportional_to_argument_size.dag @@ -0,0 +1,24 @@ +module gunbc.recurring_failure_mode.memo_key_cost_proportional_to_argument_size + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, decl_ref } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data memo_key_cost_proportional_to_argument_size: RecurringFailureMode = RecurringFailureMode { + identity: "memo_key_cost_proportional_to_argument_size" as NonEmptyStr, + + receipts: [ + "INVALID STATE: a memo that keys every call by the content of its arguments derives that key at a cost proportional to the arguments' size, on every call, including calls that can never recur. A recursion threading one large value through its steps then pays O(|value|) per step and O(|value|^2) in total, and the cost lands on whichever program happens to thread a large value, not on the provider that charged it.", + "HARM: extdeps.languages.json parse_json_document was reported as not returning on a large document and was read as a parser defect. The parser is linear with the memo off; the cost was the interpreter's default-on eval-call memo (v1_interpreter eval_pure_named_call -> eval_recompute_key). A cost defect in a shared provider presents as a defect in its most visible consumer, which is how a victim gets repaired instead of the cause.", + "DISTINGUISHING FACTS: the instrument is tools/json_parse_ladder, run through `gunbc run`. The discriminator is GUNBC_EVAL_MEMO=0, the memo's declared diagnostic realization switch with identical values: a curve that bends with the memo on and runs linear with it off locates the cost in the key and not in the consumer. A recursion that only PASSES the value, never reading it, paying the whole cost is the second tell; holding the same value in scope without passing it pays nothing.", + "TWO INSTANCES, ONE CLASS. (1) A String argument was hashed in full per call because strings bypassed the identity hash memo that composites use: CLOSED by gunbc#12065, RcStr carries its content hash once per lineage (v1.compiler.runtime_rust rt_string_carrier), key values unchanged, re-serialized documents byte-identical. (2) A freshly built accumulator argument -- a list_push result, a new Rc every step -- misses the identity hash memo on every call and is rehashed in full: OPEN, and it cannot close at a carrier. Whether the memo should key a call that cannot recur is a provider-admission question under std.materialization_ladder, dispatched as its own work item by the DS4.1 lane (adhoc-55498ebb-b04).", + "RUNG FOUND AT: mitigatable, for the closed instance, and ONLY by measurement. No committed control pins the interpreter ROUTE: reverting either Value::Str key site in eval_recompute_arg_key or eval_recompute_value_hash back to eval_recompute_str_hash(s) leaves every committed test green. The v1-stage0-runtime rc_str_content_hash_tests pin the carrier (one hash per lineage, carried equals free) and are silent about whether the key sites read it. That is DESIGN section 3's pairing obligation unmet: deleting the integration does not make a control fail.", + "CEILING: mechanically preventable for the route -- a control that observes how many content hashes an interpreted pure recursion over one threaded string performs, and goes red when that count scales with the step count. The accumulator instance's ceiling is set by its provider-admission ruling, not here.", + "NEXT TRIGGER, NAMED AS A CAPABILITY: an observation of content-hash computations that the INTERPRETER'S OWN TESTS can read -- a work-counter cargo feature in the text_lookup_work_counter pattern, or an exported accessor on RcStr -- SUFFICIENT FOR a v1-compiler test that evaluates a memoized pure recursion threading one string through many steps and goes red when a key site is reverted to rehashing. A carrier-level test in v1-stage0-runtime does not discharge it: that crate's cfg(test) is invisible to the interpreter, which is why this row exists.", + ], + + evidence: [ + decl_ref(module_path: "v1.compiler.runtime_rust", decl_name: "rt_string_carrier"), + decl_ref(module_path: "extdeps.languages.json.parse", decl_name: "parse_json_document"), + ], +} From 28079cb0910abaa273fd428b487bfbac05f9a73b Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Tue, 22 Sep 2026 16:05:59 +0000 Subject: [PATCH 3/3] Land the ladder as a self-contained instrument: documents built in .dag, 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) --- dag/gunbc/instruments/json_parse_ladder.dag | 148 ++++++++++++++++++ ...key_cost_proportional_to_argument_size.dag | 6 +- dag/gunbc/v1/v1_maintenance_standing.dag | 2 +- tools/json_parse_ladder/.gitignore | 1 - tools/json_parse_ladder/acc.dag | 8 - tools/json_parse_ladder/depth.dag | 14 -- tools/json_parse_ladder/json_ladder.dag | 62 -------- tools/json_parse_ladder/split.dag | 16 -- 8 files changed, 154 insertions(+), 103 deletions(-) create mode 100644 dag/gunbc/instruments/json_parse_ladder.dag delete mode 100644 tools/json_parse_ladder/.gitignore delete mode 100644 tools/json_parse_ladder/acc.dag delete mode 100644 tools/json_parse_ladder/depth.dag delete mode 100644 tools/json_parse_ladder/json_ladder.dag delete mode 100644 tools/json_parse_ladder/split.dag diff --git a/dag/gunbc/instruments/json_parse_ladder.dag b/dag/gunbc/instruments/json_parse_ladder.dag new file mode 100644 index 00000000000..5f6248009da --- /dev/null +++ b/dag/gunbc/instruments/json_parse_ladder.dag @@ -0,0 +1,148 @@ +module tools.json_parse_ladder + +import std.types { String, Int, List } +import extdeps.languages.json.parse { parse_json_document, JsonDocumentParsed, JsonDocumentUnreadable, json_object_unique_member, JsonMemberFound } +import extdeps.languages.json.emit { JsonObject, serialize_json } +import std.process { ProcessExit, ExitSuccess, ExitFailure, exit_failure } +import extdeps.filesystem.filesystem_io { Filesystem } + +// THE INSTRUMENT THAT LOCATED THE COST OF extdeps.languages.json parse_json_document ON LARGE +// DOCUMENTS (gunbc#12065, gunbc.recurring_failure_mode memo_key_cost_proportional_to_argument_size). +// Each entry below is one rung, run as its own process so each reading is independent: +// +// gunbc run --source-root dag --source-root src/v2 \ +// --entry dag/gunbc/instruments/json_parse_ladder.dag --function +// +// with the process timed by the caller. EVERY RUNG CHECKS ITS OWN VALUE: it exits 0 only when it +// returned what it owes (the member count it built, the step count it walked), and otherwise exits +// with a located failure naming what it saw -- so a semantic regression stops the reading instead of +// scrolling past it. The equivalence rungs take `--arg path=` and write the re-serialized +// document there, for a byte comparison across binaries. Every run pays one corpus-resolve baseline, and every +// document rung also pays its own construction, so a rung is read as its distance above the +// build_only rung of the same size -- and only CURVES are compared, never absolute seconds: memo on +// against GUNBC_EVAL_MEMO=0 on one binary, or a fixed binary against an unfixed one on one tree. +// +// THE DOCUMENTS ARE BUILT HERE, NOT READ FROM FIXTURES, so the instrument re-derives on a fresh +// clone with nothing beside it. They mimic a safetensors index: a small metadata object and one +// weight_map object whose member count is the rung. The builder deliberately never threads a growing +// value through a deep recursion -- entries are assembled in chunks of at most +// ladder_chunk_width and joined -- because that is exactly the shape this instrument measures, and +// a builder that paid it would confound every reading it produced. + +fn ladder_chunk_width() -> Int { 300 } + +fn ladder_entry(i: Int) -> String { + let layer = i / ladder_chunk_width() + let expert = i - layer * ladder_chunk_width() + join([ + " \"model.layers.", to_string(value: layer), + ".mlp.experts.", to_string(value: expert), + ".w", to_string(value: i - (i / 3) * 3), + ".weight\": \"model-", to_string(value: 10001 + i - (i / 46) * 46), + "-of-00046.safetensors\"", + ], "") +} + +fn ladder_chunk_entries(i: Int, hi: Int, acc: List) -> List { + if i >= hi { acc } else { ladder_chunk_entries(i: i + 1, hi: hi, acc: acc |> list_push(ladder_entry(i: i))) } +} + +fn ladder_chunks(lo: Int, n: Int, acc: List) -> List { + if lo >= n { + acc + } else { + let hi = if lo + ladder_chunk_width() > n { n } else { lo + ladder_chunk_width() } + ladder_chunks(lo: hi, n: n, acc: acc |> list_push(join(ladder_chunk_entries(i: lo, hi: hi, acc: []), ",\n"))) + } +} + +fn ladder_document(n: Int) -> String { + join([ + "{\n \"metadata\": {\n \"total_size\": 123456789\n },\n \"weight_map\": {\n", + join(ladder_chunks(lo: 0, n: n, acc: []), ",\n"), + "\n }\n}\n", + ], "") +} + +fn weight_map_members(text: String) -> Int { + match parse_json_document(s: text) { + JsonDocumentUnreadable { gap: _ } => 0 - 1 + JsonDocumentParsed { value: v } => + match json_object_unique_member(v: v, key: "weight_map") { + JsonMemberFound { value: JsonObject { members: ms } } => count(ms) + _ => 0 - 2 + } + } +} + +fn reserialized(text: String) -> String { + match parse_json_document(s: text) { + JsonDocumentUnreadable { gap: _ } => "UNREADABLE" + JsonDocumentParsed { value: v } => serialize_json(v: v) + } +} + +fn rung_owes(rung: String, observed: Int, owed: Int) -> ProcessExit { + if observed == owed { + ExitSuccess + } else { + exit_failure(reason: join([rung, " returned ", to_string(value: observed), " where it owes ", to_string(value: owed)], "")) + } +} + +fn rung_built(rung: String, text: String) -> ProcessExit { + if text.length() > 0 { ExitSuccess } else { exit_failure(reason: concat(rung, " built an empty document")) } +} + +fn rung_written(path: String, content: String) -> ProcessExit { + let w = Filesystem.Write(path: path, content: content) + if w.success { ExitSuccess } else { exit_failure(reason: concat(concat("write failed for ", path), concat(": ", w.error))) } +} + +// PARSER RUNGS: the parse over the built document, and its construction-only baseline. +fn build_only_1000() -> ProcessExit { rung_built(rung: "build_only_1000", text: ladder_document(n: 1000)) } +fn build_only_16000() -> ProcessExit { rung_built(rung: "build_only_16000", text: ladder_document(n: 16000)) } +fn build_only_48000() -> ProcessExit { rung_built(rung: "build_only_48000", text: ladder_document(n: 48000)) } +fn build_only_96000() -> ProcessExit { rung_built(rung: "build_only_96000", text: ladder_document(n: 96000)) } +fn parse_1000() -> ProcessExit { rung_owes(rung: "parse_1000", observed: weight_map_members(text: ladder_document(n: 1000)), owed: 1000) } +fn parse_4000() -> ProcessExit { rung_owes(rung: "parse_4000", observed: weight_map_members(text: ladder_document(n: 4000)), owed: 4000) } +fn parse_16000() -> ProcessExit { rung_owes(rung: "parse_16000", observed: weight_map_members(text: ladder_document(n: 16000)), owed: 16000) } +fn parse_48000() -> ProcessExit { rung_owes(rung: "parse_48000", observed: weight_map_members(text: ladder_document(n: 48000)), owed: 48000) } +fn parse_96000() -> ProcessExit { rung_owes(rung: "parse_96000", observed: weight_map_members(text: ladder_document(n: 96000)), owed: 96000) } + +// SEMANTIC EQUIVALENCE: the parsed document re-serialized to `path`. Two binaries agree on a +// document when the files they write are byte-identical. +fn reserialize_1000(path: String) -> ProcessExit { rung_written(path: path, content: reserialized(text: ladder_document(n: 1000))) } +fn reserialize_16000(path: String) -> ProcessExit { rung_written(path: path, content: reserialized(text: ladder_document(n: 16000))) } + +// THE ROUTE DISCRIMINATOR: one recursion of fixed step count that only PASSES a string, over a large +// text and a small one. If the large one costs more, the cost is in carrying the argument, not in +// anything the recursion does with it. pass_held_in_scope is the control: the same recursion with +// the large text bound in scope but not passed. +fn pass_steps() -> Int { 50000 } + +fn pass_only(s: String, i: Int) -> Int { if i >= pass_steps() { i } else { pass_only(s: s, i: i + 1) } } +fn scope_only(i: Int) -> Int { if i >= pass_steps() { i } else { scope_only(i: i + 1) } } + +fn pass_large_text() -> ProcessExit { rung_owes(rung: "pass_large_text", observed: pass_only(s: ladder_document(n: 96000), i: 0), owed: pass_steps()) } +fn pass_small_text() -> ProcessExit { rung_owes(rung: "pass_small_text", observed: pass_only(s: substring(s: ladder_document(n: 96000), start: 0, end: pass_steps()), i: 0), owed: pass_steps()) } +fn pass_held_in_scope() -> ProcessExit { + let t = ladder_document(n: 96000) + rung_owes(rung: "pass_held_in_scope", observed: scope_only(i: 0) + t.length(), owed: pass_steps() + t.length()) +} + +// THE INTERPRETER CONTROL: plain recursion at fixed total call count and varying depth. Flat curves +// here rule out call cost as the explanation for anything above. +fn deep(i: Int, n: Int, acc: Int) -> Int { if i >= n { acc } else { deep(i: i + 1, n: n, acc: acc + 1) } } +fn chunked(k: Int, per: Int, acc: Int) -> Int { if k <= 0 { acc } else { chunked(k: k - 1, per: per, acc: acc + deep(i: 0, n: per, acc: 0)) } } +fn depth_20000_times_4() -> ProcessExit { rung_owes(rung: "depth_20000_times_4", observed: deep(i: 0, n: 20000, acc: 0) + deep(i: 0, n: 20000, acc: 0) + deep(i: 0, n: 20000, acc: 0) + deep(i: 0, n: 20000, acc: 0), owed: 80000) } +fn depth_80000() -> ProcessExit { rung_owes(rung: "depth_80000", observed: deep(i: 0, n: 80000, acc: 0), owed: 80000) } +fn flat_80000() -> ProcessExit { rung_owes(rung: "flat_80000", observed: chunked(k: 400, per: 200, acc: 0), owed: 80000) } +fn flat_320000() -> ProcessExit { rung_owes(rung: "flat_320000", observed: chunked(k: 1600, per: 200, acc: 0), owed: 320000) } + +// THE ACCUMULATOR DISCRIMINATOR: a recursion threading a growing list. Its curve with the memo on +// against GUNBC_EVAL_MEMO=0 is the reading for the instance the carrier repair does not close. +fn grow(i: Int, n: Int, acc: List) -> Int { if i >= n { count(acc) } else { grow(i: i + 1, n: n, acc: acc |> list_push(i)) } } +fn accumulate_10000() -> ProcessExit { rung_owes(rung: "accumulate_10000", observed: grow(i: 0, n: 10000, acc: []), owed: 10000) } +fn accumulate_20000() -> ProcessExit { rung_owes(rung: "accumulate_20000", observed: grow(i: 0, n: 20000, acc: []), owed: 20000) } +fn accumulate_40000() -> ProcessExit { rung_owes(rung: "accumulate_40000", observed: grow(i: 0, n: 40000, acc: []), owed: 40000) } diff --git a/dag/gunbc/recurring_failure_mode/memo_key_cost_proportional_to_argument_size.dag b/dag/gunbc/recurring_failure_mode/memo_key_cost_proportional_to_argument_size.dag index 2ae0e1fb022..d44236c46c0 100644 --- a/dag/gunbc/recurring_failure_mode/memo_key_cost_proportional_to_argument_size.dag +++ b/dag/gunbc/recurring_failure_mode/memo_key_cost_proportional_to_argument_size.dag @@ -10,7 +10,7 @@ data memo_key_cost_proportional_to_argument_size: RecurringFailureMode = Recurri receipts: [ "INVALID STATE: a memo that keys every call by the content of its arguments derives that key at a cost proportional to the arguments' size, on every call, including calls that can never recur. A recursion threading one large value through its steps then pays O(|value|) per step and O(|value|^2) in total, and the cost lands on whichever program happens to thread a large value, not on the provider that charged it.", "HARM: extdeps.languages.json parse_json_document was reported as not returning on a large document and was read as a parser defect. The parser is linear with the memo off; the cost was the interpreter's default-on eval-call memo (v1_interpreter eval_pure_named_call -> eval_recompute_key). A cost defect in a shared provider presents as a defect in its most visible consumer, which is how a victim gets repaired instead of the cause.", - "DISTINGUISHING FACTS: the instrument is tools/json_parse_ladder, run through `gunbc run`. The discriminator is GUNBC_EVAL_MEMO=0, the memo's declared diagnostic realization switch with identical values: a curve that bends with the memo on and runs linear with it off locates the cost in the key and not in the consumer. A recursion that only PASSES the value, never reading it, paying the whole cost is the second tell; holding the same value in scope without passing it pays nothing.", + "DISTINGUISHING FACTS: the instrument is tools.json_parse_ladder (dag/gunbc/instruments/json_parse_ladder.dag), one `gunbc run --function ` per rung, each rung exiting non-zero when its value is not the one it owes. The discriminator is GUNBC_EVAL_MEMO=0, the memo's declared diagnostic realization switch with identical values: a curve that bends with the memo on and runs linear with it off locates the cost in the key and not in the consumer. A recursion that only PASSES the value, never reading it, paying the whole cost is the second tell; holding the same value in scope without passing it pays nothing.", "TWO INSTANCES, ONE CLASS. (1) A String argument was hashed in full per call because strings bypassed the identity hash memo that composites use: CLOSED by gunbc#12065, RcStr carries its content hash once per lineage (v1.compiler.runtime_rust rt_string_carrier), key values unchanged, re-serialized documents byte-identical. (2) A freshly built accumulator argument -- a list_push result, a new Rc every step -- misses the identity hash memo on every call and is rehashed in full: OPEN, and it cannot close at a carrier. Whether the memo should key a call that cannot recur is a provider-admission question under std.materialization_ladder, dispatched as its own work item by the DS4.1 lane (adhoc-55498ebb-b04).", "RUNG FOUND AT: mitigatable, for the closed instance, and ONLY by measurement. No committed control pins the interpreter ROUTE: reverting either Value::Str key site in eval_recompute_arg_key or eval_recompute_value_hash back to eval_recompute_str_hash(s) leaves every committed test green. The v1-stage0-runtime rc_str_content_hash_tests pin the carrier (one hash per lineage, carried equals free) and are silent about whether the key sites read it. That is DESIGN section 3's pairing obligation unmet: deleting the integration does not make a control fail.", "CEILING: mechanically preventable for the route -- a control that observes how many content hashes an interpreted pure recursion over one threaded string performs, and goes red when that count scales with the step count. The accumulator instance's ceiling is set by its provider-admission ruling, not here.", @@ -20,5 +20,9 @@ data memo_key_cost_proportional_to_argument_size: RecurringFailureMode = Recurri evidence: [ decl_ref(module_path: "v1.compiler.runtime_rust", decl_name: "rt_string_carrier"), decl_ref(module_path: "extdeps.languages.json.parse", decl_name: "parse_json_document"), + decl_ref(module_path: "tools.json_parse_ladder", decl_name: "parse_96000"), + decl_ref(module_path: "tools.json_parse_ladder", decl_name: "pass_large_text"), + decl_ref(module_path: "tools.json_parse_ladder", decl_name: "pass_held_in_scope"), + decl_ref(module_path: "tools.json_parse_ladder", decl_name: "accumulate_40000"), ], } diff --git a/dag/gunbc/v1/v1_maintenance_standing.dag b/dag/gunbc/v1/v1_maintenance_standing.dag index 9bdc5a758c8..035b19cb555 100644 --- a/dag/gunbc/v1/v1_maintenance_standing.dag +++ b/dag/gunbc/v1/v1_maintenance_standing.dag @@ -553,7 +553,7 @@ data v1_seed_standing: SeedLifecycleStanding = SemanticsFrozen { maintenance: Ma // not, so a recursion threading one text through its steps rehashed all of it per step. The parser // is a victim; the link that violated its own contract is the key derivation, which states one // relation (content hash) and realized it at O(|text|) per call for exactly one value kind. The -// instrument is the ladder named in the PR that lands this row. +// instrument is tools.json_parse_ladder, whose rungs re-derive every reading above. // // THE PURPOSE TEST IS SATISFIED FOR THE SAME REASON AS THE ASCII FLAG: the interpreter is the engine // the v2 self-host program runs on, and a pure recursion over source text is that program's diff --git a/tools/json_parse_ladder/.gitignore b/tools/json_parse_ladder/.gitignore deleted file mode 100644 index d0c2b43c6a0..00000000000 --- a/tools/json_parse_ladder/.gitignore +++ /dev/null @@ -1 +0,0 @@ -doc_*.json diff --git a/tools/json_parse_ladder/acc.dag b/tools/json_parse_ladder/acc.dag deleted file mode 100644 index d1af5590f69..00000000000 --- a/tools/json_parse_ladder/acc.dag +++ /dev/null @@ -1,8 +0,0 @@ -module tools.json_parse_ladder.acc - -fn grow(i: Int, n: Int, acc: List) -> Int { - if i >= n { count(acc) } else { grow(i: i + 1, n: n, acc: acc |> list_push(i)) } -} -fn acc_10k() -> Int { grow(i: 0, n: 10000, acc: []) } -fn acc_20k() -> Int { grow(i: 0, n: 20000, acc: []) } -fn acc_40k() -> Int { grow(i: 0, n: 40000, acc: []) } diff --git a/tools/json_parse_ladder/depth.dag b/tools/json_parse_ladder/depth.dag deleted file mode 100644 index 1dec59986d2..00000000000 --- a/tools/json_parse_ladder/depth.dag +++ /dev/null @@ -1,14 +0,0 @@ -module tools.json_parse_ladder.depth - -fn deep(i: Int, n: Int, acc: Int) -> Int { - if i >= n { acc } else { deep(i: i + 1, n: n, acc: acc + 1) } -} - -fn chunks(k: Int, per: Int, acc: Int) -> Int { - if k <= 0 { acc } else { chunks(k: k - 1, per: per, acc: acc + deep(i: 0, n: per, acc: 0)) } -} - -fn deep_20k() -> Int { deep(i: 0, n: 20000, acc: 0) + deep(i: 0, n: 20000, acc: 0) + deep(i: 0, n: 20000, acc: 0) + deep(i: 0, n: 20000, acc: 0) } -fn deep_80k() -> Int { deep(i: 0, n: 80000, acc: 0) } -fn flat_80k() -> Int { chunks(k: 400, per: 200, acc: 0) } -fn flat_320k() -> Int { chunks(k: 1600, per: 200, acc: 0) } diff --git a/tools/json_parse_ladder/json_ladder.dag b/tools/json_parse_ladder/json_ladder.dag deleted file mode 100644 index 62a36d4b46c..00000000000 --- a/tools/json_parse_ladder/json_ladder.dag +++ /dev/null @@ -1,62 +0,0 @@ -module tools.json_parse_ladder.json_ladder - -import extdeps.languages.json.parse { parse_json_document, JsonDocumentParsed, JsonDocumentUnreadable, json_object_unique_member, JsonMemberFound } -import extdeps.filesystem.filesystem_io -import extdeps.languages.json.emit { JsonObject, serialize_json } - -fn members_of(path: String) -> Int { - let r = filesystem_read(path: path) - match parse_json_document(s: r.content) { - JsonDocumentUnreadable { gap: _ } => 0 - 1 - JsonDocumentParsed { value: v } => - match json_object_unique_member(v: v, key: "weight_map") { - JsonMemberFound { value: JsonObject { members: ms } } => count(ms) - _ => 0 - 2 - } - } -} - -fn n1000() -> Int { members_of(path: "tools/json_parse_ladder/doc_1000.json") } -fn n4000() -> Int { members_of(path: "tools/json_parse_ladder/doc_4000.json") } -fn n16000() -> Int { members_of(path: "tools/json_parse_ladder/doc_16000.json") } -fn n48000() -> Int { members_of(path: "tools/json_parse_ladder/doc_48000.json") } -fn n96000() -> Int { members_of(path: "tools/json_parse_ladder/doc_96000.json") } -fn read_only_96000() -> Int { filesystem_read(path: "tools/json_parse_ladder/doc_96000.json").content.length() } - -fn walk_from(s: String, i: Int, n: Int, acc: Int) -> Int { - if i >= n { acc } else if char_at(s: s, pos: i) == "\"" { walk_from(s: s, i: i + 1, n: n, acc: acc + 1) } else { walk_from(s: s, i: i + 1, n: n, acc: acc) } -} -fn walk_of(path: String) -> Int { - let t = filesystem_read(path: path).content - walk_from(s: t, i: 0, n: t.length(), acc: 0) -} -fn w1000() -> Int { walk_of(path: "tools/json_parse_ladder/doc_1000.json") } -fn w4000() -> Int { walk_of(path: "tools/json_parse_ladder/doc_4000.json") } -fn w16000() -> Int { walk_of(path: "tools/json_parse_ladder/doc_16000.json") } -fn w48000() -> Int { walk_of(path: "tools/json_parse_ladder/doc_48000.json") } - -fn big_walk_50k() -> Int { - let t = filesystem_read(path: "tools/json_parse_ladder/doc_96000.json").content - walk_from(s: t, i: 0, n: 50000, acc: 0) -} -fn small_walk_50k() -> Int { - let t = substring(s: filesystem_read(path: "tools/json_parse_ladder/doc_96000.json").content, start: 0, end: 50001) - walk_from(s: t, i: 0, n: 50000, acc: 0) -} -fn big_len_walk(s: String, i: Int, acc: Int) -> Int { - if i >= 50000 || i >= s.length() { acc } else { big_len_walk(s: s, i: i + 1, acc: acc + 1) } -} -fn big_len_50k() -> Int { big_len_walk(s: filesystem_read(path: "tools/json_parse_ladder/doc_96000.json").content, i: 0, acc: 0) } -fn big_substr_walk(s: String, i: Int, acc: Int) -> Int { - if i >= 50000 { acc } else { big_substr_walk(s: s, i: i + 1, acc: acc + substring(s: s, start: i, end: i + 1).length()) } -} -fn big_substr_50k() -> Int { big_substr_walk(s: filesystem_read(path: "tools/json_parse_ladder/doc_96000.json").content, i: 0, acc: 0) } - -fn reserialized(path: String) -> String { - match parse_json_document(s: filesystem_read(path: path).content) { - JsonDocumentUnreadable { gap: _ } => "UNREADABLE" - JsonDocumentParsed { value: v } => serialize_json(v: v) - } -} -fn rs1000() -> String { reserialized(path: "tools/json_parse_ladder/doc_1000.json") } -fn rs16000() -> String { reserialized(path: "tools/json_parse_ladder/doc_16000.json") } diff --git a/tools/json_parse_ladder/split.dag b/tools/json_parse_ladder/split.dag deleted file mode 100644 index a9b92b41e45..00000000000 --- a/tools/json_parse_ladder/split.dag +++ /dev/null @@ -1,16 +0,0 @@ -module tools.json_parse_ladder.split - -import extdeps.filesystem.filesystem_io - -fn big() -> String { filesystem_read(path: "tools/json_parse_ladder/doc_96000.json").content } - -fn pass_only(s: String, i: Int) -> Int { if i >= 50000 { i } else { pass_only(s: s, i: i + 1) } } -fn a_pass_only() -> Int { pass_only(s: big(), i: 0) } - -fn charat_closed(i: Int, acc: Int, t: String) -> Int { if i >= 50000 { acc } else { charat_closed(i: i + 1, acc: acc + char_at(s: t, pos: 3).length(), t: "x") } } -fn b_charat_small() -> Int { charat_closed(i: 0, acc: 0, t: "x") } - -fn let_bound(i: Int) -> Int { - if i >= 50000 { i } else { let_bound(i: i + 1) } -} -fn c_big_in_scope() -> Int { let t = big() let_bound(i: 0) + t.length() }