-usecache link failure on the clang/gcc backend (macOS arm64): duplicate __global symbols (g_autostr_*_stack) from cached builtin.o #70
Describe the bug
-usecache produces a binary that fails to link when the C backend is clang (or any compiler whose linker enforces ODR for tentative definitions, i.e. everything except tcc). On macOS the default system cc is clang, so v -cc cc -usecache ... is broken for every program — including a bare println('hello').
The link fails with duplicate symbols for the __global cycle-guard variables declared in vlib/builtin/autostr.v:
duplicate symbol '_g_autostr_addr_stack' in:
/var/folders/.../T/hello-XXXX.o
/Users/me/.vmodules/.cache/68/6808fcb...module.builtin.o
duplicate symbol '_g_autostr_type_stack' in:
/var/folders/.../T/hello-XXXX.o
/Users/me/.vmodules/.cache/68/6808fcb...module.builtin.o
ld: 2 duplicate symbols
The reason this has gone unnoticed: the only -usecache coverage in cmd/tools/vtest-all.v runs against the default cc, which on the CI hosts is tcc. tcc treats these as tentative/common definitions and silently merges them, so the clang/gcc-linker path is never exercised.
Reproduction Steps
On macOS arm64 (any program reproduces; hello-world is enough):
echo "fn main() { println('hello') }" > /tmp/h.v
v wipe-cache
v -cc clang -usecache run /tmp/h.v # or: v -cc cc -usecache run /tmp/h.v
For contrast, the tcc path succeeds:
v wipe-cache
v -usecache run /tmp/h.v # default cc = tcc -> prints "hello"
Expected Behavior
v -cc clang -usecache run /tmp/h.v links and prints hello, matching both the non-cached build (v -cc clang run /tmp/h.v) and the tcc-cached build.
Current Behavior
Link fails with duplicate _g_autostr_addr_stack / _g_autostr_type_stack symbols (see above), exit code 1.
Root cause
g_autostr_type_stack / g_autostr_addr_stack (and the _len companions) are __global declarations in vlib/builtin/autostr.v:
__global g_autostr_type_stack = [autostr_type_stack_max_depth]int{}
__global g_autostr_type_stack_len = 0
__global g_autostr_addr_stack = [autostr_type_stack_max_depth]voidptr{}
The C backend emits a definition (not an extern declaration) for each __global into every translation unit that sees it. Without -usecache everything is one TU, so there is a single definition. With -usecache, builtin is compiled into a standalone module.builtin.o that defines them, and the main program's generated .c also defines them — two definitions of the same symbol at link time.
tcc accepts this because it treats uninitialised/__global definitions as tentative (common-block) symbols and merges them; ld64 (and GNU ld without -fcommon) reject duplicate strong definitions, which is why the failure only shows up off the tcc path.
The fix is presumably to emit __global definitions in exactly one TU under -usecache (the owning module's object) and extern declarations everywhere else — the same shape that already works for ordinary cached module functions.
Environment
v version: V 0.5.1, commitb156327(master, 2026-06-28)- OS: macOS 26.4.1 (build 25E253), arm64
- cc: Apple clang 21.0.0 (
clang-2100.1.1.101) - Also reproduces with
-cc gcc(any non-tcc linker).
[!NOTE] You can use the 👍 reaction to increase the issue's priority for developers.
Please note that only the 👍 reaction to the issue itself counts as a vote. Other reactions and those to comments will not be taken into account.