medvednikov

/

vq Public
0 commits 21 issue 12 pull requests 0 contributors Discussions Projects CI

-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, commit b156327 (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.