Standalone contexts link C++ modules: one process-wide registry lifetime, a dense function table, AOT headers for the modules the program calls - #3947
Conversation
dbe2dc2 to
43a817f
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
It changes core AOT emission and process-wide module-registry lifetime semantics, which warrants final human review despite added tests and documentation.
Pull request overview
This PR fixes standalone (-ctx) context generation and runtime teardown when the emitted program reaches C++ modules (handled types / externs), by ensuring the right modules and AOT headers are linked, module registration happens safely with a single process-wide lifetime, and the generated function table is sized/densely indexed to avoid uninitialized rows.
Changes:
- Track linked modules more accurately (callees in used code, handled types) and prune standalone by the linked set (not host
fromExtraDependencystate). - Emit a dense function table (0..N-1 in emission order) and size
Context::functions/totalFunctionsby emitted function count. - Add process-wide standalone module registration support (
standalone_modules.h) plus new fixtures/tests and a full-runtime standalone example.
File summaries
| File | Description |
|---|---|
| tests/aot/test_standalone_emit.das | Adds emission assertions for module linking/registration, dense function table, and DEFAULT_MODULE_ORDER parity. |
| tests/aot/_standalone_handle_fixture.das | Fixture to ensure handled-type-only usage links the owning module. |
| tests/aot/_standalone_fio_fixture.das | Fixture to ensure extern usage links/registers fio and includes its AOT header. |
| tests-cpp/big/standalone_ctx/test_standalone_modules.cpp | C++ test harness for multiple generated contexts sharing one registry lifetime. |
| tests-cpp/big/standalone_ctx/standalone_modules_fixture.das | fio-only generated context used by the multi-context C++ test. |
| tests-cpp/big/standalone_ctx/CMakeLists.txt | Builds/runs the new standalone-modules C++ tests in the small lane (when dasHV enabled). |
| skills/internal/writing_cpp_tests.md | Updates guidance for module-registry ownership with standalone contexts. |
| skills/cpp_integration.md | Documents full-runtime standalone contexts and module-registry ownership rules. |
| nano/README.md | Clarifies that 06_full_runtime is not a nano example and why. |
| install/CLAUDE.md | Updates skill-table description for cpp_integration scope. |
| include/daScript/simulate/standalone_modules.h | New header implementing process-wide standalone module list + one-time init/shutdown. |
| examples/standalone/CMakeLists.txt | Adds in-tree build + small-lane test target for the full-runtime standalone example. |
| examples/standalone/06_full_runtime/service_probe.das | Full-runtime standalone script using dasHV + fio exports. |
| examples/standalone/06_full_runtime/README.md | Explains full-runtime standalone tradeoffs and build/run shape. |
| examples/standalone/06_full_runtime/main.cpp | Driver executable for the full-runtime standalone example. |
| daslib/REVIEW.md | Tightens wording on where daslib↔C++ fact pairs must be recorded. |
| daslib/ARCHITECTURE_EMIT.md | Records new standalone/module-linking invariants (linked-set pruning, default order, dense table, registry lifetime). |
| daslib/aot_standalone.das | Implements dense function table emission and emits/uses standalone module registration tables. |
| daslib/aot_cpp.das | Improves linked-module discovery (callees in used code, handled types) and adds DEFAULT_MODULE_ORDER + registration planning. |
| CMakeLists.txt | Installs the new standalone_modules.h header. |
| CLAUDE.md | Updates cpp_integration skill description in the root skill table. |
| CHANGELIST.md | Adds changelist entry describing the standalone-context improvements. |
Review details
Suppressed comments (2)
examples/standalone/06_full_runtime/main.cpp:31
- Casting the "--child" literal to
char*discards constness and can be undefined behavior if modified; it also tends to warn. Use a mutablechar[]for the literal and pass it directly.
const int code = ctx.run_child(argv[0], (char *)"--child");
tests-cpp/big/standalone_ctx/test_standalone_modules.cpp:40
- Casting the "--child" string literal to
char*discards constness and can be undefined behavior if the callee writes to it; it also tends to trigger compiler warnings. Prefer a mutablechar[]for the argument.
const int code = probe.run_child(self, (char *)"--child");
- Files reviewed: 22/22 changed files
- Comments generated: 4
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
43a817f to
d59f2fa
Compare
There was a problem hiding this comment.
🟡 Changes recommended
UseTypeMarker still records type/module reachability from unused code paths, which can incorrectly prevent standalone module pruning and force extra C++ modules to be linked/registered.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 22/22 changed files
- Comments generated: 1
- Review effort level: Lite
d59f2fa to
9900f5b
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
It changes core standalone AOT emission and process-wide module registry lifetime behavior across both daslib and C++ runtime, which warrants careful human validation across platforms/configurations.
Review details
- Files reviewed: 23/23 changed files
- Comments generated: 1
- Review effort level: Lite
9900f5b to
6105674
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
collectUsedModules can still treat C++ modules as runtime-reached via enum/struct type references in dead code because UseTypeMarker records structs/enums outside inUsedCode, which can reintroduce incorrect module linking/registration for standalone pruning.
Review details
Suppressed comments (1)
daslib/aot_cpp.das:687
UseTypeMarker.markstill records structs/enums regardless ofinUsedCode, andcollectUsedModulesthen treats those types’ owning modules as “runtime reached”. This can incorrectly pull/link/register a C++ module when it’s referenced only from dead code via an enum/struct type (e.g. dasHV defines several enumerations in C++), which undermines standalone pruning in the same way the handled-type case did.
Consider tracking “used-code” structs/enums separately (or adding a usedOnly mode to UseTypeMarker) and have collectUsedModules consult only the used-code sets when deciding which modules are linked/registered, while keeping the existing all-code sets for dumpDependencies filtering.
if (decl.baseType == Type.tStructure) {
assert(decl.structType != null);
if (!(useStructs |> key_exists(decl.structType))) {
useStructs.insert(decl.structType);
for (fld in decl.structType.fields) {
- Files reviewed: 23/23 changed files
- Comments generated: 0 new
- Review effort level: Lite
…ters what it links through one process-wide registry lifetime, the function table is dense, and modules the program calls keep their AOT headers A `-ctx` program reaching dasHV or fio did not build, and the fixture that did build crashed on teardown. Three generator defects, one shape: - `getRequiredModulesFor` pruned builtin modules as compile-time-only: externs carry no `used` flag, so `collectUsedModules` never saw them and the generated C++ called `builtin_popen_argv` with no `aot_builtin_fio.h`. `UseTypeMarker` now reads the callee of every call, operator and `@@` address inside used functions and globals, and marks handled types; once a program reaches a C++ module beyond the builtin one, the builtin C++ modules the compiler loaded plus every linked module's dependency closure join the set - module constructors `Module::require` those by name. - The ctor sized `context.functions` by the program's `totalFunctions` but filled only the rows the context emits; the destructor's shutdown walk read uninitialized rows. The table is dense now: rows numbered in emission order, sized by that count. - Handled types resolve through the bound module registry, which a standalone context never bound. Each generated TU adds its linked C++ modules to a process-wide list before main (`standalone_modules.h`); the first context constructed registers the union the registry does not hold, in `DEFAULT_MODULE_ORDER` (the C++ registrar's order) then dependencies-first, and initializes once; a single static destructor shuts down with `Module::ShutdownStandalone` - `Module::Shutdown` resets the fusion engine through a pointer only the interpreter installs. Two contexts with different module sets in one binary share that lifetime, and a host that registered every module first keeps its own. A program reaching no C++ module beyond builtin generates what it did before, so the nano examples stay registry-free. `examples/standalone/ 06_full_runtime` (dasHV + fio, static, compiles nothing at run time, loads no shared module) is the worked example and a small-lane ctest; `tests-cpp/big/standalone_ctx/test_standalone_modules` puts two contexts in one binary, with and without a pre-registering host; and `tests/aot/test_standalone_emit.das` pins the emitted registration, the dense table, the pruning, and `DEFAULT_MODULE_ORDER` against `src/builtin/modules.cpp`. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
6105674 to
b7ddddd
Compare
There was a problem hiding this comment.
🟡 Changes recommended
The new service_probe.das uses := for a string assignment (req.url := url), which violates LINT016 and should be changed to = (or clone_string when needed).
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 23/23 changed files
- Comments generated: 1
- Review effort level: Lite
Why. A
-ctxstandalone context that reaches a C++ module (dasHV, fio, any handled type) did not compile, and the one that did crashed on teardown: the generator pruned the module's AOT header as compile-time-only, sized the function table by the program's count while filling only the rows it emitted, and never bound the module registry that handled types resolve through.What changes.
collectUsedModulesmarks the module of every callee inside used functions and globals (externs carry nousedflag) and of every handled type a used function or global names; once a program reaches a C++ module beyondbuiltin, the builtin C++ modules the compiler loaded and each linked module's dependency closure join the linked set.fromExtraDependencyrecords how the host process first loaded a shared module (a-jithost loadsstrings/fio_core/maththat way), so under the JIT test lane the generator used to prune the modules the program calls.include/daScript/simulate/standalone_modules.h), and the generated class takesStandaloneModuleScopeas its first base, ahead ofContext: the first context constructed registers the union in the C++ registrar's order, then dependencies-first, and initializes once; the last context destroyed shuts down withModule::ShutdownStandalone, inside main. A host that registeredbuiltinowns the registry; a linked module it did not register stops the program by name.examples/standalone/06_full_runtime(dasHV + fio, static, no shared module) is the worked example and a small-lane ctest;test_standalone_modulesputs two contexts in one binary, with and without a pre-registering host, plus the partial-host refusal;tests/aot/test_standalone_emit.daspins the emission, the dead-code prune, and theDEFAULT_MODULE_ORDER/register_builtin_modules_implpair.Observable behavior.
-ctxon a script callingpopen_argv-> generated C++ fails to compile -> it compiles, linkslibDaScript+ the module archives, and runs.TypeInfo::resolveAnnotation-> serves the request.builtin) -> unchanged output.-ctxgeneration inside a-jithost (the JIT test lane) -> every module afterbuiltinpruned -> the linked modules are included and registered.Where to look.
daslib/aot_cpp.das(collectLinkedModules,standaloneModuleRegistration,UseTypeMarker),daslib/aot_standalone.das(table sizing, the emitted registration table, the class bases), and the new header'sstandaloneAcquireModules/standaloneReleaseModules.Validation, claims, ledger
Validation
Module::Shutdowninstead ofShutdownStandalone(crash at exit), registration dropped (emission case +resolveAnnotationcrash), callee marking off (fio pruned: emission cases fail, generated C++ does not compile),DEFAULT_MODULE_ORDERreversed (emission order/pair cases fail, dasHV construction crashes), builtin modules not linked unless used (dasHV requiresrtti_coreby name: crash), handled-type mark ungated (the dead-code case fails). Restored tree green.fnByMangledNameand the AOT-hashFillFunction; the partial-host double-shutdown finding is fixed (fatal by name) and pinned bystandalone_modules_host_partial(WILL_FAIL).DAS_FATAL_ERROR's debugger break underWILL_FAIL(now a plain log +exit(-1)); windows 32 Release caught the registry shutdown running from a static destructor after main (0xc0000374, both generated-lifetime tests) - the exit-time race the-exepath avoids, now avoided the same way by shutting down when the last context is destroyed.DAS_HV_DISABLED=OFF, which CI's release-module configure sets;standalone_full_runtime,standalone_modules,standalone_modules_host,standalone_modules_host_partialcarryLABELS "small"and run in every per-PR lane'sctest -L small.tests-cpp-smallbinary stale from the rebase, and the JIT-host prune above) were fixed and re-validated with--only tests-cppand--only tests-jit.Claims - stated, not tested
Module::requirereturning null at construction.Not done
ast.h-name rule to "any TU compiled with nano's headers, generated code included", and a REVIEW.das gate that everyexamples/standalone/NN_*directory appears in both CMake files.daslib/REVIEW.dasgate for the emit-entry-point error-check rule (mechanically checkable per the dragon).