Skip to content

fix(codegen): rename leading-underscore functions out of C's reserved namespace - #1490

Merged
paul-hammant merged 1 commit into
mainfrom
fix/leading-underscore-c-collision
Aug 10, 2026
Merged

fix(codegen): rename leading-underscore functions out of C's reserved namespace#1490
paul-hammant merged 1 commit into
mainfrom
fix/leading-underscore-c-collision

Conversation

@paul-hammant

Copy link
Copy Markdown
Collaborator

Implements asks/leading-underscore-fn-collides-with-c-runtime.md.

The bug

A top-level Aether function was emitted as a C function with the same name verbatim, so _write(path, content) became void _write(const char*, const char*) — squarely inside the namespace C11 §7.1.3 reserves for the implementation. MSVCRT/UCRT populate it heavily (_write, _read, _open, _close, _access, …), so on Windows:

error: conflicting types for '_write'; have 'void(const char*, const char*)'
note: previous declaration ... int(int, const void*, unsigned int)

Windows-only — glibc declares none of them, so the identical program built clean on Linux. And the diagnostic points at generated C rather than at the function name, which is what makes it expensive. On the aeb line one shared _write fixture broke 10 of 118 tests.

Option 2 (rename), not option 1 (static)

The ask offers three options and explains why the cheapest is insufficient — I agree and took the rename. A file-scope static whose name matches a declared CRT prototype is still a conflicting-types error at compile time. Renaming closes the class: _read and _close are fixed by the same change, as is any name a future libc adds.

The mechanism is deliberately the one already in the file. #1366 renames a function colliding with an extern to ae_<name> and rewrites its call sites; this adds the same shape for a different trigger, so the two read as one concept rather than two special cases. _write becomes ae_write — still readable in a backtrace, and out of the reserved namespace because the leading character is no longer _.

Scope, kept narrow on purpose

Only a leading underscore, only top-level user functions, skipping imported and @c_callback ones (whose symbols must stay externally addressable verbatim).

I checked the emitted C rather than assuming: runtime internals like _aether_ctx_push, _aether_thunk_force and a genuine _setmode CRT call are untouched. Renaming those would break the runtime.

The trailing-underscore file-local convention (#279) is untouched, and is what a caller should reach for instead.

Verified where the bug actually was

Rebuilt the compiler on winbaz (real MSYS2 MINGW64) and ran the ask's own 10-line repro:

Built: us.exe
leading underscore: a b

Then the new regression test on the same box — All PASS, on the platform where every one of those names previously refused to compile.

Tests

tests/regression/test_leading_underscore_fn.ae covers _write, _read and _close (real CRT names), a leading-underscore name the CRT does not declare (the rule is the namespace, not a blocklist), and the trailing-underscore form — so #279 and this cannot silently drift apart.

Note the test runs everywhere but only Windows could ever have failed to compile it. That is the point: a Linux-only assertion would not have caught the original bug, so what every platform checks is that the rename is transparent.

Verification

  • make ci — C suite 230/230; .ae 978/980 with [PASS] regression_test_leading_underscore_fn, count up to 980. The two failures are integration_http_server_h2 (pre-existing, diagnosed in pesky_bug.md) and http_client_insecure_tls, which passes in isolation and is the known load-sensitive flake.
  • gcc -Werror -Wall -Wextra on the changed codegen, locally before pushing.
  • winbaz run above.

Also

The ask file was not on main — it had been committed onto the fix/nightly-ffmpeg-dep branch (#1485) and so was absent here. Recovered from b735c932 and included.

🤖 Generated with Claude Code

… namespace

Implements asks/leading-underscore-fn-collides-with-c-runtime.md.

A top-level Aether function was emitted as a C function with the same name
verbatim, so `_write(path, content)` became `void _write(const char*, const
char*)` — squarely inside the namespace C11 §7.1.3 reserves for the
implementation. MSVCRT/UCRT populate that namespace heavily (_write, _read,
_open, _close, _access, _aligned_malloc, ...), so on Windows the build failed:

    error: conflicting types for '_write'; have 'void(const char*, const char*)'
    note: previous declaration ... int(int, const void*, unsigned int)

Windows-only: glibc declares none of them, so the identical program built clean
on Linux. The diagnostic points at generated C rather than at the function
name, which is what makes it expensive to diagnose. Reported from the aeb line,
where one shared `_write` fixture broke 10 of 118 tests.

Took the ask's option 2 (rename) rather than option 1 (static). Option 1 is
insufficient on its own and the ask says so: a file-scope static whose name
matches a declared CRT prototype is STILL a conflicting-types error at compile
time. Rename closes the class, not the instance — `_read` and `_close` are
fixed by the same change, as is any name a future libc adds.

The mechanism is deliberately the one already in the file: #1366 renames a
function colliding with an extern to `ae_<name>` and rewrites its call sites.
This adds the same shape for a different trigger, so the two read as one
concept. `_write` becomes `ae_write` — still readable in a backtrace, and out
of the reserved namespace because the leading character is no longer `_`.

Scope is narrow on purpose: only a LEADING underscore, only top-level user
functions, skipping imported and @c_callback ones (whose symbols must stay
externally addressable verbatim). Runtime internals like `_aether_ctx_push` are
untouched — verified in the emitted C, where they and a genuine `_setmode` CRT
call remain as they were. The trailing-underscore file-local convention (#279)
is untouched and is what a caller should reach for instead.

Verified on winbaz (real MSYS2 MINGW64), the platform where every one of these
names previously refused to compile: the ask's 10-line repro now builds and
prints "leading underscore: a b", and the new regression test reports All PASS.

Tests: tests/regression/test_leading_underscore_fn.ae covers `_write`, `_read`
and `_close` (real CRT names), a leading-underscore name the CRT does NOT
declare (the rule is the namespace, not a blocklist), and the trailing-
underscore form, so the two conventions cannot silently drift apart.

Ask file recovered from b735c932, where it had been committed onto an unrelated
branch and so was absent from main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@paul-hammant
paul-hammant merged commit a62ce80 into main Aug 10, 2026
24 checks passed
@paul-hammant
paul-hammant deleted the fix/leading-underscore-c-collision branch August 10, 2026 21:28
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.

1 participant