[Pre-RFC] Genericity over static values

AFAIK, the plan is to make such overflow/underflow in constant expressions an error, just like division or reminder by zero are already errors. Integer overflow in general will be a breaking change, so I’m not worrying about that.

@glaebhoerl

I have made some changes based on your feedback, but there are a couple of difficulties.

When I started writing this, I did start to use Foo<const 1 + 2>. The problem is that you can also have Foo<const 1>>2>, which is ambiguous. The comparison operators also seemed like they could cause problems (either due to outright ambiguity or simply being really confusing to read). After trying a couple of alternatives, I started to feel like the only sane option was to use paired delimiters, and {} seemed to be the obvious choice. In effect, the [T; N] syntax is already a variation on this solution, since the closing ] is what closes the constant expression.

My motivation for treating ranges specially now is because they don’t raise issues with coherence checks, and because I need this specialization for libraries I’m writing. I could be convinced to either modify the syntax or to split the where part off into a separate RFC, but I do want to try to get this feature by some means pretty soon after the rest lands.

Actually, what I would really like is for the where clauses to allow a match pattern, since the following is a similar capability at the value level to what I want to be able to do at the type level:

match N {
    1 => foo(1),
    2...10 => bar(N),
    _ => (),
}

The reasons that I didn’t try to include this are:

  1. The inclusive match syntax is being bikeshed right now.
  2. The current match pattern syntax only allows inclusive matches bounded at both ends. But of course what I need the most is a range that only has a lower bound, as in RangeFrom.
  3. Match patterns are not checked to ensure that the alternatives are non-overlapping, but for the coherence check changes that I want, it’s important that the compiler does check whether patterns are non-overlapping.

Nonetheless I could try to wait/advocate for match patterns to be improved, and then propose a solution where any match pattern could be used in a where clause and also used for coherence checks.

@aepsil0n

@glaebhoerl beat me to the punch regarding under/overflow checks. My proposal is AFAICT specifying the the same arithmetic semantics for parameters as are used for any other const values.

What do you think of using match patterns rather than (or in addition to) predicates? I want to syntactically distinguish constraints that can inform coherence checking from those that can’t, to make it easier for developers to figure out which is which.

That way it makes much more sense to me. There will most likely be some overlap between pattern matching and predicates, but that is already true outside the type-level, so I don't see it as an issue. Still I'd argue to stick to equality constraints only first and then deal with pattern and predicate constraints in separate orthogonal RFCs.

Oh, that's definitely unfortunate. I wonder though (parsers aren't my specialty)... my thinking was that const would simply introduce a term context within a type context. At the term level, when someone writes 1>>2, we don't have any difficulty with determining whether the expression ends at 1 or also encompasses the >>2. So why here? (But yes, the visual confusion is a bad thing irrespective of whether or not the parser would also be confused.)

A more radically austere solution, not that I'm necessarily advocating it, might be to allow only literals and named constants, and then any more complicated expressions could only be used by first assigning them to a named constant.

My motivation for treating ranges specially now is because they don't raise issues with coherence checks, and because I need this specialization for libraries I'm writing.

That does make sense; it's just not obvious to me that the proposed solution/syntax is the "right" one. Just brainstorming, but a couple of other ideas:

  • We could write where n > 0, where n == 0, where n > 0 && n < 10, and so on, but unlike the term level, restrict these expressions to only allow expressing predicates which are (obviously) equivalent to some range constraint as in the draft RFC.

  • We could add a value in range: bool expression, which is allowed at both the term and type levels. E.g. 1 in 0.. == true, 2 in 5..10 == false, and so on. Of course this is likely to conflict dramatically with @pnkfelix's proposed placement new syntax, and possibly with for..in as well.

But maybe match patterns make sense too... I was going to object that unlike match these are unordered and required to be non-overlapping (as you note), but I guess that's a property of match, and not the patterns themselves. (I wonder if instead of in, a general expr is pattern: bool operator might be appropriate here, as has been proposed before, again at the term as well as type levels, especially with a view to the future when we may add type-level enums?)

The simple answer to your question is that if you write let x = 1 >> 2; the parser knows that the expression continues as soon as it hits the >>, purely by knowing that no type context has been established, without needing any other context at all, whereas in a parameter list it would have to look ahead to see if whatever is farther to the right is an expression or something that can follow a type. (A similar kind of difficulty is why we have to write Vec::<usize>::new() instead of Vec<usize>::new().)

My understanding is that right now the rustc parser expects to be able to tell whether it's in an expression or type context without having to do lookahead. This is admittedly a property of the current reference compiler that sometimes seems to be too restrictive, and could change, but it does have its good points (besides the reference compiler, other parsing tools are easier to create if the language is designed this way).

Regarding your "austere" solution, I don't think that allowing only literals and named constants will give you the same expressiveness, because there would be no way to translate something like this:

trait SomeTrait {
    fn do_stuff<const N: usize>(x: Foo<N>) -> Foo<{N-1}>;
}

I will think about where clauses and your comments some more tonight. It occurs to me that the ability of match patterns to overlap might not be such a big deal after all. What's important for coherence checks is that it has to be possible to give some kind of verdict on whether two constraints can overlap, whether or not match patterns do the same thing. Furthermore, we could recover something approximating the fuller match pattern semantics if negative bounds eventually become possible:

// Don't worry about the syntax for this example.
// The point is that you can define overlapping impls, then make them compatible by giving one a negative bound that handles it.
impl<const N: usize> SomeTrait<N> for Foo<5>
      where const N: 5...7 {/* ... */}
impl<const N: usize> SomeTrait<N> for Foo<N>
      where const !N: 5...7, const N: 2...10 { /* ... */ }

Updated again. I’ve removed discussion of where clauses, and added a Parameter trait that is implemented by only those types that can be const parameters.

I’ll put together another RFC for where extensions, though it’s going to depend somewhat on what happens with range syntax in match patterns (at least for the examples). As you can tell, I’m leaning toward using patterns to some extent, but I’m still trying to put together a more complete picture in my own head before I propose specifics.

On another topic, the idea behind the Parameter trait (which I hope is clear from the RFC draft) is that it’s a special marker trait like Sized, which clears up a minor issue now, and which will become more useful as we get a more complete system for dependent types. My vague idea for how we could expand this incrementally is something like the following:

  • Add integer primitives (and bool) to deal with the immediate issues regarding interaction with [T; N].
  • Add some combination of tuples, arrays, &'static, and any other sized built-in types I’ve somehow forgotten. Tuple and array types will, of course, only implement Parameter if the contained types do. If we add it, &'static doesn’t need this restriction.
  • Allow struct/enum types with some strict conditions (e.g. they implement Copy, Eq is derived, all fields implement Parameter, all fields are public).
  • With CTFE, it may be possible to loosen some conditions a bit.
  • Add unsized types and floating-point values very carefully or never.
2 Likes

I’ve proposed this as RFC #884.

I got sidetracked by the integer range patterns question before I got very far into writing down my thoughts for a where clause followup, but I’ll probably take a crack at it again pretty soon.

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