A library module that owns its configuration (var g_limits = Limits(...) next to the functions that read it) is the ordinary way to write daslang, and it survives the interpreter and plain -aot unchanged. Put the same program through utils/aot/main.das -- -ctx on the shipped v0.6.4-RC2 bundle and nothing about it works: emission aborts with invoke null method function ... visitGlobalLetVariableInit; if the globals are moved into the entry module so emission succeeds, the generated .cpp calls a das::InitGlobalVar overload the shipped runtime library does not define; and if that link is forced with a forwarding shim, the constructor never runs the global initializers, so every global reads back as zero.
All three are fixed on master by 8f4d87c (PR #3838, 2026-08-23), four days after the RC2 tag. This issue is about the release, and about the ctest lane that would have caught it never running in CI.
Version / platform: release bundle v0.6.4-RC2 (bin/daslang --version → 0.6.4), Linux x86_64 (Debian, kernel 6.12.107), g++ for the C++ steps. Identical on a self-built checkout of 1524b3b (2026-08-10); only the reported line differs (243 instead of 227).
Test case
Proposed home: tests-cpp/big/standalone_module_global/, a sibling of the existing tests-cpp/big/standalone_ctx/ (auto-globbed by tests-cpp/CMakeLists.txt). Four files.
standalone_module_global_dep.das
options gen2
module standalone_module_global_dep public
// The configuration a library module owns and initializes for its embedders.
struct Limits {
retries : int
timeout_ms : int
}
var g_limits = Limits(retries = 3, timeout_ms = 250)
var g_backoff = fixed_array(10, 40, 160)
var g_service_name = "ingest"
def public limits : Limits {
return g_limits
}
def public backoff_at(attempt : int) : int {
return attempt < length(g_backoff) ? g_backoff[attempt] : g_backoff[length(g_backoff) - 1]
}
def public service_name : string {
return g_service_name
}
standalone_module_global_fixture.das
options gen2
require standalone_module_global_dep
// The entry module reads the required module's initialized globals. This is the
// ordinary shape of a library module that owns its configuration, and it has to
// survive -ctx generation, compilation and linking.
var g_total_budget_ms = limits().retries * limits().timeout_ms
[export]
def get_retries : int {
return limits().retries
}
[export]
def get_total_budget_ms : int {
return g_total_budget_ms
}
[export]
def get_backoff(attempt : int) : int {
return backoff_at(attempt)
}
[export]
def service_name_length : int {
return length(service_name())
}
// So the same file can be checked by the plain interpreter and by -aot, not only
// through the generated context.
[export]
def main {
var failures = 0
if (get_retries() != 3) {
failures ++
}
if (get_total_budget_ms() != 750) {
failures ++
}
if (get_backoff(0) != 10 || get_backoff(1) != 40 || get_backoff(7) != 160) {
failures ++
}
if (service_name_length() != 6) {
failures ++
}
if (failures != 0) {
panic("standalone_module_global: {failures} check(s) failed")
}
print("standalone_module_global: PASSED\n")
}
test_standalone_module_global.cpp
#include "daScript/daScript.h"
#include "standalone_module_global_fixture.das.h"
using namespace das;
int main( int, char * [] ) {
standalone_module_global_fixture::Standalone ctx;
TextPrinter tout;
int failures = 0;
auto expect = [&]( const char * name, int32_t have, int32_t want ) {
if ( have != want ) {
tout << name << " = " << have << ", expected " << want << "\n";
failures ++;
}
};
// the required module's initialized globals have to be live in the context
expect("get_retries()", ctx.get_retries(), 3);
expect("get_total_budget_ms()", ctx.get_total_budget_ms(), 750);
expect("get_backoff(0)", ctx.get_backoff(0), 10);
expect("get_backoff(1)", ctx.get_backoff(1), 40);
expect("get_backoff(7)", ctx.get_backoff(7), 160);
expect("service_name_length()", ctx.service_name_length(), 6);
return failures ? 1 : 0;
}
CMakeLists.txt
# Big test: a standalone AOT context whose required module owns initialized
# globals generates, compiles, links and reads those globals back.
set(STANDALONE_MODULE_GLOBAL_GEN "${CMAKE_CURRENT_BINARY_DIR}/_generated")
file(MAKE_DIRECTORY "${STANDALONE_MODULE_GLOBAL_GEN}")
add_custom_command(
OUTPUT "${STANDALONE_MODULE_GLOBAL_GEN}/standalone_module_global_fixture.das.cpp"
"${STANDALONE_MODULE_GLOBAL_GEN}/standalone_module_global_fixture.das.h"
COMMAND $<TARGET_FILE:daslang>
"${PROJECT_SOURCE_DIR}/utils/aot/main.das"
-- -ctx "${CMAKE_CURRENT_SOURCE_DIR}/standalone_module_global_fixture.das"
"${STANDALONE_MODULE_GLOBAL_GEN}/"
DEPENDS daslang
"${CMAKE_CURRENT_SOURCE_DIR}/standalone_module_global_fixture.das"
"${CMAKE_CURRENT_SOURCE_DIR}/standalone_module_global_dep.das"
"${PROJECT_SOURCE_DIR}/utils/aot/main.das"
"${PROJECT_SOURCE_DIR}/daslib/aot_standalone.das"
"${PROJECT_SOURCE_DIR}/daslib/aot_cpp.das"
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
COMMENT "Standalone AOT: standalone_module_global_fixture.das"
VERBATIM
)
add_executable(test_standalone_module_global
test_standalone_module_global.cpp
"${STANDALONE_MODULE_GLOBAL_GEN}/standalone_module_global_fixture.das.cpp")
target_link_libraries(test_standalone_module_global PRIVATE
libDaScript ${SRC_LIBRARIES} ${DAS_MODULES_LIBS})
target_include_directories(test_standalone_module_global PRIVATE "${STANDALONE_MODULE_GLOBAL_GEN}" ${NEED_MODULES_PATH})
SETUP_CPP11(test_standalone_module_global)
set_target_properties(test_standalone_module_global PROPERTIES FOLDER "tests-cpp/big")
add_test(NAME standalone_module_global COMMAND test_standalone_module_global
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR})
set_tests_properties(standalone_module_global PROPERTIES LABELS "big")
add_dependencies(test-big test_standalone_module_global)
(The CMakeLists.txt was written after tests-cpp/big/standalone_ctx/CMakeLists.txt and has not been configured, since the shipped bundle has no build tree.)
What happens per tier on v0.6.4-RC2
$SDK is the unpacked release bundle.
Interpreter: correct.
$ $SDK/bin/daslang standalone_module_global_fixture.das
standalone_module_global: PASSED
$ echo $?
0
Plain -aot: correct.
$ $SDK/bin/daslang $SDK/utils/aot/main.das -- -aot standalone_module_global_fixture.das out_aot/gen.cpp
[I] Aot to out_aot/gen.cpp
$ echo $?
0
-ctx, blocker A: nothing is generated.
$ $SDK/bin/daslang $SDK/utils/aot/main.das -- -ctx standalone_module_global_fixture.das out_ctx
daslib/aot_standalone.das:227:33: error[31206]: macro caused exception during visitGlobalLetBody -- invoke null method function, type<aot_standalone::StandaloneContextGen>.visitGlobalLetVariableInit
EXCEPTION: AOT codegen failed: 1 codegen error(s) during emission (see log)
at daslib/aot_cpp.das:4410:4
FATAL: exiting with code 1 -- 3 smart pointer(s) still alive at shutdown
$ echo $?
1
$ ls out_ctx
(empty)
Expected: the two generated files and exit 0, then test_standalone_module_global exits 0.
The condition is narrow and reproducible (19-case matrix, every failing case has a passing neighbour that differs in one dimension): the global must belong to a required module, be used, and have any initializer. var public g = 0 fails exactly like an array-of-struct literal; let/var, public/private and the shape of the initializer do not matter. Same-file globals pass (the pvar._module == prog.getThisModule guard skips them), unused globals pass, and var g : int without initializer passes. let public g_len = 5 passes only because the constant is folded at every use and the variable arrives unused.
Blocker B: the generated context does not link. Because blocker A stops generation, this was isolated with the same program restated with its globals in the entry module, which RC2 does generate:
$ g++ -std=c++17 -O1 -fno-rtti -I$SDK/include -Iout -c out/samefile_global.das.cpp -o out/ctx.o # ok
$ g++ out/host.o out/ctx.o -L$SDK/lib -llibDaScript -llibDaScript_runtime -llibDaScript \
-llibUriParser -lpthread -ldl -lm -o out/samefile_standalone
samefile_global.das.cpp:(.text+0x18aa): undefined reference to `das::InitGlobalVar(das::Context&, das::GlobalVariable*, das::GlobalVarInfo)'
collect2: error: ld returned 1 exit status
That is the only undefined reference. include/daScript/simulate/standalone_ctx_utils.h:65 declares the by-value form; the shipped library defines only the const & one:
$ nm -C --defined-only $SDK/lib/liblibDaScript_runtime.a | grep InitGlobalVar
0000000000000170 T das::InitGlobalVar(das::Context&, das::GlobalVariable*, das::GlobalVarInfo const&)
A one-function shim defining the by-value overload and forwarding to the exported symbol makes the link succeed, which is the proof that the signature mismatch is the whole of blocker B.
Blocker C: a context that does link never initializes.
$ ./out/samefile_standalone
get_retries() = 0
get_total_budget_ms() = 0
$ echo $?
1
Expected 3 and 750. RC2's generated constructor ends with
FillFunction(context, getGlobalAotLibrary(), id_to_funcs);
context.runInitScript();
}
__init_script is emitted in the same translation unit but is never called, and context.globals is never zeroed.
With the fix
Master commit 8f4d87c ("standalone aot: the constructor actually runs init", PR #3838) addresses all three:
include/daScript/simulate/standalone_ctx_utils.h: InitGlobalVar(Context&, GlobalVariable*, GlobalVarInfo) → ... const GlobalVarInfo & (blocker B);
daslib/aot_standalone.das: visitGlobalLet becomes preVisitGlobalLet and the variable.init := visitGlobalLetVariableInit(...) line is removed (blocker A);
- the same file replaces
context.runInitScript() with memset(context.globals, 0, context.getGlobalSize()) + __init_script(&context, true) + the ordered [init] calls (blocker C).
GET /compare/v0.6.4-RC2...8f4d87cac02c reports status: ahead, behind_by: 0, so the release predates it. Master also already carries the emit-side regression test for blocker A (tests/aot/test_standalone_emit.das over tests/aot/_standalone_cross_module_fixture.das).
Not verified here: master was read, not built and run; the checks above are against the shipped RC2 bundle only.
Two requests
- A release that includes at least 8f4d87c. As shipped,
-ctx is unusable for any program whose required module owns an initialized, used global, and the contexts that do generate cannot be linked or initialized.
- Run
ctest -L big somewhere in CI. tests-cpp/big/standalone_ctx, the test that links and runs a generated context, already exists and is the only lane that can catch blockers B and C, but every ctest invocation in .github/workflows/build.yml is -L small, and the standalone examples under examples/standalone/ and tutorial 20 are compile-only, so nothing executes a generated context in CI.
Where it goes wrong (RC2 sources)
daslib/ast.das:322 declares def abstract visitGlobalLetVariableInit(...); class public CppAot (daslib/aot_cpp.das:1349) overrides visitGlobalLet, preVisitGlobalLetVariable, visitGlobalLetVariable and preVisitGlobalLetVariableInit but never visitGlobalLetVariableInit.
daslib/aot_standalone.das:227 calls that unimplemented slot on itself, right after the pvar._module == prog.getThisModule guard at :216 has let a required module's variable through (blocker A).
include/daScript/simulate/standalone_ctx_utils.h:65 is blocker B; daslib/aot_standalone.das emitting context.runInitScript() instead of calling the emitted __init_script is blocker C.
A library module that owns its configuration (
var g_limits = Limits(...)next to the functions that read it) is the ordinary way to write daslang, and it survives the interpreter and plain-aotunchanged. Put the same program throughutils/aot/main.das -- -ctxon the shippedv0.6.4-RC2bundle and nothing about it works: emission aborts withinvoke null method function ... visitGlobalLetVariableInit; if the globals are moved into the entry module so emission succeeds, the generated.cppcalls adas::InitGlobalVaroverload the shipped runtime library does not define; and if that link is forced with a forwarding shim, the constructor never runs the global initializers, so every global reads back as zero.All three are fixed on master by 8f4d87c (PR #3838, 2026-08-23), four days after the RC2 tag. This issue is about the release, and about the ctest lane that would have caught it never running in CI.
Version / platform: release bundle
v0.6.4-RC2(bin/daslang --version→0.6.4), Linux x86_64 (Debian, kernel 6.12.107), g++ for the C++ steps. Identical on a self-built checkout of 1524b3b (2026-08-10); only the reported line differs (243 instead of 227).Test case
Proposed home:
tests-cpp/big/standalone_module_global/, a sibling of the existingtests-cpp/big/standalone_ctx/(auto-globbed bytests-cpp/CMakeLists.txt). Four files.standalone_module_global_dep.dasstandalone_module_global_fixture.dastest_standalone_module_global.cppCMakeLists.txt(The
CMakeLists.txtwas written aftertests-cpp/big/standalone_ctx/CMakeLists.txtand has not been configured, since the shipped bundle has no build tree.)What happens per tier on v0.6.4-RC2
$SDKis the unpacked release bundle.Interpreter: correct.
Plain
-aot: correct.-ctx, blocker A: nothing is generated.Expected: the two generated files and exit 0, then
test_standalone_module_globalexits 0.The condition is narrow and reproducible (19-case matrix, every failing case has a passing neighbour that differs in one dimension): the global must belong to a required module, be used, and have any initializer.
var public g = 0fails exactly like an array-of-struct literal;let/var,public/privateand the shape of the initializer do not matter. Same-file globals pass (thepvar._module == prog.getThisModuleguard skips them), unused globals pass, andvar g : intwithout initializer passes.let public g_len = 5passes only because the constant is folded at every use and the variable arrives unused.Blocker B: the generated context does not link. Because blocker A stops generation, this was isolated with the same program restated with its globals in the entry module, which RC2 does generate:
That is the only undefined reference.
include/daScript/simulate/standalone_ctx_utils.h:65declares the by-value form; the shipped library defines only theconst &one:A one-function shim defining the by-value overload and forwarding to the exported symbol makes the link succeed, which is the proof that the signature mismatch is the whole of blocker B.
Blocker C: a context that does link never initializes.
Expected
3and750. RC2's generated constructor ends withFillFunction(context, getGlobalAotLibrary(), id_to_funcs); context.runInitScript(); }__init_scriptis emitted in the same translation unit but is never called, andcontext.globalsis never zeroed.With the fix
Master commit 8f4d87c ("standalone aot: the constructor actually runs init", PR #3838) addresses all three:
include/daScript/simulate/standalone_ctx_utils.h:InitGlobalVar(Context&, GlobalVariable*, GlobalVarInfo)→... const GlobalVarInfo &(blocker B);daslib/aot_standalone.das:visitGlobalLetbecomespreVisitGlobalLetand thevariable.init := visitGlobalLetVariableInit(...)line is removed (blocker A);context.runInitScript()withmemset(context.globals, 0, context.getGlobalSize())+__init_script(&context, true)+ the ordered[init]calls (blocker C).GET /compare/v0.6.4-RC2...8f4d87cac02creportsstatus: ahead, behind_by: 0, so the release predates it. Master also already carries the emit-side regression test for blocker A (tests/aot/test_standalone_emit.dasovertests/aot/_standalone_cross_module_fixture.das).Not verified here: master was read, not built and run; the checks above are against the shipped RC2 bundle only.
Two requests
-ctxis unusable for any program whose required module owns an initialized, used global, and the contexts that do generate cannot be linked or initialized.ctest -L bigsomewhere in CI.tests-cpp/big/standalone_ctx, the test that links and runs a generated context, already exists and is the only lane that can catch blockers B and C, but every ctest invocation in.github/workflows/build.ymlis-L small, and the standalone examples underexamples/standalone/and tutorial 20 are compile-only, so nothing executes a generated context in CI.Where it goes wrong (RC2 sources)
daslib/ast.das:322declaresdef abstract visitGlobalLetVariableInit(...);class public CppAot(daslib/aot_cpp.das:1349) overridesvisitGlobalLet,preVisitGlobalLetVariable,visitGlobalLetVariableandpreVisitGlobalLetVariableInitbut nevervisitGlobalLetVariableInit.daslib/aot_standalone.das:227calls that unimplemented slot on itself, right after thepvar._module == prog.getThisModuleguard at:216has let a required module's variable through (blocker A).include/daScript/simulate/standalone_ctx_utils.h:65is blocker B;daslib/aot_standalone.dasemittingcontext.runInitScript()instead of calling the emitted__init_scriptis blocker C.