diff --git a/CHANGELOG.md b/CHANGELOG.md index b7a9b5e3f..c6534ed84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,16 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Changed — Ruby mixins read the shared base-relations path (#400, slice 4a) + +Ruby's `include`/`extend`/`prepend` are now captured by `queries/ruby/tags.scm` as `@reference.mixin` and turned into +inherit references by the shared capture consumer — the same emitter every language's base clause goes through. The +Ruby-only `captureRubyMixinBases` and its `captureIncludes` call site are gone; the query states the call shape and +the C++ reader keeps the two guards a tags pattern cannot state (the directive's text, and its class-body position). +Output is byte-identical: the map, `--deps --limit=100000`, `--skipped`, `--callers`, `--callees`, `--impact` and +`--uses` all match the base build over a real Rails corpus and the Ruby fixtures, and `--lego` lists the same +implementors. `kParserVer` moves 171 -> 173 (the extraction path changed; 172 is 171's rich file tag). + ### Added — Clojure (`.clj`/`.cljs`/`.cljc`/`.bb`) Vendored Yogthos' ABI-15 `tree-sitter-clojure` parser and added syntax-level definitions and direct-call captures for the four Clojure-family extensions. The grammar comparison parsed 4,930 of 4,933 files (99.94%, 55.5 MB) from Metabase, clojure-lsp, clj-kondo and Babashka checkouts in 2.24 seconds; two failures were intentionally invalid clj-kondo fixtures and one was a Metabase test. This measures parser completion on that corpus, not semantic call-resolution accuracy. Namespace/import resolution, macro expansion and exact multi-arity matching remain outside this first pass; see `docs/ARCHITECTURE.md` for the extraction limits. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 889931733..9347a1ae6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -559,7 +559,7 @@ review will credit it, but it is never a condition for merging the language. | 1. Imports | `ingest_importcap.h`: `@import.path` captures, `emitCapturedImport`, one `normaliseImportSpecifier` arm per `DepDialect`. Resolution in `resolve.h`. | partly. C-family, JS/TS, Go and Python read the capture (Python since #382). The rest still go through per-language arms of `directiveTargetOf` (`ingest_relations.h`): `csharpUsingTarget`, `phpUseTarget`, `luaRequireTarget`, `bashSourceTarget`, `rubyRequireTarget`, `elixirDirectiveTarget`, plus Rust `use`/`mod`, Kotlin `import_header` and Swift `import_declaration`. | **1a** one language per PR onto `@import.path` plus its normaliser arm, deleting its arm in `directiveTargetOf`: S each for C#, PHP, Lua, Bash, Kotlin and Swift; M for Rust (`use` trees, `mod`), Ruby (`autoload`) and Elixir (`alias`/`use`), whose directives carry more meaning. Claim on #358. | | 2. Aliases | None. Renames reach the resolver as `RawBind` records (`ingest_cache.h`) from separate producers: Python's `@import.names` / `@import.alias` captures, read in `captureTagsFacts` (`ingest_sidecap.h`) and turned into binds by the Python-only `emitCapturedPythonImportBind` (`ingest_importcap.h`); `captureJsImportFacts`, `captureJsModuleAliases` and `captureGoImportFacts` (`ingest_jsimports.h`), C/C++ aliases in `fnPtrAliasName` (`ingest_binds.h`) and `captureTypeAlias` (`ingest_relations.h`). The readers are separate too: `addTypeAliasBases` and `buildUsingReexports` (`resolve.h`). | duplicated: one "local name → target" concept in about five shapes. | **2a** one shared binds channel (`@import.alias` / `@import.names` → one `RawBind` kind): M, *maintainers*, after the #358 import slices. **2b** after 2a, one language per PR moves its renames onto it: S each (Rust `use a as b`, Kotlin `import x as y`, PHP `use … as`, C# `using X = …`). | | 3. Types | `captureTagsFacts` (`ingest_sidecap.h`) reads `@definition.*` captures for every language. | shared, but language decisions are `Lang::` branches inside the loop: 36 non-comment `Lang::` mentions, 15 of them Elixir. | **3a** #374: one constexpr per-language hook table that `captureTagsFacts` calls once per capture, one language per commit, Elixir first. M. | -| 4. Inheritance | `captureBases` (`ingest_relations.h`): base clauses for C++, TS/JS, Java, Python, Swift, C#, PHP, Kotlin and Ruby, chosen by node kind (`isBaseTypeNodeIn`). | partly. Rust `impl … for` is a separate pass; Ruby mixins have their own `captureRubyMixinBases`. | **4a** Ruby `include`/`extend`/`prepend` as captures read by the shared path, retiring `captureRubyMixinBases`: S–M, Ruby (offered on #339). **4b** the base-clause node kinds move from `isBaseTypeNodeIn` into `tags.scm` captures: M, all of the languages above. **4c** structural conformance (#386): a class or type whose methods cover a structural base's required methods (Python `typing.Protocol`, Go interfaces, possibly TS interfaces) is listed as an implementor marked `structural=1`, never as a nominal subclass, with a disclosed minimum-method threshold; step (c), one shared method-set check, per-language data for which bases are structural. M; Python and Go first. | +| 4. Inheritance | `captureBases` (`ingest_relations.h`): base clauses for C++, TS/JS, Java, Python, Swift, C#, PHP, Kotlin and Ruby, chosen by node kind (`isBaseTypeNodeIn`). | partly. Rust `impl … for` is a separate pass; Ruby mixins read the same path through `@reference.mixin` captures since slice 4a (#400). | **4a** done (#400): Ruby `include`/`extend`/`prepend` read the shared path through `queries/ruby/tags.scm` captures, `captureRubyMixinBases` retired. **4b** the base-clause node kinds move from `isBaseTypeNodeIn` into `tags.scm` captures: M, all of the languages above. **4c** structural conformance (#386): a class or type whose methods cover a structural base's required methods (Python `typing.Protocol`, Go interfaces, possibly TS interfaces) is listed as an implementor marked `structural=1`, never as a nominal subclass, with a disclosed minimum-method threshold; step (c), one shared method-set check, per-language data for which bases are structural. M; Python and Go first. | | 5. Callables | Definitions come from `tags.scm`, read by `captureTagsFacts`. | partly. Keyword-headed definitions keep keyword tables in C++: `elixirFunctionKeyword` and `elixirMetadataAttribute` (`ingest_elixir.h`) repeat `macroBodyKeyword` (`ingest_relations.h`), 0.82 and 0.96 similar by `--clones`. | **5a** #355: the tags pass evaluates `#eq?`/`#any-of?`/`#not-any-of?`, then Elixir's table moves into its `tags.scm`. M; Elixir, then Clojure. | | 6. Dispatch | `buildGraph` (`graph.h`): a SCIP pin, then the canonical tier (`appendCanonicalCandidates`, `resolve.h`), bindings and receiver rules (`receiverevidence.h`), then name tiers. Per-language qualified-call rules: `ElixirResolver` (`elixir_resolve.h`), `keepRustQualifiedCandidates` and `keepStdQualifiedCandidates` (`graph.h`). | partly. Facts about each language are scattered `l == Lang::X \|\| …` predicates (`implicitReceiverLang`, `memberNeverReachesFreeFunction` and `classNameReceiverLang` in `receiverevidence.h`; `langUsesHashComment` in `quality.h`), and `--clones` reports them as one group. | **6a** the shared refusal for an unplaced module qualifier (the known gap above): S, *maintainers*, not yet scheduled. **6b** the **language-neutral qualified-call resolver**: generalise `ElixirResolver`'s module table (alias/import/require facts → `Module::name`) so that Clojure `ns/f`, Elixir and other module-qualified languages use one resolver driven by per-language data. M; Elixir byte-identical, Clojure as the first new user. **6c** those predicates become columns of one constexpr per-`Lang` table: S, all languages. | | 7. Data-driven dispatch | One mechanism per shape: fn-pointer and callback bindings `buildFnPtrBindTables` (`graph.h`, C/C++); HTTP routes `routesVisitNode` (`ingest_sidecap.h`, a JS/TS and Python walk) into `RouteDef`/`RouteUse` (`model.h`); FFI aliases `BindingAlias` (`model.h`). | partly. Calls by a literal method name (`send(:m)`) resolve for Ruby only. | **7a** #375: one capture for a call whose method name is a literal, resolved through the ordinary receiver rules. M; Python `getattr`, JS/TS `obj["m"]`, Go `MethodByName`. **7b** route registration read from captures instead of the walk: M; JS/TS and Python. | diff --git a/queries/ruby/tags.scm b/queries/ruby/tags.scm index d44ff08bf..843b39cbf 100644 --- a/queries/ruby/tags.scm +++ b/queries/ruby/tags.scm @@ -96,3 +96,22 @@ ; never contributes: a class-wrapped create_table (string- or symbol-named) is refused by the ; class/module-nest gate — the schema is the ONE source of column names; add_column/remove_column/ ; change_column are argument data and read nowhere (a column added then dropped never registers). + +; ---- mixin directives (family 4, slice 4a) ---- +; `include M` / `extend M` / `prepend M` make M an ANCESTOR of the enclosing class or module: the class's own +; method lookup reaches M's methods, which is the edge `class C < B` mints. Each constant argument is captured +; here as @name and read by the shared base-relations path (src/ingest_sidecap.h captureTagsFacts -> emitBaseRef, +; the same emitter every language's base clause goes through), so no Ruby-only C++ function walks these +; directives any more (slice 4a retired captureRubyMixinBases). +; +; UNANCHORED on purpose, exactly like the import patterns above: the tags pass evaluates NO predicates +; (#eq?/#match? are silently ignored — test/matchgrammarcheck.sh), so this pattern states the call SHAPE and the +; C++ reader owns the two guards a query cannot state — the directive's own TEXT (without it `Logger.new( Config )` +; and `raise Error` at class-body level match the same shape) and the POSITION (class/module body, `class << self`, +; or a concern's `included do … end`; a method body, another block and the file top level emit nothing). Both guards +; already existed for this lane and stayed where they were; the query is the language gate, so the reader spends no +; Lang:: test on them. The division is the one the shared import vocabulary uses for `require` (issue #358, +; src/ingest_importcap.h). +(call + method: (identifier) + arguments: (argument_list [(constant) (scope_resolution)] @name)) @reference.mixin diff --git a/src/graph.h b/src/graph.h index 658a09606..720caa5e3 100644 --- a/src/graph.h +++ b/src/graph.h @@ -8788,7 +8788,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null for( NodeId baseId : *baseIds ) { // lane lego-transitive (M1): a Ruby MODULE is a base here too. `include M` / `extend M` / `prepend M` put it in - // the class's ancestors (captureRubyMixinBases, parser version 131), so the class implements its contract the way + // the class's ancestors (the @reference.mixin capture, parser version 131), so the class implements its contract the way // a subclass does — and --lego=M listed nobody (rake: `include Rake::DSL`). The test is BuiltinMethodGate's // class-like rule, the one that already counts a Ruby module (Ruby && SymKind::Other: @definition.module maps to // Other for every language, and only Ruby's tags have no other Other). Other languages need nothing here: an diff --git a/src/ingest_cache.h b/src/ingest_cache.h index 2b7366370..d56bf7605 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -320,7 +320,13 @@ constexpr std::uint32_t kCacheVersion = 31; // 31: train 27 — lane l // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 171; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 173; // bump on any grammar/.scm/extraction change + // 173 = 2026-10-15 (slice 4a, #400): the Ruby mixin directives (`include` / `extend` / `prepend`) + // move off the captureIncludes walk onto the shared base-relations path — + // queries/ruby/tags.scm captures the directive as @reference.mixin and captureTagsFacts + // emits each constant's inherit reference through emitBaseRef (captureRubyMixinBases retired). + // Output byte-identical; the extraction path changed, so the number moves. 173, not 172: 172 is + // 171's rich file tag. Rich file tag 174. kCacheVersion stays 31. // 171 = 2026-10-13 (PR #349 lands, @tag-und-nacht): Clojure grammar, tags.scm and extraction // (ingest_clojure.h) and the qualified-call namespace in Reference::qualifier. The branch built // 156, 158 and 169 (rich file tag 170); main is 167 (rich file tag 168). 171, not 168: a lean 168 @@ -494,7 +500,7 @@ constexpr std::uint32_t kParserVer = 171; // bump on any grammar/.sc // 131 = 2026-10-02 (test/rubyreachcheck.sh): a Ruby MIXIN is an // ancestor — `include`/`extend`/`prepend` at class-DSL // position mints one inherit RawRef per constant, at the - // constant's byte (ingest_relations.h captureRubyMixinBases). + // constant's byte (ingest_relations.h emitCapturedMixinBase). // New records, same layout: kCacheVersion unchanged (27). A // Ruby cache written at 130 holds none of them and must re-parse. // 130 = 2026-10-01 (test/rubybarecallcheck.sh): Ruby's BARE-WORD call — an diff --git a/src/ingest_relations.h b/src/ingest_relations.h index cbba4cf32..66916c35d 100644 --- a/src/ingest_relations.h +++ b/src/ingest_relations.h @@ -944,7 +944,7 @@ inline void wrappedBase( TSNode wn, std::uint32_t fileId, Lang lang, std::string // C#/PHP/Kotlin/Ruby. Ruby reuses the `superclass` clause name Java's extends clause already has, and // names its base with (constant)/(scope_resolution) — see isBaseTypeNodeIn. A Ruby MIXIN // (`include M` / `extend M` / `prepend M`) is a call in the class BODY, not a clause, so it is captured where -// the call is read instead: captureRubyMixinBases (parser version 131; test/rubyreachcheck.sh). Lua is deliberately absent and it is a DISCLOSED non-goal, not an omission: Lua inheritance IS +// the call is read instead: the @reference.mixin capture (parser version 131; test/rubyreachcheck.sh). Lua is deliberately absent and it is a DISCLOSED non-goal, not an omission: Lua inheritance IS // `setmetatable( Derived, { __index = Base } )`, an ordinary runtime call over an ordinary table, so there // is no syntax to read and a Lua corpus correctly reports no inheritance edges at all. // @@ -1988,6 +1988,11 @@ inline std::string_view fieldIdentifierText( TSNode n, NodeField field, std::str // (measured: NoMethodError at class level) — captured for third-party DSLs that define it; the // disclosure lives in the CHANGELOG and the tags.scm header. inline constexpr std::array kRubyConstantDirectives = { "include", "extend", "prepend", "autoload" }; +// The MIXIN subset of the set above (family 4, slice 4a): the three directives that make each constant argument an +// ANCESTOR of the enclosing class. `autoload` is in the set above and not here — it names a file to load, never a base. +// The shared capture consumer (ingest_sidecap.h captureTagsFacts) reads this table for the @reference.mixin captures +// queries/ruby/tags.scm emits; a language whose bases come from a clause emits no such capture and needs no row. +inline constexpr std::array kRubyMixinDirectives = { "include", "extend", "prepend" }; inline constexpr std::array kRubyAttrFamilyNames = { "attribute", "attributes", "attr_reader", "attr_writer", "attr_accessor" }; // `module_function` is NOT a visibility wrapper here: `module_function attr_accessor :x` raises in either position // (measured, Ruby 4.0.7: NoMethodError in a class, which undefines module_function; TypeError in a module, which @@ -2010,7 +2015,7 @@ inline std::string_view rubyNamedDirective( TSNode n, std::string_view src, std: // `include A, B` / `extend M` / `prepend P` — ONE directive naming N constants and therefore N Include // records, in SOURCE order (the same shape as elixirAliasGroup). A non-constant argument (`include // Object.const_get(:X)`, `include mod`) contributes nothing; the constant ones beside it still do. The constant -// NODES: captureIncludes reads each one's text for its Include record, and captureRubyMixinBases its byte. +// NODES: captureIncludes reads each one's text for its Include record, and the @reference.mixin capture its byte. inline std::vector rubyMixinTargets( TSNode n, std::string_view src ) { std::vector out; @@ -2108,6 +2113,10 @@ inline bool rubyAttrAtClassBodyLevel( TSNode n, std::string_view src ) noexcept // else — a method body, another block, a file's top level — emits none. Since lane lego-transitive the reference is also an // IMPLEMENTOR edge: graph.h's implementors builder admits a Ruby module as a base, so --lego=M lists every class (and // module) that mixes M in, and test/rubyinheritcheck.sh floor (b) is closed. +// SLICE 4a moved the READ off the captureIncludes walk: queries/ruby/tags.scm captures the directive's call shape as +// @reference.mixin and the shared capture consumer applies the two guards below — the directive's TEXT (kRubyMixinDirectives) +// and the POSITION — then emits the same reference through the shared emitBaseRef. Both position readers stay here because +// they are the guards a tags pattern cannot state. // Is `call` a statement of an ActiveSupport::Concern `included do … end` block written at a module's body level? When a // class includes the concern the block runs in that class, so a mixin there joins the INCLUDER's ancestors; read as an // ancestor of the concern itself, it reaches the includer through the concern — activerecord's AttributeMethods @@ -2126,17 +2135,21 @@ inline bool rubyInConcernIncludedBlock( TSNode call, std::string_view src ) noex && rubyAttrAtClassBodyLevel( owner, src ); } -inline void captureRubyMixinBases( TSNode call, const std::vector& constants, std::uint32_t fileId, std::string_view src, +// The mixin directive a @reference.mixin capture names (family 4, slice 4a): when `call` is a receiverless +// `include`/`extend`/`prepend` at class-DSL position, its constant argument is an ANCESTOR of the enclosing class, so +// it emits one inherit reference through the shared emitBaseRef — the shape a superclass mints. Both guards are here +// because a tags pattern can state neither: the TEXT (tags predicates do not run, so `Logger.new( Config )` matches +// the same call shape) and the POSITION (a method body, another block and the file top level emit nothing). Kept out +// of captureTagsFacts — the densest dispatch point in that file — behind one call, like the scope readers above. +inline void emitCapturedMixinBase( TSNode call, TSNode constant, std::uint32_t fileId, std::string_view src, std::vector& refs ) { - if( constants.empty() || !( rubyAttrAtClassBodyLevel( call, src ) || rubyInConcernIncludedBlock( call, src ) ) ) + if( rubyNamedDirective( call, src, kRubyMixinDirectives ).empty() + || !( rubyAttrAtClassBodyLevel( call, src ) || rubyInConcernIncludedBlock( call, src ) ) ) { return; } - for( const TSNode a : constants ) - { - emitBaseRef( a, fileId, Lang::Ruby, src, refs ); - } + emitBaseRef( constant, fileId, Lang::Ruby, src, refs ); } // A CONSTANT CHAIN: `Name`, `A::B::C`, `::A::B` — every segment a constant, the head a constant or absent (`::A`). @@ -3013,13 +3026,14 @@ void captureIncludes( TSNode root, Lang lang, std::uint32_t fileId, std::string_ else if( lang == Lang::Ruby && kindIs( t, "call" ) ) { // `include A, B` / `extend M` / `prepend P` (parser version 82): N constants off one directive node, in - // SOURCE order, each a symbolic Include — the Ruby twin of the Elixir alias group below. + // SOURCE order, each a symbolic Include — the Ruby twin of the Elixir alias group below. The ANCESTOR half + // of the same directive (the inherit reference each constant mints) is read by the shared base-relations + // path from queries/ruby/tags.scm's @reference.mixin captures since slice 4a. const std::vector mixins = rubyMixinTargets( n, src ); for( const TSNode member : mixins ) { emitDirective( rubyConstantText( member, src ), true, isLazy, false ); } - captureRubyMixinBases( n, mixins, fileId, src, refs ); // parser version 131: and each is an ancestor } else if( lang == Lang::Ruby && kindIs( t, "argument_list" ) ) { diff --git a/src/ingest_ruby.h b/src/ingest_ruby.h index 9dc6c2f02..278254d86 100644 --- a/src/ingest_ruby.h +++ b/src/ingest_ruby.h @@ -615,7 +615,7 @@ inline void captureRubySingletonAccessors( TSNode root, std::uint32_t fileId, st // Parser version 145 (test/rubyclassrecvcheck.sh): each mixin the CLASS OBJECT's lookup reaches rather than its // instances' — the constants of an `extend M`, and of an `include`/`prepend` inside `class << self` — as a -// LocalBindKind::RubyClassMixin binding at the constant's own start byte, where captureRubyMixinBases puts its inherit +// LocalBindKind::RubyClassMixin binding at the constant's own start byte, where emitCapturedMixinBase puts its inherit // reference, read where that reads the directive (class-body level, or a concern's `included do`). One a concern's // `included do` writes extends the includer, not the concern: importedName kRubyIncluderMark. A class body's `extend self` // names no constant: its binding sits at the directive, var "self". A file that never writes `extend` or `<<` is not @@ -627,7 +627,7 @@ inline bool rubyExtendsSelf( TSNode n ) noexcept return !ts_node_is_null( args ) && ts_node_named_child_count( args ) == 1 && kindIs( ts_node_type( ts_node_named_child( args, 0 ) ), "self" ); } -// The RubyClassMixin bindings of the mixin directive `n` (an `extend` when `extend`), read where captureRubyMixinBases reads +// The RubyClassMixin bindings of the mixin directive `n` (an `extend` when `extend`), read where emitCapturedMixinBase reads // it: one per constant a class object's lookup reaches, and a module body's `extend self`. inline void rubyNoteClassMixins( TSNode n, bool extend, std::uint32_t fileId, std::string_view src, std::vector& binds ) { diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index 7983f738c..614dd81db 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -2433,6 +2433,16 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t continue; } + // family 4 / slice 4a: a @reference.mixin capture is Ruby's `include`/`extend`/`prepend` — the shape + // lives in queries/ruby/tags.scm, so the capture IS the language gate and no Lang:: test is spent + // here. The two guards a tags pattern cannot state (the directive's TEXT, and its POSITION) live in + // the helper, beside the emitter. The directive's own CALL reference is a separate match, below. + if( refCapSv == "reference.mixin" ) + { + emitCapturedMixinBase( roleNode, nameNode, fileId, src, refs ); + continue; + } + RawRef r; r.fileId = fileId; r.startByte = ts_node_start_byte( roleNode ); diff --git a/src/model.h b/src/model.h index 970e1897c..4e6bf0dd2 100644 --- a/src/model.h +++ b/src/model.h @@ -1077,7 +1077,7 @@ enum class LocalBindKind : std::uint8_t // ActiveSupport::Concern extends onto the includer (ingest_ruby.h rubyDefSide). RubyClassMixin, // parser version 145: a mixin the CLASS OBJECT's lookup reaches rather than its instances' — the constant // of an `extend M`, or of an `include`/`prepend` inside `class << self` — at startByte, the - // constant's own start, where ingest_relations.h captureRubyMixinBases puts its inherit reference; + // constant's own start, where ingest_relations.h emitCapturedMixinBase puts its inherit reference; // var and typeName empty; importedName=kRubyIncluderMark when the directive sits in a concern's // `included do … end`, which extends the includer, not the concern. A module body's `extend self` // has no constant: var="self", at the directive's own start byte (ingest_ruby.h diff --git a/src/quality.h b/src/quality.h index 0a0b746df..3cc0dd208 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2268,7 +2268,12 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 31; // MUST equal ingest.cpp's kCacheVersion (gated); 31 = train 27: lego-transitive's inherit-ref baseBind (the lane's 30), 29 = FE-B ref memberPath/memberCtor + 4 bind kinds, 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 171; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 173; // MUST equal ingest.cpp's kParserVer (gated) + // 173 = 2026-10-15 (slice 4a, #400): Ruby's mixin directives onto the shared + // base-relations path — queries/ruby/tags.scm captures them as @reference.mixin + // and captureTagsFacts emits the inherit reference; captureRubyMixinBases + // retired. Output byte-identical, extraction path changed. 173, not 172: 172 + // is 171's rich file tag. // 171 = 2026-10-13 (PR #349 Clojure lands above main's 167 / rich tag 168 and the // branch's own 169 / rich tag 170; see the kParserVer note) // 167 = 2026-10-11 (train 27 release: lane fix27-cpp-subscript's 166 renumbered above the lane build; diff --git a/src/resolve.h b/src/resolve.h index 5221412a5..5cd3edaf6 100644 --- a/src/resolve.h +++ b/src/resolve.h @@ -1954,7 +1954,7 @@ struct RubyDirectiveSite }; // Every symbolic directive that is not a value use, per file in byte order — the records a MIXIN reference is joined to -// (parser version 131: ingest_relations.h captureRubyMixinBases puts the reference at its constant, the directive sits at +// (parser version 131: ingest_relations.h emitCapturedMixinBase puts the reference at its constant, the directive sits at // its `include` call). Stable, so two constants of one `include A, B` keep their source order at their shared byte. inline std::vector> rubyDirectiveSitesByFile( const IngestResult& ing, const RubyConstantIndex& ix ) { diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index f1ce3205d..189e81021 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -23cb6ecf5949981b75965273ff9af22fc68cdfe109bbd7f99b7167d59bdbc0cb +82085ad1d2fbf866750fb7e3f928f611464f8ae1227e4453c9b6e12d6af122de diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index c3ad1b7fe..16ef175af 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,12 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-10-15, slice 4a (#400, Ruby mixins onto the shared base-relations path): RE-DERIVED with UPDATE_GOLDEN=1. +# The only watched text that moved is the kParserVer declaration (and its mirror): 171 -> 173. Ruby's +# `include`/`extend`/`prepend` are captured by queries/ruby/tags.scm as @reference.mixin and read by +# captureTagsFacts instead of the retired captureRubyMixinBases; output is byte-identical, but the extraction +# path changed, so the number moves. 173, not 172: 172 is 171's rich file tag. +# kCacheVersion 31 and kQSnapCacheScheme 20 are unchanged. # 2026-10-13, PR #349 lands (maintainer renumber commit on the contributor's branch): RE-DERIVED with UPDATE_GOLDEN=1. # The only watched text that moved is the kParserVer declaration (and its mirror): provisional 158 -> 171. The branch # built 156, 158 and 169 (rich file tag 170); main is 167 (rich file tag 168); 171 / rich file tag 172 is above all of diff --git a/test/rubyinheritcheck.sh b/test/rubyinheritcheck.sh index fb782e497..cde613d10 100755 --- a/test/rubyinheritcheck.sh +++ b/test/rubyinheritcheck.sh @@ -498,7 +498,8 @@ then fi # The expected count: every `class X < Const` in the fixture (a computed superclass mints no inherit reference) and, # since parser version 131, every constant of a class-body `include`/`extend`/`prepend` — a mixin is an ancestor too -# (ingest_relations.h captureRubyMixinBases), so the seam drops its directive from the join as well. +# (queries/ruby/tags.scm @reference.mixin since slice 4a; captureRubyMixinBases before that), so the seam drops its +# directive from the join as well. SUPERS="$( cat "$FIX"/*.rb | grep -cE '^[[:space:]]*class [A-Z][A-Za-z0-9_:]* < (::)?[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*[[:space:]]*(;|#|$)' )" MIXINS="$( cat "$FIX"/*.rb | grep -E '^[[:space:]]+(include|extend|prepend) ' | grep -oE '(::)?[A-Z][A-Za-z0-9_]*(::[A-Z][A-Za-z0-9_]*)*' | grep -c . )" WANT=$(( SUPERS + MIXINS )) @@ -521,5 +522,57 @@ then || no "seam: --legend=compact carries ruby_bases_unscoped= with no row defining it" fi +# ---- slice 4a: the mixin read moved to queries/ruby/tags.scm (@reference.mixin) ------------------------------- +# The constants of a class-body `include`/`extend`/`prepend` are inherit references. Since slice 4a the tags query +# states the SHAPE and the shared capture consumer owns the two guards a tags pattern cannot: the directive's TEXT +# and its POSITION. The query matches ANY call holding a constant argument, so those two guards are the only thing +# between `Logger.new( MixA )` and an invented ancestor — this fixture pins all three boundaries in one count. +MIXQ="$DIR/mixq"; mkdir -p "$MIXQ" +cat > "$MIXQ/q.rb" <<'RUBY' +module MixA; end +module MixB; end +module Space + module Mod; end +end +class Mixed + include MixA, MixB + include Space::Mod + Logger.new( MixA ) + def f + include MixA + end + class << self + include MixA + end +end +RUBY +if RIPWIRE_TEST_RUBY_BASE_UNSCOPED=1 runq MIXSEAM "$MIXQ" --no-cache --lego=MixA --legend=full +then + if echo "$MIXSEAM" | grep -q ' ruby_bases_unscoped="4"' + then + ok "slice 4a: 4 mixin inherit refs (2 off one directive, 1 scope_resolution, 1 under class << self) — and neither Logger.new( MixA ) nor the method-body include is one" + else + no "slice 4a: expected ruby_bases_unscoped=\"4\" (2 + 1 + 1, the call and the method body refused), got: $( echo "$MIXSEAM" | grep -o 'ruby_bases_unscoped="[0-9]*"' )" + fi + if echo "$MIXSEAM" | sed 's/>\n/dev/null \ + && ! grep -qE 'void captureRubyMixinBases *\(' "$ROOT/src/ingest_relations.h" 2>/dev/null +then + ok "slice 4a: the mixin read is queries/ruby/tags.scm's @reference.mixin capture, and the retired captureRubyMixinBases definition is gone" +else + no "slice 4a: the migration is undone — tags.scm @reference.mixin=$( grep -c '@reference.mixin' "$ROOT/queries/ruby/tags.scm" 2>/dev/null ), ingest_relations.h captureRubyMixinBases definition=$( grep -cE 'void captureRubyMixinBases *\(' "$ROOT/src/ingest_relations.h" 2>/dev/null )" +fi + echo [ "$fail" -eq 0 ] && echo "ALL PASS" || { echo "SOME CHECKS FAILED"; exit 1; }