Repository navigation
refactor(roles): declare every paying construct as a role row in every language - #1708
Merged
Merged
Conversation
…ays lists The root crate's metric dispatchers become consumers of the Comparison and ShortCircuit rows in #1694, so everything a table emits is now `pub` (the AST crate is outside STABILITY.md). A `pays [..]` list walks the same continuation-passing splice an `owns` list does, so a shared payer fragment spells its gate inline (`LT gated_by applied,`) and the gate resolves at the splicing row; `utils/role_tables.py` expands the same fragments and the differential test holds the two readers together. `Getter::get_paying_role` is the Halstead hook for the paying-construct rows, read between `get_keyword_role` and the byte-less `get_op_type` on every path the walk takes (`paying_roles!` emits it from a language's two rows). It defaults to `Unknown` while the rows land one package at a time. The bypass gate learns that a macro body which only hands `kind_id()` to a row restates no membership. `cpp_operator_is_applied` moves to `roles/c_family.rs` as `cpp_family_operator_is_applied`, where the C++ dialects' rows gate on it; the root re-exports it for the consumers that have not migrated. `impl_cyclomatic_java_like_rows!` reads a `ShortCircuit` row in place of the `&&` / `||` kind list; the list form stays for Groovy until #1695. Refs #1694
…with every consumer Java's `Comparison` row gains the `instanceof` keyword as its payer — the grammar spells it in no other production, so one pays per test, plain or pattern form — and `InstanceofExpression` becomes an ordinary owner. `ShortCircuit` is declared: `binary_expression` gated on its `operator` field, `&&` / `||` paying ungated, projected through the field. Every consumer reads the rows: ABC's token arm reads `comparison::pays_at_leaf`, its `scores_itself` reads both rows' `owns`, its chain walker reads `short_circuit::pays_at_leaf`; cyclomatic reads the short-circuit leaf through `impl_cyclomatic_java_like_rows!`; cognitive's boolean-sequence arm is guarded by `short_circuit::owns` and keys on the row's `PAYERS`; the getter bills the leaf projection through `paying_roles!(java)`, keeping only the generic-bracket `<` / `>` — the row's gated-out spelling — in the byte-less arm under a `role-bypass: allow(Comparison)`. The C-family ABC `scores_itself` closures now capture `code`, so `count_negated_operand` takes `impl Fn`. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on 12 Java corpus files plus a fixture spelling every row kind, and on the Python and C++ populations the change does not touch (73 / 1048 / 1048 for Mozcpp). Bypass baseline −24 (every Java comparison / short-circuit site), census −1, coverage −3 (`&&`, `||`, `instanceof`). Perturbations, observed failures: - drop `LT` from the payers: role_rows_tests, java_assignments_with_conditions, java_generic_angles_are_one_pair_1559, comparison_row_separates_the_owner_from_its_token - `project` returns `None`: 22 tests across abc, cognitive, roles - `BinaryExpression` listed as a payer: rejected at expansion by the owner/payer disjointness assertion - drop `LTEQ`: java_comparison_and_short_circuit_spellings_bill_1694 Refs #1694
- `project` falls back to the owner's first declared-payer child when the grammar gives the owner no projection field: Java's `instanceof_expression` carries its keyword as a bare child, so the field-only projection answered `None` for a declared owner. The test now projects every owner of a fixture spelling both type-test forms. - Java's byte-less `LT | GT` arm is unconditional again: a token error recovery reparents under `ERROR` (`return a < ;`) is under no `binary_expression`, so the row's gate leaves it out, and it billed before the row existed. The arm's exemption says which spellings it covers and that the Halstead load-bearing evidence is the other seven payers; a recovery fixture pins the classification. - The bypass gate keeps reporting a macro that reads a row *and* still matches on the kind — a row mention no longer launders a `match`. Paired self-tests for the forwarding and the mixed shape. - The census self-tests cover a payer fragment with inline gates, so reverting the Python reader's `pays` expansion fails by name. - Two comments that described the C++ rows as already reading the moved gate, and the module doc's `pays_itself` example, now say what holds. Refs #1694
…ry consumer through its rows Python's `ShortCircuit` row is declared — `boolean_operator` owns the chain, `and` / `or` pay ungated — beside the `Comparison` row R01 proved. The row is the owner-only-for-one-consumer case of the pilot: ABC reads `comparison::pays_at_wrapper` (one condition per `comparison_operator`, however many operators it chains) and reads `short_circuit::pays_at_leaf` only to find the chain whose operand slots pay, never adding for the token (#395); cyclomatic counts that same token; cognitive's boolean-sequence arm and its outermost-chain test read `short_circuit::owns`; the getter bills each leaf through `paying_roles!(python)`, keeping the loop `in` — the row's gated-out spelling — in the byte-less arm under a `role-bypass: allow`. `Node::count_specific_ancestors` takes `impl Fn` for `check`, so the cognitive ancestor test can capture `code`; the method is outside STABILITY.md's covered set. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on 73 Python corpus files plus a fixture spelling every row kind (and still on the 12 Java / 1048 Cpp / 1048 Mozcpp populations). Bypass baseline −15 (every Python comparison / short-circuit site), census −1, coverage −3 (`and`, `or`, `boolean_operator`). Perturbations, observed failures: - drop `In` from the payers: bare_in_and_is_pay_only_under_the_wrapper - drop `LTEQ`: python_comparison_and_short_circuit_spellings_bill_1694 - drop `Or`: 13 tests across abc (role_rows, slot_pays, three behaviour tests), cognitive (seven), roles and halstead — the three root consumers and Halstead all read the one row Refs #1694
…ortCircuit through shared fragments Cpp and Mozcpp declare both rows by splicing the `cpp_family_*` fragments of `roles/c_family.rs` (contract 5 of #1690): the owners are the aliased `binary_expression` ids and the fold, each gated on its `operator` field holding one of the row's payers; the payers are the eight comparison tokens (`<=>` and `not_eq` included) and `&&` / `||` / `and` / `or`, each gated by `applied` — the parent-only test that was `cpp_operator_is_applied`, now `cpp_family_operator_is_applied` beside the rows. The gates are emitted per dialect by `cpp_family_gates!`, since each reads its dialect's ids. The fold and the ISO alternative spellings are the fold and suppression cases of the pilot; `ConstraintReach` is untouched and the rows are read under it, with the comment at the call site saying why a parenthesised constraint cannot be the row's business. Every consumer reads the rows in both dialects: ABC's token arm and chain walker, its `scores_itself` (the shared C-family slot helpers now take a `ScoresItself` parameter — Cpp and Mozcpp pass row-keyed closures, C and Objective-C the name-keyed one until #1695); cyclomatic through `impl_cyclomatic_c_family_rows!`, which replaces the `applied_if` escape; cognitive's keyed boolean sequence, guarded by `short_circuit::owns` with the `and` → `&&` key beside the table; Halstead through `paying_roles!`, the non-applying spellings staying in the byte-less arm under a `role-bypass: allow`. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on 1048 C++ corpus files plus a fixture spelling every row kind, run as Cpp and again as Mozcpp, and on the 12 Java and 73 Python populations. Bypass baseline unchanged (the C++ sites were name-keyed and invisible to it), census −4, coverage −30 (15 ids per dialect). `tests/parity/cpp_mozcpp_parity.rs` gains a row fixture with its expected values written out; `role_rows_tests` runs the same rows on both dialects. Perturbations (both dialects), observed failures: - drop `LTEQGT` from the comparison payers: 7 (role_rows ×2, three abc tests, two parity tests) - drop `And` from the short-circuit payers: 15 across abc, cognitive, cyclomatic, the alternative-token suite and parity - `applied` answering true for every parent: 17 across abc, cyclomatic, halstead, ops and parity - `FoldExpression` dropped from the ShortCircuit owners: 5 (slot_pays ×2, cognitive fold ×2, parity) Refs #1694
…agments The TypeScript and TSX `Modifier` and `DeclKeyword` rows were byte-level copies (contract 5 of #1690 assigned the fold to R04). Each now splices `..ts_family_modifiers!()` / `..ts_family_decl_keywords!()` from `roles/js_family.rs` beside the JS-family fragments; the `type` gate inside the fragment resolves at each dialect's row, as the C++ `applied` gate does. The derivations stay on the TypeScript rows and TSX's own `String3` exclusion stays on its `Primitive` row. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on the six TS/TSX corpus files, each run as both dialects (24 outputs). Refs #1694
- Java's `instanceof` payer is gated to its `instanceof_expression`. Error recovery keeps a keyword whose expression it lost (`return x instanceof` at end of input) as a bare child of an `ERROR`, where the ungated payer scored an ABC condition the expression arm it replaced never did — the one drift the corpus oracle could not see. Halstead keeps billing the stranded keyword through the byte-less arm, as it did before the row. A truncated-input row in `role_rows_tests` and a recovery line in the Halstead spelling test pin both; a sweep of every fixture truncated at each line boundary and sixteen byte offsets (780 outputs, both binaries) differs nowhere. - The bypass gate's forwarding-macro exemption is positive: the body binds the kind once and every later mention is the first argument of a `…_kind(` call. A cached-kind comparison beside a row read is reported again, with a self-test. - The Java and Python Halstead spelling tests say which entries pin the row and which pin the vocabulary the byte-less arm keeps. Refs #1694
…d ShortCircuit rows The two C-family dialects declare both rows through shared `c_family_*` fragments (`roles/c_family.rs`): the owners are both aliased `binary_expression` ids gated on their `operator` field, the comparison payers `<=` `>=` `==` `!=` ungated (each grammar emits them only from a `binary_expression` or the `preproc_binary_expression` aliased onto it) and `<` / `>` gated to that parent — Objective-C spells them bare in `generic_specifier`, `protocol_reference_list`, `parameterized_arguments` and `argument_list` too — and `&&` / `||` ungated. `c_family_gates!` emits the three gates per dialect. Every consumer reads the rows: ABC's token arm, chain walker and a row-keyed `scores_itself` (the name-keyed `c_family_condition_scores_itself` is gone — all four C-family dialects now pass row-keyed closures to the shared slot helpers); the hand-written cyclomatic impls; cognitive's boolean-sequence arm, guarded by `short_circuit::owns`; Halstead through `paying_roles!`, with `<` / `>` (and ObjC's `LT2`) kept in the byte-less arm for the gated-out spellings under a `role-bypass: allow`. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on the C and Objective-C fixtures spelling every row kind (no corpus file routes to either), on a 100-output recovery-truncation sweep of both, and still on the 1048 C++ corpus files run as both C++ dialects. Census −4, coverage −20 (10 ids per dialect), bypass unchanged (the sites were name-keyed). Dropping `GTEQ` from the shared comparison fragment fails four tests (role_rows and the Halstead spelling test, per dialect). Refs #1695
…nd ShortCircuit rows Three grammars with one `binary_expression` kind told apart by its `operator` field: each row gates the owner on the field and `<` / `>` on the parent (Rust's generic lists and turbofish, Lua's `<const>` attribute — #1297 — and every grammar's error recovery spell the same tokens). Lua's inequality is `~=` and its chain operators the words `and` / `or`. Rust's `ShortCircuit` row also owns the Rust 2024 let-chain (`LetChain`, and the hidden `_let_chain` supertype), whose `&&` tokens are its direct children with no field to project through — the row's payer fallback lands on the first, which is the one the ABC walker reads the chain from (#396). Rust's `||` pays ungated: an empty closure's bars are two `|` tokens, and the only other spelling is a bare token in a macro's `token_tree`, which the counting consumers always paid and Halstead never billed. That split lives in `RustCode::get_paying_role`, a hand-written reading of the row that keeps the Halstead policy where the macro cannot express it. Every consumer reads the rows in all three languages: ABC's token arm and chain walker (Go's walker is the old terminal-kind design and asks the rows nothing else; Rust's and Lua's `scores_itself` read both owners), the hand-written cyclomatic impls, cognitive's boolean-sequence arm, and Halstead through `paying_roles!` (Go, Lua) with `<` / `>` kept in each byte-less arm for the gated-out spellings. One drift the first cut had and the Rust corpus caught: `&&` inside a macro's `token_tree` reaches the chain walker with a parent that owns no chain, and the walker now asks the row before reading operands (two serde files moved; a `m!(a && b)` row pins it, failing 0 → 100 without the guard). Zero drift after it: `bca ops` and `bca metrics` JSON byte-identical on 173 Rust corpus files and the Go / Lua fixtures spelling every row kind. Census −6, coverage −29. Dropping any payer fails that language's Halstead spelling test and `role_rows_tests`. Refs #1695
…ortCircuit rows The Tcl family's `binop_expr` names no operator field: its operator is one token among its children. So each row's owner is `BinopExpr` gated on holding one of the row's payers, and the row's projection — the first child whose kind is a declared payer — is the lookup, the shape the `project` fallback was added for in R04. `roles/tcl_family.rs` holds the shared payer fragments (the six symbols, `eq` / `ne`, `in` / `ni`, `&&` / `||`) and the per-dialect gates; iRules adds its string and glob comparators (`starts_with`, `ends_with`, `contains`, `equals`, `matches`, `matches_regex`, `matches_glob`) and the word forms `and` / `or` beside them. Every consumer reads the rows in both dialects: ABC's token arm, chain walker and `scores_itself`; the hand-written cyclomatic impls (iRules ahead of its switch-arm count); cognitive's boolean-sequence arm, Tcl keyed on the token and iRules folding `and` → `&&` beside the row; and Halstead through `paying_roles!`, reached through the braced-word revision both dialects route the walk through, so every arm the two getters carried for these kinds is gone. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on the Tcl and iRules fixtures spelling every row kind (no corpus file routes to either). Census −4, coverage −35. Dropping `In` from the shared comparison fragment fails the Halstead spelling test and the ABC row test of each dialect. The baseline refresh records the one cognitive dispatch that grew by its guard arm and drops the iRules getter effort entry the row's arms took below its limit. Refs #1695
…tCircuit rows JavaScript, MozJS, TypeScript and TSX declare both rows through shared `js_family_*` fragments (`roles/js_family.rs`): the owner is the one `binary_expression` kind gated on its `operator` field; the comparison payers are the four equalities, `<=` / `>=`, the `instanceof` keyword, and `<` / `>` gated to that parent — the TypeScript grammars spell them as generic brackets and every one of the four as JSX tag delimiters (#1297); `&&` / `||` pay ungated. `in` is left unclaimed: no counting consumer scores it (the slot holding one pays), and claiming it would move output. `??` and the compound `&&=` / `||=` / `??=` keep their consumer arms until the `NullSafe` row. Every consumer reads the rows in all four grammars: the two ABC macros through a per-grammar row-keyed `scores_itself` (the shared slot helpers take it as a parameter, as the C family's do), `js_cognitive!` on an owner guard, `impl_cyclomatic_js_family!` — now its own body, since the kind-list `impl_cyclomatic_c_family!` has no user left and is gone — and Halstead through `paying_roles!` inside `impl_js_family_get_op_type!`, with `<` / `>` kept in the byte-less arm for the gated-out spellings. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on 389 JavaScript corpus files run as both JS dialects, 8 TypeScript and the TSX fixture, each spelling every row kind. Census −8, coverage −48. Dropping `EQEQEQ` from the shared fragment fails the Halstead spelling test and the three ABC row tests. rustfmt-bail −4 (the deleted macro). Refs #1695
…t rows The worked example of a node / token pairing. A `binary_expression` owns a comparison through its `operator` field, `<` / `>` gated to that parent (the grammar also spells them as generic brackets, in four productions). The dekobon grammar gives Groovy's own comparisons nodes of their own, each wrapping its token: `spaceship_expression` / `<=>`, `identity_expression` / `===` `!==`, `regex_find_expression` / `=~`, `regex_match_expression` / `==~`, `membership_expression` / `in` (gated to that parent: the same keyword opens a `for (x in xs)` header) and `instanceof_expression` / `instanceof`. The negated `a !in b` and `o !instanceof T` emit no token at all, so the row needs an owner that pays for itself in one spelling and through its token in the other. `pays_itself_when f` is the one representation extension this package adds to `role_table!`: the owner pays at the leaf projection under its gate and `f`, which here is "holds no keyword token" — `project(node).is_none()`. The Python reader accepts the attribute; the differential test holds the two readers together; a row test pins that `a in b` pays at `in`, `a !in b` at the node, and a `for` header's `in` nowhere. Every consumer reads the rows: ABC's token arm, chain walker and `scores_itself`; cyclomatic through `impl_cyclomatic_java_like_rows!` (the kind-list `impl_cyclomatic_java_like!` has no user left and is gone); cognitive's boolean-sequence arm; Halstead through `paying_roles!`, with `<` / `>` / `in` kept in the byte-less arm for the gated-out spellings. The elvis `?:` and the safe-navigation forms keep their consumer arms until the `NullSafe` row. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on the Groovy fixture spelling every row kind (no corpus file routes to Groovy). Census −2, coverage −22, rustfmt-bail −3 (the deleted macro). Making the membership owner pay unconditionally fails six tests (the row test, three ABC tests, two production-construct sweeps). Refs #1695
…ortCircuit rows Kotlin: a `binary_expression` owns a comparison through its `operator` field, `<` / `>` gated to that parent (tree-sitter-kotlin-ng spells them in `super<A>`, `type_arguments` and `type_parameters` too, and misparses a generic call as nested comparisons — #1297, #1394); the type test `is_expression` and membership test `in_expression` own theirs through their keyword, each a token of its own in both the positive and negated spelling (`is` / `!is`, `in` / `!in`) and gated to the expression, since a `when` entry's `type_test` / `range_test` and a `for` header spell the same keywords and the entry pays there. C#: both `binary_expression` aliases own a comparison through the field, every comparison token gated to that parent — the grammar sweep (six other productions, from the generic brackets to an `operator_declaration`, #1420, and a `relational_pattern`, #1383) and the fail-closed polarity move onto the row's gate — and the two type tests `is_expression` / `is_pattern_expression` own theirs through the one `is` keyword each wraps. Every consumer reads the rows in both languages: ABC's token arm, chain walker and `scores_itself`; the hand-written cyclomatic impls; cognitive's boolean-sequence arm (the elvis `?:` and `??` keep their place in the guard and the key under an exemption naming the `NullSafe` row that takes them over); Halstead through `paying_roles!`, with the gated-out spellings kept in the byte-less arms. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on 22 C# corpus files and the Kotlin fixture spelling every row kind. Census −4, coverage −30. Ungating Kotlin's `is` fails the row test (a `when` entry's `is` would pay twice) and `kotlin_subjectful_when_arms_keep_the_per_entry_count`; dropping C#'s `Is` payer fails ten tests across abc, slot_pays and halstead. The baseline refresh drops Kotlin's ABC cyclomatic entry (the dispatch now carries the standard suppression) and records the C# cognitive dispatch's effort, 0.1 over its limit by the guard arm every sibling now has. Refs #1695
…rtCircuit rows PHP: the one `binary_expression` kind carries every binary operator, `instanceof` included; the row's owner is gated on its `operator` field and the payers are the four equalities, `<>`, the orderings, the spaceship and the `instanceof` keyword, `<` / `>` gated to the parent for a recovered token. The short-circuit payers are the punctuation `&&` / `||` and the low-precedence `and` / `or`; `xor` evaluates both operands and stays with `EagerLogical` (#1536), `??` with `NullSafe`. Ruby: the `binary` node — three aliased ids — owns through the field, and each of the ten comparison tokens (`==` `!=` `===`, the orderings, `<=>`, `=~` / `!~`) is gated to a `binary` parent, since every one doubles as an operator method's name and `<` as a superclass clause (#1280). `&&` / `||` / `and` / `or` pay ungated; the chain walker still asks the row whether the token's parent owns a chain, since error recovery can parent one under an `ERROR` (`f(&&)`). Every consumer reads the rows in both languages: ABC's token arm, chain walker and `scores_itself`; the hand-written cyclomatic impls; cognitive's boolean-sequence arm (`??` keeps its place in PHP's guard and key under an exemption naming the `NullSafe` row; Ruby's key fn sits beside its row read); Halstead through `paying_roles!`, with Ruby's gated-out spellings and PHP's `<` / `>` kept in the byte-less arms. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on the PHP and Ruby fixtures spelling every row kind (no corpus file routes to either). Census −4, coverage −33. The baseline refresh records the two getters' effort falling with their arms and the PHP cognitive dispatch's guard arm. Refs #1695
…son and ShortCircuit rows The three text-gated grammars, which complete the two roles across every language. Elixir: the `binary_operator` node (three aliased ids) owns through its `operator` field, and every payer is gated on *applying* its operator under one, since a token also names the operator in a capture (`&==/2`), an atom and a `Kernel.||(a, b)` call (#1531, #1534). The remote call applies the macro too but has no `binary_operator`, so it stays the counting consumers' text-keyed extra (#1574, #1582), named beside each row read. Perl: tree-sitter-perl names no `operator` field on a comparison, so the row's projection — the field where there is one, the first payer child otherwise — is the lookup. The fourteen comparison payers are the numeric, string and spaceship / `cmp` forms plus the bindings `=~` / `!~`; a binding that rewrites (`$x =~ s///`) is an edit the consumer declines as its own extra (#1540). `<` / `>` are gated to a `binary_expression`, a readline's brackets being the same tokens. The chain row owns the `unary_expression` the grammar builds for `and`, with `and` gated by `applied_and` so an auto-quoted key pays nothing (#1539); `//` stays a consumer arm until `NullSafe`, `xor` is `EagerLogical`'s. Bash: the symbols inside `[[ … ]]` / `(( … ))` are owned by every `binary_expression` alias, `<` / `>` gated to that parent (a redirection spells them, #1280); inside `[ … ]` the comparison is a `test_operator`, one kind the grammar shares with the `-a` / `-o` connectives, so that payer is `text_gated_by` — a new payer keyword that expands as `gated_by` and documents the byte read at the row, accepted by the Python reader. The chain row owns the command `list` and the test `binary_expression`s, with `-a` / `-o` as the text-gated test spelling. ABC reads that row for nothing (Fitzpatrick Rule 9 does not map onto a command-list separator, #1536), and `BashCode::get_paying_role` keeps a `test_operator` billing nothing, as the getter always had. Every consumer reads the rows in all three: ABC's token arm, chain walker and `scores_itself`; the hand-written cyclomatic impls; cognitive's boolean-sequence arm (Bash's comparison-run bookkeeping still fires on every `binary_expression`, with the row read inside); Halstead through `paying_roles!` (Elixir, Perl) or the hand-written reading (Bash), with the gated-out spellings kept in the byte-less arms. `cognitive/bash.rs` names `Bash::UnaryExpression` by id rather than by string, since the bypass gate now resolves that string to a claimed kind. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on 144 Bash corpus files and the Elixir / Perl fixtures spelling every row kind. Census −6 (no `Comparison` / `ShortCircuit` cell is pending anywhere), coverage −55. Dropping Elixir's `Notin` fails two tests; ungating Perl's `and` fails eight (the row test and the keyword-key suite); ungating Bash's `test_operator` fails three (the row test and the connective suite). The baseline refresh records the Bash getter's effort falling with its arms. Refs #1695
Four findings from the fresh-context review of the twenty-language rollout, each a drift the corpus oracle could not see, now measured on every population (0 of 6,640 outputs differ) and on a recovery sweep of every fixture truncated at each line and sixteen offsets (1,004 outputs, 0 differing): - `paying_roles!` billed a `pays_itself` owner as a Halstead operator: Groovy's `a !in b` and `o !instanceof T` have no token, so the counting consumers pay at the node while Halstead had nothing to bill, and the macro's leaf read put `membership_expression` into the vocabulary. The macro now bills only a kind the row lists as a payer, which is the rule for any future tokenless owner; the Groovy spelling test asserts the exact vocabulary. - Perl's rewrite exception applied to every comparison owner: `$a == s/x/y/` (valid Perl) made `scores_itself` false, so a slot holding it paid twice. `perl_binding_rewrites` now asks the row's projection whether the operator is `=~` / `!~` before reading the right operand, and the token arm reads the same predicate, so the two cannot part again. Two rows pin it, inside and outside a slot. - C#'s `is` payer is gated to its two type-test expressions, as Java's `instanceof` was in the pilot review: error recovery strands the keyword under an `ERROR` (`var x = o is` at end of input), where the ungated payer scored a condition the node arm never did. The getter's byte-less arm keeps billing the stranded keyword; a truncated-input row and a spelling row pin both. - Perl cognitive now reads the `ShortCircuit` row: the arm's guard asks `short_circuit::owns` (whose `applied_and` gate replaces the key-misparse check) or names the `//` / `op=` extras pending `NullSafe`, and the sequence key asks the row per child before folding the word forms onto their symbols. Dropping `Or` from the row now fails `perl_word_operators_continue_their_symbol_sequence`. Refs #1695
The second role of #1695: a boolean operator that evaluates both sides. Recognition only — no count and no Halstead classification moves, which is what the role exists to record beside the chain operators it must not be confused with. Three languages have one. Perl's and PHP's `xor` is a `Xor` token the grammar spells only as a `binary_expression`'s operator (a hash key or member named `xor` is a bareword / `name`), so each row owns the expression gated on its `operator` field and pays the token; the three ABC / cyclomatic / cognitive exclusions that #1536 wrote now name the row, and Halstead bills the token through `paying_roles!` — which reads the row beside `Comparison` and `ShortCircuit` — instead of each getter's byte-less arm. Kotlin's infix `and` / `or` / `xor` are functions: tree-sitter-kotlin-ng gives an `infix_expression` no field and no operator kind, its name being an `identifier` leaf, so the row is owner-only and `text_gated_by eager_infix`, which reads the second identifier child's bytes. Nothing pays: the name stays the operand it was, and a slot holding the call pays once under the ordinary slot rule, as for any call. Five languages record a recognition boundary rather than an absence: Java (JLS §15.22.2), C#, Rust, Groovy and Ruby spell a boolean `&` / `|` / `^` with the integral operator's kind, which syntax alone cannot tell apart, so each row is `owns []` with the three tokens `excludes`d and the reason. Elixir's `and` / `or` short-circuit and its `&&&` / `|||` are bitwise, so its cell is `absent`; Bash's `absent` reason now says that `-a` / `-o` are the `ShortCircuit` row's text-gated payer, not an eager form. An `absent` row now emits empty `OWNERS` / `PAYERS` constants, so a reader that asks several rows about one token can ask an absent one alike — which is how `paying_roles!` reads `EagerLogical` in the twenty-two languages without a payer. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on every population (0 of 6,638 outputs). Census −9 (no `EagerLogical` cell is pending), coverage −18. Emptying Perl's or PHP's payer list fails that language's Halstead spelling test; the Kotlin row test pins which infix calls the gate admits and that nothing pays; the ABC row tests pin each eager form in and outside a slot. The baseline refresh records the two getters' effort falling with their `Xor` arm. Refs #1695
- Kotlin's `eager_infix` gate read the second *identifier* child, so a compound left operand (`(a && b) xor a`), a nested chain's outer node and a right operand *named* `and` were all misread. It reads the second non-comment child and asks that it is an identifier; the row test pins all four shapes, and says the name is all that is read — a user `infix fun and` spells the same call. - Bash's `-a` / `-o` are arguments to the `test` builtin, which evaluates both sides (`[ 1 -eq 2 -a nope -eq 1 ]` still complains about `nope`), so calling them lazy was wrong. The row is now a recognition boundary — `owns []`, `TestOperator` excluded with the reason that every consumer scores them as the `&&` / `||` they spell (#1536) and the `ShortCircuit` row claims them. - Python's `&` / `|` / `^` on two `bool`s are eager and return a `bool`, so its pre-existing `absent` claim was also wrong; it takes the same shared-kind exclusion the other five languages have. - Perl's and PHP's rows gain a direct test of the owner gate and the projection, which no consumer observes: `xor` owns, `or` does not, a key named `xor` does not, and the projected token is `xor`. Zero drift re-measured on the touched languages (Bash 144, Python 73, and the Kotlin / Perl / PHP fixtures). Coverage −3. Refs #1695
The third role of #1695. Eighteen languages have a conditional expression; each row names the node that owns it and, where the grammar has one, the `?` that pays for it, and every consumer reads the row: ABC at the projection it always used (the token in the C family, Java, Groovy, the JS family, C# and Ruby; the node in Perl, PHP, Python, Tcl, iRules and Bash — the latter for the #1273 misparse Perl routes around, for PHP's and Python's tokenless designs, and for Tcl's marker-relative slot walk), cyclomatic and cognitive at the node, and Halstead at the token through `paying_roles!` (or Bash's hand-written reading). Where the grammar spells a bare `?` elsewhere, the payer is gated to its owner and the getter's byte-less arm keeps billing the gated-out spelling under an exemption naming the row: Java's and Groovy's wildcard bound (#1274), TypeScript's ten type productions (#1275), C#'s nullable type and constraint — and its conditional access, the `NullSafe` row's, which C# ABC still counts through its denylist arm until that row lands — PHP's `?int`, and Bash's `${y?msg}`. The C family, Ruby, Perl, Tcl and iRules spell the token nowhere else and pay it ungated; Bash's `TernaryExpression2` is listed defensively and its `QMARK2` excluded, with `bash_ternary_expression_alias_is_unreachable` pinning both. Python's `a if c else b` has no operator token, so its row is owner-only and its `if` / `else` keep billing as keywords. The C-family and Java-like cyclomatic macros take `ternary = <row>` beside `short_circuit = <row>`; the JS one reads both rows by name. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on every population (0 of 6,638 outputs) and on the recovery sweep (1,004 outputs). Census −18 (no `Ternary` cell is pending), coverage −36. `role_rows_tests` gains a ternary row per language — outside a slot, inside one, with negated branches, and each gated-out spelling — with every expected value taken from the base binary, and one Halstead spelling test covers the `?` of sixteen grammars. Perturbations: ungating Java's `?` fails 2 tests (the wildcard scores), dropping C++'s payer fails 8+, ungating C#'s fails 6, and ungating Bash's fails the new row test in `roles/bash.rs` (the expansion's `?` pays). The baseline refresh drops three entries the standard dispatch suppression now covers and records the Perl and Ruby getters' effort falling with their `?` arm. Refs #1695
Four drifts and misstatements, all on recovery trees or in prose:
- JavaScript's and MozJS's `?` payer was gated to the ternary, but the
arm it replaced was not: those grammars spell the bare token nowhere
else, and a `?` error recovery strands under an `ERROR` (`let c = a
?` at end of input) had always paid. The two rows pay it ungated
again; the TypeScript rows keep their gate. A recovery row per JS
grammar pins both readings.
- PHP's `<` / `>` were gated the same way, against an arm that was
not: the pinned grammar spells the two only as a `binary_expression`
operator, so a stranded one paid before. Both pay ungated now, and
the ABC row test holds the recovery spelling. Found by widening the
recovery sweep to cut one byte after every operator punctuation
(4,310 outputs over the R04 and R05 fixtures, 0 differing — the
R04-era sweep cut only at line ends and sixteen offsets, which is
how both slipped past).
- Python's row said cyclomatic does not count the ternary; it does,
through the `if` keyword the expression spells. Cyclomatic now reads
the row at the node and declines the keyword under it, so the row's
owner is what counts; dropping the owner fails the row test, and so
does removing the keyword's decline (the ternary scores twice).
- Bash's row claimed excluding `QMARK2` would make a reachable spelling
fail the coverage gate, which forgives excluded ids. The claim is
gone; `bash_ternary_expression_alias_is_unreachable` now asserts the
second `?` id is not emitted either, over the arithmetic sweep plus
an expansion's `${y?msg}` and `$?`.
Zero drift re-measured on the touched languages.
Refs #1695
The fourth and fifth roles of #1695, together as the issue steps them: which arm of a multi-way branch is a decision, and which guard on an arm or a head is one more. Every language's rows and every consumer of them; zero drift. The arm row's shape follows the grammar. Where the arm has a keyword the counting consumers pay at — the C family's, Java's, Groovy's, the JS family's and C#'s `case` — the label node owns and the keyword pays, gated where another production spells it (C#'s `goto case`, #1450); `default` is excluded on every such row with lesson 11's reason, and Halstead keeps billing it as the keyword it is. Where the arm has no keyword, or the consumers pay at the node — Kotlin's `when_entry`, Rust's `match_arm`, Python's `case_clause`, Go's three case kinds, Ruby's `when` / `in_clause`, PHP's `case_statement` / `match_conditional_expression`, Perl's `when`, Elixir's `stab_clause`, Bash's `case_item`, iRules' `switch_arm`, and Tcl's `command` under a brace-list `switch` — the node owns and pays, gated against the fallback its grammar spells: an absent `condition` field, a bare `_` (`discard` / `var _` in C#, an `identifier` spelled `_` in Ruby), a sole `*`, a `default` name, or Elixir's catch-all and head rules. C#'s `switch_expression_arm` is `pays_itself` beside the `case` payer, the one row reading both projections. The guard rows own Java's `guard`, C#'s `when_clause` / `catch_filter_clause`, Ruby's `if_guard` / `unless_guard` (and the hidden `_guard`), Python's `if_clause` under a `case_clause` (a comprehension's filter shares the kind), Rust's `match_pattern` with a `condition` field (its `if` pays, gated to the pattern), and Elixir's `when` operator (its token pays, gated to a guard position — a typespec's `when a: integer` spells the same). Two spec corrections. Java's `Guard` was R01's `pays never` proof row; cyclomatic pays the guard node, so the owner is the paying projection and `pays never` now has no live user (the DSL keeps the form). C#'s `when` is excluded rather than paid because it is also a `_reserved_identifier` at this pin (`int when = 1;`), which the old consumer comment recorded and the row now carries. The gate predicates moved into the AST crate so the rows could read them: `src/metrics/elixir_decisions.rs` is now `lang_helpers/elixir_decisions.rs` (a move; `is_operand` inlined), and `lang_helpers/case_arms.rs` holds the bare-default predicates that were in `npa/shared.rs` and `cyclomatic.rs` plus the Tcl / iRules switch-arm readers (the per-switch arm *counters* are gone: each arm is read at its own node now, with the same total). The C-family and Java-like cyclomatic macros take one `roles = <module>` argument in place of three row paths; Java's `[Guard]` extra is the row. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on every population (0 of 6,638 outputs) and on the recovery sweep (4,310 outputs). Census −30 (no `CaseArm` or `Guard` cell is pending), coverage −80, bypass baseline 86 → 84 (Java's two guard sites). A case-arm / guard row test per language with every expected value from the base binary; a Halstead spelling test over twelve grammars; owner tests in `roles/python.rs`. Perturbations: ungating C#'s expression arm fails 6+, Rust's arm 6+, Tcl's arm 3, Elixir's guard 2, Python's guard the new row test. Refs #1695
Three findings, two of them drifts the corpus and recovery oracles
could not see and one a complexity regression:
- iRules' `switch_arm` owner accepted an arm with no `switch` parent,
where the per-switch counter it replaced reached only a switch's
direct children: an arm error recovery strands under an `ERROR` with
its `switch` lost at end of input scored a decision it never had.
The gate reads the parent now, and a row pins the truncated input.
- Tcl's cyclomatic read of the `CaseArm` row returned before the
command-name dispatch, so an arm whose *pattern* spells `for` or
`switch` — which the grammar cannot tell from the command — stopped
paying as that command too. It falls through again, as it always
did, and a row pins `for { … }` as an arm.
- Tcl's arm gate rescanned the switch's argument list per arm to find
the sole braced word, `O(A × W)` for `A` arms and `W` options where
the deleted per-switch counter scanned once. The scan is memoised
per `word_list` on the walk's `FoldCache` under a new
`MemoQuestion::SwitchArmList`; `cyclomatic/tcl-wide-switch` grows
options and arms together and reads exponent 0.93 with the memo
and 2.01 without.
The Rust `| _ =>` fixture's comment said the leading `|` leaves the
`_` bare; it does not — tree-sitter-rust nests it in an `or_pattern`
and the arm has always counted — and the comment now says so.
Zero drift re-measured: every population (0 of 6,638) and the
recovery sweep (4,310 outputs, 0 differing).
Refs #1695
The last role of #1695: optional navigation and null coalescing. Ten languages have one; every consumer reads the row, and each keeps the reading it had — the role records which consumer scores which payer, not a new policy (#281, #452). The rows. JS family: `binary_expression` gated on `??` owns, `??` and the optional chain's `?.` pay — the `optional_chain` token itself in the JavaScript grammars, the `?.` under an `optional_chain` wrapper in the TypeScript ones, where the wrapper is excluded so a chain pays once. C#: `??` pays at its token and the `conditional_access_expression` pays for itself — its operator is the same bare `?` the ternary and two type productions spell (#1275), so the node is the payer and the token keeps billing through the byte-less arm. Kotlin: the elvis `binary_expression` and the `navigation_expression` holding `?.`; `!!` excluded (it throws, no branch). Groovy: the four `safe_*` / `elvis_expression` owners, four tokens. PHP: `??` and the two nullsafe kinds' `?->`. Perl: the defined-or `//`. Ruby: a `call` (four aliased ids) holding `&.`. The compound `??=` / `//=` is excluded on every row as the consumers' own: an assignment to ABC, a sequence to cognitive, a decision to cyclomatic (#236, #248, #249). The consumers. Cyclomatic pays every row payer (the JS macro's `$opt_chain` argument and Groovy's four extras are gone; C#'s access pays at the node as before). Cognitive folds `??` / elvis / `//` into a boolean sequence through `null_safe::owns`, the navigations never reaching its key. ABC scores the coalescing operator as a condition where that language's ABC always did — gated to that one payer under a `role-bypass: allow(NullSafe)` saying which, since the navigations are no boolean test — walks Perl's `//` chain operands (Rule 9), and pays C#'s access at the node, the C#-internal agreement with its own cyclomatic no sibling shares; PHP's `??` stays a slot-only condition. Halstead bills each payer through `paying_roles!`; the getters' `??` / `?.` / `?:` / `?->` / `//` / `&.` arms and the JS family's `op_extras` entries are gone. The Ruby `call` claim is exempted at `is_call`, the ABC branch, and the `attr_*` / visibility walks, none of which is membership. Zero drift: `bca ops` and `bca metrics` JSON byte-identical on every population (0 of 6,638 outputs) and on the recovery sweep (4,372 outputs, the fixtures gaining Groovy's `??.` / `?[`, C#'s `a?[0]` and Ruby's chained `&.`). Census −10 (no `NullSafe` cell is pending, and no cell of any of the seven paying roles), coverage −42. A null-safe row test per language with every expected value from the base binary, and a Halstead spelling test over nine grammars (which records that the JS family's `??=`, like its `&&=` / `||=`, never billed — a pre-existing gap the exclusion neither opens nor closes). Perturbations, one payer dropped per language: JS 5 failures, TS 5, C# 4, Kotlin 3, Groovy 5, PHP 3, Perl 5, Ruby 3. The baseline refresh records the Perl, PHP and Ruby getters' effort falling with their arms. Refs #1695
One drift the oracles could not see, and the stale prose around it: - C#'s `scores_itself` read every `NullSafe` owner, so a negated conditional access — `!(a?.b)`, valid C# — stopped paying as the negated operand of its slot: 2 conditions became 1 while the access itself still paid once. Only the coalescing owner is what the slot predicate must decline (its `??` arm pays it); the access was never a `scores_itself` case. The predicate now asks the row's projection for `??`, as Kotlin's and Groovy's ask for the elvis, and a row pins `!(o?.ToString()?.Length)` at 3 — which reads 2 with the gate removed. The C# fixture gains the negated form, so the corpus oracle covers the class from here. - Nine comments still described `??` / elvis / `//` as "pending `NullSafe`", two described billing paths that do not exist (the JS getter has no `??=` arm; C#'s getter named an `under_conditional_access` gate the row does not have), and the `count_boolean_slot` doc said `scores_itself` "must agree with those arms exactly" by hand — it now says the two read the same rows and agree by construction, and where a hand-listed arm or a multi-owner row still needs care. Zero drift re-measured on C# (44 outputs) and the recovery sweep (4,388 outputs, 0 differing). Refs #1695
The final branch review found a scaling regression the zero-drift oracles could not: `paying_roles!` asked the `Guard` row for every `when` token, and that row's payer gate climbs the chain of nested `when` operators a repeated guard (`when a when b when …`) builds — once per token, quadratic in the chain. 8,000 guards went from 0.02 s to 0.88 s on a Halstead-only walk, with byte-identical output: the gate decides nothing for Halstead, since a `when` it leaves out (a typespec's `when a: integer`) bills as an operator through the byte-less arm all the same. `ElixirCode::get_paying_role` is hand-written now, as Rust's and Bash's are: it bills the `Guard` row's payer in every position without asking the gate, and reads the other rows as the macro does. The byte-less `E::When` arm is gone, since nothing reaches it. A `halstead/elixir-repeated-guards` depth probe reads exponent 0.98 so and 2.18 with the row asked. The counting consumers still pay the climb once per guard, where it decides something. Refs #1695
The predicates were gated per language, but the `role_table!`s that import them are compiled unconditionally — `mk_langs!` generates every language's enum and table whatever the feature set — so any build without `bash`, `csharp`, `irules`, `python`, `ruby` and `tcl` all on failed on the imports. The gates are gone: the enums they read exist in every build, as the other `lang_helpers` modules already assume. Found by the per-language isolation run (`-p big-code-analysis-ast --no-default-features --features elixir`), which the workspace-wide run cannot see since the CLI and web crates re-enable every feature. Refs #1695
The `simplify-rust` pass over the branch, three reviewers (reuse, clarity, efficiency), every finding applied: Reuse. Two gates every field-projected owner and every parent-gated payer restated as a one-line fn per language — thirty copies of `project(node).is_some_and(|op| PAYERS.contains(…))` and `parent_has_kind(node, K)` across seventeen files — are now the DSL's: an owner `holds_payer` gates on its projection landing on one of the row's payers, and a payer `under P` / `under [P, Q]` on its parent's kind. The gate fns, the three shared gate macros they lived in (`c_family_gates!`, `js_family_operator_gates!`, `tcl_family_gates!`) and `cpp_family_operator_in` are gone; the per-language rationale they carried moved onto the rows (the TypeScript `?` productions in particular). `utils/role_tables.py` reads both forms (the fragment arm regex now closes at `] $($rest)*`, since `under [..]` closes a bracket inside the list), with a census self-test and a `dsl_gates` Rust table pinning them. Beside them the DSL gains `pays_at_token_kind` — the leaf projection restricted to declared payers, which `paying_roles!` and the three hand-written `get_paying_role`s read instead of repeating `PAYERS.contains && pays_at_leaf_kind` — and `owns_via(payer, …)`, the owner test narrowed to one payer, which C#'s, Kotlin's and Groovy's `scores_itself` read instead of `owns && project == K`. `rows_own!` in `abc.rs` is every `scores_itself`'s leading clause and carries the "asked with no chain" rationale once; `compute_booleans_from` is the ten identical boolean-sequence closures; `ScoresItself` has one home; the two #1694 Halstead tests use `assert_spellings_bill_as_operators`. Clarity. `case_arms` is `pub(crate)` with `tcl_switch_arm_list` re-exported (its one root reader), the three row-only Elixir predicates and the C++ `applied` helper are `pub(crate)`, and `canonical()` is `pub(crate)` again. Eighteen module docs that said "every other role is pending", the `roles/mod.rs` doc and its `dead_code` allow that said only the keyword rows had a consumer, and a cyclomatic test doc naming the deleted `[Assert, QMARKCOLON]` extras are rewritten; `pays never` is documented as a kept shape no row uses. Python's cyclomatic asks the ternary owner of a keyword's parent with no chain rather than the keyword's. Tcl's no-`return` row read says so in its first line. Efficiency. Every cognitive `compute` binds `kind` once and reads `owns_kind`, as ABC and cyclomatic already did; Perl's boolean arm drops a three-scan guard the body made redundant; the `scores_itself` projection reads happen once through `owns_via`. Zero drift (0 of 6,638 outputs; recovery sweep 4,388, 0 differing). The rustfmt-bail baseline is ratcheted down for the arms the deleted macros held and up by one in `cognitive/perl.rs`, whose match bails on a pre-existing in-pattern comment and whose row arm is now short enough to probe. The self-scan baseline gains `roles/mod.rs` at 562 PLOC: the DSL is one file by design and the role-name mapping is a third of it. Refs #1695
… gate fns The `rust-optimize` pass over the branch. Every change preserves the walk's output byte for byte (0 of 6,638 corpus and fixture outputs; recovery sweep 4,388, 0 differing); the parity suite passes. - The two JS ABC macros were token-identical once both read their rows (`QMARK` gated vs ungated had been the one difference, and the rows carry that now): `js_abc_compute!` is gone and the four grammars expand `js_family_abc_compute!`. The JS-vs-TS rationale moved above the invocations; the tests that named "separate macro bodies" say what they pin now. - `MozcppCode`'s ABC and cognitive impls were `CppCode`'s with the ids swapped: `cpp_family_abc!` / `cpp_family_cognitive!` expand both, against each dialect's rows, with `tests/parity/cpp_mozcpp_parity.rs` pinning them equal as before. `abc/mozcpp.rs` is 28 lines, `cognitive/mozcpp.rs` 18. - `impl_cyclomatic_c_family_rows!` takes `extra = [..]` for the dialect's own both-count kinds, so C (none) and ObjC (`CatchClause`) expand it instead of hand-writing the same body for want of a parameter. - `project()` is emitted for every row, from the declared field where there is one and the first payer child otherwise — a `const PROJECT` records which — so `holds_payer` serves the last eight "holds its token" gate fns (`is_case_label` ×4 and its two shared macros, `is_case_section`, `safe_navigation` ×2) and the four single-payer projection gates (`null_coalescing_operator` ×2, `elvis_operator`, `defined_or_operator`); Bash's five-alias `under_binary_expression` is `under [..]`. The DSL doc's example rows spell the new forms. - `Ancestors::memoised(question, node, || answer)` is the get-or-record read `argument_slots_are_readable` and the Tcl arm-list test both spelt out; `bash_case_item_is_bare_wildcard` reads the `value` fields through `children_by_field_name` instead of a hand-driven cursor. - Getter comments that named the deleted gate fns name the rows' `under` kinds. Clippy pedantic is already at `-D warnings` workspace-wide, so the triage step found nothing. Net −580 lines over the branch's Rust. Refs #1695
The `audit-tests` pass over the branch's ~90 new tests, two auditors
verifying each finding by perturbation. No production code moves;
every fix below is a test that now fails where it did not, confirmed
by re-running the auditor's perturbation against it.
In the DSL's proof tests (`roles/mod.rs`): the `dsl_gates` table
declares its gated `holds_payer` owner under `always`, so the
projection half of `@clause_payer … gated_by` decides (under `never`
it never ran); lists the real parent *second* in an `under [..]`, so
an expansion reading only the first alternative fails; carries a gated
`pays_itself` owner whose gate admits it, so "no clause emitted" and
"gate said no" part; and keeps a `pays never` row — the shape no
production table declares since Java's `Guard` moved — so
`pays_at_wrapper`'s `!PAYS_NEVER` and the `never` mode stay pinned.
The absent-row test asks `pays_at_token_kind` (the one projection
Halstead reads) and the empty `OWNERS` / `PAYERS`. The differential
test's doc says what `canonical()` compares — the kind lists — and
that gate attributes are the Python self-tests' to pin; one of those
now reads `pays_itself_when` as a self-payer, the one attribute no
check had read. A counter on the Tcl arm-list scan and a test beside
`tcl_scans_an_erroring_list_once_per_walk` pin the `SwitchArmList`
memo's effect in `make pre-commit`, which only the quarterly bench
probe saw before.
In the root crate: the C "recovered `<`" fixtures (`return a < ;`)
were not recovered at all — tree-sitter-c keeps `a <` as a
`binary_expression` with a zero-width operand, and the ABC assertion
passed by reading the unit's own count rather than the function's.
They spell `{ < a; }`, which strands, and the ABC row reads the sum
and pins both shapes. Ruby's `!~` and Elixir's `===` / `!=` / `!==` /
`<=` gain the outside-slot row that makes dropping the payer fail
(each was covered only by a slot row, which the slot itself
satisfies). The Halstead spelling helper's doc no longer claims every
dropped payer fails its entry — where the byte-less arm still lists
the kind the vocabulary is the union, and the row's reach is the ABC
row tests' — and the C / ObjC test docs say so. The two #1695 bench
probes' readings are asserted to grow with the shape, per probe,
since `probe_workload_is_exercised`'s floor-of-one would pass a
`tcl_wide_switch` that lost its arms.
Refs #1695
…found
Four review passes over the branch (algorithmic performance, the
`review` skill, a gap review of the non-code artefacts, and a
`code-review` run), every finding applied.
Two zero-drift breaks, both on error-recovery shapes a keyword-
boundary truncation reaches and the operator-boundary sweep did not:
- Elixir billed `when` as an operator inside an `operator_identifier`
(`&when/2`), where `get_op_type`'s first test had returned `Unknown`
before the `Guard` row's reader ran first. The reader now makes the
same test. A Halstead row pins the name position beside `&==/2`.
- Groovy's `Comparison` row paid a stranded `instanceof` (`{ o
instanceof }` at end of input, the keyword under `ERROR`) where the
ABC arm it replaced paid the expression node; the payer is `under
InstanceofExpression`, as Java's is, and the getter's byte-less arm
keeps billing the stranded spelling. An ABC row and a Halstead row
pin both readings.
The recovery oracle (`/tmp/opencode/1695/recovery_oracle.py`) now
cuts after every word as well as every operator: 14,588 outputs, 0
differing; corpus oracle 0 of 6,638.
Performance, measured against the pre-branch binary:
- `project()` resolved its field by name on every ask, a string scan
over the grammar's field table, and a `binary_expression` is asked
by up to four rows per consumer. `Node::child_by_field_id` /
`field_id` resolve it once per row (a `OnceLock` in the emitted fn).
- Perl ABC's `scores_itself` scanned a `binary_expression`'s children
once per row (four rows own the kind, none through a field); it
dispatches on the kind and reads the shared projection once. Perl
ABC on a 20k-line file: +10.5% → +5%; other languages ±1–3%.
- Two pre-existing quadratics the branch's rows became the reader of:
Elixir's repeated-guard climb (`when a when b …`, per token) is
memoised per `when` operator under `MemoQuestion::GuardOperator`
(881 ms → 21 ms at 8,000 guards), and Python cognitive's
`boolean_operator` stops at the first enclosing chain instead of
counting them all (204 ms → 10 ms at 8,000 operands). Probes
`cyclomatic/elixir-repeated-guards` and `cognitive/nested-and` pin
both, and the benchmarking doc lists all four #1695 probes.
Hygiene: `canonical()` is `pub(crate)` (the earlier message claimed it
and the edit was lost); four comments naming the renamed
`cpp_family_operator_is_applied`; four inert `role-bypass:
allow(NullSafe)` markers on `??=`-only arms removed (an excluded kind
is not claimed, so the gate never read them); the `get_paying_role`
trait doc no longer says languages are "still pending"; the roles
module doc says what a row's comment may cross-reference, since ~100
row comments do name their consumers' readings; a 125-column doc line
reflowed. Baseline refreshed for `Node`'s two new methods and the DSL's
two new lines.
Refs #1695
The `code-review --effort max` pass over the branch, four findings applied (a fifth, folding Elixir's hand-written `get_paying_role` into `paying_roles!`, is declined: the three hand-written readers each state their reason, and a macro exception would hide it). - `perl_condition_scores_itself`'s comment named a helper that does not exist and counted four rows where the scan reads three; and its comparison branch re-ran the projection scan it had just done — a comparison payer *is* the row's projection, so only the binding question is left. Perl ABC on a 20k-line file: +5% → +3.5%. - `project()` emits its field-id cache only for a row that declares a field, through an `@projected` arm, instead of a `OnceLock` every row consults through a nested `Option`. - `Node::child_by_field_id` / `field_id` are `#[doc(hidden)]` with the reason they are `pub` (the exported macro) on the doc, and the CHANGELOG entry records them, the widened recovery sweep, the two drifts it found and the two walks made linear. Zero drift holds (0 of 6,638; recovery sweep 0 of 14,588). Refs #1695
The astra-review of the review fixes: the Groovy stranded-`instanceof` assertion had been appended to the Kotlin row test as well as the Groovy one (the two fixture tables end alike, and the edit matched both), and the gate it earned — `all(groovy, kotlin)` — dropped the whole Kotlin table from any build without Groovy. The duplicate and the widened gate are gone; `--compare bdd70eb` reports no test lost a build. Refs #1695
…r it climbs Four findings from the review of the review fixes: - `elixir_when_is_guard` memoised only the asking token's operator, so "a chain is climbed once" held only when the walk visited the tokens outermost first. The climb now records its answer on every `when` operator it passes, and a counter test asks the tokens innermost first — the order that leaves nothing memoised above the first asker — and reads one position check; without the record it reads the chain's length. - `perl_condition_scores_itself` asks the `Ternary` row of a `unary_expression` no longer, since only the chain rows own that kind; and Perl's `Comparison` row says the ABC consumer reads its projection directly, so a gate added there is read here too. - The `cognitive/nested-and` rationale described the chain its render does not produce (left-nested) and cited that shape's timing; it now cites both. Zero drift holds (0 of 6,638; recovery sweep 0 of 14,588). Refs #1695
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1708 +/- ##
==========================================
- Coverage 98.30% 98.29% -0.01%
==========================================
Files 380 386 +6
Lines 110089 111166 +1077
Branches 109654 110731 +1077
==========================================
+ Hits 108222 109273 +1051
- Misses 1202 1239 +37
+ Partials 665 654 -11
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
The patch coverage report named two owner gates no test executed: Bash's `comparison_operator`, which reads a `test_operator`'s bytes to keep `-a` / `-o` out of the `Comparison` row, and Elixir's `is_when_operator`, the `Guard` owner. Every consumer of both rows reads the token projection, so `owns` ran only under the role-table self-checks. Each row now has a test walking a fixture that spells both readings and asserting the owners, the payers, and that a payer's parent owns; dropping the byte read fails the first, and widening the Elixir owner to every operator fails the second. Refs #1695
The astra-review of the owner-gate tests: the Bash fixture spelt `-a` inside `[[ ]]`, which tree-sitter parses and `bash -n` rejects, so it pinned a grammar shape no script can have; it is `[ ]`'s now. And the payer check asked whether *some* row owned a payer's parent, which an ungated or swapped payer gate still satisfies; each row's payers are now listed and held to that row's owner, asked with the chain above it. Elixir's test asks `pays_at_leaf` of every node rather than only the `when` tokens, so a payer the row gained would show. Verified by perturbation: the connective check, each payer gate's text gate, the two gates swapped, the Elixir owner widened and its payer ungated each fail the test that names it. Refs #1695
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 #1694, closes #1695. Part of #1690.
What
Every paying construct —
Comparison,ShortCircuit,EagerLogical,Ternary,CaseArm,Guard,NullSafe— is now a declaredrole_table!row in every language (big-code-analysis-ast/src/roles/), and every consumer of each (ABC, cyclomatic, cognitive, Halstead throughGetter::get_paying_role) reads the row in place of its own kind list. No cell of the seven roles ispendinganywhere.Each row records the policy the consumers already had rather than moving it: Kotlin's infix
and/or/xorand Perl/PHPxorare eager;?pays the ternary only under its node where the grammar spells?elsewhere;default/_/*)arms pay nothing (lesson 11); guards are the node or the token their consumers pay; elvis and optional navigation areNullSafe, with compound??=///=excluded as the consumers' own. Four specification corrections found while writing the rows are recorded on them and on #1695.DSL
role_table!gained: ownerholds_payer(projection lands on a row payer), payerunder P/under [P, Q],pays_itself_when f, payertext_gated_by f,project()on every row (field by cached grammar id, payer-child fallback),pays_at_token_kind(what Halstead reads),owns_via(payer, …), and emptyOWNERS/PAYERSonabsentrows. ~30 identical one-line gate fns and three shared gate macros went away.utils/role_tables.pyreads the new forms; thedsl_gatesproof table and the census self-tests pin them.Zero drift
bca opsandbca metricsare byte-identical tomain(base rebuilt at44184ad7):Ten drifts found by that sweep or by review were fixed before landing (listed in the CHANGELOG entry). Two pre-existing quadratics the rows became the reader of are linear — Elixir repeated guards (881 ms → 21 ms at 8k), Python cognitive
andchains (204 ms → 10 ms) — plus the Tcl wide-switcharm list;make bench-scalingis 86/86 with probes for each.Process
After the two issues closed, the branch went through
simplify-rust,rust-optimize,audit-tests, four independent review agents, and anastra-reviewof the fixes; each pass's findings are their own commits with the evidence in the messages. Lesson 102 records why the first recovery sweep missed two keyword-operator drifts.Rebased onto
main;make pre-commitpasses;check-test-lang-gates-compareagainstmainreports no test lost a build.