carbon-lang
carbon-lang copied to clipboard
What kinds of patterns are allowed in a class parameter list?
The documented design for class parameters does not specify what exactly is allowed to parameterize a class, though only gives examples where it is parameterized by a list of generic bindings. Currently Explorer allows you to declare a class parameter list with more general patterns, such as class B((T:! Type, U:! Type), V:! Type) {}. What do we want?
Reference: https://github.com/carbon-language/carbon-lang/pull/1194/files/a3d8a4c013927dc6fe30e805cd72caf3df6a0513#r865432125
Structurally, I think we should keep these aligned with function parameter patterns.
I think there are still questions about what kinds of patterns we want to support in these positions, but that seems like it'll need a larger investigation.
FWIW, the example destructuring pattern doesn't worry me specifically.
On the function parameter side, something interesting has come up in the explorer implementation:
// We can support this.
fn F(T:! Type, x: X(T));
// We can support this.
fn G((M: i32, N: i32), x: X(i32));
// We can't really support this.
fn H((T:! Type, N: i32), x: X(T));
The reason is that when type-checking a call to F, we always have an argument expression corresponding to the T parameter, so we can evaluate that expression at compile time to find the type T. And when type-checking a call to G, we can evaluate the argument corresponding to the (M, N) tuple at runtime and then decompose it to find the values of M and N. But when type-checking a call to H, there's no good way to handle the argument corresponding to the (T, N) parameter -- if we evaluate it at compile-time, we require the N piece to be a compile-time constant, and if we evaluate it at runtime, we don't have the type T to hand when type-checking.
So I think I'd like a restriction that :! parameters can only appear either at the top level in a pattern (where the individual parameters of a function are considered top-level, but nested tuples and structs and similar are not), or in a "type position": on the right-hand side of a : or :! or inside the parentheses of a parameterized type:
// OK, top-level
fn A(T:! Type) {}
// Error, not top-level, even though this might be supportable
fn B((T:! Type, U:! Type)) {}
// Error, not top-level
fn C({.T:! Type}) {}
// OK, if we allow patterns in this type position at all
fn D(x: (T:! Type)) {}
// Similarly OK
fn E(x: (T:! Type, U:! Type)) {}
Applying the same rule to classes seems appropriate for consistency to me (that is, we'd give the same answers if fn is replaced with class throughout the above example and the x: are replaced with X:!s), even though there are no "troublesome" : bindings in a class.
The reason is that when type-checking a call to
F, we always have an argument expression corresponding to theTparameter, so we can evaluate that expression at compile time to find the typeT.
That won't necessarily be true once we have variadics, though. That will presumably let us write calls like F(..., [:]Bar()) (using the syntax I sketched in #1162), where I think the language will have to to evaluate Bar() at compile time, thereby evaluating the argument expressions for both T and x, and forcing the argument expression for x to be a compile-time constant, even though F doesn't need it to be.
In other words, T and x can be bound at different phases of compilation only if the callsite has a syntactic form that makes it possible to evaluate them independently.
So I think I'd like a restriction that
:!parameters can only appear either at the top level in a pattern (where the individual parameters of a function are considered top-level, but nested tuples and structs and similar are not), or in a "type position": on the right-hand side of a:or:!or inside the parentheses of a parameterized type:
It seems like the underlying mental model here is that Carbon can evaluate different elements of a tuple literal during different phases of execution only if it's the argument expression of a function call. I would prefer to avoid introducing new distinctions between argument expressions and other kinds of tuple literals, so I'd prefer to solve this problem by dropping the "only if" part.
In other words, I'd prefer the rule to be that when pattern matching during a given phase of compilation, Carbon will only evaluate the parts of the scrutinee expression that will be bound during that phase of compilation, if the scrutinee expression has a syntactic form that permits its parts to be evaluated independently. And tuple literals are one such syntactic form (possibly the only one, although it's tempting to generalize to structs as well).
We triage inactive PRs and issues in order to make it easier to find active work. If this issue should remain active or becomes active again, please comment or remove the inactive label. The long term label can also be added for issues which are expected to take time.
This issue is labeled inactive because the last activity was over 90 days ago.
It seems like we got pretty far along on an answer here, though the door is still open to choose something simple like "no tuple destructuring patterns in function signatures."
We triage inactive PRs and issues in order to make it easier to find active work. If this issue should remain active or becomes active again, please comment or remove the inactive label. The long term label can also be added for issues which are expected to take time.
This issue is labeled inactive because the last activity was over 90 days ago.
Just to note, we decided in #2188 to not allow a nested name binding in the type position and make the type position of a binding just an expression (and not, itself, a pattern).
I still have two other questions here:
- a) How does the proposed "top level" restriction manifest in pattern matching more broadly?
- b) I'm not sure I fully understand the suggested strengthening from @geoffromer.
I'm not sure I fully understand the suggested strengthening from @geoffromer.
My point is that restricting !: bindings to top-level isn't sufficient to solve this problem. More broadly, we can't solve this problem solely with restrictions on the pattern, unless we're willing to say that all bindings in a pattern must have the same kind, which seems unacceptable. We want both fn F(T:! Type, x: X(T)); and fn F(T:! Type, x:! X(T)) to be valid function declarations, and we want F(..., [:]Bar()) to be a valid way of calling the second declaration, but it cannot be a valid way of calling the first one, so the rule needs to consider the scrutinee expression as well as the pattern. We need to require the scrutinee expression to have a syntactic form that corresponds closely enough to the pattern that we can identify and evaluate only the subexpressions that will be bound in a given phase of compilation.
I think one way of codifying this as a user-facing rule would be to say that if two bindings in a pattern have different kinds, their counterparts in the scrutinee expression must be separated by at least one comma. Note that this rule allows !: bindings below top-level; we could disallow that, but I'm not sure that restriction will gain us much. In the absence of that restriction, this is neither a strengthening nor a weakening of @zygoloid's rule; it's just a different rule.