@stable decorator

I added a proposal for @stable decorator in Mojo. This is something we want to implement soon-ish, so that the standard library can adopt it.

Please share your thoughts and opinions.

In particular:

  1. 3p packages. We’re not planning on making @stable available in third-party packages because there is no package description files in Mojo, and so package authors cannot ‘opt-in’ into ‘unstable-by-default’. Does anyone see an alternative way / place / syntax for that?
  2. The “escape hatch” idea of using @stable(recursive=True) on import and comptime needs some thought.
  3. Versioning scheme. I did not put any constraints there, though we’re thinking about standard semantic versioning.

Happy to discuss and change the proposal as we build the consensus.

cheers,

Denis

5 Likes

Package Opt-in and Default Stability

I agree the the default should be unstable, but I think that making packages opt out of this would be better. There aren’t a lot of libraries (22) which would need updates, and all of those libraries need to be recompiled for every release anyway. Could a flag be added to mojo package that makes everything stable by default, and that can be used for compatibility as libraries migrate? Adding a flag to each library in the mojo community repo + Modular’s stuff shouldn’t be super difficult, especially since as far as I know most libraries are quite unstable at the moment, with major API changes most releases, so unstable by default may better fit the current state of the ecosystem.

Compiler Flag: -warn-on-unstable-apis

I don’t think this is a good default. It would be very easy for someone not paying attention to Mojo development to stumble across a feature that does exactly what they want which is unstable, and which we later change due to soundness or API design issues. The next logical solution to this is a -no-warn-on-unstable-apis flag, but I don’t think that’s a good idea either. As Mojo evolves, different parts of the stdlib may be at different levels of unstable, from what we had with early reflection (compiler segfaults all around), to __type_of which, at least to my external observation, was more or less done from when it was introduced. I think that looking at Rust’s system of stability feature flags works out a lot better here, since someone who wishes to make use of an unstable API can opt into a single unstable feature-set without removing the warnings when they use other unstable APIs. This allows for people who want to write production code with unstable features that they require (for example, the Linux kernel and Rust’s allocation support) to ensure a very narrow scope of risk. For instance, this would allow Modular to determine when something is “stable enough” for use in MAX or other products without requiring additional linting or review to avoid highly unstable features slipping in to production code.

See The Unstable Book - The Rust Unstable Book and Stability attributes - Rust Compiler Development Guide for examples.

I think that links to tracking issues is particularly useful, since that can be used to point users towards a place where they can figure out if the feature is “stable enough” for their use-case. However, I’d like to expand this idea to the ecosystem instead of making the stdlib special, so a full link instead of just an issue number would be desirable. I also think that having the stable feature still have the associated feature flag is helpful because when a feature is marked as stable, the compiler can help with the move to stability by issuing warnings for everywhere that is explicitly marked as unstable with that feature.

Whether Mojo wants to adopt Rust’s policy of making nightly the only compiler that can access unstable features is another discussion. This is done via some compile-time configuration of the compiler and can be overriden via the bootstrap variable, but I can see arguments that people using unstable features should be updating fairly frequently, and others that auditing unstable features for soundness can be difficult so they would prefer to stay on stable releases instead of doing constant audits.

@stable(recursive=True)

I think that feature flags can help here as well. Presumably, when someone uses this kind of import they will audit the code under it. In order to avoid accidentally opting into things they weren’t expecting, having specific features given as part of this would ensure that users are warned of new unstable features which may not have been audited, and allows for enabling, say, an unstable data structure implementation without also dragging in its highly experimental yield-based iterator which has some outstanding compiler bugs.

Q: Should tests be able to use unstable APIs without warnings?

I think that making tests document which features they depend on would be helpful if they depend on unstable API surfaces, since this can be also be re-used to categorize tests by feature flag. For example, @test(feature="collections/dict") should be able to enable that feature in the test code and allow the user to run collections or collections/dict as a test category via some integrations between TestSuite and either environment variables or a future mojo test cli.

Versioning scheme. I did not put any constraints there, though we’re thinking about standard semantic versioning.

Would @stable(if: fn() -> Bool) work? I can think of a lot of things which could make some APIs unstable under some circumstances. For instance, there are plenty of MAX APIs which are only stable on NVIDIA or AMD CDNA, and I think we will find that some SIMD APIs only really work if AVX512, ARM SVE or RISC-V Vectors is present in the target and there may be a lag time before a good implementation for Scalar, SSE, AVX, AVX2 and NEON can come about. Of course, “compare with a constant we shoved in the package root somewhere” is easy to do and a function for that can be provided. I’ve seen Rust run into a few situations where stability was blocked because of difficulties on one target, when most other targets where perfectly fine, and I think that allowing library developers to express a more granular “stability” would be helpful. This could also neatly roll in feature flags if and make those library level constructs (aka a big pile of constants). It will generate some extra work for the compiler, so I’d be happy if @stable_if was broken out and moved to its own thing while @stable remained fairly restrictive but fast.

I think that avoiding strings for the simple reason of not needing to do big piles of string parsing as part of this would be helpful, so perhaps a SemVer(major, minor, bugfix) type would be helpful?

Hey Owen, thank you for being so quick!

In this post you offered quite a bit of insightful comments, please bear with with the team while we process your ideas.

One thing to not, we’ve been looking at this feature as something that would be “opt in”, used by a small percentage of the Mojo ecosystem initially. The goal here is to introduce the stability concept as something “one can try” but doesn’t have to. In other words, I wanted to make this feature to have minimal friction (none) for someone who just want to author a Mojo package or wants to use it.

Yes, this is a neat idea!

Do you see this compatible with the “unstable by default”?

I mean, in the current proposal, all APIs are unstable by default and hence there is no place for the API author to put this tracking issue or any other metadata.

I’m afraid not (yet). Mojo decorators are special-cased in the compiler now, they are not true meta-functions, and parsing of decorator inputs is very primitive as of now.

Everything is doable, and having decorators able to refer to functions is reasonable, but just not on the timeline for 1.0 imo.

Since you mentioned strings vs SemVer class, what are your observations on how the string versions are used in Rust, do you observe any friction there?

I like the proposal. One example made me remember a discussion in the community about struct fields that are only safe to be read but should never be modified like capacity:

@stable(since="1.0")
struct List[T: Copyable & Movable](
    Boolable, Copyable, Defaultable, Iterable, Movable, Sized
):
    # unstable by default
    var _data: UnsafePointer[Self.T, MutOrigin.external]

    @stable
    var capacity: Int

I know that access modifiers/restrictions are not on the Mojo roadmap for Phase 1. Nevertheless it might look strange or raise misleading expectations to mark a field as @stable that is unsafe/forbidden to modify.

1 Like

One thing to not[e], we’ve been looking at this feature as something that would be “opt in”, used by a small percentage of the Mojo ecosystem initially.

If you and the team are fine with a flag day at some point which flips this, then leaving the default as stable and having an “opt into unstable” flag seems fine.

I think that links to tracking issues is particularly useful

Yes, this is a neat idea!

Do you see this compatible with the “unstable by default”?

I mean, in the current proposal, all APIs are unstable by default and hence there is no place for the API author to put this tracking issue or any other metadata.

I think this is compatible, since the default can still be unstable. What I’m picturing is an @unstable decorator that gives places to plug in this information, and a flag for the compiler that warns on unstable APIs without an annotation and tracking issue (mostly for the stdlib, but of course others can use it). I think this is a reasonable use-case for an unstable decorator.

Would @stable(if: fn() -> Bool) work?

I’m afraid not (yet). Mojo decorators are special-cased in the compiler now, they are not true meta-functions, and parsing of decorator inputs is very primitive as of now.

I assume this also means that an expression which yields a boolean isn’t an option?

Everything is doable, and having decorators able to refer to functions is reasonable, but just not on the timeline for 1.0 imo.
Since you mentioned strings vs SemVer class, what are your observations on how the string versions are used in Rust, do you observe any friction there?

One of the main reasons I’d like a type instead of strings is that there are a lot of disagreements on what semver numbers mean in the face of ambiguity. For instance, in many package managers, 1 often means “any version compatible with 1.0.0”, but someone may intend for it to mean 1.0.0 exactly, omitting the zeros as math allows us to. I can see arguments to both sides, since some people may want to use 1 to refer to something that exists in 1.0.0 but was removed in 2.0.0 based on python packaging conventions. As such, I think the easiest way to make it clear that it’s a single version number which needs all three components using a type.

@christoph_schlumpf I think this is an orthogonal discussion. There are solutions to this in the form of something like C#'s properties, which provides affordances for “public read but you can’t modify it except inside of a member function”, and I think that one would mark the property as stable. This does lean on inlining a bit, but some of the other consequences of that language feature (such as validating on write) are extremely useful.

Yes, I think this is where we are going towards. World-wide default is “stable”, and a flag that flips it. The flag, if I get what you meant, would need to be somewhere in the package description. It can’t be an input to ‘mojo –package’ because we want both source packages and .mojopkg have same semantics. So it ought to be some kind of config file. I wonder if anyone thought about this already, how Mojo packages should be designed?

I thogught more about this, and I think we can quickly add something like this. I mean, parsing it is doable. Then executing the function at compile time is a bit of gymnastics, but also doable.

What is the syntax you have in mind? Something like this?

@stable(condition=functionThatReadsEnvVarAndReturnsTrueOrFalse)

When I said “flag day”, I meant that if we start with unstable by default now, there will be a day when you change the flag to be stable by default and it will cause a bunch of churn as that change is dealt with, especially in the absence of an @unstable annotation to explicitly mark unstable things. If an @unstable exists, then this becomes a lot less of a problem.

It can’t be an input to ‘mojo –package’ because we want both source packages and .mojopkg have same semantics. So it ought to be some kind of config file. I wonder if anyone thought about this already, how Mojo packages should be designed?

From my perspective, I think we may be slowly accumulating a need for information about Mojo packages. In addition to the stability flag, I can think of a few more things that packages need a place to express:

For each output (binary, library, future other things):

  • The name of the output (mandatory)
  • The type of the output (library, binary, etc) (mandatory)
  • The relative path from the config file to the root of the
  • Highest known working Mojo version
  • Minimum supported Mojo version
  • Linker flags
  • Names of dynamically loaded libraries, if they are known statically.
  • Ideally metadata about the exposed defines which are intended for public use. (ex: -DENABLE_MT_SAFE=1 would be documented but not -DENABLE_EXTREMELY_SLOW_INTERNAL_DEBUG_HELPER)
  • Author name/email
  • Repo link
  • Everything else needed for Software Bill of Materials (SBOM) | CISA (Security people will like having this as a first class citizen, but this can wait a bit)
  • Somewhere to shove arbitrary json-like data so the ecosystem can handle anything we can’t think of.
  • Possibly something like the Linux Kernel’s MAINTAINERS file which associates particular parts of a codebase with people or groups. This might help people tag the correct team in issues for Mojo/MAX proper, and is generally nice to have for large multi-maintainer projects.

Ideally, you’d want way to set some of this package wide and then inherit/override per sub-package. I’m not married to a format but toml has worked well for Rust since it looks like ini for simple things but has the full expressiveness of JSON when it’s required.

I [thought] more about this, and I think we can quickly add something like this. I mean, parsing it is doable. Then executing the function at compile time is a bit of gymnastics, but also doable.

What is the syntax you have in mind? Something like this?

@stable(condition=functionThatReadsEnvVarAndReturnsTrueOrFalse)

I think some syntax like that is good. If it can be made to take an expression, that would be nice, but not being able to do that isn’t the end of the world. That should lead to syntax like:

@stable(is_comptime_warn_stable)
fn print_compiler_warning(...):
    ...


@stable(target_has_masked_simd) # Scalar fallback means this is very slow without that feature
fn mask_simd_kernel_with_fallback(...):
    ...

# if expressions are easy to add
@stable(MOJO_VERSION >= "1.0")
fn foo():
    ...

Not having expressions makes one-off options a little bit more verbose, but that can be fixed later if it annoys people enough to be worth changing since the change should be possible to do in a backwards compatible way.

Thank you again Owen, the list of package metadata looks very neat. Just listing all these properties pushes us towards finding time/bandwidth to design the proper package system.

1 Like