|
| 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