From a4a20da09b7a7b763491b1bbf01a0bc95fa80d43 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Thu, 28 May 2026 14:04:28 +0000 Subject: [PATCH] fix #20439 - undefined reference to `internal` for CTFE instance in struct .init with -c A CTFE-evaluated class or struct instance baked into a struct's `.init` needs a local backing symbol (named "internal", STB_LOCAL) in every object module that emits the init image. That symbol was cached on the shared AST node (`cre.value.origin.sym` / `sle.sym`), so when several modules are compiled in one invocation (`dmd -c a.d b.d`) the symbol created for the first object module leaked into the later ones, which then emitted only an undefined reference to a local symbol they never define: dmd -c a.d b.d dmd a.o b.o -of=app # undefined reference to `internal' Track the literals that cache a backing symbol in the current object module and clear those caches at each object-module boundary (resetCtfeSymbolCache, called from obj_start, alongside the existing per-object resets), so every object module re-creates and emits its own self-contained local copy. This matches the array-literal path (DtBuilder.dtoff) and the separate-compilation behavior, and keeps intra-module dedup intact. Also fixes the `-lib` variant (#19439). Co-Authored-By: Claude Opus 4.7 --- compiler/src/dmd/glue/package.d | 1 + compiler/src/dmd/glue/tocsym.d | 25 +++++++++++++++++++ compiler/test/dshell/extra-files/issue19439.d | 8 ++++++ compiler/test/dshell/extra-files/issue20439.d | 15 +++++++++++ compiler/test/dshell/imports/issue19439b.d | 4 +++ compiler/test/dshell/imports/issue19439c.d | 4 +++ compiler/test/dshell/imports/issue20439a.d | 11 ++++++++ compiler/test/dshell/issue19439.d | 20 +++++++++++++++ compiler/test/dshell/issue20439.d | 24 ++++++++++++++++++ 9 files changed, 112 insertions(+) create mode 100644 compiler/test/dshell/extra-files/issue19439.d create mode 100644 compiler/test/dshell/extra-files/issue20439.d create mode 100644 compiler/test/dshell/imports/issue19439b.d create mode 100644 compiler/test/dshell/imports/issue19439c.d create mode 100644 compiler/test/dshell/imports/issue20439a.d create mode 100644 compiler/test/dshell/issue19439.d create mode 100644 compiler/test/dshell/issue20439.d diff --git a/compiler/src/dmd/glue/package.d b/compiler/src/dmd/glue/package.d index eeda75a730a5..80f73770393b 100644 --- a/compiler/src/dmd/glue/package.d +++ b/compiler/src/dmd/glue/package.d @@ -1255,6 +1255,7 @@ private void obj_start(ref OutBuffer objbuf, const(char)* srcfile) //printf("obj_start()\n"); bzeroSymbol = null; + resetCtfeSymbolCache(); rtlsym_reset(); clearStringTab(); diff --git a/compiler/src/dmd/glue/tocsym.d b/compiler/src/dmd/glue/tocsym.d index 8a8d93e4e360..151ea45fed48 100644 --- a/compiler/src/dmd/glue/tocsym.d +++ b/compiler/src/dmd/glue/tocsym.d @@ -780,6 +780,29 @@ Symbol* toInitializer(EnumDeclaration ed) /* CTFE stuff */ /*****************************************************/ +/* A CTFE-evaluated literal (struct literal or class instance) that is baked into + * static data gets a backing LOCAL symbol (named "internal"), cached on the literal's + * AST node (`sle.sym` / `cre.value.origin.sym`). The AST is shared across all modules + * compiled in a single invocation, so without intervention that cache leaks the symbol + * created for the first object module into later ones — which then emit only an + * undefined reference to a LOCAL symbol they never define (link error: + * `undefined reference to 'internal'`). + * + * Track every literal that cached a symbol in the current object module and clear those + * caches at each object-module boundary (resetCtfeSymbolCache, called from obj_start), so + * each object module re-creates and emits its own self-contained local copy. This matches + * the array-literal path (DtBuilder.dtoff) and the already-accepted separate-compilation + * behavior, while keeping intra-module dedup intact. + */ +private __gshared Array!StructLiteralExp ctfeSymbolLiterals; + +void resetCtfeSymbolCache() +{ + foreach (sle; ctfeSymbolLiterals[]) + sle.sym = null; + ctfeSymbolLiterals.setDim(0); +} + Symbol* toSymbol(StructLiteralExp sle) { //printf("toSymbol() %p.sym: %p\n", sle, sle.sym); @@ -793,6 +816,7 @@ Symbol* toSymbol(StructLiteralExp sle) s.Sflags |= SFLnodebug; s.Stype = t; sle.sym = s; + ctfeSymbolLiterals.push(sle); auto dtb = DtBuilder(0); Expression_toDt(sle, dtb); s.Sdt = dtb.finish(); @@ -814,6 +838,7 @@ Symbol* toSymbol(ClassReferenceExp cre) s.Stype = t; cre.value.sym = s; cre.value.origin.sym = s; + ctfeSymbolLiterals.push(cre.value.origin); auto dtb = DtBuilder(0); ClassReferenceExp_toInstanceDt(cre, dtb); s.Sdt = dtb.finish(); diff --git a/compiler/test/dshell/extra-files/issue19439.d b/compiler/test/dshell/extra-files/issue19439.d new file mode 100644 index 000000000000..fb1de8d1d680 --- /dev/null +++ b/compiler/test/dshell/extra-files/issue19439.d @@ -0,0 +1,8 @@ +module issue19439; +import issue19439b; + +void main() +{ + auto b = new B(); + assert(b.obj !is null); +} diff --git a/compiler/test/dshell/extra-files/issue20439.d b/compiler/test/dshell/extra-files/issue20439.d new file mode 100644 index 000000000000..d9def6da0086 --- /dev/null +++ b/compiler/test/dshell/extra-files/issue20439.d @@ -0,0 +1,15 @@ +module issue20439; +import issue20439a; + +// This module independently bakes the `.init` images of S and T (and the CTFE instances +// embedded in them). Before the fix, the backing `internal` symbols were cached on the +// shared AST nodes and emitted only into the first object module, so this module's object +// file held only undefined references to symbols it never defines. +__gshared S s; +__gshared T t; + +void main() +{ + assert(s.c !is null && s.c.x == 42); + assert(t.p !is null && t.p.y == 7); +} diff --git a/compiler/test/dshell/imports/issue19439b.d b/compiler/test/dshell/imports/issue19439b.d new file mode 100644 index 000000000000..9af1fcc55344 --- /dev/null +++ b/compiler/test/dshell/imports/issue19439b.d @@ -0,0 +1,4 @@ +module issue19439b; +import issue19439c; + +class B : C { } diff --git a/compiler/test/dshell/imports/issue19439c.d b/compiler/test/dshell/imports/issue19439c.d new file mode 100644 index 000000000000..d697c8f285f3 --- /dev/null +++ b/compiler/test/dshell/imports/issue19439c.d @@ -0,0 +1,4 @@ +module issue19439c; + +static this() { } +class C { Object obj = new Object; } diff --git a/compiler/test/dshell/imports/issue20439a.d b/compiler/test/dshell/imports/issue20439a.d new file mode 100644 index 000000000000..d540d5e793d2 --- /dev/null +++ b/compiler/test/dshell/imports/issue20439a.d @@ -0,0 +1,11 @@ +module issue20439a; + +// Reduced, Phobos-free form of the bug. The original trigger was `SysTime.max`, which +// bakes a CTFE-allocated immutable class instance (its time zone) into a struct's +// `.init`; a plain `new C` reproduces the same toSymbol(ClassReferenceExp) path. The +// `new Inner` field additionally covers the toSymbol(StructLiteralExp) path. +class C { int x = 42; } +struct S { C c = new C; } + +struct Inner { int y = 7; } +struct T { Inner* p = new Inner; } diff --git a/compiler/test/dshell/issue19439.d b/compiler/test/dshell/issue19439.d new file mode 100644 index 000000000000..c451444afb2c --- /dev/null +++ b/compiler/test/dshell/issue19439.d @@ -0,0 +1,20 @@ +// https://github.com/dlang/dmd/issues/19439 +// +// The `-lib`/multiobj variant of #20439: a CTFE `new Object` baked into a class's `.init` +// is emitted with one object module per symbol, so the class init image and its "internal" +// backing symbol can land in different archive members. The symbol was cached on the shared +// AST node and emitted (locally) into only one member, leaving the member that holds +// `C.__init` with an undefined reference: +// `lib(c.o):(.data._D...1C6__initZ+0x10): undefined reference to 'internal'`. +import dshell; + +int main() +{ + // class C (with the CTFE field) and derived class B live in separate modules, so their + // codegen lands in separate archive members. + run("$DMD -m$MODEL -lib -of$OUTPUT_BASE/issue19439$LIBEXT $IMPORT_FILES/issue19439b.d $IMPORT_FILES/issue19439c.d"); + run("$DMD -m$MODEL -I$IMPORT_FILES -of$OUTPUT_BASE/issue19439$EXE $EXTRA_FILES/issue19439.d $OUTPUT_BASE/issue19439$LIBEXT"); + run("$OUTPUT_BASE/issue19439$EXE"); + + return 0; +} diff --git a/compiler/test/dshell/issue20439.d b/compiler/test/dshell/issue20439.d new file mode 100644 index 000000000000..c848b0b9cbbb --- /dev/null +++ b/compiler/test/dshell/issue20439.d @@ -0,0 +1,24 @@ +// https://github.com/dlang/dmd/issues/20439 +// +// A CTFE class/struct instance baked into a struct's `.init` needs a local backing symbol +// ("internal") in *every* object module that emits the init image. The symbol was cached on +// the shared AST node, so compiling two modules in one invocation (`dmd -c a.d b.d`) leaked +// the first object module's local symbol into the second, which then emitted only an +// undefined reference to a symbol it never defines: +// `b.o:(.data.rel.ro+0x8): undefined reference to 'internal'`. +// +// The issue used `SysTime.max`; reduced here to a plain CTFE instance to avoid importing +// Phobos (the test suite must not), exercising both toSymbol overloads (class + struct). +import dshell; + +int main() +{ + // One invocation, two source files, separate object files (-c): issue20439a defines the + // types (and emits the .init image + its local `internal`), issue20439 bakes __gshared + // instances of them (and must define its own `internal`, not reference the other's). + run("$DMD -m$MODEL -od$OUTPUT_BASE -I$IMPORT_FILES -c $IMPORT_FILES/issue20439a.d $EXTRA_FILES/issue20439.d"); + run("$DMD -m$MODEL -of$OUTPUT_BASE/issue20439$EXE $OUTPUT_BASE/issue20439a$OBJ $OUTPUT_BASE/issue20439$OBJ"); + run("$OUTPUT_BASE/issue20439$EXE"); + + return 0; +}