I've been thinking off-and-on about the various "effect system"-like ideas people have proposed for various function behaviors, like "this code won't panic", "this code won't block the async runtime", etc. Recently, I was reading about how the restrictions on Send
and Sync
are enforced through the library functions imposing requirements, not language magic, and it got me thinking about if one could do a similar thing with relatively minimal compiler support, and I've come to this idea:
All functions and methods can by annotated with effects through a syntax like:
#[effects(no_unwind, no_panic)]
fn wrapping_add(a: usize, b: usize) -> usize { .. }
Where the #[effects(..)]
attribute is just a list of identifiers (feel free to bikeshed syntax and keywords).
From a language design perspective, the main requirement is that a function that uses an effect can only call other functions which also use the same effect. So, for example, this would be rejected:
// This function doesn't supply the attribute.
fn panics_if_given_zero(num: usize) {
// note that the actual `panic!` in here is irrelevant to this example,
// the compiler only looks at the lack of an attribute on the function.
if num == 0 { panic!() }
}
#[effects(no_panic)]
fn wrong_effects(num: usize) {
// Compile error: `no_panic` effect is required, but `panics_if_given_zero` doesn't have it.
panics_if_given_zero(num);
}
As an escape hatch, there's a new block hold_my_beer(..)
(feel free to bikeshed the name and syntax, I think this would need to be a keyword) that allows you to call functions which are missing specific attributes:
#[effects(no_panic)]
fn will_not_panic(num: NonZero<usize>) {
// `num` is non-zero, so this call can't actually panic.
hold_my_beer(no_panic) {
panics_if_given_zero(num.into())
};
}
(this block is conceptually similar to unsafe
in that it disables certain checks, but probably shouldn't be called unsafe
since this isn't a safety requirement).
The escape hatch is meant to let you come up with whatever effects you want, and then you can still use other libraries that weren't written with that in mind (including using stdlib with some invented effect of your own), albeit with some extra effort.
Stdlib can annotate all of its methods with commonly-wanted effects, like no_panic
, nonblocking
, etc. And, since you can work around your dependencies not using an effect you want, we could always let the ecosystem experiment and annotate a new effect when we see demand for it rising in the ecosystem (would there be a demand for no_alloc
? idk, but we could see what people do and figure it out).
I'm sure there's plenty of edge cases to figure out, like function pointers and traits. It already seems hard to me to figure out how to annotate all the methods on Result
, since a lot of them would inherit effects supplied by one or more function arguments, which is why I marked this as a pre-pre-rfc. I'm wondering if anyone else has thought about doing this before (I feel like this is natural enough that I can't be the only one to have thought of it, though I haven't seen an approach like this discussed elsewhere), and what edge cases there might be that I'm missing that might make this harder than I think to implement, before I try to make a sketch of how it would interact with every feature in the language.