Skip to content

Standalone contexts link C++ modules: one process-wide registry lifetime, a dense function table, AOT headers for the modules the program calls - #3947

Merged
borisbat merged 1 commit into
masterfrom
bbatkin/standalone-ctx-modules
Sep 6, 2026
Merged

Standalone contexts link C++ modules: one process-wide registry lifetime, a dense function table, AOT headers for the modules the program calls#3947
borisbat merged 1 commit into
masterfrom
bbatkin/standalone-ctx-modules

Conversation

@borisbat

@borisbat borisbat commented Sep 5, 2026

Copy link
Copy Markdown
Collaborator

Why. A -ctx standalone 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.

  • collectUsedModules marks the module of every callee inside used functions and globals (externs carry no used flag) and of every handled type a used function or global names; once a program reaches a C++ module beyond builtin, the builtin C++ modules the compiler loaded and each linked module's dependency closure join the linked set.
  • The function table is dense: rows numbered in emission order, sized by that count.
  • A standalone context's module prune trusts its linked set alone: fromExtraDependency records how the host process first loaded a shared module (a -jit host loads strings/fio_core/math that way), so under the JIT test lane the generator used to prune the modules the program calls.
  • Each generated TU adds its linked C++ modules to a process-wide list before main (include/daScript/simulate/standalone_modules.h), and the generated class takes StandaloneModuleScope as its first base, ahead of Context: 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 with Module::ShutdownStandalone, inside main. A host that registered builtin owns 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_modules puts two contexts in one binary, with and without a pre-registering host, plus the partial-host refusal; tests/aot/test_standalone_emit.das pins the emission, the dead-code prune, and the DEFAULT_MODULE_ORDER / register_builtin_modules_impl pair.

Observable behavior.

  • -ctx on a script calling popen_argv -> generated C++ fails to compile -> it compiles, links libDaScript + the module archives, and runs.
  • A standalone context reaching dasHV -> segfault in TypeInfo::resolveAnnotation -> serves the request.
  • Context destructor on a program with pruned functions -> reads uninitialized table rows -> clean teardown.
  • Nano-tier programs (reach no C++ module beyond builtin) -> unchanged output.
  • -ctx generation inside a -jit host (the JIT test lane) -> every module after builtin pruned -> 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's standaloneAcquireModules / standaloneReleaseModules.

Validation, claims, ledger

Validation

  • Negative controls, each reverting one mechanism and running the emission dastest plus the C++ ctests: dense table reverted (3/3 ctests fail), Module::Shutdown instead of ShutdownStandalone (crash at exit), registration dropped (emission case + resolveAnnotation crash), callee marking off (fio pruned: emission cases fail, generated C++ does not compile), DEFAULT_MODULE_ORDER reversed (emission order/pair cases fail, dasHV construction crashes), builtin modules not linked unless used (dasHV requires rtti_core by name: crash), handled-type mark ungated (the dead-code case fails). Restored tree green.
  • Woodpecker: two rounds. Round 1's two P1s (dependency walk skipped already-used modules; per-TU shutdown owners) are fixed here. Round 2: the "embedded program-wide function indices" finding was rejected - generated code reaches functions only through fnByMangledName and the AOT-hash FillFunction; the partial-host double-shutdown finding is fixed (fatal by name) and pinned by standalone_modules_host_partial (WILL_FAIL).
  • CI rounds: linux Debug caught DAS_FATAL_ERROR's debugger break under WILL_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 -exe path avoids, now avoided the same way by shutting down when the last context is destroyed.
  • The dasHV-linked tests and the example build only with DAS_HV_DISABLED=OFF, which CI's release-module configure sets; standalone_full_runtime, standalone_modules, standalone_modules_host, standalone_modules_host_partial carry LABELS "small" and run in every per-PR lane's ctest -L small.
  • The full preflight ran once; its two reds (a tests-cpp-small binary stale from the rebase, and the JIT-host prune above) were fixed and re-validated with --only tests-cpp and --only tests-jit.

Claims - stated, not tested

  • Registration order across TUs on a real dependency chain: the rank sort is exercised by two TUs whose non-default sets are {dasHV} and {}; a binary whose TUs bring two non-default modules that depend on each other is not in the tree. A break would show as a module constructor's Module::require returning null at construction.
  • macOS bundles of a dasHV-linked standalone exe depend on Homebrew's OpenSSL dylibs (libhv links them there); Linux and Windows are unchecked.

Not done

  • nano/REVIEW.md rulings proposed by its auditor, not applied (rule restructuring): split the tier/seam rule into two, widen the ast.h-name rule to "any TU compiled with nano's headers, generated code included", and a REVIEW.das gate that every examples/standalone/NN_* directory appears in both CMake files.
  • A daslib/REVIEW.das gate for the emit-entry-point error-check rule (mechanically checkable per the dragon).
  • The watchdog rewrite this arc unblocks.

Copilot AI lite review requested due to automatic review settings September 5, 2026 21:50
@borisbat
borisbat force-pushed the bbatkin/standalone-ctx-modules branch from dbe2dc2 to 43a817f Compare September 5, 2026 21:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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 fromExtraDependency state).
  • Emit a dense function table (0..N-1 in emission order) and size Context::functions/totalFunctions by 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 mutable char[] 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 mutable char[] 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.

Comment thread daslib/aot_standalone.das
Comment thread examples/standalone/06_full_runtime/main.cpp
Comment thread tests-cpp/big/standalone_ctx/test_standalone_modules.cpp
Comment thread tests-cpp/big/standalone_ctx/test_standalone_modules.cpp
Copilot AI review requested due to automatic review settings September 5, 2026 22:03
@borisbat
borisbat force-pushed the bbatkin/standalone-ctx-modules branch from 43a817f to d59f2fa Compare September 5, 2026 22:03

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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

Comment thread daslib/aot_cpp.das
Copilot AI review requested due to automatic review settings September 5, 2026 22:11
@borisbat
borisbat force-pushed the bbatkin/standalone-ctx-modules branch from d59f2fa to 9900f5b Compare September 5, 2026 22:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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

Comment thread include/daScript/simulate/standalone_modules.h
Copilot AI review requested due to automatic review settings September 5, 2026 22:18
@borisbat
borisbat force-pushed the bbatkin/standalone-ctx-modules branch from 9900f5b to 6105674 Compare September 5, 2026 22:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 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.mark still records structs/enums regardless of inUsedCode, and collectUsedModules then 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>
Copilot AI review requested due to automatic review settings September 5, 2026 22:34
@borisbat
borisbat force-pushed the bbatkin/standalone-ctx-modules branch from 6105674 to b7ddddd Compare September 5, 2026 22:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 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

Comment thread examples/standalone/06_full_runtime/service_probe.das
@borisbat
borisbat merged commit 4766d8d into master Sep 6, 2026
51 of 52 checks passed
@borisbat
borisbat deleted the bbatkin/standalone-ctx-modules branch September 6, 2026 00:01
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.

2 participants