-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expression form basics #5545
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
base: trunk
Are you sure you want to change the base?
Expression form basics #5545
Conversation
Co-authored-by: josh11b <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry for taking so long to figure out what was going on here. I think I understand the path forward better now.
Co-authored-by: josh11b <[email protected]>
Co-authored-by: josh11b <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally looks really good. Some minor nits inline, and resolving some open threads that seem to all be sorted out now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there's a fairly big mismatch between what this proposal is doing and what I was expecting. In particular, both form composition and form decomposition were a surprise to me, and while for the most part I think a model where you have those has the same consequences as a model where you don't, there do seem to be some differences that we should make sure we agree on, such as whether you can initialize a class from a struct value rather than only from a struct literal (or more generally from an expression with struct form). I'd like to take some open discussion time to dig into these differences.
Co-authored-by: Chandler Carruth <[email protected]> Co-authored-by: Richard Smith <[email protected]>
Co-authored-by: Carbon Infra Bot <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The approach here looks good to me. Comments are mostly about how we're presenting the approach in the updates to the design documents.
docs/design/classes.md
Outdated
type, when `source` is converted to a struct or class type `Dest` that has the | ||
same set of field names, the conversion is an initializing expression of type | ||
`Dest` that initializes each field `field` from a | ||
[member access](expressions/member_access.md) `source.field`, in `Dest`'s field |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This form of description, seemingly expressed as a rewrite to the Carbon member access expression source.field
, isn't working well for me. It sounds to me like:
fn F(n: i32) -> i32;
var a: {.x: i32, .y: i32} = {.y = F(1), .x = F(2)};
... would be transformed into ...
fn F() -> i32;
var a: {.x: i32, .y: i32} =
{! .x = {.y = F(1), .x = F(2)}.x,
.y = {.y = F(1), .x = F(2)}.x !};
where the {! ... !}
is a primitive struct initializing expression. But that doesn't seem right: it calls F()
four times instead of twice. Even if we interpret source.field
as meaning that we throw away the rest of source
rather than evaluating it, we'll need to be careful about evaluation order (we want F(1)
called before F(2)
, not after).
(I think the behavior we want is:
- If
source
has primitive form and initializing category, and the types match exactly, initialize from it and stop. - If
source
has primitive form and initializing category, and the types don't match, materialize a temporary and initialize from that instead. - If
source
has primitive form and value or reference category, it is evaluated, and each field is initialized from the corresponding field of the evaluated result (as if we create a binding to refer to the result and name the binding repeatedly). - If
source
has struct form, initialize each field from the corresponding portion of the struct form. Evaluate the subexpressions in the order in which they appear (source field order), but perform any additional steps that are part of the initialization (such as a copy / move operation) in destination field order.)
Maybe something like:
... that initializes each field
field
from the expression element with the same name insource
if it has struct form, and from a reference to or value of the struct field with the same name insource
if it has reference or value category respectively. Any required conversions are performed inDest
's field order, after the evaluation ofsource
.
... could work? (Though I guess we also want to say that an initializing expression gets materialized somewhere here.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe something like:
... that initializes each field
field
from the expression element with the same name insource
if it has struct form, and from a reference to or value of the struct field with the same name insource
if it has reference or value category respectively. Any required conversions are performed inDest
's field order, after the evaluation ofsource
.... could work?
Hmm. My main concern with that formulation is that we'd need to define "expression element" in a way that accommodates cases where source
has struct form, but isn't a struct literal (e.g. if it's a call to a function with a struct return form, or names a :?
binding with a struct form). We can do that, but it seems like we'd wind up duplicating a lot of the specification of member access.
Instead, I've tried to rephrase this so that it's defined in terms of the semantics of member access, but not as a textual rewrite to a member access, thereby hopefully avoiding the impression that the source expression is duplicated. What do you think?
If this looks workable to you, I can make similar changes in pattern_matching.md
and the other places where you raised this issue.
(Though I guess we also want to say that an initializing expression gets materialized somewhere here.)
Do we? Consider e.g.
var {.x = x: i32, .y = y: i32} = {.y = 0 as i32, .x = 1 as i32};
IIUC we don't want there to be any temporary materialization when executing this code; we want the initializing expressions 0 as i32
and 1 as i32
to directly initialize the elements of the var
's storage. For that to work, there can't be any materialization as part of the type conversion, which is what we're specifying here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this looks workable to you, I can make similar changes in pattern_matching.md and the other places where you raised this issue.
Yeah, I think that's working for me. It has enough distance from a syntactic rewrite that it seems clear that's not what's happening. Thanks!
Though I guess we also want to say that an initializing expression gets materialized somewhere here.)
Do we?
Discussed here. I think the general principle is that we want every initializing expression to be "consumed" by something, since otherwise it won't have a destination to initialize. In the case where the initializing representation is by copy, the "consumption" step can and should be a no-op (other than running the destructor), but in the case where the initializing representation is in-place, we really need to materialize a temporary to pass as the return slot to the initializing expression.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm now trying to address this in a more principled way with "outcomes". I've also tried to be more explicit about the fact that initializing expressions must be materialized, particularly by codifying what it means to "discard" an outcome.
docs/design/pattern_matching.md
Outdated
|
||
The binding is _bound_ to the result of these conversions. This makes a runtime | ||
or template binding an alias for the converted scrutinee expression, with the | ||
same form and value. Symbolic bindings are more complex: the binding will have |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "same form" here makes me nervous. I'd prefer that we spell out the properties we're inheriting here, like we do for symbolic bindings. (Though it also seems like we could instead say we get these properties from the declaration of the binding rather than from the scrutinee, and I would prefer that.)
My main concern is that we will probably add other portions of the form beyond type, category, phase, and value. Some possible examples:
- further subdivisions of expression category
- the source location and/or spelling of the expression (to support writing
Assert
by using form deduction) - whether the expression denotes a bitfield (which is part of the equivalent of "form" in C++)
- lifetime or alias set information about the expression
- the pack arity of the expression
Using a "same form" rule would tend to imply that this is valid:
fn X[F:! Core.Form](A:? F, B:? F);
let a: i32 = ...
let b: i32 = a;
X(a, b);
... because the form of b
would be the same as the form of a
. That creates an evolution problem for the language: it'd become a breaking change to add extra information to the form in the future.
To better support language evolution, I think we should consider instead saying that two different expressions never have the same form even if all the currently observable properties that contribute to the form are the same.
I also think that inheritance is not the right default here, even though it is the right behavior for all the currently identified aspects of the form. Bindings are a form of signature, and following the principle (not yet adopted) that the signature is the contract, we shouldn't be using information from the scrutinee that isn't in the signature of the binding.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think these problems apply to your approach as well as mine (and are pretty manageable). For example, suppose we update forms to add information about whether the expression denotes a bitfield. Under either wording approach, we need to make some kind of change here to capture the fact that e.g. given let x: i32 = ...
, the form of an ID-expression x
does not have the is-bitfield property. The only difference is what kind of change we have to make: under my approach, we would convert the initializer to a form that doesn't have the is-bitfield property, and then bind the name to that, whereas under your approach we would bind the name to an initializer that might have the is-bitfield property, but then specify that uses of the binding do not have the is-bitfield property regardless of what the name is bound to.
Source location and spelling are a little different, because (or so I assume) those components of the form of an ID expression would be independent of what the name is bound to. But by the same token, they would also be independent of the binding declaration syntax, because they're determined by the location and content of the usage, so both your approach and mine would need to special-case them.
So I think either approach works, but I prefer mine because it seems like unnecessary conceptual complexity to say that the binding binds to something that is neither the original initializer expression, nor the thing that the ID-expression will evaluate to, but some third intermediate thing. Also, I expect that in a lot of cases we'll have to do a conversion anyway (as we do for type, category, and phase), in which case my approach lets us specify the form of the ID-expression "for free".
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, OK.
I still prefer the other approach: to me it seems less complex to say that the properties of a binding are those specified in the declaration of the binding, rather than to say that the initializer is converted to have the properties specified in the declaration, and then to say that the properties of the binding are those of the converted initializers, and leave it to the reader to prove for themselves that this means that the type and category of the binding always match those specified in the declaration of the binding. But I see your point that this approach avoids the need to distinguish between properties of the binding and the thing it binds to (except for the constant value of a symbolic binding), which would be a different kind of complexity.
I agree that either approach works, and I'm happy to go with your preference here.
Oh, or, maybe there's a third approach that might satisfy both of our preferences: could we first define the properties of the binding, and then say that the initializing expression is converted as needed to match those properties?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, or, maybe there's a third approach that might satisfy both of our preferences: could we first define the properties of the binding, and then say that the initializing expression is converted as needed to match those properties?
Good idea. How's this?
docs/design/pattern_matching.md
Outdated
for it. Then, each nested _proper-pattern_ is matched against `s.i`, where `s` | ||
is the possibly materialized scrutinee expression and `i` is the 0-based index | ||
of the nested pattern within the tuple pattern. The tuple pattern matches if all | ||
of these sub-matches succeed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Similarly to the suggestion in classes.md, talking about the Carbon expression s.i
isn't working well for me here. How about something like:
for it. Then, each nested _proper-pattern_ is matched against `s.i`, where `s` | |
is the possibly materialized scrutinee expression and `i` is the 0-based index | |
of the nested pattern within the tuple pattern. The tuple pattern matches if all | |
of these sub-matches succeed. | |
for it. Then, each nested _proper-pattern_ is matched against the corresponding | |
element of the resulting tuple value, reference, or form. The tuple pattern matches if all | |
of these sub-matches succeed. |
(I'm still a bit uncomfortable with this because there seems to be a category error here, in that an element of a tuple form is an expression whereas the other two things aren't. But I'm mostly concerned about appearing to rewrite this as an expression that uses s
repeatedly.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've introduced the concept of "outcomes" to try to resolve this kind of problem. What do you think?
docs/design/pattern_matching.md
Outdated
[initializing expression](/docs/design/values.md#initializing-expressions), then | ||
a [temporary is materialized](/docs/design/values.md#temporary-materialization) | ||
for it. Then, for each subpattern of the struct pattern in left-to-right order, | ||
the subpattern is matched with `s.f`, where `s` is the possibly materialized |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Similar here.)
docs/design/tuples.md
Outdated
If `source` is an expression whose type `Source` is a tuple type, when `source` | ||
is converted to a tuple or array type `Dest` that has the same arity, the | ||
conversion is an initializing expression of type `Dest` that initializes each | ||
element `.i` from a [member access](expressions/member_access.md) `source.i`, in |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Similar here.)
docs/design/values.md
Outdated
type. When an expression is used in such an operand position, it is implicitly | ||
converted to the expected type, if a suitable conversion is available. Note that | ||
this conversion takes place even if the expression already has the expected | ||
type, because the conversion can still change the expression's form. If the | ||
result of the conversion has a composite form, it is converted to the expected | ||
type again (this second conversion is guaranteed to exist, and have a primitive | ||
form). Finally, category and phase conversions are applied as needed to satisfy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain what's going on here -- how can the conversion produce a composite form the first time?
My expectation was that the expected category (if any) would be part of the input to the conversion process, along with the expected type (if any), and the conversion process would produce a value with an acceptable type and category (or fail). That's roughly how conversion works in the toolchain at the moment -- it takes (effectively, more or less) a set of acceptable expression categories and a target type as input, with the target type set to the type of the input in the case where the caller doesn't care about the type and only wants to constrain the category.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain what's going on here -- how can the conversion produce a composite form the first time?
As I understand it, we expect implicit conversions to be re-specified in terms of forms and form-generics, in a way that allows the impl
to specify the form of the conversion result. For example:
impl form(ref Foo) as ImplicitAsPrimitive((i32, i32)) where .ResultForm = form(ref i32, ref i32);
So if a reference expression of type Foo
is used where an (i32, i32)
is expected, the first conversion (i.e. the first invocation of ImplicitAsPrimitive.Convert
) will have a non-primitive form (ref i32, ref i32)
. In order to get to a primitive form, we convert the result to (i32, i32)
. We can ensure that second conversion has a primitive result form (presumably var (i32, i32)
), because only the prelude can define impl form(ref i32, ref i32) as ImplicitAsPrimitive((i32, i32))
.
My expectation was that the expected category (if any) would be part of the input to the conversion process, along with the expected type (if any), and the conversion process would produce a value with an acceptable type and category (or fail). That's roughly how conversion works in the toolchain at the moment -- it takes (effectively, more or less) a set of acceptable expression categories and a target type as input, with the target type set to the type of the input in the case where the caller doesn't care about the type and only wants to constrain the category.
I think that all corresponds to what I'm trying to say here, so can you be more specific about where you see a disconnect? The only difference I see is that you're focusing on the inputs to the form conversion algorithm, but I'm also trying to specify the algorithm itself (at a high level).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the difference is that I would not expect the final adjustment from form(ref i32, ref i32)
to form(var(i32, i32))
to be found by impl
lookup nor performed by doing the whole conversion process again, but instead to be done by built-in logic as the final step of the conversion process -- and that, unlike impl
lookup for ImplicitAsPrimitive
, the built-in form conversion's behavior would depend on the desired result form.
I don't think the final form conversion can be done by converting an expression to its own type, because I'd expect the impl
in the prelude to be
final impl forall [F:! Form] F as ImplicitAsPrimitive(F.type) where .ResultForm = F {
fn Convert[self:? Self]() ->? Self = "identity";
}
That is, I'd expect that converting an expression to its own type should be a no-op, not a conversion to a primitive form.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I'm trying a different approach now.
Co-authored-by: Richard Smith <[email protected]>
I'm working on an update of this based on recent discussions, so you can hold off on reviewing this for now. |
- Introduce outcomes. - Don't materialize temporaries to reorder struct initialization. - Introduce owning/non-owning references.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In addition to responding to these comments, I've introduced a new refinement of the reference expression categories; see the proposal doc for rationale.
docs/design/pattern_matching.md
Outdated
|
||
The binding is _bound_ to the result of these conversions. This makes a runtime | ||
or template binding an alias for the converted scrutinee expression, with the | ||
same form and value. Symbolic bindings are more complex: the binding will have |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, or, maybe there's a third approach that might satisfy both of our preferences: could we first define the properties of the binding, and then say that the initializing expression is converted as needed to match those properties?
Good idea. How's this?
docs/design/pattern_matching.md
Outdated
for it. Then, each nested _proper-pattern_ is matched against `s.i`, where `s` | ||
is the possibly materialized scrutinee expression and `i` is the 0-based index | ||
of the nested pattern within the tuple pattern. The tuple pattern matches if all | ||
of these sub-matches succeed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've introduced the concept of "outcomes" to try to resolve this kind of problem. What do you think?
docs/design/values.md
Outdated
type. When an expression is used in such an operand position, it is implicitly | ||
converted to the expected type, if a suitable conversion is available. Note that | ||
this conversion takes place even if the expression already has the expected | ||
type, because the conversion can still change the expression's form. If the | ||
result of the conversion has a composite form, it is converted to the expected | ||
type again (this second conversion is guaranteed to exist, and have a primitive | ||
form). Finally, category and phase conversions are applied as needed to satisfy |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I'm trying a different approach now.
docs/design/classes.md
Outdated
type, when `source` is converted to a struct or class type `Dest` that has the | ||
same set of field names, the conversion is an initializing expression of type | ||
`Dest` that initializes each field `field` from a | ||
[member access](expressions/member_access.md) `source.field`, in `Dest`'s field |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm now trying to address this in a more principled way with "outcomes". I've also tried to be more explicit about the fact that initializing expressions must be materialized, particularly by codifying what it means to "discard" an outcome.
In particular, clean up obsolete mentions of initializing expressions, and update discussion of function returns. The latter required adding a discussion of form literals.
Miscellaneous other cleanups.
I believe I've now resolved all FIXMEs and addressed all outstanding comments. I've also cleaned up some places I noticed where existing text needed to be updated to cover expression forms. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not a complete review, but I wanted to send this feedback without waiting.
docs/design/values.md
Outdated
expression: the expression is first converted to an initializing expression if | ||
necessary, and then temporary storage is materialized to act as its output, and | ||
as the referent of the resulting ephemeral reference expression. | ||
There are no contexts in Carbon that always require an initializing expression, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
partial T -> T conversion requires (or is about to require) an initializing expression. In general, the forms generic proposal allows operator interfaces to be sensitive to whether its arguments are initializing.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
partial T -> T conversion requires (or is about to require) an initializing expression. In general, the forms generic proposal allows operator interfaces to be sensitive to whether its arguments are initializing.
I've rephrased this to try to clarify that I'm talking about syntactic contexts (to parallel the discussion of durable and ephemeral references on lines 313 and 367). Neither of these is a syntactic context, as far as I can tell.
More fundamentally, owning ephemeral reference expressions are intended to take the place of initializing expressions as operation inputs. Most notably, var
patterns (including var
parameters) now expect the scrutinee to be an owning ephemeral reference, not an initializing expression. So I think the partial T -> T conversion should require the input to be an owning ephemeral reference expression, just as if it were defined as a function with a var
parameter. Similarly, I think we shouldn't allow operator interfaces to be sensitive to the difference between an initializing expression and an owning ephemeral reference expression.
The motivation for this change is to efficiently support use cases like this:
fn F() -> (X, Y);
let (var x: X, var y: Y) = F();
We couldn't support that in the old model because an initializing expression can't be efficiently decomposed into two separate initializing expressions -- we have to materialize F()
up front, and then decompose it into separate reference expressions (which must be owning because var
only matches complete objects, and must be ephemeral so that let (ref x: X, ref y: Y) = F();
doesn't compile).
docs/design/values.md
Outdated
expression, but this default can be overridden by the | ||
[function signature](#function-calls-and-returns). | ||
|
||
The other case that requires forming an initializing expression in Carbon is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this contradict the thesis of the previous paragraph?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My phrasing was awkward. Is this better?
docs/design/values.md
Outdated
> **Future work:** At present these requirements are somewhat trivial because an | ||
> initializing outcome is always fulfilled immediately, but a forthcoming | ||
> proposal is expected to introduce a way to bind a name to an initializing | ||
> outcome. That proposal may impose stricter requirements than the ones | ||
> described here, which are just the minimum necessary for initializing outcomes | ||
> to be coherent. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I can tell, you are referring to let x:? auto = F();
. In the case F()
returns an initializing expression, x
becomes a var
. I don't think #5389 will introduce a way to bind a name to an initializing outcome.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about this?
~i
allows you to forward the initializing expression used to initializei
wheni
is avar
parameter.
That sounds to me like i
is bound to an initializing outcome, not a variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay. Now that #5389 is out for review, perhaps you could suggest how it should be changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I left a suggestion on #5389 for rephrasing that sentence, and I've revised this section to focus on initializing outcomes as a specification convenience.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While responding to comments I noticed that the pre-existing special-case behavior of addr self
/ref self
is kind of broken, so I've removed it; see the new alternative-considered for more.
docs/design/values.md
Outdated
expression: the expression is first converted to an initializing expression if | ||
necessary, and then temporary storage is materialized to act as its output, and | ||
as the referent of the resulting ephemeral reference expression. | ||
There are no contexts in Carbon that always require an initializing expression, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
partial T -> T conversion requires (or is about to require) an initializing expression. In general, the forms generic proposal allows operator interfaces to be sensitive to whether its arguments are initializing.
I've rephrased this to try to clarify that I'm talking about syntactic contexts (to parallel the discussion of durable and ephemeral references on lines 313 and 367). Neither of these is a syntactic context, as far as I can tell.
More fundamentally, owning ephemeral reference expressions are intended to take the place of initializing expressions as operation inputs. Most notably, var
patterns (including var
parameters) now expect the scrutinee to be an owning ephemeral reference, not an initializing expression. So I think the partial T -> T conversion should require the input to be an owning ephemeral reference expression, just as if it were defined as a function with a var
parameter. Similarly, I think we shouldn't allow operator interfaces to be sensitive to the difference between an initializing expression and an owning ephemeral reference expression.
The motivation for this change is to efficiently support use cases like this:
fn F() -> (X, Y);
let (var x: X, var y: Y) = F();
We couldn't support that in the old model because an initializing expression can't be efficiently decomposed into two separate initializing expressions -- we have to materialize F()
up front, and then decompose it into separate reference expressions (which must be owning because var
only matches complete objects, and must be ephemeral so that let (ref x: X, ref y: Y) = F();
doesn't compile).
docs/design/values.md
Outdated
expression, but this default can be overridden by the | ||
[function signature](#function-calls-and-returns). | ||
|
||
The other case that requires forming an initializing expression in Carbon is |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My phrasing was awkward. Is this better?
docs/design/values.md
Outdated
> **Future work:** At present these requirements are somewhat trivial because an | ||
> initializing outcome is always fulfilled immediately, but a forthcoming | ||
> proposal is expected to introduce a way to bind a name to an initializing | ||
> outcome. That proposal may impose stricter requirements than the ones | ||
> described here, which are just the minimum necessary for initializing outcomes | ||
> to be coherent. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about this?
~i
allows you to forward the initializing expression used to initializei
wheni
is avar
parameter.
That sounds to me like i
is bound to an initializing outcome, not a variable.
Some people find this restriction counterintuitive, and there is no technical | ||
reason for it; in fact, removing it would somewhat simplify the conversion | ||
rules. However, other people's intuition is that a tuple is different enough |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand this will ultimately be the leads' decision, but my preference is to say that the array
initializer is fine accepting non-form tuple type values as long as all the elements have types that convert to the array's element type. But that could possibly be part of the library implementation of arrays, rather than something that is built into the language. My expectation, though, is that users will just want to write a variadic tuple and have things work (and maybe they will since tuple forms degrade into tuple types?).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand this will ultimately be the leads' decision, but my preference is to say that the
array
initializer is fine accepting non-form tuple type values as long as all the elements have types that convert to the array's element type.
I'm OK either way, with the slight caveat that if we want to support this kind of use case, it will be a lot easier to do so if array
actually is such a use case, so that we're eating our own dogfood. Otherwise it may be too easy to break or degrade those cases by accident (e.g. by allowing code like the initialization of d
in the example below).
not doing the conversion to primitive form discussed here).
But that could possibly be part of the library implementation of arrays, rather than something that is built into the language.
Just to be clear, that's what I'm proposing -- see the next paragraph.
My expectation, though, is that users will just want to write a variadic tuple and have things work (and maybe they will since tuple forms degrade into tuple types?).
I'm inclined to agree, because a tuple literal that contains a pack expansion is still a tuple literal. In fact, I think libraries probably shouldn't ever distinguish between otherwise-equivalent tuple literals that have different variadic shapes. My hunch is that the language rules won't be able to fully prevent that, but we should do our best to create a "pit of success" around that principle when we extend forms to support variadics.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm OK either way, with the slight caveat that if we want to support this kind of use case, it will be a lot easier to do so if
array
actually is such a use case, so that we're eating our own dogfood.
I don't find this to be a compelling argument. It sounds a bit like "we are going to degrade the developer experience for this relatively more common use case where the feature isn't helping in order to make sure we support it well in the relatively less common cases where it could help." Even if this makes the job harder for the compiler developers, there will (hopefully) be way fewer compiler developers than developers using the language, so it makes sense to for us take on that burden rather than them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've filed leads issue #6160 for this.
The declaration of `c` is valid, because the `as` conversion operates | ||
element-wise on its tuple-form input, so its output should likewise have a tuple | ||
form. On the other hand, the declaration of `d` is not valid, because the input | ||
to `as` has a primitive form, so its output should also have a primitive form | ||
(and we somewhat-arbitrarily choose its category to be "initializing"). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems like it would be a surprise with the only explanation for why the code is being rejected involving having to talk about forms, impacting progressive disclosure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think this will be any more surprising than the fact that we don't allow let b: array(X, 3) = x_tuple;
? Personally, I feel like it would be much more surprising if we disallowed b
but allowed d
. For example, it would mean that b
doesn't compile because the type of the initializer isn't different enough from the type being initialized.
You make a very good point about diagnostics, but here again I think b
has the same problem, so this is an issue with array
requiring a tuple form to begin with, not with the fact that MakeYs() as (X, X, X)
doesn't satisfy that requirement.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think both b
and d
should be accepted.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've tried to cover these points in #6160, but feel free to follow up there with more.
- If the type of `source` is `Dest`, return `source`. | ||
- If `source` is a struct outcome, for each field name `F` in `Dest`, in | ||
`Dest`'s field order, type-convert `source.F` to `Dest.F`. Return a struct | ||
outcome where each field `F` is set to the outcome of the corresponding | ||
conversion. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Won't this be determined by the form of the result of the implicit conversion, once that is under control of the impl?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, because that would lead to the "surprising consequences" discussed in the previous paragraph.
Sorry for the long delay! |
Co-authored-by: josh11b <[email protected]>
Co-authored-by: Carbon Infra Bot <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
May need a new comment thread in order to count as having reviewed.
This proposal introduces the concept of a form, which is a generalization of
"type" that encompasses all of the information about an expression that's
visible to the type system, including type and expression category. Forms can be
composed into tuple forms and struct forms, which lets us track the
categories of individual tuple and struct literal elements.
The proposal PR also adds
ref
bindings to the pattern matching documentation,but that is not part of the proposal itself; it's just bringing the documentation
up to date with proposal #5434.