Skip to content

Commit 5e50779

Browse files
gennaroprotasdarwin
authored andcommitted
Add Gennaro's 2026 Q1 update
1 parent cb08461 commit 5e50779

1 file changed

Lines changed: 154 additions & 0 deletions

File tree

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
---
2+
layout: post
3+
nav-class: dark
4+
categories: gennaro
5+
title: "Mr.Docs: Niebloids, Reflection, Code Removal, New XML Generator"
6+
author-id: gennaro
7+
author-name: Gennaro Prota
8+
---
9+
10+
This quarter, I focused on two areas of Mr.Docs: adding first-class support for
11+
function objects, the pattern behind C++20 Niebloids and Ranges CPOs, and
12+
overhauling how the tool turns C++ metadata into documentation output (the
13+
reflection layer).
14+
15+
## Function objects: documenting what users actually call
16+
17+
In modern C++ libraries, many "functions" are actually global objects whose type
18+
has `operator()` overloads. The Ranges library, for instance, defines
19+
`std::ranges::sort()` not as a function template but as a variable of some
20+
unspecified callable type. Users call it like a function and expect it to be
21+
documented like one. Before this quarter, Mr.Docs didn't know the difference: it
22+
would document the variable and its cryptic implementation type.
23+
24+
The new function-object support (roughly 4,600 lines across 38 files) bridges
25+
this gap. When Mr.Docs encounters a variable whose type is a record with no
26+
public members but `operator()` overloads and special member functions, it now
27+
synthesizes free-function documentation entries named after the variable. The
28+
underlying type is marked implementation-defined and hidden from the output.
29+
Multi-overload function objects are naturally grouped by the existing overload
30+
machinery. So, given:
31+
32+
```cpp
33+
struct abs_fn {
34+
double operator()(double x) const noexcept;
35+
};
36+
inline constexpr abs_fn abs = {};
37+
```
38+
39+
Mr.Docs documents it as simply:
40+
41+
```
42+
double abs(double x) noexcept;
43+
```
44+
45+
For cases where auto-detection isn't quite right — for example, when the type
46+
has extra public members — library authors can use the new `@functionobject` or
47+
`@functor` doc commands. There is also an `auto-function-objects` config option
48+
to control the behavior globally. The feature comes with a comprehensive test
49+
fixture covering single and multi-overload function objects, templated types,
50+
and types that live in nested `detail` namespaces.
51+
52+
## Reflection: from boilerplate to a single generic template
53+
54+
The bigger effort — and the one that kept surprising me with its scope — was the
55+
reflection refactoring. Mr.Docs converts its internal C++ metadata into a DOM (a
56+
tree of lazy objects) that drives the Handlebars template engine. Before this
57+
quarter, every type in the system required a hand-written `tag_invoke()`
58+
overload: one function to map the type's fields to DOM properties, another to
59+
convert it to a `dom::Value`. Adding a new symbol kind meant touching half a
60+
dozen files and following a pattern that was easy to get wrong.
61+
62+
The goal was simple to state: replace all of that with a single generic template
63+
that works for any type carrying a describe macro.
64+
65+
### Phase 1: Boost.Describe
66+
67+
The first attempt used Boost.Describe. I added `BOOST_DESCRIBE_STRUCT()`
68+
annotations to every metadata type and wrote generic `merge()` and
69+
`mapReflectedType()` templates that iterated over the described members. This
70+
proved the concept and eliminated a great deal of boilerplate. However, we
71+
didn't want a public dependency on Boost.Describe, which meant the dependency
72+
was hidden in .cpp files and couldn't be used in templates living in public
73+
heades,
74+
75+
### Phase 2: custom reflection macros
76+
77+
So I wrote our own. `MRDOCS_DESCRIBE_STRUCT()` and `MRDOCS_DESCRIBE_CLASS()`
78+
provide the same compile-time member and base-class iteration as Boost.Describe,
79+
but with no external dependency. The macros live in `Describe.hpp` and produce
80+
`constexpr` descriptor lists that the rest of the system iterates with
81+
`describe::for_each()`.
82+
83+
### Phase 3: removing the overloads
84+
85+
With the describe macros in place, I could write generic implementations of
86+
`tag_invoke()` for both `LazyObjectMapTag` (DOM mapping) and `ValueFromTag`
87+
(value conversion), plus a generic `merge()`. Each one replaces dozens of
88+
per-type overloads with a single constrained template. The `mapMember()`
89+
function handles the dispatch: optionals are unwrapped, vectors become lazy
90+
arrays, described enums become kebab-case strings, and compound described types
91+
become lazy objects — all automatically.
92+
93+
Removing the overloads was not as straightforward as I had hoped. The old
94+
overloads were entangled with:
95+
96+
- **The Handlebars templates**, which assumed specific DOM property names.
97+
Renaming `symbol` to `id`, `type` to `underlyingType`, and `description` to
98+
`document` required updating templates and golden tests in lockstep.
99+
- **The XML generator**, which silently skipped types that weren't described.
100+
Adding `MRDOCS_DESCRIBE_STRUCT()` to `TemplateInfo` and `MemberPointerType`
101+
made the XML output more complete, requiring schema updates and golden-test
102+
regeneration.
103+
104+
### The result
105+
106+
Out of the original 39 custom `tag_invoke(LazyObjectMapTag)` overloads, only 7
107+
remain — each with genuinely non-reflectable logic (computed properties,
108+
polymorphic dispatch, or member decomposition). Roughly 60
109+
`tag_invoke(ValueFromTag)` boilerplate overloads were also removed. Adding a new
110+
metadata type to Mr.Docs now requires nothing beyond `MRDOCS_DESCRIBE_STRUCT()`
111+
at the point of definition.
112+
113+
## The XML Generator: a full rewrite in 350 lines
114+
115+
The XML generator was the first major payoff of the reflection work (although it
116+
was initially done when we were using Boost.Describe). The old generator had its
117+
own hand-written serialization for every metadata type, completely independent
118+
of the DOM layer. It was a parallel set of per-type functions that had to be
119+
kept in sync with every schema change.
120+
121+
I replaced it with a generic implementation built entirely on the describe
122+
macros. The core is about 350 lines: `writeMembers()` walks `describe_bases` and
123+
`describe_members`, `writeElement()` dispatches on type traits for primitives,
124+
optionals, vectors, and enums, and `writePolymorphic()` handles the handful of
125+
type hierarchies (`Type`, `TParam`, `TArg`, `Block`, `Inline`) via
126+
.inc-generated switches. The old generator needed a new function for every type;
127+
the new one handles them all, and the 241 files changed in that commit were
128+
almost entirely golden-test updates reflecting the now-more-complete and totally
129+
changed output.
130+
131+
## Smaller fixes
132+
133+
Alongside the two main efforts, I fixed several bugs that came up during
134+
development or were reported by users:
135+
136+
- Markdown inline formatting (bold, italic, code) and bullet lists were not
137+
rendering correctly in certain combinations.
138+
- `<pre>` tags were missing around HTML code blocks.
139+
- `bottomUpTraverse()` was silently skipping `ListBlock` items, causing
140+
doc-comment content to be lost.
141+
- Several CI improvements: faster PR demos, better failure detection, increased
142+
test coverage for the XML generator.
143+
144+
## Looking ahead
145+
146+
The reflection infrastructure is now in good shape, and most of the mechanical
147+
boilerplate is gone. The remaining `tag_invoke()` overloads are genuinely custom
148+
— they compute properties that don't exist as C++ members, or they dispatch
149+
polymorphically across type hierarchies. Those are worth keeping. Going forward,
150+
I'd like to explore whether the describe macros can replace more of the manual
151+
visitor code throughout the codebase.
152+
153+
As always, feedback and suggestions are welcome — feel free to open an issue or
154+
reach out on Slack.

0 commit comments

Comments
 (0)