types: a module's interface carries the records and opaque types it references - #78
Merged
Conversation
…eferences ModuleExports held only what a module declares, so anything its exports referred to fell off at the next boundary: an extern type could not be named across a module at all (#72, there was no slot for it), and a record reached only through another module's exports had invisible fields (#75, the field registry crossed one hop while the type crossed two). Both get the same treatment the scheme side got in #26: the interface now closes over what it references. extern type declarations export as name + arity and register in a consumer like any imported type, clash check included. Records and opaque types mentioned by exported schemes, constructors, or record fields are pulled from the imports' interfaces, tagged with their declaring module, and re-exported; the walk follows newly pulled records and terminates on cycles. A carried type registers under its bare identity name only, so it can be named, unified, and field-accessed, while construction and patterns still require importing the declaring module. A taken bare name skips the carried entry silently (local and direct declarations win; the same record along two paths is admitted once), and a shadowed record's fields feed the unknown-field diagnostic, which now names the record and the module that declares it. Record updates in a consumer that never imports the declaring module reconstruct via that module's class, with its import hoisted. Closes #75 Closes #72
Constructing or pattern-matching a transitively carried record bare said "not a record type", which the surrounding program visibly contradicts: the same name reads fields and updates fine. The two rejection sites now use the carried entry's declaring module (Decls::carried_record_home) to state the real situation and the fix: "the record `Config` is declared in module `Inner`; import `Inner` to construct its values here" (the pattern site says "to match"). A name that is no record at all keeps the old message, and qualified tags behave as before.
A carried record used to join the field-owner multimap as a peer, so a dependency two hops away could make a working access ambiguous: a consumer with a local record declaring size stopped compiling when a module it never imports gained a record with the same field. By-name field lookup now applies the precedence the bare-name rule already established: records declared here or imported directly form the deciding tier, and a carried record owns a field only when no record in that tier declares it (field_owner_tier, consulted by record_of_field and the pending-field fallback for a base still unknown at the end of its binding). Ambiguity is reported within whichever tier decided, so two carried records that alone declare a field still tie and the message names them. Every resolution that worked before is unchanged, and the issue 75 repro still resolves, since its field has a single owner.
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.
What
ModuleExportsheld only what a module declares, not what its exports refer to, and two bugs fell out of that gap:extern typecould not be named across a module boundary. There was no export slot for opaque handle types, sotype Holder = { h: Gen.Rng }failed withunknown typeand cascaded into a bogusnot a record typeon every later use ofHolder.MiddleexportedWrap { wConf: Inner.Config }, and a consumer importing onlyMiddlecould hold and pass theConfiginside but gotunknown record fieldonx.wConf.cSize.How
The interface now closes over what it references, the same treatment the scheme side got in #26.
extern typeexports as name + arity (ExportedOpaque) and registers in a consumer under its bare identity name and a qualified key, exactly like a sum type, clash check included.Records and opaque types mentioned by exported schemes, constructor schemes, or exported record fields are pulled from the imports' interfaces, tagged with the module that declares them, and re-exported (
close_over_references). Newly pulled records are walked in turn, so a chain of any depth crosses, and a seen set terminates the walk on mutually referencing records. Each import's interface is itself closed, so one pull per hop reaches everything.A carried type registers under its bare identity name only: it can be named, unified, and field-accessed. Constructing it, pattern-matching it, or writing the qualified spelling still requires importing the declaring module directly. By-name field lookup treats carried records as a fallback tier: records declared here or imported directly decide first, and a carried record owns a field only when no record in that tier declares it, so a dependency two hops away can never make a working access ambiguous. Two carried records that alone declare a field still tie, and the ambiguity message names them.
A carried name already taken (by a local type or a direct import) is skipped silently, since the consumer never wrote that name: local and direct declarations win, and the same record arriving along two import paths (a diamond) is recognized by its declaring module and admitted once. A genuinely shadowed record's fields feed the diagnostics instead:
and constructing or matching a carried record bare is refused with the fix spelled out:
the record `Config` is declared in module `Inner`; import `Inner` to construct its values here(the pattern site says "to match"). A name that is no record at all keeps "not a record type".The lowering side keeps up: a record update in a consumer that never imports the declaring module reconstructs via that module's class (
inner.Config(...), withimport innerhoisted).ModuleExports::carried_recordsfeeds the project driver's per-moduleImportContext, andrecord_class_nameaccepts the carried modules.A genuinely unknown qualified type still reports one clean
unknown typeerror, covered by a test.DESIGN.md§6.1 andINTERNALS.mddescribe the closure and the name rules; the staleExportedTypedoc comment about records/measures/externs is corrected.Tests
New in
tests/project.rs: both issue repros end to end (e2e_an_extern_type_can_be_named_across_the_module_boundary,e2e_a_transitive_record_field_is_accessible), updates through the boundary, depth 3, a record cycle, the direct-over-carried field precedence (including the breaking program from review, end to end), carried-vs-carried ambiguity, the shadowed-name hint, the diamond, direct-plus-carrier coexistence, construction refusal without a direct import (qualified, and bare with the construct/match messages), bare naming of a carried opaque type, the opaque clash check, and the clean unknown-type error.cargo test,cargo clippy --all-targets, andcargo fmt --checkare clean.Closes #75
Closes #72