feat: support Lua and JavaScript extensions#1196
Conversation
✨ Highlights
🧾 Changes by Scope
🔝 Top Files
|
ef7ea6b to
2156b1c
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## develop #1196 +/- ##
========================================
Coverage 82.12% 82.12%
========================================
Files 33 33
Lines 3149 3149
Branches 734 734
========================================
Hits 2586 2586
Misses 387 387
Partials 176 176
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
An automated preview of the documentation is available at https://1196.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-27 08:44:05 UTC |
e58fb63 to
075f7bc
Compare
There was a problem hiding this comment.
The PR is great. It's really nice to see we already have examples that transform the corpus via the scripts.
I don't see much value in putting Lua scripts as Handlebars helpers (a cheap improvement win in an extension type that already exists) with Corpus-mutation extensions in Lua and JavaScript (a completely new feature that's huge).
a new
Describe.hpp
Doesn't it already exist? And wasn't the distinction between Describe.hpp and other headers related to reflection that Describe.hpp would only be about porting Boost.Describe?
MRDOCS_DESCRIBE_HIERARCHY
I believe this needs better in-source documentation. We even support that now (or are about to).
I also don't understand these *Hierarchy.hpp files that much. The PR describes that they exist, but not why they exist. All I know is they're using this pattern of including everything, which defeats the purpose of public headers. I also don't see why the user ever needs that information to be public.
They also seem to defeat the purpose of things in a sense. Because we're trying to avoid manually listing things with reflection, but we are still doing it there. The typical pattern we use for that is a single source of truth as inc files.
I also don't understand how a hierarchy can be described as a list unless there's some special convention about this list.
Tests
There's enough new functionality (even public functionality) here that seems to deserve unit tests.
Golden tests
Do they really cover everything that's possible? What are the typical use cases for this? Do we get errors on invalid transformations? How do we merge symbols in the corpus? How do you add symbols to the corpus?
Third-party:
third-party/patches/lua/CMakeLists.txtupdated for the vendored Lua build.
What do you mean by vendored Lua build here?
- No CI workflow changes needed — the existing golden-test job runs all of
test-files/golden-tests/on every build, so the new trees stay covered automatically going forward.
Documentation
I think the documentation should make the same distinction you made here. It could maybe be described as corpus-mutation extensions or something. Because we have this concept of extensions. Then, things people understand as extensions are these customizations of the template system and the corpus-mutation extensions. They are very different layers and share a common logic of knowing where to put addons, etc. They also have to know about supplemental addons, which they're probably going to use the most. The documentation concerns the user journey.
Also, any reference should be generated automatically. I don’t understand the API very well. Is mrdocs.set the only function we have?
I also don’t see the pattern we had discussed about taking inspiration from Lua/Darktable. It seems the new extension system uses the same pattern as the helpers extension system, but that pattern is not appropriate there. What if an extension wants to provide different functionalities? At a minimum, it's important to include a comparison and an explanation of why the PR chose a different strategy.
Is that the best Python and Lua API we could have on the Lua/JS side? What are the alternatives? They look very different from what things look like on the C++ side. Are they extendable, or will we need to break the Lua/JS API every time there’s a new feature?
The "Corpus argument" section describes a subset of a symbol's API (I don't really understand why, since the full reference is elsewhere), but the API of the corpus object itself isn't described.
|
Thanks for the thorough review. Great stuff in there :-). Here's a point by point reply:
Open decisions for you:
|
22b6462 to
f68a074
Compare
Up to you. I'll let you come up with the best design you can and we can iterate on it. |
f68a074 to
19fa6fa
Compare
I moved the new macro out of Describe.hpp (which now only contains the Boost.Describe port) and renamed it to |
|
Thanks for the iteration. Mostly looking good. In the branch: the Still open from our back-and-forth:
A few things from the original review that didn't get picked up in the back-and-forth:
One thing I'd like us to revisit before we close: "intentionally not supported, structural changes break invariants" on merging and adding symbols is technically true but too compact to act on. I understand there's a trade-off here between not breaking invariants and being able to perform anything useful. But I don't know what exactly is the trade-offs and what operations can help us escape the trade-off by ensuring invariants aren't broken. Because a transform extension that only receives constants as arguments is not useful for anything. To pick the right boundary we'd need to formalize in docs:
It's OK to leave things for the future. It's not OK to not be clear about what we have right now and where we're going with this. And without that written down in the docs, any group discussion in the channel will derail once more people join, since nobody's reasoning from the same data. Maybe some of that could become its own section in #1210, or a sibling design issue. Thanks for what we got here so far 😁 I know this is a much more complex feature than anything else we've been doing so far. |
|
Here's another review by claude:https://gist.github.com/alandefreitas/837bfc0f10e70fce33feffff5527b35a |
6875227 to
85ae3ec
Compare
|
I addressed all your and Claude's points. In particular, the new "Invariants and operations" section of extensions.adoc should answer your eight questions above. |
|
Thanks, @gennaroprota! Reviewing right away. |
|
Great! 😊🤩 Small things still worth confirming before merge:
That's it on my end. |
|
Thanks, Alan :-).
|
The `__index` metamethod in `domObject_push_metatable()` retrieved the value correctly via `Object::get(key)`, then called `lua_replace(L, 1)` to move the result into the userdata's slot. `lua_replace` also pops the top, so, on return, the key string was at the top of the stack and Lua picked it up as the metamethod's single return value, making every field access on a `dom::Object` userdata silently return the key it was asked for. This was latent until now because no Lua script in the test suite previously read fields off a `dom::Object` userdata. Surfaced while wiring corpus extensions: a script doing `corpus.symbols[i]` saw `"symbols"` (the key) instead of the array.
This mirrors the existing JS helpers for Lua. *.lua files placed in an
addon's generator/{common|<ext>}/helpers/ directory are auto-registered
as Handlebars helpers; files whose name starts with '_' run first as
utility scripts. Two golden fixtures (lua-helper/, lua-helper-layering/)
mirror their JS counterparts and cover the `addons-supplemental`
override.
Incidental fixes to issues uncovered by this patch:
- Added a qualification to `MRDOCS_TRY` / `MRDOCS_CHECK_*` /
`MRDOCS_CHECK_OR_*` / `MRDOCS_CHECK_OR_CONTINUE` to make them work
with nested namespaces named `detail`.
- Dropped onelua.c and ltests.c from the Lua build patch, because the
former defines `main`, which conflicted with our `main`, and the
latter is test scaffolding which shouldn't ship in a library build.
- Added `extern "C"` around the Lua includes.
Add a `MRDOCS_DESCRIBE_KINDS` macro that registers the closed set of
most derived classes ("kinds") of a polymorphic base, and apply it to
every polymorphic base in MrDocs. Generic code can then dispatch over
the closed set (`describe::for_each(describe::describe_kinds<Base>{},
...)`) without the per-base X-macro boilerplate every consumer would
otherwise need.
The macro and its supporting machinery live in a dedicated
`DescribeKinds.hpp` header, so consumers that only need
`MRDOCS_DESCRIBE_STRUCT`, `MRDOCS_DESCRIBE_CLASS`, and
`MRDOCS_DESCRIBE_ENUM` keep a slimmer include. Each per-base
registration lives in a small private include under src/lib/Metadata/
fed by the existing *Nodes.inc X-macro files, so the registered set has
one source of truth; the kind information is a compile-time
consumer-side concern, so no public header exposes it.
docs/mrdocs.yml excludes the reflection helpers from the generated
reference.
Run user-provided scripts under <addons>/extensions/<name>.{lua,js}
between corpus finalization and the first generator invocation. Each
script may define a global `transform_corpus(corpus)`; the host calls it
once with a read view of the corpus (the same DOM the generators see)
and the script applies changes through a `mrdocs.set(id, field, value)`
API.
The write surface is a narrow, allowlist-gated setter driven by
reflection: `mrdocs.set` dispatches on the normalized C++ member name,
so the script-facing fields track the C++ model without a hand-written
binding table. It understands strings, booleans, described enums,
`Optional<T>`, `vector<T>`, described structs, and `Polymorphic<T>` (the
`kind:` selector picks a derived class registered with
`MRDOCS_DESCRIBE_KINDS`). The allowlist (`name`, `extraction`,
`isCopyFromInherited`, `loc`, `doc`, `returnType`) is generated at build
time from src/lib/Extensions/AllowedFields.json, so the runtime gate and
the rendered reference table share one source of truth.
The extension stack is split across small translation units:
AddonDiscovery (collect scripts across addon roots), SetMember (the
write surface and the corpus DOM), LuaBinding and JsBinding (the
per-language adapters), and RunExtensions (the orchestrator).
`CorpusImpl` invokes `runExtensions` after finalization. The shared
addonRoots helper used by both the Handlebars layer and the extension
layer moved to src/lib/Support/AddonRoots.hpp; the `dom::Array`
metatable in src/lib/Support/Lua.cpp is now a Lua-convention 1-indexed
sequence so corpus.symbols iterates with ipairs.
Golden fixtures under test-files/golden-tests/extensions/ exercise
rename, brief rewrite, clearing an optional, a `Polymorphic<Type>`
return-type write, script ordering across addon roots, and the silent
skipping of a script that defines no transform_corpus. A unit test
covers the `mrdocs.set` error paths.
Add end-to-end documentation for the Lua and JavaScript scripting surface. extensions.adoc documents the corpus-mutation model: a worked example (synthesizing briefs from a naming convention), the file layout, the `transform_corpus` hook, the `corpus` argument, the `mrdocs.set` API and its allowlist (the table is generated from AllowedFields.json), the lifecycle, an "Invariants and operations" section spelling out the corpus invariants and what scripts can and cannot do today, a "Stability" section stating the extensibility contract, and a "Design rationale" section comparing the alternatives considered for the write surface. addons.adoc is a new shared page for the addon concept (lookup paths, `addons` vs `addons-supplemental`, override vs layering) used by both the helper and the extension layers. generators.adoc gains a `[#custom-helpers]` section covering the JS and Lua Handlebars helpers and the layering model. nav.adoc lists the new addons page.
85ae3ec to
b00938b
Compare
This mirrors the existing JavaScript helpers for Lua, and adds support for corpus-mutation extensions written either in Lua or Javascript.
Adds two related script-extension capabilities and the reflection plumbing they rely on:
<addons>/extensions/*.{lua,js}run between corpus finalisation and the first generator invocation. Each script may expose a globaltransform_corpus(corpus)function; the host calls it once with a read view of the corpus, and the script applies mutations through amrdocs.set(id, field, value)API. The script side is intentionally a narrow, allowlist-gated write surface (name,extraction,isCopyFromInherited,doc,loc,returnType). Reads piggy-back on the same DOM the generators already see, so scripts and templates have one shape to learn.The follow-up design for richer entry points (multiple capabilities per file, paired helpers, generator registration, enable/disable, auto-generated
mrdocs.*reference) lives in issue #1210; this PR ships the simplest rung of that ladder.To make the script-facing types stay in sync with the C++ model without a hand-maintained binding table, the extension layer is driven by reflection:
MRDOCS_DESCRIBE_KINDSmacro registers the closed set of concrete derived classes of a polymorphic base. The macro lives ininclude/mrdocs/Support/DescribeKinds.hpp. Each per-base list lives privately undersrc/lib/Metadata/and is fed by the existing*Nodes.incX-macro files, so the registered set has one source of truth.mrdocs.setbuilds aPolymorphic<Base>from a DOM object whosekind:field picks a concrete derived class registered withMRDOCS_DESCRIBE_KINDS.A
dom::Objectfield-lookup bug surfaced during the work (lookup returned the key instead of the value) and is fixed in the same PR.Changes
src/lib/Extensions/AddonDiscovery.{hpp,cpp}-- collect<root>/extensions/*.{lua,js}across addon roots (sorted by full path).src/lib/Extensions/SetMember.{hpp,cpp}-- the entire write surface: allowlist,assignFromDomoverload set,trySetMember,buildPolymorphic,ExtensionState,buildCorpusDom,setMemberImpl. This is the file that a maintainer touching the allowlist or the type machinery edits.src/lib/Extensions/LuaBinding.{hpp,cpp}-- Lua-side glue (state plumbing,mrdocs.setregistration,luaValueToDom).src/lib/Extensions/JsBinding.{hpp,cpp}-- JavaScript-side glue (dom::Function-backedmrdocs.set).src/lib/Extensions/RunExtensions.{hpp,cpp}-- orchestrator: collect scripts then dispatch to the right per-script entry point.CorpusImpl.cppinvokesrunExtensions()after finalisation.addonRoots()helper used by both the Handlebars layer and the extension layer was promoted tosrc/lib/Support/AddonRoots.hpp(a previous copy insrc/lib/Gen/hbs/AddonPaths.hppis gone).src/lib/Support/Lua.cpp'sdom::Arraymetatable is now a Lua-convention 1-indexed sequence (arr[1]is first,#arris the length,ipairs/pairswork).src/lib/Gen/hbs/Builder.cppupdated to load Lua helpers alongside JS ones; newinclude/mrdocs/Support/DescribeKinds.hppdefines theMRDOCS_DESCRIBE_KINDSmacro and its trait/query types; per-base kind lists undersrc/lib/Metadata/; small additions toinclude/mrdocs/Support/MergeReflectedType.hpp,Expected.hpp, andTypeTraits.hpp;dom::Objectfield-lookup fix.src/test/Extensions/SetMember.cppcovers themrdocs.seterror paths: argument-shape errors, unknown id, off-allowlist field, allowlisted-but-missing-on-this-symbol-kind, type mismatch for string/bool/enum fields, unknown enum name, polymorphic-write errors (not-an-object, missingkind, unknown kind, unknown sub-field), unknown sub-field on a struct field, and the happy path forOptional<T>null-reset.src/test/Support/DescribeKinds.cppcovers thehas_describe_kindstrait and thefor_each(describe_kinds<Base>{}, ...)iteration in both the variadic and the BEGIN/END forms of the registration macro.extensions/{js-set-name,lua-set-name,lua-set-return-type}/exercise rename, doc rewrite, and aPolymorphic<Type>write. Three more trees lock in claims the docs make:extensions/lua-extension-ordering/(alphabetical-by-full-path ordering across two addon roots),extensions/lua-clear-doc/(mrdocs.set(id, "doc", nil)clears the optional), andextensions/lua-empty-script/(an extension file with notransform_corpusis silently skipped). Newgenerator/hbs/{lua-helper,lua-helper-layering}/exercise Lua Handlebars helpers, including helper layering across multiple addon directories.docs/modules/ROOT/pages/addons.adocdocuments addon discovery (lookup paths,addons,addons-supplemental, override vs. layering semantics).docs/modules/ROOT/pages/extensions.adocdocuments the corpus-mutation extension model end to end, including a worked example, the writable-fields table, an "Invariants and operations" section spelling out which corpus properties the rest of Mr.Docs depends on and which structural operations are/aren't reachable from scripts, a Stability section (the extensibility contract scripts can rely on), and a Design rationale section comparing the four alternatives considered for the write surface and explaining why (D) was picked.docs/modules/ROOT/pages/generators.adocgains a[#custom-helpers]section covering JS and Lua helpers and the layering model.docs/mrdocs.ymlexcludes the reflection helpers from the generated reference.third-party/patches/lua/CMakeLists.txtupdated. The Lua source lives in-tree underthird-party/lua/and is built as part of MrDocs; this file is MrDocs's CMake override for that in-tree build (upstream Lua does not ship aCMakeLists.txt). The diff is to MrDocs's override, not to upstream Lua. The change drops two files from the Lua build:onelua.c(a unity-build entry point that defines its ownmain, which would conflict with MrDocs'smainwhen statically linked) andltests.c(Lua's internal test harness, only valid whenLUA_USER_Henables it -- not meant to ship in a library build).Testing
test-files/golden-tests/extensions/andtest-files/golden-tests/generator/hbs/lua-helper*/execute real Lua and JS scripts against fixed fixtures and assert the.xml,.html, and.adocoutput byte for byte. Any future regression in the helper or extension surface fails these tests.clang.mrdocs.Extensions.SetMember,clang.mrdocs.Support.DescribeKinds) cover, respectively, themrdocs.seterror paths and theMRDOCS_DESCRIBE_KINDStrait and iteration.test-files/golden-tests/on every build, so the new trees stay covered automatically.Documentation
extensions.adocdocuments the corpus-mutation extension model: a worked example (synthesising briefs from a naming convention), file layout, thetransform_corpushook, thecorpusargument, themrdocs.setAPI and its hand-maintained allowlist (with a note pointing at the future auto-generated table on issue Design the extension entry-point ladder beyondtransform_corpus#1210), the lifecycle (where extensions run relative to extraction and rendering), an "Invariants and operations" section, a "Stability" section spelling out the extensibility contract, and a "Design rationale" section comparing four shapes (A-D) considered for the write surface.generators.adocgains the[#custom-helpers]section covering JS and Lua Handlebars helpers, including the underscore-prefix utility convention, the options-object stripping rule, and resolution order.addons.adocis a new shared page covering lookup paths and the override-vs-layering semantics shared by both helpers and extensions.