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.
Summary
external fnsignature 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 anexternal fndeclaration, 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:min.inf:No binding, no
.wasm, no manifest — the failure is in signature validation, before anything aboutuse { … } from …matters.Boundary
external fnparameter, qualifiedexternal fnreturn type, qualifiedexternal fnparameter, bare local structfnparameter, qualifiedletbinding + qualified struct literalBoundness is irrelevant: an unbound
external fnfails identically.Cause
check_collectingrunscollect_function_and_constant_definitionsbeforeresolve_imports, andvalidate_signaturesafter it.validate_typeinsidecollect_for_def— part of collection, so before imports bind.resolve_qualified_type_pathfails and the unimported-namespace diagnostic fires. The message is true at that instant and false by the time a user reads it.validate_signature_for_def, driven byvalidate_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 fnbehind.Fix: a relocation, into an arm that already exists
validate_signature_for_defalready has aDef::ExternFunctionarm in the correctly-phased pass; it currently performs onlyreport_duplicate_parameters. The comment directly above it is what sends type validation elsewhere:Three lines below, the
Def::Structarm validates field types in this same pass, with the precedent spelled out:So: move the extern arm's two
validate_typecalls (parameters and return) out ofcollect_for_definto 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 undeclaredCustomreaching code generation and panicking) is preserved, sincevalidate_signaturesstill 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:
The code runs those two in the opposite order. Every inline comment inside
check_collectingdescribes 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'slower_value_typehas noTypeNode::Qualifiedarm; such a type falls to its catch-all and returnsUnsupportedType. 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 aQualifiednode arrives asQualified.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
i32when 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 giveQualifiedan unconditionali32, matching how the same function already treatsCustom; 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
Qualifiedand on an unresolvableCustom. Worth correcting whichever option is taken.