fix(types): reject bare/qualified nominal collision by owner identity#2663
Merged
Conversation
A root-local type and an imported type that share a bare final segment (a root-local `Widget` vs an imported `widgeti8.Widget`) were conflated by `Ty::names_match_qualified`, the context-free suffix compare used inside `unify`. That primitive accepts any bare↔qualified pair sharing a final segment, so the local value flowed into the import's layout and the mismatch surfaced only downstream as an `E_CODEGEN_FRONT` LLVM-verify fail-closed instead of a checker diagnostic (issue #2651). Add a compare-only gate at the `expect_type` boundary, `reject_nominal_owner_conflict`, that fires exactly when the permissive suffix rule would ACCEPT a pairing that owner-qualified definition identity REJECTS. It is purely read-only — it rewrites no stored name and binds no inference variable, so the canonical spelling never leaks into a `Subst` binding or the checker→HIR/MIR name handoff (the record-layout registry keeps single-module references bare on purpose). It fires only when both sides are fully concrete after substitution, so an already-bound generic `T` inferred to a callee-frame type is seen as the type it denotes. `canonical_nominal_name` resolves a bare name to its UNIQUE registered owner (mirroring the `resolved_name` resolution cascade, read-only) and `strict_nominal_identity` additionally strips a redundant current-module self-qualifier, so genuine same-def aliases still compare equal: a callee frame's bare `Box` vs `nestbox.Box`, a prelude `MonitorError` vs `link_monitor.MonitorError`, a builtin `HashSet` vs `collections.HashSet`, and a machine referenced both bare (`Lifecycle`) and through its own module (`lifecycle.Lifecycle`). A real structural mismatch is not suffix-equal and is left for `unify` to report, preserving its dyn-trait and `LocalPid` coercion-recovery paths. The permissive primitive is kept unchanged (its unit tests still hold) so every legitimate non-local bare-name alias keeps unifying leak-free; the owner discrimination lives entirely in the new checker-side gate, which is the only place with the resolution tables needed to tell a same-def alias from a cross-owner collision. This deviates from the plan's original "narrow the primitive" mechanism, which empirically broke those legit non-local aliases (prelude types, callee-frame bare returns, generic inference) that cannot be leak-free canonicalized before unification. Reuses the owner-qualified identity precedent from `canonicalize_type_identity` (trait-signature comparison) without touching any shared resolution helper (`published_bare_type_qualified` is read, not modified), so the #2626 package-collision lane stays independent.
Add three fixtures pinning the issue #2651 boundary: - local_shadow_import_divergent_reject: a root-local `Widget { v: i64 }` passed where the imported `widgeti8.Widget { v: i8 }` is required. This is the headline case that previously reached codegen as an `E_CODEGEN_FRONT` LLVM-verify fail-closed; the harness asserts a clean nominal type-mismatch diagnostic AND the absence of any codegen-front/D10 fall-through. - local_shadow_import_same_layout_reject: the same shadow against `widgeti64.Widget { v: i64 }`, an identical layout. Proves the gate rejects on DEFINITION IDENTITY, not on a layout heuristic that would wrongly accept a byte-compatible collision. - bare_import_alias_accept: an opt-in `import hew::widgeti8::{ Widget }` where the bare `Widget` and the qualified `widgeti8.Widget` denote ONE definition; a value of either spelling satisfies a parameter typed with the other. Guards that the gate does not over-reject a legal alias.
Add two end-to-end regressions closing coverage gaps in the issue #2651 owner-identity gate, each verified to fail without the corresponding carve-out: - self_qualified_type_identity: module `selfqualtype` references its OWN type `Meter` both bare (`build`'s return) and through its own module qualifier (`read`'s `selfqualtype.Meter` parameter). `roundtrip` crosses the bare value into the self-qualified parameter with the current module set to `selfqualtype`, so the gate compares bare `Meter` against `selfqualtype.Meter`. This is the type-level equivalent of a generic machine's self-qualified transition (the stdlib `Lifecycle<T>` case), reached deterministically without machine plumbing. Reverting the current-module self-qualifier strip in `strict_nominal_identity` reintroduces a false `type mismatch: expected selfqualtype.Meter, found Meter`; the fixture prints 3. - callee_frame_bare_alias_accept: `framealias.make()` returns its result spelled bare (`Widget`) in the callee frame, reached at the import site through the module-qualified `framealias.Widget`. Under a plain (non-opt-in) import the bare name is not a published bare binding, so the gate must qualify it through the UNIQUE registered-owner scan over `type_defs`/`known_types`. Neutering that scan reintroduces a false `type mismatch: expected framealias.Widget, found Widget`; the fixture prints 11. Scope stays #2651 only — no #2626/#2400/#2647 surface is touched.
…port The #2651 owner-identity gate rejected any suffix-equal bare/qualified nominal pair whose two spellings could not both be resolved to a registered owner. That over-fired on a LEGAL alias: a foreign type spelled bare in its own module's method signature (`TlsHandler` in `std.net.tls`) compared against a caller's alias-qualified `tls.TlsHandler`, with no `TlsHandler` def key in the importer's frame to canonicalize either side. Standalone `hew check` of the TLS active-attach acceptance fixture regressed to a `type mismatch` on `LocalPid<TlsHandler>` vs `LocalPid<tls.TlsHandler>`. Narrow the strict side of `nominal_owner_conflict` to report a conflict only when the pairing is a PROVABLE collision: one side is a genuine root-local / current-module declaration and the other is a foreign-owner qualifier (`Widget` vs `widgeti8.Widget`). An unresolved bare↔qualified alias — neither side provably distinct — stays accepted, preserving the permissive suffix behaviour it relied on. This is strictly narrower than the previous predicate, so it can only remove false rejects; the root-local divergent/same-layout reject fixtures still fail closed.
…cted failures The new #2651 package-import accept fixtures import `hew::<pkg>`, which resolves only under the package harness (`tests/pkg-import/run.sh`, via `--pkg-path`). Standalone `hew check` — run by `make hew-check-all` over the whole tracked corpus — reports `module hew::<pkg> not found`, which the sweep classifies as an unexpected failure. List the three flat accept fixtures and the self-qualified package source under `requires-package-resolver`, matching the existing pattern for every other `tests/pkg-import/` accept fixture. The reject fixtures are already auto-excluded (basename contains `reject`); `pkgs/framealias/framealias.hew` checks clean standalone and is intentionally not listed. The package harness continues to prove real success for all of them.
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.
Summary
The type checker now rejects a bare nominal type and a qualified nominal type as distinct when they resolve to different owning modules, closing a hole where
Widget(defined locally) andpkg.Widget(imported) could unify as the same type and reach LLVM codegen with two conflicting struct layouts. The new check runs entirely inside the checker'sexpect_typecoercion boundary, reports the ordinary type-mismatch diagnostic, and rewrites no stored type names, so it introduces no new interaction with inference, substitution, or the checker→HIR/MIR handoff. Legitimate aliasing — selected imports, callee-frame bare returns, self-qualified same-module references, and known builtin bare/qualified pairs — continues to unify as before.Verification
make test-pkg-import: passed — all package-import fixtures, including the new collision, self-qualified, and callee-frame-alias regressionsmake hew-check-all: passed — 1,587 checked, 254 intentional rejects skipped, 133/133 expected vs. actual failuresmake test-lane CRATE=hew-types: 2,524 passed, 3 skippedtarget/debug/hew check tests/vertical-slice/accept/tls_active_attach.hew: passedmake ci-preflight: passed (comprehensive profile)Out of scope
expect_typecoercion path only; other direct unification entrypoints (method-receiver,?-operator error types, generic inference) still fall back to the pre-existing codegen fail-closed behavior rather than this checker diagnostic. Widening coverage to those paths is a follow-on, not a correctness gap — codegen still fails closed on any uncovered collision.Fixes #2651