-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
const fn
and generics
#1
Comments
Here is the simplest possible thing I could come up with, and I hope @Centril et al will tell me why that is not sufficient. ;) I propose that we have So, for example, in const fn ptr_eq<T>(x: &T, y: &T) -> bool {
unsafe { x as *const _ == y as *const _ }
} we check that we have a In const fn foo<T: PartialOrd>(x: &T, y: &T) -> bool {
x < y || y < x
} we don't actually check anything locally, because the I just realized one complication with this is default implementations and specialization. I suppose that we could let you write What I am trying to avoid here is requiring annotations like Has such an idea already been discussed? Is there any reason why this does not work? |
First, I infer from your setup that we have the following typing rule:
is that accurate? If not, the system is pretty unusable. Our primary goal with The idea of implicitly bounding traits as pub trait Hash {
fn hash<H>(&self, state: &mut H)
where H: Hasher; // As defined today...
} If we accept the typing rule above, then a const impl Hash for Foo {
fn hash<H>(&self, state: &mut H)
where H: Hasher // this is **NOT** `H: const Hasher`
{
// can't use any methods of `H`!
}
} We can't write: const impl Hash for Foo {
const fn hash<H>(&self, state: &mut H)
where H: const Hasher
{
...
}
} If we did, then either the typing rule above can't hold, or it does hold and if we allow the impl, the system is unsound. It is unsound because we can write something like this: trait Bar {
fn bar() -> usize;
}
trait Foo {
fn foo<T: Bar>() -> usize;
}
struct Wibble;
struct Wobble;
impl Bar for Wibble {
fn bar() -> usize {
read_usize_from_file()
}
}
const impl Foo for Wobble {
const fn foo<T: const Bar>() -> usize {
T::bar()
}
}
const ITEM: usize = Foo::foo::<Bar>(); So the contravariant position of type parameters is causing problems for us if we wish to support existing code. Instead, if we encode things as (this is not the surface syntax, but rather a language we can desugar to...): trait Hash<effect E = io> {
E fn hash<H: E Hasher>(&self, state: &mut H);
} then the definition is backwards compatible. Here, ...to be continued... |
Ah, that's where all that stuff is. :)
I can only guess what exactly that means, but every
I think restricting the type system to just what I described makes this discussion just so much simpler.
Under my proposal, this can use the methods of I can try to express this using fancy syntax as well: Let's say we have "const mode variables for<\c> \c impl Hash for Foo {
fn hash<H>(&self, state: &mut H)
where H: \c Hasher
{
// *can* use all methods of `H`!
}
} So, when you need a EDIT: Looking at some of your work, it seems you would write that using |
One thing I should may more explicit: I claim that every program accepted by the const type system would also be accepted by the "normal" type system. This is interesting because const-safety of a function does not imply safety (because const-safety only talks about what happens when you use the function with const-valid inputs). IOW, I am not taking an effect system point-of-view here. Instead I think of there being two separate type systems, but they have this useful relationship. I think that means we can avoid all the usual complexity that effect systems bring. |
Reading over https://github.com/Centril/rfcs/blob/rfc/const-everywhere/text/0000-const-everywhere.md, I realized I did not talk about what to do with
IOW, it is sugar for
which I think you would write
but I think this lacks the crucial information that the two The trouble with this is that we already allow creating |
I want to reuse the same mechanism for other forms of restrictions such as "this can't panic" and "this will always terminate" as well as adding more abilities such as "this can use RandomIntelExtension" or "this can use async", so I'd like a system that is composable, flexible, expressive, and ergonomic to use.
The rule says that if
Sure, but then you lose out on a bunch of interesting things you might want to encode :) fn foo<F: const Fn() -> usize>(fun: F) {
write_to_file("bar", fun()); // `fun` may not cause side-effects.
}
Yeah, this is much clearer wrt. what you mean, which is totally different from what I inferred (subtyping) at first :) for<effect C> C impl Hash for Foo {
fn hash<H>(&self, state: &mut H) where H: C Hasher {
...
}
} where the possible effect is Also note that if you have some existing trait (as defined today) with provided methods where their definitions have side effects, then you'd need to require the
Well, you could use
Right; but it amounts to an effect-polymorphic system, albeit a limited and implicit one. pub fn replace_with<T, F>(val: &mut T, closure: F)
where
F: no_panic FnOnce(T) -> T
{
unsafe {
let old = ptr::read(val);
let new = closure(old);
ptr::write(val, new);
}
} Since we've guaranteed that
Yes; this is one of the major problems with the "exposed body" approach. So what am I currently contemplating instead? We can use lightweight quantification: pub trait Hash<#E = io> { // #E denotes an effect variable... We could possibly elide `io` here
fn hash<H>(&self, state: &mut H)
where H: #E Hasher; // As defined today...
} (We can totally bikeshed the sigil We can use implicit quantification on functions: pub trait Iterator<#A> { // = io elided
type Item;
#A fn next(&mut self) -> Option<Self::Item>;
...
#A fn chain<U>(self, other: U) -> Chain<Self, <U as IntoIterator<#B>>::IntoIter>
where U: IntoIterator<#B, Item = Self::Item>,
{ ... }
#A fn zip<U>(self, other: U) -> Zip<Self, <U as IntoIterator<#B>>::IntoIter>
where U: IntoIterator<#B>,
{ ... }
#A fn map<B, F: FnMut<#C>(Self::Item) -> B>(self, f: F) -> Map<Self, F> { ... }
#A #B fn for_each<F: FnMut<#B>(Self::Item)>(self, f: F) { ... }
#A fn filter<P>(self, predicate: P) -> Filter<Self, P>
where P: FnMut<#B>(&Self::Item) -> bool,
{ ... }
#A fn filter_map<B, F>(self, f: F) -> FilterMap<Self, F>
where F: FnMut<#B>(Self::Item) -> Option<B>,
{ ... }
...
#A fn flat_map<U, F>(self, f: F) -> FlatMap<Self, U, F>
where
F: FnMut<#B>(Self::Item) -> U,
U: IntoIterator<#C>,
{ ... }
#A fn flatten(self) -> Flatten<Self> where Self::Item: IntoIterator<#B> { ... }
// Same as:
#A fn flatten<#B>(self) -> Flatten<Self> where Self::Item: IntoIterator<#B> { ... }
...
#A #C #D fn try_fold<B, F, R>(&mut self, init: B, f: F) -> R
where
F: FnMut<#C>(B, Self::Item) -> R,
R: Try<#D, Ok = B>,
{ ... }
...
} I don't think this is too bad. It is backwards compatible and also scales to more effects and restrictions. We can regain the syntax If writing Of course, all of this is more complex, but also more flexible and expressive.
with one more on the way:
so people will already be used to quantify over a bunch of stuff. @alercah had more ideas so I'll let them fill in... |
I had also thought about some level of inference: e.g. by default, traits are considered to be fully effect-polymorphic (by adding an implicit effect parameter and applying it to all functions). This would only be done where it was safe (e.g. no higher-order functions involved? or perhaps only in limited cases of higher-order functions), but would help make effect-polymorphism usable on existing traits without having to add it explicitly, creating a giant migration headache. |
It seems to me that @RalfJung's syntax is both forward-compatible with an effect system, and eminently reasonable as an actual syntax. Even if we ever get an effect system, I strongly suspect we would want such a shorthand anyway. It also seems to me that the sorts of things @Centril wants to add are... well beyond the complexity budget. Fun to think about, perhaps useful as a tool to make |
Notice that Yes, my system is deliberately restricted. I don't see Rust getting a full effect system any time soon, and I wouldn't want to wait with I agree, though, that terminology from a power powerful system can be useful in this discussion.
Again, I'd like to separate
I do not understand what you are saying here.
The key difference between your |
I talked with @nikomatsakis about this and realized that what I proposed for trait objects doesn't work, for the not-very-surprising reason that they are a lot like function pointers. The following works in stable today: const X : &'static std::fmt::Debug = &(); So, I came up with a different variant for run-time function pointers (i.e., const fn twice<T>(arg: T, fun: const fn(T) -> T) -> T {
fun(fun(arg)) // OK. We know that `fun` respects the rules of `const`.
} This function signature means that when With some kind of quantification, that would translate to for<\c> \c fn twice<T>(arg: T, fun: \c fn(T) -> T) -> T {
fun(fun(arg)) // OK. We know that `fun` respects the rules of `const`.
} So, essentially, all struct Foo<T> {
x: usize,
f: const fn(T) -> T,
}
const fn twice<T>(arg: T, fun: Foo<T>) -> T {
fun.f(fun.f(arg)) // OK. We know that `fun` respects the rules of `const`.
} Again, We would need something similar for I don't like how this is getting more complicated, but I can't think of anything that is simpler and backwards-compatible. One possible concern with this proposal is the asymmetry between trait bounds (where a "const-context only |
What meaningful work could a |
@Ixrec I have an example in my post
Also, we have no other choice if we want to remain backwards compaible. Safe const code can already create references to |
Let's not less this stagnate yet. :-) |
Summarizing today's discussion of @oli-obk, @Centril and me on Discord: We all agreed that whatever proposal we come up with should be able to desugar into a full-blown effect system. That helps for discussion, for future addition of such a system (if that ever happens), and to check the design. The first implementation will likely not use that desugaring though but implement the rules directly, as that is much simpler. @Centril's main point of contention with my proposal are (1) handling of provided methods in traits (i.e. those where the trait definition provides a default implementation), and (2) uniformity issues around explicit As for (2), we can easily decide to make people write For (1), we realized that my proposal had a problem: I imagined that when you write I think that's it? Please amend if I missed anything. |
@RalfJung I'm undecided on the last point too, but I think it's quite permissible to leave that as an unresolved question for now. Do you feel we're at a point an RFC can be written for all this now? |
Maybe? I plan to submit something on const safety and promotion to this repo within this month. I'd be happy for someone else to write up the generics stuff :D |
@RalfJung I think promotion is already sorted... I did that in a PR a few months back. |
I am not talking about implementation, but about https://github.com/rust-rfcs/const-eval/blob/master/promotion.md |
@RalfJung I thought point 2 was already disallowed, but it seems someone added a |
Why should unsafe const code not be able to do that? It is needed, for example, to eventually make |
Because it causes non-determinism (and thus non-referential transparency)... I don't see why |
No, that is not the case. Casting a pointer to a usize in miri does not actually create an integer. It keeps a pointer value at integer type. See this blog post and the definition of
It stores a pointer and a boolean together in a single Also, maybe let's stop this off-topic discussion here? This issue is about generic,s not about pointers or |
struct Foo<T> {
x: usize,
f: const fn(T) -> T,
}
const fn twice<T>(arg: T, fun: Foo<T>) -> T {
fun.f(fun.f(arg)) // OK. We know that `fun` respects the rules of `const`.
} Does this mean fn foo<T>(t: T) -> T { t }
let x = Foo { x: 42, f: foo }; is not legal because |
(Hmm. I thought an analogous case would be that you can create |
No; I think of Rust as having two typesystems at this point, one used for typechecking code in const context and one for the rest. They are related by the fact that const-well-typed code is also runtime-well-typed. The const type system is the only one making any distinctions based on In rust-lang/rust#53972, you suggested almost the same scheme, except you planned to also allow struct Bar(const fn());
const fn bar(f: Bar) {
(f.0)() // not allowed
} This should be allowed, we said |
@glaebhoerl I do see a formulation in terms of validity/safety though -- in const context, a |
So when using const fn foo(b: Bar) -> (Bar, i32) { (b, 42) } Would be impossible to write for allowing non const fns for fn bar(b: Bar) -> (Bar, i32) { (b, 42) } Is ok without |
Yes, you asked for I could imagine it to be useful that one can write struct Foo(fn());
fn foo(f: const Foo) {
(f.0)(); // allowed because of `const Foo`
} I.e., one could "add constness" -- though I'd really like to see a concrete usecase before considering this seriously. But once it is const, I'd find it very strange to have to repeat that on every level. |
That kind of constness adding would then add constness to all function pointers inside the struct, even if that is not necessary due to only one of them being called inside the function |
Yeah, I'm not a fan either. But at least it keeps newtypes working, which I think is something we should absolutely not break. What you are asking for is essentially types which are generic wrt. const-ness. I'd rather avoid such an effect system until we have a clearly demonstrated need. |
Sgtm. I am for the simple version, since it covers the real use cases I can think of. artificial examples are easy to come up with but rarely useful in practice. I have just one more thing: I presume we can now transmute function pointers to const function pointers (unsafely) and call them and then either weird things happen (like half of full miri things working without feature gates and super useless errors for very nonconst things) or we error out with "tried to call non const fn". I personally am for the former, for the lulz, but I'm happy with either scheme. Unsafe is unsafe after all, and we kind of give the user a way around the guarantees of the language. |
What currently happens is the latter, I think -- I hope (should add a test once calling fn pointers is possible). And indeed I think we should error out immediately when calling a non-const fn, to avoid accidentally stabilizing whatever the heck happens then.^^ |
Yes that's the current behaviour. OK, let's keep it. it's way saner, even if less fun |
Now that I came to like const soundness, of course I had to start thinking about how to achieve const soundness in the presence of generics. The problem is that in a
const fn foo<T: Trait>
, when we callT::method
, how can we know if that method is safe to call in const context? When type-checking the method body, we cannot know if that will later resolve to aconst fn
or not.There was plenty of discussion on this that I did not follow closely, but it seems it mostly happened in chat so I don't know of a good summary. @Centril was involved, probably @eddyb and @oli-obk and I am not sure who else?
I propose to discuss this further here so that we have a place to look at. I see two fundamental approaches:
const
fn. (By "static" I mean that we detect this before even starting CTFE, just on the type system level. In our context here, "dynamic" means "during CTFE evaluation", i.e. when miri runs).(I understand there are other open questions around
const fn
, like avoiding having to manually mark every single function asconst
. Things likeconst mod
have been proposed. Feel free to open a separate issue for those concerns, but I consider such discussion to be off-topic in this issue.)The text was updated successfully, but these errors were encountered: