Summary
Root-caused and fixed as part of awto-au/linux-rs#42. Filing here for the fork's own tracker with the full before/after evidence.
c2rust-ast-exporter's VisitFunctionDecl (c2rust-ast-exporter/src/AstExporter.cpp) computed is_inline_externally_visible by calling FD->isInlineDefinitionExternallyVisible() directly on FD -- but FD here is always the canonical decl (the function runs once per canonical decl, gated by an isCanonicalDecl() early return). For a TU where a plain prototype is declared before the inline definition, the canonical decl IS that bodyless prototype, not the definition.
FunctionDecl::isInlineDefinitionExternallyVisible() asserts it must be called on the definition (doesThisDeclarationHaveABody() || willHaveBody() || hasAttr<AliasAttr>()) and walks that node's getPreviousDecl() chain to find an external prototype. Calling it on the prototype node instead violates that precondition -- in a release build the assert is compiled out, so it doesn't crash, it just silently returns a wrong answer (in practice: the exporter's own can_query_inline_visibility guard, which had the same FD-not-def bug, was already false for the prototype node since it has no body, so the query was never even executed and the result was hard-coded false).
Downstream, c2rust-transpile's prune_unwanted_decls treats is_inline_externally_visible=false as "not a translation root" -- so a function Clang's own codegen emits as a real external symbol gets pruned from the AST before translation ever runs, with c2rust transpile exiting 0 and zero diagnostic anywhere in the pipeline.
Fix
c2rust-ast-exporter/src/AstExporter.cpp: resolve def = FD->getDefinition() (already computed for is_inline) and query both can_query_inline_visibility and is_inline_externally_visible against def when a definition exists in this TU, falling back to FD only when there is none (e.g. bodyless alias declarations).
Commit: b13c3c3ef on master (merged via 65a4a5222).
Verification
Minimal repro (prototype declared before inline definition, no kernel headers needed):
typedef unsigned char u8;
int myfunc(unsigned char *inbuf, long len); /* prototype FIRST */
inline int myfunc(u8 *input, long in_len) { (void)input; return (int)in_len; }
nm on a plain clang -c build shows T myfunc (real external symbol). Pre-fix c2rust output: myfunc completely absent. Post-fix: unsafe extern "C" fn myfunc(...) present.
Real kernel case: lib/decompress_unlz4.c's unlz4() (declared via a separate prototype in include/linux/decompress/unlz4.h, #included before the definition). Pre-fix: unlz4 absent from transpile output. Post-fix: full function body present (308-line real implementation, #[link_section = ".init.text")] #[cold] #[inline] attributes correctly carried over, matching the kernel's INIT/__init macro expansion).
Controls (all 4 re-verified pre-fix vs post-fix):
| Variant |
Pre-fix |
Post-fix |
| prototype-before-definition (the bug) |
dropped |
present |
| inline-only, no separate prototype |
absent (correct C99 "not externally visible") |
absent (unchanged, correct) |
| prototype-after-definition |
present (already worked) |
present (unchanged) |
| gnu_inline/cold/section attrs + prototype-before |
dropped |
present |
Corpus regression check: full 601-file corpus (dev.py c2rust-baseline --include-stable, SLOW_FILES_EXCLUDED exclusions removed earlier the same day so this is the complete corpus), before-rev 21b2f77e5 vs after-rev b13c3c3ef. dev.py c2rust-regress 21b2f77e5 b13c3c3ef: 17803 decls both revisions, 0 regressed, 0 fixed, 0 new/removed decls within the routine corpus sweep -- decompress_unlz4.c itself can't reach AST export in the routine harness at all (separate, pre-existing -Werror=incompatible-pointer-types-discards-qualifiers blocker on its error("literal") call sites, same class of issue as linux-rs#35/the bunzip2.c case -- confirmed by direct isolated transpile with -- -Wno-error=incompatible-pointer-types-discards-qualifiers, where the fix is clearly visible). No other corpus file's function count changed, so no other latent instance of this exact shape currently exists in the 601-file corpus -- a real negative finding, not just "no regressions."
Residual risk / known untested shapes
Only the exact single-TU repro shape from the issue was tested (prototype and inline definition both in the same TU, at most 2 redeclarations). NOT tested: a 3rd redeclaration in the chain, or inline definition in one header with an external prototype declared in a different header (multi-header redeclaration chain) -- the fix (querying FD->getDefinition()) should generalize correctly per Clang's own getPreviousDecl() chain walk inside isInlineDefinitionExternallyVisible(), but this specific shape was not independently verified with a real repro.
Summary
Root-caused and fixed as part of
awto-au/linux-rs#42. Filing here for the fork's own tracker with the full before/after evidence.c2rust-ast-exporter'sVisitFunctionDecl(c2rust-ast-exporter/src/AstExporter.cpp) computedis_inline_externally_visibleby callingFD->isInlineDefinitionExternallyVisible()directly onFD-- butFDhere is always the canonical decl (the function runs once per canonical decl, gated by anisCanonicalDecl()early return). For a TU where a plain prototype is declared before theinlinedefinition, the canonical decl IS that bodyless prototype, not the definition.FunctionDecl::isInlineDefinitionExternallyVisible()asserts it must be called on the definition (doesThisDeclarationHaveABody() || willHaveBody() || hasAttr<AliasAttr>()) and walks that node'sgetPreviousDecl()chain to find an external prototype. Calling it on the prototype node instead violates that precondition -- in a release build the assert is compiled out, so it doesn't crash, it just silently returns a wrong answer (in practice: the exporter's owncan_query_inline_visibilityguard, which had the same FD-not-def bug, was alreadyfalsefor the prototype node since it has no body, so the query was never even executed and the result was hard-codedfalse).Downstream,
c2rust-transpile'sprune_unwanted_declstreatsis_inline_externally_visible=falseas "not a translation root" -- so a function Clang's own codegen emits as a real external symbol gets pruned from the AST before translation ever runs, withc2rust transpileexiting 0 and zero diagnostic anywhere in the pipeline.Fix
c2rust-ast-exporter/src/AstExporter.cpp: resolvedef = FD->getDefinition()(already computed foris_inline) and query bothcan_query_inline_visibilityandis_inline_externally_visibleagainstdefwhen a definition exists in this TU, falling back toFDonly when there is none (e.g. bodylessaliasdeclarations).Commit:
b13c3c3efonmaster(merged via65a4a5222).Verification
Minimal repro (prototype declared before
inlinedefinition, no kernel headers needed):nmon a plainclang -cbuild showsT myfunc(real external symbol). Pre-fix c2rust output:myfunccompletely absent. Post-fix:unsafe extern "C" fn myfunc(...)present.Real kernel case:
lib/decompress_unlz4.c'sunlz4()(declared via a separate prototype ininclude/linux/decompress/unlz4.h,#included before the definition). Pre-fix:unlz4absent from transpile output. Post-fix: full function body present (308-line real implementation,#[link_section = ".init.text")] #[cold] #[inline]attributes correctly carried over, matching the kernel'sINIT/__initmacro expansion).Controls (all 4 re-verified pre-fix vs post-fix):
Corpus regression check: full 601-file corpus (
dev.py c2rust-baseline --include-stable,SLOW_FILES_EXCLUDEDexclusions removed earlier the same day so this is the complete corpus), before-rev21b2f77e5vs after-revb13c3c3ef.dev.py c2rust-regress 21b2f77e5 b13c3c3ef: 17803 decls both revisions, 0 regressed, 0 fixed, 0 new/removed decls within the routine corpus sweep --decompress_unlz4.citself can't reach AST export in the routine harness at all (separate, pre-existing-Werror=incompatible-pointer-types-discards-qualifiersblocker on itserror("literal")call sites, same class of issue aslinux-rs#35/thebunzip2.ccase -- confirmed by direct isolated transpile with-- -Wno-error=incompatible-pointer-types-discards-qualifiers, where the fix is clearly visible). No other corpus file's function count changed, so no other latent instance of this exact shape currently exists in the 601-file corpus -- a real negative finding, not just "no regressions."Residual risk / known untested shapes
Only the exact single-TU repro shape from the issue was tested (prototype and inline definition both in the same TU, at most 2 redeclarations). NOT tested: a 3rd redeclaration in the chain, or inline definition in one header with an external prototype declared in a different header (multi-header redeclaration chain) -- the fix (querying
FD->getDefinition()) should generalize correctly per Clang's owngetPreviousDecl()chain walk insideisInlineDefinitionExternallyVisible(), but this specific shape was not independently verified with a real repro.