Skip to content

feat: move template specializations and deduction guides off parent scope page#1199

Open
gennaroprota wants to merge 1 commit into
cppalliance:developfrom
gennaroprota:feat/move_template_specializations_and_deduction_guides_off_parent_scope_page
Open

feat: move template specializations and deduction guides off parent scope page#1199
gennaroprota wants to merge 1 commit into
cppalliance:developfrom
gennaroprota:feat/move_template_specializations_and_deduction_guides_off_parent_scope_page

Conversation

@gennaroprota
Copy link
Copy Markdown
Collaborator

@gennaroprota gennaroprota commented May 13, 2026

Class template specializations, function template specializations, and deduction guides used to share the enclosing scope's listing with their primary template. Users have repeatedly reported this as confusing: vector and vector<int> appearing side by side in the namespace index reads as if they were independent siblings, and a primary's variants were nowhere on its own page.

Specializations now appear in a dedicated "Specializations" section on the primary's documentation page, and deduction guides in a "Deduction Guides" section on the deduced class's page. The parent scope's listing carries only the primary itself. An orphan specialization (one whose primary has been excluded from extraction) stays in the parent's listing so the index can still reach it.

The primary-to-variant link is corpus-resident: RecordSymbol and FunctionSymbol gain Specializations (and, on records, DeductionGuides) lists plus an IsListedOnPrimary flag, populated by a new SpecializationFinalizer pass. Every Handlebars template, schema consumer, and downstream tool that reads the corpus sees the new shape automatically through reflection.

Closes issue #1154.

[danger skip docs].


Summary

Moves class template specializations, function template specializations, and deduction guides off the enclosing scope's listing and onto the primary template's own documentation page. Closes #1154.

Specializations and primaries used to share the enclosing scope's listing, so vector and vector<int> appeared side by side in the namespace index. Readers reported this as confusing, since the two are not independent siblings, and the primary's own page did not mention its variants at all.

After this PR:

  • Each primary's documentation page gains a "Specializations" section listing the specializations of that primary.
  • Each deduced class's page gains a "Deduction Guides" section listing the guides that deduce it.
  • The parent scope's listing carries only the primary, not its variants.
  • An orphan specialization (one whose primary has been excluded from extraction) stays in the parent's listing, so the index can still reach it.

The primary-to-variant link is corpus-resident: RecordSymbol and FunctionSymbol gain Specializations (and, on records, DeductionGuides) lists plus an IsListedOnPrimary flag. Populated by a new SpecializationFinalizer pass. Every Handlebars template, schema consumer, and downstream tool that reads the corpus sees the new shape automatically through reflection.

Changes

  • Source: New finalizer at src/lib/Metadata/Finalizers/SpecializationFinalizer.{cpp,hpp} populates the back-pointers and sorts them. include/mrdocs/Metadata/Symbol/Function.hpp and include/mrdocs/Metadata/Symbol/Record.hpp declare the new fields and add them to their MRDOCS_DESCRIBE_STRUCT lists so reflection picks them up. src/lib/Support/Handlebars.cpp gains a filter_out helper (mirroring the existing filter_by) used by the parent-tranche template to drop entries listed elsewhere. Templates updated under share/mrdocs/addons/: common/partials/symbol/tranche.hbs (the per-tranche listing that no longer prints specializations alongside the primary), and the per-format adoc/partials/symbol.adoc.hbs and html/partials/symbol.html.hbs rendering the new "Specializations" / "Deduction Guides" sections.
  • Tests: All exercised through golden tests; no separate unit tests added.
  • Golden tests: HTML and adoc golden-test fixtures updated across test-files/golden-tests/symbols/, test-files/golden-tests/config/, test-files/golden-tests/templates/, test-files/golden-tests/javadoc/, and test-files/golden-tests/filters/ to reflect the new layout: specializations and deduction guides moved off parent listings and onto primary pages. The .xml fixtures also gain new tags (<specializations>, <deduction-guides>, <is-listed-on-primary>) emitted by the reflection-driven XML writer for the new fields. Each updated fixture is an intentional rendering or schema-output change, not a regeneration.
  • Breaking changes: None at the user-facing CLI. The XML output and metadata schema gain additive elements (<specializations>, <deduction-guides>, <is-listed-on-primary>); readers that don't know about them are unaffected. The rendered HTML / adoc output for templates changes: downstream tooling that scrapes the parent scope's listing and expects to find specializations alongside the primary will need to look on the primary's own page instead.

Testing

  • The updated HTML, adoc, and XML golden fixtures are themselves the verification: each runs the full pipeline against a fixed .cpp input and asserts the resulting output byte-for-byte. The new SpecializationFinalizer is exercised through these.
  • No CI workflow changes needed: the existing golden-test job runs all of test-files/golden-tests/ on every build, so the new expectations stay enforced going forward.

Documentation

No user-facing documentation page is added ([danger skip docs]). The change is a layout / discoverability improvement for the generated documentation: it does not introduce a new option, flag, or configuration knob a user would need to look up. The new "Specializations" / "Deduction Guides" sections are self-describing in the rendered output.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 13, 2026

✨ Highlights

  • 🧪 Existing golden tests changed (behavior likely shifted)

🧾 Changes by Scope

Scope Lines Δ% Lines Δ Lines + Lines - Files Δ Files + Files ~ Files ↔ Files -
🥇 Golden Tests 88% 3049 2568 481 99 - 99 - -
🛠️ Source 12% 418 409 9 10 2 8 - -
🏗️ Build <1% 9 7 2 1 - 1 - -
Total 100% 3476 2984 492 110 2 108 - -

Legend: Files + (added), Files ~ (modified), Files ↔ (renamed), Files - (removed)

🔝 Top Files

  • test-files/golden-tests/symbols/record/class-template-specializations-1.xml (Golden Tests): 364 lines Δ (+364 / -0)
  • test-files/golden-tests/symbols/record/class-template-specializations-1.html (Golden Tests): 352 lines Δ (+221 / -131)
  • test-files/golden-tests/symbols/record/class-template-specializations-1.adoc (Golden Tests): 224 lines Δ (+136 / -88)

Generated by 🚫 dangerJS against 8094506

@codecov
Copy link
Copy Markdown

codecov Bot commented May 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.12%. Comparing base (74bf1a3) to head (8094506).

Additional details and impacted files
@@           Coverage Diff            @@
##           develop    #1199   +/-   ##
========================================
  Coverage    82.12%   82.12%           
========================================
  Files           33       33           
  Lines         3149     3149           
  Branches       734      734           
========================================
  Hits          2586     2586           
  Misses         387      387           
  Partials       176      176           
Flag Coverage Δ
bootstrap 82.12% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cppalliance-bot
Copy link
Copy Markdown

cppalliance-bot commented May 13, 2026

An automated preview of the documentation is available at https://1199.mrdocs.prtest2.cppalliance.org/index.html

If more commits are pushed to the pull request, the docs will rebuild at the same URL.

2026-05-25 09:24:03 UTC

@gennaroprota gennaroprota force-pushed the feat/move_template_specializations_and_deduction_guides_off_parent_scope_page branch 3 times, most recently from fb11020 to 956839a Compare May 19, 2026 06:52
@alandefreitas
Copy link
Copy Markdown
Collaborator

This is adding exceptions to reflection. It’s reintroducing the exact problem we were fixing with reflection. It’d be easier to just store the specialization and deduction guides again in the corpus as members of the records and functions. This way, everyone has this data readily available in O(1), and the template changes are trivial. If we need exceptions to reflection, it means the data model is bad.

@alandefreitas
Copy link
Copy Markdown
Collaborator

Also, since we touched on this topic, I think we ended up forgetting about the lazy DOM object directly based on reflection. That could make things much faster.

@gennaroprota gennaroprota force-pushed the feat/move_template_specializations_and_deduction_guides_off_parent_scope_page branch 2 times, most recently from 5dec630 to cc36d55 Compare May 20, 2026 09:09
@gennaroprota
Copy link
Copy Markdown
Collaborator Author

Please check the new approach. As it concerns building DOM objects lazily, I opened an issue: #1211.

@alandefreitas
Copy link
Copy Markdown
Collaborator

Please check the new approach

What is it?

@gennaroprota
Copy link
Copy Markdown
Collaborator Author

gennaroprota commented May 21, 2026

Please check the new approach

What is it?

It adds new members to RecordSymbol (specifically, IsSpecialization, Specializations and DeductionGuides) and FunctionSymbol (IsSpecialization, Specializations), listing them in their MRDOCS_DESCRIBE_STRUCT invocations, so reflection picks them up everywhere. A new SpecializationFinalizer appends the specialization IDs to the primary's Specializations, sets IsSpecialization=true on specializations, and likewise appends each deduction guide ID to the deduced record's DeductionGuides. This is for each regular specialization whose primary is also regular. "Orphan specializations", i.e. specializations whose primary is excluded from extraction, keep IsSpecialization=false and, thus, stay in the parent scope listing, as before.

Of course, the bespoke (! :-)) tag_invoke overloads on RecordSymbol and FunctionSymbol, and the lazy inverse-index in DomCorpus, are gone.

Copy link
Copy Markdown
Collaborator

@alandefreitas alandefreitas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ASTVisitor can already know if something is a specialization. In fact, even the templates know this in O(1), but I was surprised I didn't see this convenience flag set during the extraction phase, since it needs no info about other types.

Unless you are foreseeing a case I didn't think about: what happens with a library that provides specializations for symbols outside itself. For instance, if my library provides a specialization for std::hash, where would I see this specialization under the new model?

specializations (primary excluded from extraction) keep
the flag `false` so they remain reachable from the parent.
*/
bool IsSpecialization = false;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This one seems redundant. Doesn't the template info give us that in O(1)?

{{>symbol/members-table members=tranche.namespaces title="Namespaces"}}
{{>symbol/members-table members=tranche.namespaceAliases title="Namespace Aliases"}}
{{>symbol/members-table members=(concat tranche.records tranche.typedefs) title=(concat (select label (concat label " ") "") "Types")}}
{{>symbol/members-table members=(concat (reject_by tranche.records "isSpecialization") tranche.typedefs) title=(concat (select label (concat label " ") "") "Types")}}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is precisely the issue we discussed in the other PR, where we noticed that removing the "is*" items makes it more inconvenient. We need a solution to pass predicates to these kinds of helpers or we'll have to keep creating is* members. But is* members is a problem for reflection.


hbs.registerHelper("filter_by", filter_by_fn);

static auto reject_by_fn = dom::makeVariadicInvocable([](
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never seen a view by this name. Maybe "remove_if"? We'd have to check what the options are and how they compare to what we have (which are Python-inspired right now). But I've never seen this name for this operation.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful pass. All four points addressed.

(1) Function.hpp + the extraction-phase observation + the std::hash question

IsSpecialization was badly named: it didn't mean "is this a template specialization", which the AST already answers in O(1) via Template->specializationKind(). It actually meant:

(a) the symbol is a template specialization, and (b) its primary is being extracted in Regular mode.

I've renamed the field to IsListedOnPrimary and expanded the doc-comment on both RecordSymbol and FunctionSymbol to spell out both conditions and call out the presentation-layer purpose.

std::hash<MyType> exercises exactly the orphan branch: std::hash isn't extracted, so condition (b) fails, the flag stays false, the specialization stays in your library's namespace listing.

(2) The is*-as-filter pattern

Renaming the field to isListedOnPrimary is a partial mitigation - the template now reads "drop the entries that are listed on a primary's page," which at least is no longer encoding a structural property as a Boolean field. But the deeper point stands: filtering by is* fields is a recurring tax on the data model.

For this PR, I'm keeping the renamed flag - the predicate-passing redesign is its own design problem (subexpressions in Handlebars, how it composes with the existing filter_by/filter_out shape, implications for the lazy-DOM work in #1211). Opened #1213 to track it, so the conversation has a home.

(3) The helper name

Renamed reject_by to filter_out, which mirrors the filter_* prefix of the existing filter_by.

@gennaroprota gennaroprota force-pushed the feat/move_template_specializations_and_deduction_guides_off_parent_scope_page branch 2 times, most recently from f93594e to ff4f6fc Compare May 23, 2026 07:21
@alandefreitas
Copy link
Copy Markdown
Collaborator

Thanks. The PR is going to look great.

Regarding 1) and 2), this is much better. Do specializations provide cheap access to the primary? Is this something we need? And if so, wouldn't IsListedOnPrimary be also O(1)? I agree it's O(1), but annoying to calculate from templates. But it's something worth discussing, and this seems to come up recurrently.

I think there was also a question about ASTVisitor, but I guess the answer is implied in the change of semantics. Let me know if I'm wrong.

Regarding 3) I think this is much better. But I still don't think this matches anything I've seen in any programming language. Especially in our case, where we're (mostly) taking inspiration from Python and JavaScript (so users can more easily remember the helper names). It's also uncommon to have a double negation in a function name.

To give historical context, the problem here is that we're trying to replicate the names used in Python and JavaScript. And in both of these languages, we have a function called filter that takes a predicate, and we changed it to filter_by in our context because in Handlebars, it is very hard to pass a predicate as a parameter, though it's not impossible. So the by suffix means it's going to create a logical predicate that compares with this member. This is another common convention.

The problem now is that it's hard to apply this pattern for the negative because these languages don't have a function to filter the negative. What you do in these languages is you use the same filter function, and then you pass the negated predicate. But we can't pass the negated predicate, so we need yet another name. But all names are problematic. filter_out breaks conventions many times because there's nothing we can say we're taking inspiration from, there's no by to indicate there's no predicate, it's a double negation once you have the parameters. remove_if/erase_if (remove_if_by/erase_if_by by our convention for no predicates), the C++ names for that, don't have the double negation problem but break the whole filter pattern.

I think some languages have filter_not (actually filterNot), which would lead to filter_not_by. And, in fact, I think Laravel and Ruby use reject (i.e., reject_by) in our case. Given your _by convention, the two strongest candidates are:

  1. reject_by: mirrors filter_by, single word, no negation, backed by Laravel and Ruby. It actually reads naturally: "filter by name" / "reject by name."
  2. filter_not_by: makes the relationship to filter_by syntactically obvious

To be honest, I think I would just apologize and go with reject_by again. What do you think?

@gennaroprota
Copy link
Copy Markdown
Collaborator Author

gennaroprota commented May 25, 2026

On IsListedOnPrimary being O(1): yes. Template->Primary is a SymbolID, and corpus.find() is hash-keyed, so a specialization has O(1) access to its primary. We do exercise that today - SpecializationFinalizer uses exactly that path to walk from each specialization to its primary, and the primary-to-specializations back-pointer is the corollary. IsListedOnPrimary is then (specializationKind != Primary) && find(Primary)->Extraction == Regular. Two pointer chases.

The reason it's a corpus field today is purely that templates can't conveniently express the second half. Handlebars has no "look up a symbol by ID" helper, so although the computation is O(1) in C++ terms, from a template's perspective it might as well be impossible without a precomputed value. That's exactly the recurrent pattern you're pointing at: any time we want a template to make a decision that depends on another symbol, we end up baking the answer into the corpus as an is* field.

#1213 is where the design problem lives, and where the corpus-lookup helper would land alongside predicate-passing in filter_by/reject_by. Once both are in, IsListedOnPrimary becomes deletable: the template asks the question itself, with the same two pointer chases, no schema growth.

On the ASTVisitor question: you're right - the change of semantics is exactly the answer. The flag's value depends on the primary's Extraction status, which the AST visitor can't know yet when it walks a specialization (the extraction policy isn't decided per-symbol-in-isolation). The finalizer pass is the first stable place to compute it.

On reject_by vs. filter_out: yes, I agree; the _by convention is the deciding lens. filter_out ends up being a non-predicate function with no _by marker and a built-in double-negation when read with its parameter. reject_by mirrors filter_by exactly and has Ruby plus Laravel precedent. Renaming back; :-).

…cope page

Class template specializations, function template specializations, and
deduction guides used to share the enclosing scope's listing with their
primary template. Users have repeatedly reported this as confusing:
`vector` and `vector<int>` appearing side by side in the namespace index
reads as if they were independent siblings, and a primary's variants
were nowhere on its own page.

Specializations now appear in a dedicated "Specializations" section on
the primary's documentation page, and deduction guides in a "Deduction
Guides" section on the deduced class's page. The parent scope's listing
carries only the primary itself. An orphan specialization (one whose
primary has been excluded from extraction) stays in the parent's listing
so the index can still reach it.

The relationship is stored in the corpus: each primary record or
function template carries the IDs of its specializations (and, for class
templates, of its deduction guides), and each specialization carries an
`IsSpecialization` flag. The lists are populated by a finalizer pass and
exposed via reflection, so the XML output gains matching
`<specializations>`, `<deduction-guides>`, and `<is-specialization>`
elements and the DOM/Handlebars layer renders the new sections through
plain field access.

Closes issue cppalliance#1154.
@gennaroprota gennaroprota force-pushed the feat/move_template_specializations_and_deduction_guides_off_parent_scope_page branch from ff4f6fc to 8094506 Compare May 25, 2026 09:14
@alandefreitas
Copy link
Copy Markdown
Collaborator

One more idea we could explore is having some kind of trait that lets us express/describe computed properties for objects, in addition to the object's own properties. This way, we can define operations as functions in the .hpp of that symbol and MRDOCS_DESCRIBE_WHATEVER_NAME_FOR_THIS the list of computed properties of that type. This way, we have a clear separation of things. The XML generator can ignore the computed properties because they're cheap to compute, and the Handlebars generators can receive the symbol with their predetermined computed options that are cheap and annoying to compute. Because these are properly described, the schema file would reflect them perfectly with and without it for the Handlebars schema and for the XML schema. The documentation could even make the distinction clear.

@gennaroprota
Copy link
Copy Markdown
Collaborator Author

Nice. Yet another idea could be using the same trick as MRDOCS_DESCRIBE_KINDS. We would have a mrdocs_computed_properties_fn(T**) returning a list of {name, getter} descriptors, where each getter is a free function T const & (and possibly DomCorpus const*) -> value. That would give us:

  • No new per-type macro: just free functions, plus one declaration to make the list visible to reflection.
  • A replacement for the existing custom tag_invoke overloads: each io.map(name, expr) in SymbolBase.hpp becomes a free function with the same body. The DOM iterates the discovered list.
  • Schema-awareness: the XML writer skips it (as computed properties are derived information), the Handlebars schema includes it, the documentation can distinguish.

@alandefreitas
Copy link
Copy Markdown
Collaborator

Nice. Yet another idea could be using the same trick as MRDOCS_DESCRIBE_KINDS. We would have a mrdocs_computed_properties_fn(T**) returning a list of {name, getter} descriptors, where each getter is a free function T const &

Yes. Maybe I didn't express myself very well, but that's exactly what I had in mind (maybe MRDOCS_DESCRIBE_COMPUTED_PROPERTIES). Then all of the important "is_" functions can just become computed properties. This is important because even when we have helpers that take predicates, these would be much slower than using these computed properties directly. So this would be extremely useful for the default templates, which we don't want to be slow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

specializations and deduction guides should be listed in context

3 participants