Skip to content

external fn signature types are validated before imports resolve, so a qualified type is rejected with a misleading diagnostic #425

Description

@0xGeorgii

Summary

external fn signature types are validated in the collection pass, which runs before import resolution. Every other signature type — including struct fields — is validated in a later pass that runs after it. So a ::-qualified type is accepted everywhere except in an external fn declaration, and the diagnostic prescribes the import that is already in the file, because at the instant the check runs the import genuinely is not bound yet.

Minimal reproduction

lib/geom.inf:

pub struct Pair { x: i32; y: i32; }

min.inf:

use lib::geom;
external fn area(p: lib::geom::Pair) -> i32;
pub fn run() -> i32 { return 0; }
$ infc min.inf --analyze
Type checking failed: 2:21: namespace `lib::geom` is not imported; add `use lib::geom;` to reach `lib::geom::Pair`

No binding, no .wasm, no manifest — the failure is in signature validation, before anything about use { … } from … matters.

Boundary

construct, same file and same import result
external fn parameter, qualified rejected
external fn return type, qualified rejected
external fn parameter, bare local struct accepted
plain fn parameter, qualified accepted
struct method parameter, qualified accepted
struct field, qualified accepted
let binding + qualified struct literal accepted

Boundness is irrelevant: an unbound external fn fails identically.

Cause

check_collecting runs collect_function_and_constant_definitions before resolve_imports, and validate_signatures after it.

  • An extern's signature types are validated by validate_type inside collect_for_def — part of collection, so before imports bind. resolve_qualified_type_path fails and the unimported-namespace diagnostic fires. The message is true at that instant and false by the time a user reads it.
  • Every other signature type is validated inside validate_signature_for_def, driven by validate_signatures, after imports resolve.

Issue #63 moved ordinary signature validation — and struct field validation — into the late pass for exactly this reason, and left external fn behind.

Fix: a relocation, into an arm that already exists

validate_signature_for_def already has a Def::ExternFunction arm in the correctly-phased pass; it currently performs only report_duplicate_parameters. The comment directly above it is what sends type validation elsewhere:

Signature types of an extern are validated in collect_for_def, which has no type parameters to thread and so needs no second pass; only the parameter names are checked here…

Three lines below, the Def::Struct arm validates field types in this same pass, with the precedent spelled out:

Field types are validated here, after imports resolve, so a field declared with an item-imported or ::-qualified type is recognized exactly as a signature type is … (#63)

So: move the extern arm's two validate_type calls (parameters and return) out of collect_for_def into the existing arm, and replace the comment above it, which currently asserts the opposite of where they belong. The stated reason for keeping them early was convenience — no type parameters to thread — not a dependency. The protection the collect-site comment cites (an undeclared Custom reaching code generation and panicking) is preserved, since validate_signatures still runs well before codegen.

The documented phase order is also wrong, which is how this survived

Both the module doc and the builder's "Phase ordering" list say:

3. resolve_imports
4. collect_function_and_constant_definitions

The code runs those two in the opposite order. Every inline comment inside check_collecting describes the real order correctly. The body knows; the two phase lists at the top do not — so anyone reasoning about ordering from the module doc concludes this bug is impossible. Correct the lists in the same change, or the next reader re-derives the same wrong conclusion.

A second fix must land with it

core/inference/src/wasm_link/validate.rs's lower_value_type has no TypeNode::Qualified arm; such a type falls to its catch-all and returns UnsupportedType. That path is live today — extern signature lowering runs and succeeds for a bare local struct parameter — and the AST node is not rewritten between type checking and the driver, so a Qualified node arrives as Qualified.

Fixing the type checker alone therefore moves the failure one stage later: the program type-checks, then fails external-module validation with an "unsupported type" error. Still a rejection of a legitimate program.

Two options for that half. Mirror code generation exactly (resolve the path, yield i32 when it names a struct or enum), which needs the typed context and the declaring file's module path threaded to the lowering — the module path is the input to confirm reachable at the driver call site. Or give Qualified an unconditional i32, matching how the same function already treats Custom; simpler, and internally consistent with the file, but it preserves a divergence from code generation's conditional form.

Related: that file's doc claims its lowering matches code generation "exactly", which is inaccurate on Qualified and on an unresolvable Custom. Worth correcting whichever option is taken.

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