From efb78c593c46c8d5a64ef819d5f866447fa6c89f Mon Sep 17 00:00:00 2001 From: josh11b <15258583+josh11b@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:27:48 -0700 Subject: [PATCH] Update design for proposal #6395: Type completeness in extend (#7315) Assisted-by: Gemini via Antigravity Co-authored-by: Josh L --- docs/design/classes.md | 21 +++++- docs/design/expressions/member_access.md | 82 ++++++++++++++++++++++++ docs/design/generics/details.md | 54 +++++++++------- docs/design/generics/terminology.md | 5 +- 4 files changed, 135 insertions(+), 27 deletions(-) diff --git a/docs/design/classes.md b/docs/design/classes.md index dca747393eeb..ab960cbb5c86 100644 --- a/docs/design/classes.md +++ b/docs/design/classes.md @@ -799,13 +799,17 @@ class GraphNode { // `GraphNode` is first complete here. ``` -**Open question:** What is specifically allowed and forbidden with an incomplete -type has not yet been decided. +An incomplete type cannot be used as the target of an `extend` declaration (such +as `extend base: T` or `extend adapt T`), as the target type must be complete to +allow name lookup into it. > **TODO:** Document that qualified names can be looked up in an incomplete > type, as adopted in > [#5087: Qualified lookup into types being defined](/proposals/p005087-qualified-lookup-into-types-being-defined.md). +**Open question:** What else is specifically allowed and forbidden with an +incomplete type has not yet been decided. + ### `Self` A `class` definition may provisionally include references to its own name in @@ -1230,6 +1234,19 @@ class FinalDerived { // may not extend `FinalDerived` since not declared `base` or `abstract`. ``` +An `extend base` declaration requires the specified base class to be complete at +the point where the declaration is written, to allow name lookup to search into +the base class. + +``` +base class Incomplete; + +class InvalidDerived { + // ❌ Forbidden: `Incomplete` is not complete. + extend base: Incomplete; +} +``` + An _[abstract class](https://en.wikipedia.org/wiki/Abstract_type)_ or _abstract base class_ is a base class that may not be instantiated. diff --git a/docs/design/expressions/member_access.md b/docs/design/expressions/member_access.md index 3955b9812283..d953bd2fe715 100644 --- a/docs/design/expressions/member_access.md +++ b/docs/design/expressions/member_access.md @@ -14,6 +14,7 @@ SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - [Member resolution](#member-resolution) - [Package and namespace members](#package-and-namespace-members) - [Types, forms, and facets](#types-forms-and-facets) + - [`extend`](#extend) - [Tuple indexing](#tuple-indexing) - [Values](#values) - [Facet binding](#facet-binding) @@ -238,6 +239,85 @@ implementation for `Avatar`, ignoring `Renderable.Draw`. Similarly, a form has members, specifically the members of the form's type component. +#### `extend` + +An `extend` declaration declares that the enclosing scope extends the scope +associated with any target entity named in the declaration. That is to say name +lookups into the enclosing scope will also look into the scopes which are +nominated by the `extend` declaration. The `extend` declaration requires that +the target scopes named in an `extend` declaration are complete, or if the +target is a generic parameter, requires the type of the parameter to be +complete. + +To `extend` an entity `Y` with another `Z` means that name lookups into `Y` will +also look into `Z`. Immediately after the `extend` operation, members of `Z` +should also be found when doing name lookup into `Y`, both from outside and from +inside the definition of `Y`. In order to be able to perform lookups into `Z`, +we require that `extend` operations only target scopes that are complete. + +This requirement functions recursively. Given an interface `B` that extends +another interface `A`: By naming `A` in an extend declaration, we require `A` is +complete. This provides that its entire definition is known, and thus its +`extend` relationship to `B`. The `extend` relationship there also provides that +`B` is complete. + +If the target scope of an `extend` declaration is a generic parameter, its type +must be complete where the `extend` declaration is written, as name lookups into +the extended scope will look into the type of the generic parameter. + +```carbon +interface I { + fn F(); +} + +class C(T:! I) { + extend base: T; + // `F` names `T.F` here, found in `I`. + fn G() { F(); } +} +``` + +As any generic parameter in the enclosing scope is replaced by a more specific +value, extended scopes that depend on a generic parameter must remain complete. +This includes forming a specific for the extended scope involving the parameter +in order to surface any monomorphization errors in the resulting specific. + +In the next example, the `extend` declaration in interface `A(N)` names a +symbolic facet type which can produce monomorphization errors when a negative +value is provided for `N`. When a more specific value for the target `B(N)` is +provided, we require the specific value to be complete as well by forming the +specific. A diagnostic error would be produced while checking `C(-1)` for +completeness, as it requires `A(-1)` to be complete, which requires +`B(array(i32, -1))` to be complete, and that contains an invalid type. + +```carbon +interface B(T:! type) {} + +interface A(N:! i32) { + // Requires `B(N)` to be complete. + extend require impls B(array(i32, N)) {} +} + +class C(N! i32) { + // Requires `A(N)` to be complete, which requires `B(N)` to be complete. + extend impl as A(N); +} + +fn F() { + // Requires `C(-1)` to be complete, which requires `A(-1)` to be complete, which requires `B(array(i32, -1))` to be complete. + var c: C(-1); +} +``` + +These rules prohibit an `extend` declaration from naming its enclosing scope, +since by being part of the definition of that scope, it is implied that the +enclosing scope is not complete. This seems reasonable as all names available +inside the enclosing interface or named constraint are already available or +would conflict with the ones that are. + +> **Alternative considered:** > +> [Not requiring the target scope to be complete immediately](/proposals/p006395-type-completeness-in-extend.md#alternatives-considered). + ### Tuple indexing Tuple types have member names that are *integer-literal*s, not *word*s. @@ -959,3 +1039,5 @@ var n: i32 = 1 + X.Y; [#2360: Types are values of type `type`](https://github.com/carbon-language/carbon-lang/pull/2360) - Proposal [#2550: Simplified package declaration for the `Main` package](https://github.com/carbon-language/carbon-lang/pull/2550) +- Proposal + [#6395: Type completeness in extend](https://github.com/carbon-language/carbon-lang/pull/6395) diff --git a/docs/design/generics/details.md b/docs/design/generics/details.md index bc903005c6f8..189907e22f5d 100644 --- a/docs/design/generics/details.md +++ b/docs/design/generics/details.md @@ -310,6 +310,9 @@ Assert(p1.Scale(2.0) == p2); Assert(p1.Add(p1) == p2); ``` +For more on how `extend` affects member access, see +[member access](../expressions/member_access.md#extend). + Without `extend`, those methods may only be accessed with [qualified member names and compound member access](#qualified-member-names-and-compound-member-access): @@ -506,6 +509,9 @@ class Player { ### Avoiding name collisions +> **TODO:** This has changed. Now you can always extend, but conflicting names +> may only be found by qualified name lookup. + To avoid name collisions, you can't extend implementations of two interfaces that have a name in common: @@ -1041,6 +1047,8 @@ var s: String = "string"; var p_s: String = Identity(&s); ``` +The facet type `type` is associated with an empty scope and is complete. + In general, the declarations in `constraint` definition match a subset of the declarations in an `interface`. These named constraints can be used with checked generics, as opposed to templates, and only include required interfaces and @@ -1251,6 +1259,10 @@ Note that the expressions `A & B` and `A where .Self impls B` have the same requirements, and so you would be able to switch a function declaration between them without affecting callers. +The scope of a facet type formed by a `&` operator extends the scopes of both of +its operands. The scope of the resulting facet type is complete if every scope +it extends is complete. + **Alternatives considered:** See [Carbon: Access to interface methods](https://docs.google.com/document/d/17IXDdu384x1t9RimQ01bhx4-nWzs4ZEeke4eO6ImQNc/edit?resourcekey=0-Fe44R-0DhQBlw0gs2ujNJA). @@ -1347,6 +1359,8 @@ fn DoHashAndEquals[T:! Hashable](x: T) { > **TODO:** Update this section as needed to reflect the fact that an impl of an > interface doesn't impl the interfaces it extends, as adopted in > [#5168: Forward `impl` declaration of an incomplete interface](/proposals/p005168-forward-impl-declaration-of-an-incomplete-interface.md). +> Should link to +> [`extend` in member access](../expressions/member_access.md#extend). When implementing an interface, we allow implementing the aliased names as well. In the case of `Hashable` above, this includes all the members of `Equatable`, @@ -1864,7 +1878,9 @@ Frequently we expect that the adapter type will want to preserve most or all of the API of the original type. The two most common cases expected are adding and replacing an interface implementation. Users would indicate that an adapter starts from the original type's existing API by using the `extend` keyword -before `adapt`: +before `adapt`, which +[extends member access to lookup names in the adapted class](../expressions/member_access.md#extend) +along with `impl` lookup: ```carbon class Song { @@ -1898,8 +1914,10 @@ functions can be cast to the corresponding type with `SongByArtist` substituted in for `Song`. Unlike the similar `class B { extend base: A; }` notation, -`class B { extend adapt A; }` is permitted even if `A` is a final class. Also, -there is no implicit conversion from `B` to `A`, matching `adapt` without +`class B { extend adapt A; }` is permitted even if `A` is a final class. Like +other `extend` declarations, it requires the target `A` to be complete, or if +`A` is a generic parameter, requires the type of the parameter to be complete. +Also, there is no implicit conversion from `B` to `A`, matching `adapt` without `extend` but unlike class extension. To avoid or resolve name conflicts between interfaces, an `impl` may be declared @@ -2651,6 +2669,11 @@ between different type variables, such as that a member of one is equal to a member of another. The `where` operator is not associative, so a type expression using multiple must use round parens `(`...`)` to specify grouping. +The scope of a facet type formed by a `where` declaration +[extends](../expressions/member_access.md#extend) the scope of its first +operand, and the resulting facet type is complete if that scope it extends is +complete. + > **Comparison with other languages:** Both Swift and Rust use `where` clauses > on declarations instead of in the expression syntax. These happen after the > type that is being constrained has been given a name and use that name to @@ -5379,32 +5402,15 @@ An incomplete `C` cannot be used in the following contexts: - ❌ `T:! C` ... `T.X` - ❌ `T:! C where `... - ❌ `class `...` { extend impl as C; }` - - The names of `C` are added to the class, and so those names need to be - known. +- ❌ `interface `...` { extend require impls C; }` or + `constraint `...` { extend require impls C; }` + - An `extend` declaration requires the target scope to be complete. See + [`extend` in member access](../expressions/member_access.md#extend). - ❌ `T:! C` ... `T impls A` where `A` is an interface or named constraint different from `C` - Need to see the definition of `C` to see if it implies `A`. - ❌ `impl` ... `as C {` ... `}` -> **Future work:** It is currently undecided whether an interface needs to be -> complete to be extended, as in: -> -> ```carbon -> interface I { extend C; } -> ``` -> -> There are three different approaches being considered: -> -> - If we detect name collisions between the members of the interface `I` and -> `C` when the interface `I` is defined, then we need `C` to be complete. -> - If we instead only generate errors on ambiguous use of members with the -> same name, as we do with `A & B`, then we don't need to require `C` to be -> complete. -> - Another option, being discussed in -> [#2745](https://github.com/carbon-language/carbon-lang/issues/2745), is -> that names in interface `I` shadow the names in any interface being -> extended, then `C` would not be required to be complete. - ### Declaring implementations > **TODO:** Update this section to reflect the new rules adopted in diff --git a/docs/design/generics/terminology.md b/docs/design/generics/terminology.md index 0e416d3eec01..b438199ef643 100644 --- a/docs/design/generics/terminology.md +++ b/docs/design/generics/terminology.md @@ -464,6 +464,8 @@ A type that _extends_ the implementation of an interface has all the named members of the interface as named members of the type. This means that the members of the interface are available by way of both [simple member access and qualified member access expressions](#member-access). +See +[how `extend` affects member access](../expressions/member_access.md#extend). If a type implements an interface without extending, the members of the interface may only be accessed using @@ -648,7 +650,8 @@ also has the member names of its constraint. Effectively it is considered to An interface can be extended by defining an interface that includes the full API of another interface, plus some additional API. Types implementing the extended interface should automatically be considered to have implemented the narrower -interface. +interface. See +[how `extend` affects member access](../expressions/member_access.md#extend). ## Dynamic-dispatch witness table