fix(codegen): rewrite VALUE references to renamed functions, not just calls (#1598) - #1600
Merged
Merged
Conversation
… calls (#1598) Two codegen passes rename a top-level function — a leading underscore (#279, out of C's reserved namespace) and a collision with a declared extern (#1366) — and both fixed up references with rename_calls_to, which matches only AST_FUNCTION_CALL. A function used as a VALUE is an AST_IDENTIFIER, so it kept the original spelling while the definition moved, and the emitted C named a symbol that no longer existed: error: '_h_under' undeclared; did you mean 'ae_h_under'? the compiler suggesting the definition it had just renamed. This blocked two aeo binaries, whose every route registration is `server_get(raw, "/health", _h_health, 0)`. The extern-collision half failed far more quietly, and is fixed by the same change. There the un-renamed reference RESOLVES — to the real libc symbol the extern declared — so the program links cleanly and SEGFAULTS at runtime, handing libc's puts(const char*) an int. Verified on the pre-fix compiler: "Program crashed (signal 11)". That is why its regression is a runtime test; a compile-only check passes on the broken compiler. rename_all_refs_to now does both halves. The identifier half needs scope care the call half does not: a local may legally shadow a top-level function name and is emitted verbatim (`_thing = 42` stays `_thing` while the function becomes `ae_thing`), so a body that REBINDS the name is skipped. Conservative on purpose — it can only leave a reference un-renamed (status quo ante), never rewrite a binding that should have stayed put. Tests, all verified to FAIL on the unfixed compiler except the shadowing one (which pins behaviour that must not change, so it passes both ways): - tests/regression/test_issue1598_underscore_fn_as_value.ae — the issue's repro, plus call + non-underscore controls. - tests/regression/test_issue1598_fn_name_shadowed.ae — the shadowing case. Separate file because the skip is per top-level function. - tests/integration/fn_value_rename/ — the extern half, as a RUNTIME test with a C sidecar. docs/c-interop.md documents that a renamed function works as a value, and the one sharp edge found while testing: a single function that both shadows the name and passes the function as a value cannot work in either spelling, because the emitted C would reference the value before the local's declaration. That is pre-existing, not introduced here. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1598. The report's diagnosis was exactly right, and the suggested fix (a sibling
rename_refs_toused alongsiderename_calls_toat both call sites) is what landed.The bug
rename_calls_tomatches onlyAST_FUNCTION_CALL, but a function used as a value is anAST_IDENTIFIER. So the definition moved and the reference didn't:— the compiler suggesting the definition it had just renamed. Blocked aeo's two HTTP binaries, where every route registration is
server_get(raw, "/health", _h_health, 0).The extern-collision half is worse than the underscore half
The issue flagged
rename_extern_colliding_functions(#1366) as having the identical hole but had no live repro. I built one — and it fails much more quietly. There the un-renamed reference resolves: to the real libc symbol the extern declared. So the program links cleanly and segfaults at runtime, handing libc'sputs(const char*)an int. Verified on the pre-fix compiler:A silent wrong-symbol bug rather than a compile error. That's why its regression is a runtime test — a compile-only check passes on the broken compiler.
The scope hazard a naive fix would have hit
A local may legally shadow a top-level function name, and such a local is emitted verbatim while the function is renamed:
Renaming every matching identifier would have rewritten the variable and broken a program that compiles today. So
rename_all_refs_toskips any top-level function body that rebinds the name. That direction is deliberate: it can only leave a reference un-renamed (the status quo for that one function), never rewrite a binding that should have stayed put.One sharp edge found by testing, pre-existing
My first test put the value case and the shadowing case in the same
main(), and it failed. Investigating showed that shape emits invalid C regardless of this change — the value reference would precede the local's declaration:So it's a genuine pre-existing limitation, not something this fix should paper over. The tests are split accordingly and
docs/c-interop.mddocuments it, recommending the trailing-underscore file-local convention to keep the two meanings apart.Tests
All verified to fail on the unfixed compiler, except the shadowing one — which pins behaviour that must not change, so it correctly passes both ways.
tests/regression/test_issue1598_underscore_fn_as_value.ae— the issue's 3-line repro (no imports, as requested), plus the call and non-underscore controls from its matrix.tests/regression/test_issue1598_fn_name_shadowed.ae— the shadowing case, in its own file because the skip is decided per top-level function.tests/integration/fn_value_rename/— the extern-collision half as a runtime test with a C sidecar.Also checked by hand: value references inside a non-
mainfunction and inside a closure body both work.Verification
390 unit tests pass. Full
.aesweep: 1002 passing. Five failures, none from this change —windows_crt_symbols(verified identical on the unfixed compiler: an msvcrt-vs-ucrt archive symbol-set issue),http_server_h2(documented inasks/h2-50-stream-stress-framing-layer-error.mdas failing on clean main),http_request_header_iterandhttp_server_ops(both pass in isolation — sweep load flakes), andcontrib_vulkan_portability(no GPU on this box).🤖 Generated with Claude Code