Idea: diagnostics::fickle

Quick concept. There's a number of constants (e.g. UNICODE_VERSION) where the value is constant for any specific library version and target/configuration combination, but changing one of those is allowed to change the value of the constant. As such, introducing these constants into the type system via const generics is a potential footgun.

It'd be nice if we could mark such constant items as "fickle" and have the compiler warn when using them in a context which isn't itself marked as fickle. This would be granular; it's an accepted usage if the function or const item definition it appears in is marked as fickle. Using a fickle const as a const generic would be marked with #[allow] to acknowledge the lint and that this use is deliberate and handles the potential variance. Use outside the type system (i.e. in non-const expressions) would of course remain non linted. (I'm unsure on whether inline const blocks should lint fickle consts.)

There should probably be some looseness around "universal" usage, so e.g. all the usage of [u8; mem::size_of<T>] doesn't suddenly lint everywhere, since that's probably nonproblematic usage. The "full smart" analysis would be the lack of any non-universally-quantified impls on the type, but that's unfortunately a very global question, and an open one at that, since crates can add new trait impls. (More precise but also significantly more complicated would be only linting once a bounded obligation is required. Also deliberately ignores post-mono filtering of the const, since that's, well, post-mono[1].)

This could be extended to traits where removing an impl shouldn't be considered breaking. Currently stable, I think that's only Drop, but IIRC a primary reason Freeze is still private (despite technically being publicly observable via const restrictions) is that whether a type is Freeze (contains any shared mutable state before indirection) isn't considered API-stable information, and documenting it could give the impression that it is. Having a clear marker that it's fickle mitigates that somewhat. I also vaguely recall some other potential const fn type introspection in the vein of mem::needs_drop that haven't been provided for similar concerns.

I suppose this technically falls under the general category of the portability lint group.


  1. const panic triggered post-mono errors might potentially be moved up earlier in the pipeline to "post instantiation" but pre codegen to eliminate build/optimization-dependence. Don't quote me on this. See recent discussion w.r.t. the stabilization of inline_const for the context. ↩︎

3 Likes

This reminds me of the same problem with array lengths that came up in

2 Likes

I've mentioned this elsewhere before but rust really needs to have two levels of pub - one which exports the definition of an item and one which merely exports its existence. For the sake of explanation, let's call these pub and pub(weak).

For constants, the difference is whether the value of the constant is visible to at compile-time. eg.

mod stuff {
    pub const PUB_LEN: usize = 3;
    pub(weak) const WEAK_LEN: usize = 3;
}

use stuff::{PUB_LEN, WEAK_LEN};

// this type-checks
let foo: [u32; PUB_LEN] = [1, 2, 3];

// this does not
let bar: [u32; WEAK_LEN] = [1, 2, 3];
// error: mismatched types
// expected an array with a fixed size of WEAK_LEN elements, found one with 3 elements.
// note: stuff::WEAK_LEN equals 3 but is declared as pub(weak).

That is, constants that are declared pub(weak) won't normalize to their value when they're used outside their defining module. Constant expressions involving them will get stuck and stop normalizing once the next step of evaluation requires inspecting the value.

For const functions the exact same logic can apply - they won't normalize outside their defining module and so you can end up with the type-checker handling types like [u32; my_const_fn(3)].

For types, pub(weak) gives you a way to export a type while hiding its implementation even if the type doesn't happen to be a struct. Right now if you want to export a type from your library - and that type happens to be implemented as an enum or type alias - then the only way to hide the implementation is to use a wrapper struct. pub(use) would allow you to just export the type directly. In fact I think it would also completely negate the need for having visibility qualifiers on struct fields, since most structs either export all or none of their fields and those that export none don't need to export the fact that they're a struct at all, whereas those that export some could usually be improved by factoring those private fields out into a separate opaque type.

For traits, pub(weak) would export the existence of a trait and its signature (ie. its generic params and super-traits) but wouldn't export the trait's definition (ie. its associated items). This makes it impossible for downstream users to implement the trait or access those associated items but they could still use the trait in trait bounds. ie. pub(weak) on a trait makes it a sealed trait, with the added restriction that if you want to have methods that users can call then you need to put them in an extension trait.

Idris has this btw. public exports the signature of a declaration but keeps its definition private during type-checking, whereas public export exports the definition too.

13 Likes

I’m not 100% sure these capabilities should all be grouped under the same name, but they would all be useful. I’ll note that you can use a static for your weak const and a wrapper function for your weak const fn, but that’s pretty verbose.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.