Skip to content

FIXED: AST exporter silently drops externally-linked inline function when prototype precedes definition #30

Description

@awto-au

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions