Repository navigation
refactor(roles): declare every language's space tree, calls and string literals as role rows - #1717
Merged
Merged
Conversation
…y role The first R06 slice (#1696). `JavaCode::is_func`, `is_closure`, `is_func_space(_with_code)` and `JavaCode::get_space_kind(_with_code)` read `roles/java.rs` instead of restating its kinds, and the Java attribute walk reads the new `Member` row for membership while keeping body discovery and visibility as its own reading. Two things the proof rows could not say, settled here: - The `FuncOpener` row is "opens a `Function` space", which is wider than `is_func`: Java's `static { … }` opens one and is nobody's callee (#1184). A new owner attribute, `callable`, marks the named functions; the macro emits `is_callable`, and a `callable_when f` spelling gates it independently of the owner's gate — the JS family opens a space for every function expression and calls it a function only when bound, which neither the owner gate nor a duplicate owner could express. Both are read by `utils/role_tables.py`, pinned in the `dsl_gates` proof table and its test, and documented as the §3-amendment kind of gate a reader sees on the row. - `Member` recognises the declaration node; which body it sits in, whether it is public and how many names it binds stay with the consumer. Java's row is `FieldDeclaration` and `ConstantDeclaration`; the two bodies admit only one each at the pinned grammar. The getter's byte-less `get_space_kind` answered `Function` for a `lambda_expression` the walk never promoted (the "pair that fails quietly" of grammar-dispatch §7); through `opens_space` it answers `Unknown`. No supported output reads the byte-less form on a node the walk did not promote. The rows-vs-classifiers comparison test would have become a forwarding check, so `classifiers_read_the_rows_and_the_rows_say_what_the_walk_did` pins the expected five answers per kind independently, and `src/spaces/opens_space_parity_tests.rs` runs the real walk and holds the spaces it opens to the nodes `opens_space` labels (contract 2 of #1690: promotion against the mapping, never against `Closure`). Zero drift: Java corpus + fixtures, CLI ops / metrics / dump / find / count and the verbatim library export, 0 of 112 outputs differ; recovery sweep over the Java fixtures (line, offset, punctuation and word cuts), 6,480 outputs, 0 differing. Perturbations: dropping `callable` from `MethodDeclaration` fails 22 tests; adding it to `StaticInitializer` 1; dropping `EnumDeclaration` 2; dropping the anonymous-class body gate 5; dropping `ConstantDeclaration` 3; each consumer severed from its row fails 2–78. Bypass baseline 84 → 60, census −1, coverage −2, self-scan baseline refreshed for `roles/mod.rs`'s growth.
…pace tree by role The second R06 slice (#1696): JavaScript, MozJS, TypeScript and TSX. Each dialect's `FuncOpener`, `Closure`, `Container` and `Member` rows splice the shared `roles/js_family.rs` fragments, and the binding-site walk that tells `const f = function () {}` from `run(function () {})` (#1188) is emitted per dialect by `js_family_binding_gates!` as the `bound_function` / `bound_arrow` gates — the `callable_when` case the checkpoint review asked to see proven: every function form opens a `Function` space, and the three expression forms are functions only where the gate says bound, closures through the same gate negated. The one clause in which the arrow and function-expression gates must differ (an `identifier` child is a function expression's own name and an arrow's parameter) is kept, and the generator reading (#1186) moves onto the fragment doc. The six `checker.rs` macros that carried the walk are gone. Consumers: `is_func` / `is_closure` / `is_func_space(_with_code)` / `get_space_kind(_with_code)` read the rows in all four dialects; the npm / npa walks read `is_callable` for a method and `member::owns` for a field or property signature, keeping body, visibility and the field-as-method reading as their own; the JS cognitive boundary arm is `is_callable || ClassStaticBlock` with the row's owners as its stop list, replacing the hand-listed kind set that re-asked `is_func`. Naming (`<static-init>`) and doc-attachment sites take `role-bypass: allow` exemptions. `roles/js_family.rs` pins the 26 binding shapes independently per dialect (a generator expression was missing from the first fixture; the owner-coverage guard caught it), and the walk-level parity table gains the four dialects. Zero drift against the base binary: JavaScript 392 files, MozJS 392, TypeScript 10, TSX 2 — 0 of 6,336 CLI and verbatim outputs differ; recovery sweep over the family's fixtures, 21,930 outputs, 0 differing, 0 failed executions. Perturbations, each failing at least one named test: `FunctionExpression` losing its `callable_when` 8, an unconditionally callable arrow 5, a callable static block 1, `Closure` dropping `GeneratorFunction` 1, `bound_arrow` taking the function-expression clause 4, the stop list dropping `CallExpression` 2, `Container` dropping the class expression 1, TS `Member` dropping `PropertySignature` 2, cognitive severed from `is_callable` 7, npm severed 4, npa's interface arm inverted 4. Census −16, coverage −50, rustfmt-bail −1.
…guages' space trees by role The third R06 slice (#1696): Rust, Go, Lua, Bash, Tcl, iRules, Perl, PHP and C — the languages whose openers, closures and containers are decided by kind alone. Each declares `FuncOpener` (with `callable` on the named functions), `Closure`, `Container` and `Member`, or `absent` with its reason (C has no closure; C, Bash, Tcl, iRules, Lua and Perl have no container space to hold a member — a struct or a package is not one). `is_func` / `is_closure` / `is_func_space(_with_code)` / `get_space_kind(_with_code)` read the rows in all nine. The consumers: every cognitive function-boundary arm reads `is_callable` and takes the row's new `CALLABLE` slice — the declared callable kinds, gates aside — as its `stops` list, and the lambda arms read `closure::owns`; the Rust, Go and PHP attribute walks read `member::owns`, the Rust and Go method walks `is_callable` (Go's method is the callable with a `receiver`; Rust's bodiless signature stays the consumer's own). Which body a member sits in, its visibility, a tuple struct's positional fields and PHP's class-constant-in-an-interface reading stay with the consumers, under `role-bypass: allow` exemptions that name the roles, as do the doc-attachment, naming and `#[cfg(test)]` prune sites. `roles/space_tree_tests.rs` pins the five answers per kind for the nine languages independently, with the pinned grammar's never-emitted aliases (C's `FunctionDefinition2`, Lua's `FunctionDeclaration2/3`, PHP's `ConstDeclaration2`) as drift markers rather than fixture requirements, and the walk-level parity table gains nine rows. Zero drift against the base binary: Rust 175 files, Bash 146, C 3, and fixtures for the rest — 0 of 2,736 outputs differ, Java and JavaScript re-confirmed at 0; recovery sweep over the nine languages' fixtures, 27,138 outputs, 0 differing, 0 failed executions. Perturbations, each failing at least one named test unless noted: Rust's closure losing `opens` 7, `Member` dropping `AssociatedType` 2, Go's method losing `callable` 7, Lua's closure gaining `callable` 3, iRules dropping `WhenEvent` 11, Perl dropping `FunctionDefinitionWithoutSub` 3, PHP's trait opening `Trait` 2 and `Member` dropping `PropertyDeclaration` 11, Bash and Tcl losing `callable` 5 and 10; each consumer severed from its row 1–10. Two severed arms failed nothing on the base as on the branch — Go's and PHP's lambda channel — and gained a test each (`go_function_literal_contributes_lambda_nesting`, `php_closure_and_arrow_contribute_lambda_nesting`); Go's boundary `stops` list is unobservable in output, since Go has no nested named function, and the test says so. C's `FunctionDefinition2` alias is never emitted at the pin, so dropping it fails nothing but the drift marker would fail a bump that emits it. Census −29, coverage −46, self-scan baseline refreshed.
…and Objective-C's space trees by role The fourth R06 slice (#1696): C++, Mozcpp and Objective-C. The two C++ dialects splice the shared `cpp_family_func_openers!` (the four aliased `function_definition` ids, each callable) and `cpp_family_containers!` (the file, a namespace, and a struct / class specifier gated on `has_body` — #1555, the gate emitted per dialect) so Mozcpp stays a copy of Cpp by construction (contract 5); a lambda is a closure that opens no space. Objective-C's callables are the two function ids and `method_definition`; a block literal is a closure with no space; `@implementation` opens `Class`, `@interface` and `@protocol` `Interface`; a property and an instance variable are the members. Consumers: the checkers and getters read the rows; the C++ cognitive boundary and lambda arms read `is_callable` / `closure::owns` with `CALLABLE` as the stop list, as the ObjC ones do; the C++ method walks read `is_callable` for an inline method and `member::owns` for a data member, keeping the bodiless-method-by-declarator reading (#1300) and the access sections as their own; the ObjC walks read the row for a property or ivar and, for a method, the callable the container owns — a free C function inside `@implementation` is a callable the container does not own (#1356), which the existing `objc_npm` test caught on the first cut. Naming, non-member and bucket sites take exemptions. `roles/space_tree_tests.rs` pins the C++ fixture for both dialects — bodied and bodiless specifiers checked by text, since one kind spells both — with the three never-emitted `function_definition` aliases as drift markers (probed with `bca dump` over a free function, a method, a constructor, a destructor, a conversion operator, a template and an out-of-line definition), and ObjC's nine kinds; the walk-level parity table gains the three. Zero drift against the base binary: C++ 1,050 files under both dialects, ObjC, C, ccomment and preproc fixtures — 0 of 16,944 outputs differ; recovery sweep over the family's fixtures, 47,808 outputs, 0 differing, 0 failed executions. Perturbations, each failing at least one named test: the struct specifier losing its body gate 6, a `function_definition` losing `callable` 39, the namespace dropped 5, a lambda opening a space 8, C++'s `Member` emptied 10 and Mozcpp's 3, ObjC's method losing `callable` 12, `@interface` opening `Class` 2, the ivar dropped 2; each consumer severed from its row 1–7. The ObjC method predicate is a named fn, which is what kept `Npm::compute` under the cognitive limit. Census −12, coverage −30, self-scan baseline refreshed.
…y role The fifth R06 slice (#1696). C#'s `FuncOpener` row names every callable — the method-like declarations, a local function, an accessor, the operator forms — plus an indexer or property gated to its accessor-less expression-bodied form by `has_no_accessors` (#464, #472; lesson 65: gate, never delete), and the lambda and anonymous method as closures that open a space; `Container` opens `Class` for an enum too (#429); `Member` is the field declaration. The checker, getter, cognitive boundary and lambda arms, the method count (a property's accessor count stays the consumer's weight) and the attribute walk read the rows. One thing the row cannot say, found by the corpus oracle rather than the tests: `CALLABLE` lists kinds gates aside, so reading it as the cognitive stop list made a bodied property — whose accessors are the callables, not the property — an enclosing function above its own getter, and two `DeepSpeech` files took a depth surcharge they never had. The consumer keeps its own stop list, the module doc names the caveat, and `csharp_bodied_property_is_not_a_function_boundary_above_ its_accessor` is the only test that fails when the list is read again. The cognitive arm's old `LocalFunctionDeclaration` entry was the hidden `_local_function_declaration` rule, never emitted. `roles/space_tree_tests.rs` pins the six indexer / property spellings by text beside the kind table, and the walk-level parity table gains C#. Zero drift: 24 files, 0 of 192 outputs; recovery sweep 5,382 outputs, 0 differing. Perturbations, each failing at least one named test: the property losing its gate 5, the indexer dropped 3, the accessor losing `callable` 5, the enum opening `Interface` 2, the anonymous method dropped from `Closure` 2, `Member` emptied 15; the method count severed 11, the attribute walk inverted 11, the cognitive boundary severed 2, the lambda channel severed — nothing before, so `csharp_lambda_and_anonymous_method_contribute_lambda_nesting` was added and is what fails now. Census −4, coverage −18, self-scan baseline refreshed.
…s and Ruby's space trees by role The sixth R06 slice (#1696). Kotlin: a function and a secondary constructor are the callables; a property accessor and `init { … }` open a space without being callable (#1184); a lambda and an anonymous `fun` are closures that open none — the walk never promoted them, though the byte-less getter labelled them `Function` if asked, the same quiet disagreement Java's lambda had; one `class_declaration` kind is a class or an interface by its keyword token, which the new `opens_by f` owner attribute expresses (a named fn answers the `SpaceKind`), pinned in the `dsl_gates` proof table and read by the Python reader. Groovy: a method and a constructor are the callables, `static { … }` opens a space, a closure opens none (the same quiet disagreement), a trait opens `Interface` (#247). Ruby: a `def` in both spellings is the callable; a stabby lambda is a closure and a space; a `{ }` / `do end` block is both unless it is a stabby lambda's own body (`not_a_lambda_body`, #465, #1257 — both spellings, which the plan review's reading of the issue text had narrowed to one); a `module` opens `Namespace`; an `@ivar` / `@@cvar` is a member only as an assignment target. Consumers: the checkers and getters read the rows (Ruby's `is_func_space_with_code` override folds into the row's gate); the Kotlin and Groovy cognitive boundaries read `owns` / `OWNERS` — every opener, accessors and static blocks included — and Ruby's `is_callable` / `CALLABLE`; the three lambda arms read `closure::owns`; the attribute walks read `member::owns` (Ruby's under the statement's chain, which is all the target gate reads); Ruby's method walk reads `is_callable` with the singleton bucket as its own. Doc-attachment, naming, bucket and binding-shape sites take exemptions. `roles/space_tree_tests.rs` pins the three: Kotlin's two `class_declaration` labels by text, Ruby's four block forms and four variable positions by text, Groovy by the kind table; the walk-level parity table gains the three. Zero drift against the base binary (3 fixture files each, 0 of 72 outputs); recovery sweep 12,402 outputs, 0 differing, 0 failed. Perturbations, each failing at least one named test: Kotlin's getter losing `opens` 2, lambda gaining a space 6, `class_or_interface` always `Class` 10, constructor losing `callable` 3, `Member` emptied 13; Groovy's static block dropped 2, trait opening `Class` 1, `Member` emptied 12; Ruby's `do_block` losing its gate 5, module opening `Class` 3, the target gate dropped 1, `def self.` losing `callable` 9; each consumer severed 1–37. Census −12, coverage −36, self-scan baseline refreshed.
…ir's space trees and members by role The seventh R06 slice (#1696), the last two languages. Python's proof rows gain `callable` on the `def` and a `Member` row: an `attribute` owned where it is a bound `self.x` / `cls.x` — the target of an assignment, flat or destructuring — through a text gate that reads the receiver bytes and climbs the pattern wrappers to the assignment; the receiver reader moves to `lang_helpers/python.rs`, where the row and the attribute count share it. Elixir's rows gain `callable` on the method-macro `Call` and a `Member` row for `defstruct`; both space gates read one `elixir_declaration` of the `call`'s target, and the walk's combined promotion reads that once directly rather than each row in turn (#310), which `promotion_reads_each_calls_keyword_once` pins through a counter — one keyword read per `call` plus one `quote` climb per `def`, where the two rows read in turn would be two. Consumers: the checkers and getters read the rows (Elixir's byte-less spellings stay the kind table, as #275 documents); Python's cognitive boundary and lambda arms, method count and attribute walk read the rows, the walk carrying the chain down through the pattern wrappers; Elixir's attribute walk reads `Member`. Two consumers deliberately do not read the row: Elixir's cognitive boundary resets on the keyword wherever it sits, a `quote`d `def` included (the row declines one, #310), and its method count counts a quoted `defmodule`'s `def`s as `elixir_npm_counts_a_quoted_defmodule_as_a_class` requires. Each says so at the site. Scope-boundary and bucket sites take exemptions. The rows-vs-classifier comparisons in `roles/python.rs` and `roles/elixir.rs` are replaced by independent pins — Python's eight attribute positions by text (a bound target in both unpacking shapes; an augmented assignment, a call argument, a foreign receiver and an assignment's *value* are not), Elixir's nine calls by target. The walk-level parity table gains both. Zero drift against the base binary: Python 75 files, Elixir fixtures, 0 of 624 outputs; recovery sweep 7,902 outputs, 0 differing. Perturbations, each failing at least one named test: Python's `def` losing `callable` 21, a lambda opening a space 7, the member gate dropped 1, `cls` dropped 2, the target test accepting a value 1 (the value position was added to the fixture to make it so); Elixir's method losing `callable` 9, the `quote` gate dropped 7, the member gate dropped 3, the promotion reading both rows 1 (the counter); each consumer severed 2–13, Python's lambda channel gaining `python_lambda_contributes_lambda_nesting` as Go's, PHP's and C#'s did. Census −2, coverage −1, bypass 60 → 40, self-scan baseline refreshed.
…ware hook Closes the #1599 half of R06 (#1696). Halstead's key selection read the byte-less `Checker::is_primitive`, which Groovy and Objective-C could only answer by kind — `true` for every name — because their primitives (and ObjC's property-attribute words) are told by text. That was right only under a precondition no second consumer could see. `Checker::keys_operator_by_text(node, code, ancestors)` is the question Halstead actually asks; its default is `is_primitive`, which is the whole answer for the kind-only languages, and Groovy and ObjC override it to read the `Primitive` row's text gate — plus, for ObjC, `nullable` and the `Modifier` row's gated `identifier` (#1579). Their `is_primitive` now answers its own question from the row's kinds. Also here, the rest of R06's acceptance: the book's list of which languages' closures open a space is read from the rows by `closures_that_open_a_space_are_what_the_book_says` and gains Groovy, Mozcpp and Objective-C, whose closures open none; the CHANGELOG entry for the package. Zero drift: every population, 0 of 26,992 outputs. Perturbations, each failing at least one named test: Halstead reading `is_primitive` again 4, Groovy's hook reading the kind table 1, ObjC's hook dropping the attribute words 4, dropping `nullable` 2, keying every identifier 1. `make chain-audit` passes, `make bench-scaling` 86 of 86 probes within bound, single-feature builds for rust / go / elixir / tcl / kotlin green in both crates, `check-test-lang-gates-compare` against origin/main loses no build.
Four findings on the whole package (#1696), each verified and fixed: - The JS binding walk's arrow form evaluated its `property_identifier` sibling scan before the ancestor test, where the macro it replaced took the scan lazily as an `$extra:expr`. An array of 4,000 arrows went from 0.02 s to 4 s (exponent 2.00). `bound` now takes the extra as a closure, and `nom/wide-arrow-array` pins the shape: 0.96 on the fix, 2.00 on the regressed form, both measured. - The byte-less `is_func_space` / `get_space_kind` forwarded with `Ancestors::unknown()`, which climbs through `Node::parent` — so Ruby's answered the gated result for a stabby lambda's body block where the old kind table said `(true, Function)`. All 42 byte-less wrappers forward with a known-empty chain instead, the node read as a root; `ruby_byte_less_spellings_stay_the_kind_table` pins both spellings, and the comments say why. No supported output reads the byte-less pair, so the oracles could not see it. - Ruby's visibility-wrapped `def` (`private def x; end`) matched its kinds under a bucket exemption and so declared a method without reading the `FuncOpener` row; it reads `is_callable` first now, and the kind decides only the singleton bucket. Dropping `callable` from `Method` fails `ruby_visibility_call_wrapping_def_counts_the_method` where it did not before. - The CHANGELOG and the #1599 test's doc claimed Groovy's byte-less `is_primitive` no longer answered every name; it does, by necessity, and the test pins that. Both now say so. Also: six comment lines over the width rustfmt does not wrap, split. Zero drift re-measured on the final tree: 0 of 26,992 outputs.
… role R08 (#1698). `Call` and `StringLit` rows in all twenty-five languages, read by every consumer that restated them: - `Checker::is_call` / `is_string` and their `_with_code` spellings come from one `impl_call_row!` / `impl_string_lit_row!` pair per checker; the `_with_code` forms are the rows under their gates, now `gated_by` entries — Perl's `bareword_is_call` on the bareword call, the Tcl family's `is_braced_literal` on `braced_word`, emitted per dialect by `tcl_family_string_gates!` over its `BracedWordKinds` — and the byte-less forms are the row's kind table through a new `lists_kind`, since a known-empty chain would read a Tcl body as a script. `impl_simple_is_string!` stays for the two helper grammars whose rows are `absent`; `impl_js_family_is_string!` and Perl's and the Tcl family's overrides are gone. - Every getter's interpolation set is the row's `wraps` list; Bash's expansion read, PHP's direct-child set and Kotlin's long form read it too (Kotlin's short `$name` has no node and stays a byte read). - The dump's string flattening reads the row's owners through a shared `Alterator::flatten_verbatim`; the non-string flattening — char and rune literals, Groovy's fragments, PHP's `string` keyword alias, with its #1474 / grammar-dispatch §6 rationale — stays each language's own arm, and the Tcl family's `keeps_children` veto is untouched. - The ABC branch count reads `call::owns_kind` in every language, keeping object construction as the walker's own; the exit-name, symbol-name and operand-classification sites take `role-bypass: allow` exemptions naming the role. Java's and Elixir's cognitive boundary and lambda arms read their rows as the other languages' do. The rows record what the pinned grammars never emit, as defensive owners the tests hold unseen: C's, C++'s, Mozcpp's and ObjC's unsuffixed `CallExpression` (#1254), TypeScript's and TSX's three `call_expression` aliases (probed over a decorator call, a `typeof f()` type and a generic call), C#'s first two `invocation_expression` ids (probed over bare, member, generic and nullsafe calls in a class, struct, record, interface and field initialiser), Ruby's `Call` / `Call2` / `Call4` (probed over ten call spellings), the JS family's named `String` (every literal is the `String2` alias) and Bash's three heredoc aliases. `roles/call_string_tests.rs` pins each language's calls and literals by text — Perl's hash keys and nested callees, the Tcl family's script bodies against braced values, PHP's `string` keyword and `(string)` cast, Bash's `$"…"` in both positions. The scripted pass matched twenty-two `alterate` string arms and missed C's — the one impl whose arm carried a trailing "C has no raw string literals" line — so the C dump restated the kinds until the issue's own checklist caught it: the arm's answer was unchanged and the bypass gate reads a `<Lang>::Kind` arm it has already baselined elsewhere as the same site. Migrated as the others, and `c_string_and_concatenated_string_flattened` added, since the C++ tests cover the C++ arm only. Zero drift against the base binary: 25 populations, 0 of 27,024 CLI and library-export outputs differ; recovery sweep over every fixture, 129,042 outputs, 0 differing, 0 failed executions. Perturbations, each failing at least one named test: 25 row edits (a gate dropped, an alias dropped — the emitted one and a defensive one alike — a `wraps` entry dropped, the PHP keyword moved into `owns`, which fails to build) and 10 consumers severed; Elixir's lambda channel had no guarding test and gained `elixir_anonymous_function_contributes_lambda_nesting`. `make chain-audit` passes, `make bench-scaling` 87 of 87, single-feature builds green in both crates, `check-test-lang-gates-compare` loses no build. Bypass baseline 40 → 10 (Elixir's documented byte-less kind table, #275, and the `Exit` consumer, R07), census −40, coverage −140, self-scan baseline refreshed.
…over R06/R08 After #1696 and #1698 closed, the branch went through a simplify-rust pass (three agents: reuse, clarity, efficiency), an audit-tests pass (two agents, every finding perturbation-verified), a rust-optimize pass (clippy pedantic clean; the catalog alone), and then five parallel reviews — the `review` checklist, an algorithmic-performance and recursion audit, a least-reviewed-code audit, an acceptance-criteria audit against #1696, #1698, #1599 and #1690, and astra-review, which re-reviewed the fixes twice. This is all of it, as one change; the branch's contract held throughout: 0 differing of 27,024 corpus records and 0 of 129,042 recovery records against `7425561f`, verified after each pass. DSL (`roles/mod.rs`) - `is_callable_kind(kind, …)` beside `owns_kind`, so a cognitive guard reads one `kind_id()` per node; every `cognitive/*.rs` guard takes the `_kind` form, and `js_cognitive!` lists its arrow arm first. - `CALLABLE` is an expression-built const (the `@scan` forwarding rules are expression-form, `@callable_kind` yields `Some` / `None`). - `TEXT_GATED` per row, true iff an owner is `text_gated_by`, read off the attribute list before the fold merges it with `gated_by` — the scan arms sit first and accept any gate slot, since placed after the fold they were unreachable for `(holds_payer text_gated_by f)`. The `nargs` and `nom` byte-less `is_func` / `is_closure` readers pin the rows they depend on against it, so #1696 AC 3's exemptions are enforced rather than argued; `dsl_gates` declares the word in three positions. - `utils/role_tables.py` records `opens_by` in a field of its own. Chains - `Ancestors::of_children(body)` gives a body's members their own chain (one `Vec` per body, the walk's fold cache carried). Every `npa` / `npm` site that handed the row the *body's* chain — "the row reads neither" was the only guard, and the JS / TS `is_callable` on class-body children already reached the chain-climbing `bound_*` gates that way — asks under the member's: 20 sites, the two `Ancestors::unknown()` in `npm/ruby.rs` included, and `direct_child_funcs`, which moves to `is_func_with_code`. Built inside the body arms, never per node: built before the body test it ran for every node the walk visits and a 64,000-deep expression took 3–6 s per metric (`npa/java-deep-expression` reads 0.86 now). - `npa/{rust,go}.rs` hand the row the real chain rather than `Ancestors::unknown()`. Performance, both pre-existing on paths the branch touched - `python_walk_target_elements` was recursive and overflowed the consumer thread's stack at ~8,000 nesting levels (`binds` had gone iterative in #1597); iterative now, pinned on a 512 KiB thread. - The JS arrow binding gate's `property_identifier` sibling scan ran `O(width)` per positional arrow: `run(() => 1, …)` with 4,000 arrows took 3.4 s. Memoised per parent under `MemoQuestion::NamesAnArrow`, 0.01 s; `nom/wide-arrow-array` renders that positional form (the bound `const cb = [...]` it rendered never reached the scan). Rows and classifiers - Ccomment's and Preproc's `StringLit` rows are declared, both checkers read `impl_row_predicates!` — `impl_call_row!` and `impl_string_lit_row!` merged, naming the row and its two predicates — and `impl_simple_is_string!` is gone; the census / coverage baselines drop their last `R08` cells. #1698's "all twenty-five" is true now. - `is_primitive` reads the `Primitive` row in every language. Kotlin's and C#'s still answered the Halstead key-selection question under the precondition #1599 deleted; that moves to `keys_operator_by_text` as Groovy's and ObjC's are, and C / C++ / Mozcpp / TS / TSX read `primitive::lists_kind` with their one text-keyed kind in the hook. - `elixir_declaration` returns `Option` (no `None` variant), and the getter's `get_func_space_name` reads it; `PYTHON_TARGET_PATTERN_KINDS` is the one unpacking-wrapper list, `python_self_receiver` the `Member` row's gate half, `python_unwrap_function` reads the row and `npm` shares it (§7 parity). - `alterator.rs`, `checker/groovy.rs`, `checker/objc.rs` read `lists_kind`; the 19 single-arm `alterate` impls are an `if` / `else`, dropping an unread `From<u16>` lookup per node on the dump path. - `npa/{cpp,mozcpp}.rs` and `npm/{cpp,mozcpp}.rs` are one `cpp_family_npa!` / `cpp_family_npm!` body each, as cognitive and ABC already were; `cpp_family_gates!` emits both C++ gates; `abc/{csharp,java}.rs` fold construction into the `Call` guard as the siblings do; dead `csharp_invocation_expr_kinds!` removed; five AST items narrow to `pub(crate)`. Tests - `owners_covered` requires each defensive alias to be declared (a dropped one was never visited); every fixture's negatives are pinned as rows; TS / TSX / PHP pin the `string` keyword through its named wrapper. `check_cpp` reads the `Member` row; the C# gated, Java anonymous-class and Ruby block rows carry all five answers; Elixir asserts `Call` positively; Python pins `Call` / `StringLit` by kind string, and asks the `Member` row about the *inner* `self.x` / `self.i` with their real chains. The five lambda-channel fixtures nest the closure under an `if`, so a reset of the enclosing nesting is visible. ObjC's `plain` vector pins `is_primitive` true as well as false. Three npa gaps the simplify-rust perturbations exposed have fixtures. Docs - Eleven comments naming macros this branch deleted point at the rows; the stale "#285 enumerate here AND in …" contracts are retired; grammar-dispatch §7 describes the post-#1696 byte-less pair and names Ruby; benchmarking.md lists every probe; `npa/cpp.rs` carries `FIXME(#1299)`; CHANGELOG counts are 27,024 / 129,042. Rejected, with the reviewers' agreement: collapsing the 23 byte-less forwarders or the alterator string arms into macros (per-language rationale), any `role_table!` surface change the Python reader would have to learn, naming `lang_helpers` fns directly as gate names. Gates on the final tree: `make pre-commit` pass; chain-audit pass; `make bench-scaling` 88 / 88; role-census 272 pending, role-coverage 6536, role-bypass 10 baselined sites. `.bca-baseline.toml` refreshed for the lines the pass moved.
…chain Lesson 103, from the #1696 review passes: a consumer that holds a body's `Ancestors` and asks a predicate about the body's members hands it a chain whose `last()` is the body's parent, not the member's — off by one for every member, invisible while the predicate reads no chain, and wrong on exactly the gate's inputs the day the row grows one. The twenty files of member walks, the JS / TS `is_callable` that already reached a chain-reading gate that way, `Ancestors::of_children`, and the fix's own per-node regression when the derivation sat above the body test. Two one-clause sub-examples where the mechanism already had a home: lesson 85 (a test named for a wrapper-kind check its fixture never reaches), lesson 99 (a probe on the right axis but the bound spelling that never reaches the scan). The index gains 102's missing row. AGENTS.md records the standing obligation the lost-fixture incident produced: oracle evidence lives under a durable path, cited in the claim. The `@scan` comment in `roles/mod.rs` says why a terminal that reads a word the fold consumes must sit above the fold's rules. The `review` skill read the draft against the tree and the issue record; three factual overstatements and one over-length entry were corrected before this commit.
dekobon
force-pushed
the
refactor/1696-1698-recognition-roles
branch
from
October 26, 2026 05:21
799a62f to
5564fd9
Compare
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1717 +/- ##
==========================================
+ Coverage 98.28% 98.35% +0.06%
==========================================
Files 386 385 -1
Lines 111166 112077 +911
Branches 110731 111642 +911
==========================================
+ Hits 109264 110236 +972
+ Misses 1247 1187 -60
+ Partials 655 654 -1
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
…uilders Codecov read the patch at 94.7 % with 107 lines unexecuted. Every line was one of four shapes, none a defect: - The byte-less `is_func_space` / `get_space_kind` forwarders in 23 checkers and 19 getters: nothing on a supported output path reads the pair, and only Ruby's was pinned. Every space-tree test now asserts the byte-less pair equals the `_with_code` pair on every node of its fixture — `check` and `check_cpp` for the sixteen languages there, the Java and Python classifier tests, the four JS-family binding tests — which is the row's own claim: owners that read no chain and no bytes answer alike either way. - The byte-less `is_primitive` that now reads `primitive::lists_kind` in C, C++, Mozcpp, C#, TypeScript and TSX: Halstead reads `keys_operator_by_text` since #1599, so nothing called it. `byte_less_is_primitive_is_the_rows_kind_table` asks it of every node of a per-language fixture and requires every owner met except the named hidden / alias kinds (C's `type_specifier` supertype, ObjC's second `_Complex`, Rust's sixteen unspelled aliases). - `count_some` / `some_kinds`, which build `CALLABLE` at compile time and so execute no instrumented line: a run-time test over the five list shapes. An absent row's `lists_kind`: one test. - Single lines no fixture reached: Kotlin's byte-less string arm (the `_with_code` one answers first on the walk), the Python `Member` gate's climb running out on a `for self.i in …` target, and an anonymous C++ struct's field list, whose fields land on the unit. Three `return`s stay unexecuted and now say why: a `field_declaration_list` and a `body_statement` are never the root, and `ruby_visibility_effect` yields `Arguments` only for a call that has a list — each guard keeps its function total rather than trusting the caller.
Open
7 of 11 tasks
12 tasks
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1696, closes #1698, closes #1599. Part of #1690.
What
Every language's space tree — which nodes open a
FuncSpace(FuncOpener,Container), which are closures (Closure) and which are members (Member) — and its call and string-literal kinds (Call,StringLit) are now declaredrole_table!rows inbig-code-analysis-ast/src/roles/, and every consumer that restated them reads the row instead:Checker::is_func/is_closure/is_func_space/is_call/is_stringin both spellings,Getter::get_space_kind, the walk's promotion, the cognitive boundary and lambda arms, thenpm/npamember walks, ABC's branch count,find/count, every getter's interpolation set and the dump's string flattening. No cell of the six roles ispendinganywhere; the bypass baseline falls 84 → 10 (Elixir's documented byte-less kind table, #275, and theExitrow's consumer, R07).Recognising a closure and opening a space stay two declarations: a
FuncOpenerowner iscallable(a Javastatic { }or a Kotlin accessor opens a space and is not one),callable_when f(a JS function expression is a function only where bound), andopens Koropens_by f(Kotlin'sclass_declarationspells two containers). Each row'sCALLABLElists the marked kinds gates aside, which is why C#'s cognitive stop list stays its own. AMemberrow recognises the declaration and leaves which body, what visibility and how many names to the consumer.#1599's Halstead text-key selection moves off the byte-less
is_primitiveontoChecker::keys_operator_by_text(node, code, ancestors): thePrimitiverow's text gate for Groovy, the text-keyed modifier words for Objective-C, C#'s modifiers and Kotlin'sreifiedin their own overrides.is_primitivereads the row's kind table everywhere.DSL
role_table!gainedcallable/callable_when f/opens_by f, theCALLABLEconst,is_callable/is_callable_kind,lists_kind(the one byte-less emission, the kind table gates aside),wraps/excludesonStringLit, andTEXT_GATEDper row — true iff an owner reads the bytes, which the byte-lessnargs/nomreaders pin the rows they depend on against.Ancestors::of_children(body)gives a body's members their own chain (one allocation per body) so a gate a row grows later reads the right parent; the lastAncestors::unknown()on the member walks is gone.utils/role_tables.pyreads every new form; thedsl_gatesproof table and the census self-tests pin them.Zero drift
bca ops, every metric, the space tree,dump,findandcountare byte-identical to the base (7425561f, the merge-base;mainhas since moved by one docs-only commit):One drift the sweep found in flight — C#'s bodied property read as a function boundary — was fixed before landing.
src/spaces/opens_space_parity_tests.rsholds the real walk's promotions to the nodesopens_spacelabels in every language;roles/space_tree_tests.rsandroles/call_string_tests.rspin each language's answers independently of the classifiers that now read the rows.Two pre-existing defects on paths the rows became the reader of are fixed: the Python attribute walk was recursive and overflowed the consumer thread's stack at ~8,000 nesting levels (iterative now), and the JS arrow binding gate's sibling scan was
O(width)per positional arrow (4,000 arrows: 3.4 s → 0.01 s, memoised per parent).make bench-scalingis 88/88 with probes for both.Process
After the three issues closed, the branch went through
simplify-rust,audit-tests,rust-optimize, five parallel reviews (thereviewchecklist, performance / recursion, least-reviewed code, acceptance criteria against the four issues,astra-review) and twoastra-reviewrounds on the fixes. The findings are one commit whose message records each pass; the acceptance audit's corrections to the issue bodies are applied, and lesson 103 (a chain is the queried node's own, or it is the wrong chain) records the one new mechanism the reviews found. Lesson 101's shape (a chain rebuilt withknowndropping the walk's cache) is whatof_childrenis built to avoid.The evidence — fixtures, base and HEAD outputs and binaries, drivers, logs — is at
~/bca-evidence/1696/on the author's host (6.9 G) and summarised on #1696.Rebased onto
main's merge-base;make pre-commitpasses;make chain-auditpasses;check-test-lang-gates-compareagainstmainreports no test lost a build.