Skip to content

v0.6.4-RC2: standalone -ctx is unusable: a required module's initialized global fails emission, the emitted InitGlobalVar does not link, and a context that does link never runs its initializers #3967

Description

@lookibed

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

  1. 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.
  2. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions