Skip to content

cgen: let the C preprocessor pick <intrin.h>/<dbghelp.h> in Windows snapshots (makev.bat cannot bootstrap with the bundled tcc) - #29147

Merged
medvednikov merged 2 commits into
vlang:masterfrom
metif12:fix-windows-snapshot-msvc-only-headers
Sep 30, 2026
Merged

medvednikov merged 2 commits into
vlang:masterfrom
metif12:fix-windows-snapshot-msvc-only-headers

Conversation

@metif12

@metif12 metif12 commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

Problem

vc/v_win.c cannot be built by the bundled TinyCC, so makev.bat fails on Windows:

Building V...
 > Attempting to build "./v_win_bootstrap.exe" (from ./vc/v_win.c) with "C:\Users\MR\v\thirdparty\tcc\tcc.exe"
./vc/v_win.c:1169: error: include file 'intrin.h' not found

vc/v_win.c is generated by .github/workflows/gen_vc_ci.yml with -cross -os windows -cc msvc, and is then built by makev.bat with tcc, clang or gcc — never MSVC. But system_libc_headers() decided at generation time which headers to emit:

// vlib/v/gen/c/cleanc.v:19766 (before)
if g.ccompiler == 'msvc' {
	g.writeln('#include <intrin.h>')
	g.writeln('#include <dbghelp.h>')
}

so the two includes got baked into the snapshot as plain text. The bundled tcc ships neither header, and the bootstrap died before compiling a line of V.

Fix

Only MSVC has these two headers, and every use site is already conditionally compiled:

use site guard
_umul128 in bits__mul_64 #if defined(_MSC_VER)
_BitScanForward in bits__trailing_zeros_32 #if defined(_MSC_VER)
_BitScanForward64 in bits__trailing_zeros_64 #if defined(_MSC_VER)
SymInitialize/SymFromAddr in print_backtrace_skipping_top_frames_msvc $if msvc

So the choice belongs to the C preprocessor, not to the compiler V happened to be targeting. #if defined(_MSC_VER) is also the shape the neighbouring __linux__ / __APPLE__ blocks and the stdatomic block already use, and it is the reasoning atomic_builtin_compat_decls() already records a few hundred lines below:

Generated Windows C can be compiled by TCC even when the selected compiler recorded in g.ccompiler is different (including portable snapshots). The preprocessor must make the final choice.

The MSVC path is unchanged: cl.exe defines _MSC_VER, so it still gets both headers, and makev.bat:503 already links dbghelp.lib for that bootstrap.

Why nothing caught it

The C-compiler-agnostic CI jobs never run makev.bat, and CI Windows TCC only reaches this line once vc has been regenerated. This is the same class as #28615 (winsock2.h), #28631 (bcrypt.h) and #28650 (aclapi.h): a generated-C portability gap that is invisible until the snapshot is compiled by a second C compiler.

Tests

Two unit tests in vlib/v/gen/c/preamble_test.v and two end-to-end tests in vlib/v/compiler_tests/cross_output_codegen_test.v. All four fail on the old code and pass on the new:

# without this change
Message: the snapshot emits ['#include <intrin.h>', '#include <dbghelp.h>'] unguarded, but only
         MSVC has those headers, so a snapshot built by tcc, clang or gcc fails with
         `include file not found`
Message: -cc gcc emits [] but the msvc spelling emits
         ['#include <intrin.h>', '#include <dbghelp.h>'] instead
Message: #include <intrin.h> is not behind an _MSC_VER guard, it is behind ['#ifdef _WIN32'] -
         only MSVC ships it, so every other C compiler fails to find it

test_system_libc_headers_do_not_depend_on_the_c_compiler_v_was_generated_for and test_cross_windows_output_includes_the_same_headers_for_every_c_compiler are the general form of the regression: a snapshot's header set must not depend on the -cc that generated it, since the C compiler that compiles it is chosen at build time. The e2e version checks the real -cross -os windows output, so it also covers anything the backend adds on top of the preamble.

Verified locally:

  • v test vlib/v/gen/c/preamble_test.v — OK
  • v test vlib/v/compiler_tests/cross_output_codegen_test.v — OK
  • v test vlib/v/gen/c/ — 37 passed, 1 pre-existing failure (import_cache_reuse_test.v, fails identically on unpatched master)
  • v fmt -verify on all three files — clean
  • bootstrap: patched vc/v_win.c and ran makev.bat's exact command line
    > .\thirdparty\tcc\tcc.exe -DCUSTOM_DEFINE_v1_fallback -B.\thirdparty\tcc -bt10 -g -w ^
        -o v_win_bootstrap.exe v_win_patched.c -ladvapi32 -lws2_32 -lbcrypt -Wl,-stack=33554432
    exit=0
    > .\v_win_bootstrap.exe version
    V 0.5.2 de321be
    
  • and the generated C on the new compiler: with -cc msvc the header set is now byte-identical to -cc gcc/-cc clang/-cc tcc; <intrin.h>/<dbghelp.h> appear as #ifdef _WIN32 && #if defined(_MSC_VER), and every header the bundled tcc lacks is otherwise behind a guard that is false for it.

Not tested: a full -cc msvc build of cmd/v on Windows (no MSVC here). MSVC only ever includes the two real headers, so that path is unchanged. vc is bot-generated — this needs a gen_vc run before makev.bat picks it up.

Relationship to vlang/tccbin#96 / #98

Those add intrin.h (an empty shim) and dbghelp.h to the bundled tcc, which makes the current snapshot buildable. That is the right fix for the bundle, and it is complementary rather than alternative:

  • vc is bot-generated, so the published snapshot still carries the unconditional include — tccbin#96 is what unblocks Windows users today, until vc regenerates from this.
  • This PR removes the dependency instead of satisfying it, which is what keeps the next snapshot buildable by any C compiler, not just the bundled one. The intrin.h shim is deliberately an empty no-op, so a future unguarded MSVC-only include would then be satisfied by a header that declares nothing.

Fixes #29146.

🧙 Built with WOZCODE

…napshots

`vc/v_win.c` is generated with `-cross -os windows -cc msvc` (gen_vc_ci.yml)
and then built by makev.bat with the bundled TinyCC, clang or gcc - not with
MSVC. Choosing those two headers at generation time, from `g.ccompiler`, baked
`#include <intrin.h>` and `#include <dbghelp.h>` into every Windows snapshot as
plain text, and the bundled tcc ships neither, so the bootstrap died on

    ./vc/v_win.c:1169: error: include file 'intrin.h' not found

before compiling a line of V. Only MSVC has both headers, and every use site is
already `#if defined(_MSC_VER)` (bits__mul_64, bits__trailing_zeros_32/64) or
`$if msvc` (print_backtrace_skipping_top_frames_msvc), so a preprocessor guard is
both correct and sufficient. This is the same shape the neighbouring __linux__ /
__APPLE__ blocks and the stdatomic block already use, and the same reasoning
atomic_builtin_compat_decls() records: "Generated Windows C can be compiled by
TCC even when the selected compiler recorded in `g.ccompiler` is different
(including portable snapshots). The preprocessor must make the final choice."

The C-compiler-agnostic job only skips `makev.bat`, so nothing caught this. The
two new `system_libc_headers` tests and the two new cross-output tests all fail
on the old code and pass on the new.

Fixes vlang#29146.

This is the V-side root cause fix; vlang/tccbin#96 (amd64) and vlang#98 (i386) add
the two headers to the bundled tcc instead. They are complementary, not
alternatives: vc is bot-generated, so the snapshot published today still has the
unconditional include and tccbin#96 is what unblocks Windows users until vc
regenerates from this. Once it does, tcc no longer needs the shims for V's
output, though they still help any other consumer and any other unguarded
MSVC-only include.

🧙 Built with [WOZCODE](https://wozcode.com)

@medvednikov medvednikov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 30e70b9200784aad1edc4df06f8884c4b67ca57f, excluding CI status.

I did not find a blocking correctness issue in the production change. The _MSC_VER guard remains nested inside _WIN32, and moving this decision to the consuming C preprocessor is consistent with the compiler-specific intrinsic/backtrace use sites.

Non-blocking: strengthen the end-to-end guard assertion (vlib/v/compiler_tests/cross_output_codegen_test.v, test_cross_windows_output_guards_the_msvc_only_headers and enclosing_guards_at).

c_code.index(header) checks only the first occurrence. Consequently, output shaped like this still satisfies the first-occurrence guard assertion, the pinned-block assertion, and the include-membership comparison, despite including both headers without an MSVC guard later:

#ifdef _WIN32
#if defined(_MSC_VER)
#include <intrin.h>
#include <dbghelp.h>
#endif
#include <intrin.h>
#include <dbghelp.h>
#endif

This matters for the stated goal of catching headers added elsewhere in the generated output: a later unguarded duplicate would reintroduce the missing-header failure without these checks detecting it. Also, guards.any(it.contains('_MSC_VER')) accepts #ifndef _MSC_VER and the #else of a positive MSVC condition, while the helper does not account for #elif.

Consider checking every occurrence and requiring an effective positive MSVC-only condition, with negative fixtures for these cases; alternatively, use a focused preprocessing test with _MSC_VER both defined and undefined.

Validation: the isolated emitted guard passed eight GCC/Clang preprocessing cases covering all defined/undefined combinations of _WIN32 and _MSC_VER, using stub headers. I also confirmed the false positives above using a direct Python translation of the new assertions and GCC preprocessing. These were isolated checks, not execution of the V test suites or a native Windows bootstrap; those were not run in this environment.

@medvednikov

Copy link
Copy Markdown
Member

@codex review

@medvednikov medvednikov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed 990f07e1bea6a596871e65c353bf10397e41e1ad, excluding CI status.

The regression-test gap from my previous review is addressed. msvc_only_header_guard_error() now iterates over every matching include instead of checking only the first occurrence, and requires an explicit positive MSVC branch rather than merely finding _MSC_VER in a condition. enclosing_guards_at() replaces the active branch on #elif and no longer accepts a subsequent #else as positive. The new fixtures cover later unguarded duplicates, negative conditions, #else/#elif transitions, permissive OR conditions, closed guards, and missing headers. The strengthened check also runs for each of the four C-compiler variants in the header-comparison test.

I found no new blocking correctness issues in this follow-up. The production include-guard change is unchanged, and I consider my earlier non-blocking finding resolved.

Validation: A direct Python translation of the updated helpers rejected all 12 negative fixtures and accepted all 4 positive fixtures. Independent GCC/Clang preprocessing with stub headers confirmed the fixture behavior with _MSC_VER defined and undefined, plus all _WIN32/_MSC_VER combinations for the production include block: 72 isolated preprocessing checks passed in total. These checks did not execute the V helpers or test suites. The V test suites and a native Windows bootstrap were not run in this environment.

@medvednikov
medvednikov merged commit a25f138 into vlang:master Sep 30, 2026
@medvednikov

Copy link
Copy Markdown
Member

Thanks!

MARCROCK22 added a commit to MARCROCK22/v that referenced this pull request Sep 30, 2026
The squash of v#28973 holds what the head of the pull request held (436aebb),
which the experiment already has and extends: the 11 files it conflicts in keep
the experiment's version. Git applied two hunks of the pull request outside the
conflicts (the answers to the questions of VLS in driver.v, the exported and
markused declarations of a library in checker.v) at their old place, next to the
experiment's own copies, and they were dropped with the rest; every other line
of the pull request that the experiment lacks is one that it changed later.
From master: the Windows snapshots' headers in cleanc.v (vlang#29147) and ROADMAP.md.
@metif12
metif12 deleted the fix-windows-snapshot-msvc-only-headers branch October 1, 2026 06:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

makev.bat (all compilers): vc/v_win.c includes <intrin.h>/<dbghelp.h> unconditionally, breaking the Windows bootstrap

2 participants