I think that an equivalent to Rust’s features should be a separate proposal, since they are useful for far more than this (ex: feature os_windows / os_windows_11_24H2 for making various bits of OS-level functionality show up).
1. Where can conforming extensions (extensions that conform to a trait) be?
I think that we should look at prior art for this. Scala allows the wild west, where (using the proposed Mojo parlance) anyone can implement any Trait for any Struct in any Module. While this was mostly fine, it did lead to issues where multiple popular libraries would implement a trait from a third popular library (ex: Logging or ser/des related) on a single type. Rust has what is called the Orphan rule for this. Copying from the linked docs:
Trait Implementation Coherence
A trait implementation is considered incoherent if either the orphan rules check fails or there are overlapping implementation instances.
Two trait implementations overlap when there is a non-empty intersection of the traits the implementation is for, the implementations can be instantiated with the same type.
Orphan rules
The orphan rule states that a trait implementation is only allowed if either the trait or at least one of the types in the implementation is defined in the current crate. It prevents conflicting trait implementations across different crates and is key to ensuring coherence.
An orphan implementation is one that implements a foreign trait for a foreign type. If these were freely allowed, two crates could implement the same trait for the same type in incompatible ways, creating a situation where adding or updating a dependency could break compilation due to conflicting implementations.
The orphan rule enables library authors to add new implementations to their traits without fear that they’ll break downstream code. Without these restrictions, a library couldn’t add an implementation like impl<T: Display> MyTrait for T without potentially conflicting with downstream implementations.
[items.impl.trait.orphan-rule.general]
Given impl<P1..=Pn> Trait<T1..=Tn> for T0, an impl is valid only if at least one of the following is true:
Trait is a local trait
All of
At least one of the types T0..=Tn must be a local type. Let Ti be the first such type.
No uncovered type parameters P1..=Pn may appear in T0..Ti (excluding Ti)
Only the appearance of uncovered type parameters is restricted.
Note that for the purposes of coherence, fundamental types are special. The T in Box<T> is not considered covered, and Box<LocalType> is considered local.
I think that the “same file” restriction is going to be very annoying, so I would prefer to keep it to a module scope. However, there is the decision of whether we want to risk ecosystem incompatibilities in the name of more developer freedom or whether we want to adopt something like Rust’s Orphan rules to avoid that issue at the cost of needing users to do workarounds if a type does not implement a popular trait, such as a Deserializable trait like Rust’s Serde has, which is almost mandatory on many types for proper ecosystem integration. The ability to have that implementation live in a third-party library would mean there would be less need for integrations inside of popular libraries, since said integrations could live in third-party modules which are only pulled in if a user wants those integrations.
My position for this is that we should at least do Option A, Either in the struct’s module or the trait’s module, but consideration should be given to whether the potential ecosystem benefits of not forcing popular libraries to implement every integration themselves outweigh the risk of those integrations which are done by the library authors conflicting.
2. Should we allow non-conforming extensions?
I think that we may want them in “grouped bounds”, such as in the WarpEngineTrait example in the proposal. While it’s technically possible to work around that, it may be very annoying and I don’t want to force creating trait ListMovableT, trait ListCopyableT, trait ListImplicitlyDestructableAndExplicitlyCopyableT, etc.
3. Where can non-conforming extensions be?
I think that all extensions should follow the same rules. Having extensions which implement a trait and those which don’t have different rules is just going to cause confusion and I’d need to see a compelling reason to want to make them different. If we forbid non-conforming extensions outside of the defining file for the struct while allowing conforming extensions to work for whoever defines the trait, we will simply get a pile of one-off traits.
4. What can be in an extension?
Agree with the proposal for the main allowlist.
For initial decorators we may want to allowlist, my votes are @inline, @no_inline, @implicit, and @staticmethod. @clattner has some in-flight work on getting rid of @register_passable that should work fine with not needing decorators.
It may be useful to have additional parameters that are pulled “out of thin air” in order to satisfy parametric traits later down the line, but I agree on not adding things to the base struct.
Importing by name may run into some issues with requires clauses. My personal preference would be for non-conforming extensions to be attached to importing the struct and for the conforming extensions to be attached to the trait. This behavior matches expectations developers may have from Rust, and I think leads to better DevX when a developer can import a trait and get those functions to show up on everything that implements that trait. I’m not sure of the compiler performance implications of this behavior, but if it is combined with module-scoping trait or struct extensions then it should at least avoid whole-program analysis and figuring out which extensions go in what “bucket” should be a fairly quick analysis. My concern is that some “buckets”, like Movable, may grow rather large and cause LSP performance issues, but that would require benchmarking to validate.
5. How do we handle method conflicts?
Given that a user can also run into issues with two traits that have the same method name, I think that disambiguation syntax, like Rust’s <FooStruct as BarTrait>::bazz_func(&foo_instance), would be useful. In the case of the provided example, Mojo’s syntax could be L.Spaceship.fly_to(ship, "Corneria"), with later parametric extensions hanging parameters off of Spaceship as L.Spaceship[...].fly_to(...) for further disambiguation.
6. Are extensions automatically imported?
Per my points in 4, I think B is better. I think that feature flags are a better mechanism to control dependencies, especially given a Rust-like implementation where you can disable compiling whole structs or disable imports if an extension is not enabled.
7. import or import extension?
I agree with @Verdagon, I don’t see a need for import extension.
8. What if we import only an extension but not its target struct?
Allowing this at all seems like a recipe for user confusion unless it does the equivalent of import Spaceship on the file the struct comes from and brings other extensions from that file with it.
9. Support importing multiple extensions for the same struct?
Strong yes from me. If we don’t allow this, then having import List drag in extensions for Copyable, Movable, and ImplicitlyDestructible will cause errors. However, I think that we want those extensions imported by default so that users get the behavior they expect from List[Int].
10. Proposed Syntax
extension List(Copyable) requires T: Copyable:
fn copy(out self, other: Self):
...
fn __copyinit__(out self, existing: Self, /):
...
I don’t like T: Copyable:, but I’m not sure I have a better suggestion that wouldn’t require going back and changing a bunch of other stuff for consistency (ex: requires T is Copyable). However, I think that in the presence of parametric traits this is going to cause a mess. For instance, if we wanted a list of things which could be added to and multiplied with some generic Rhs type, we don’t really have a place to name Rhs if we want the constraint that the Rhs is the same for both add and multiply.
extension List requires T: Addable[Rhs]:
...
There’s nowhere for Rhs to come from.
My suggestion is to allow for adding parameters onto extension.
extension[Rhs: AnyType] List requires T: Addable[Rhs]:
or even better
extension[T: AnyType, Rhs: AnyType] List[T] requires T: Addable[Rhs] # requires is more relevant as types and type bounds get more complex
Which means you don’t need to know that some collections call the element type T and others call it ElementType (ex: LinkedList). This is an annoying distinction I don’t think we need to force users to deal with. At a minimum, this is necessary so that users can say that they don’t care about the value of a defaulted parameter. Since without the ability to “materialize” more types users would be stuck with the defaults for any type they use in an extension, which also means that you wouldn’t be able to make an extension for a non-mutable UnsafePointer or one which is over-aligned for SIMD reasons.