Skip to content

Conversation

geoffromer
Copy link
Contributor

@geoffromer geoffromer commented May 27, 2025

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.

@github-actions github-actions bot added the documentation An issue or proposed change to our documentation label May 27, 2025
@geoffromer geoffromer added the proposal draft Proposal in draft, not ready for review label May 28, 2025
@github-actions github-actions bot added the proposal A proposal label May 28, 2025
@geoffromer geoffromer marked this pull request as ready for review May 29, 2025 00:17
@geoffromer geoffromer added proposal rfc Proposal with request-for-comment sent out and removed proposal draft Proposal in draft, not ready for review labels May 29, 2025
@github-actions github-actions bot requested a review from zygoloid May 29, 2025 00:18
@geoffromer geoffromer requested a review from josh11b June 4, 2025 17:53
Copy link
Contributor

@josh11b josh11b left a 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.

Copy link
Contributor

@chandlerc chandlerc left a 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.

Copy link
Contributor

@zygoloid zygoloid left a 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.

geoffromer and others added 3 commits June 26, 2025 09:59
Copy link
Contributor

@zygoloid zygoloid left a 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.

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
Copy link
Contributor

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 in source if it has struct form, and from a reference to or value of the struct field with the same name in source if it has reference or value category respectively. Any required conversions are performed in Dest's field order, after the evaluation of source.

... could work? (Though I guess we also want to say that an initializing expression gets materialized somewhere here.)

Copy link
Contributor Author

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 in source if it has struct form, and from a reference to or value of the struct field with the same name in source if it has reference or value category respectively. Any required conversions are performed in Dest's field order, after the evaluation of source.

... 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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.


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
Copy link
Contributor

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.

Copy link
Contributor Author

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".

Copy link
Contributor

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?

Copy link
Contributor Author

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?

Comment on lines 331 to 334
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.
Copy link
Contributor

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:

Suggested change
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.)

Copy link
Contributor Author

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?

[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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Similar here.)

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Similar here.)

Comment on lines 671 to 677
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
Copy link
Contributor

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.

Copy link
Contributor Author

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).

Copy link
Contributor

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.

Copy link
Contributor Author

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.

@geoffromer geoffromer requested a review from zygoloid July 22, 2025 19:17
@geoffromer
Copy link
Contributor Author

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.
Copy link
Contributor Author

@geoffromer geoffromer left a 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.


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
Copy link
Contributor Author

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?

Comment on lines 331 to 334
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.
Copy link
Contributor Author

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?

Comment on lines 671 to 677
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
Copy link
Contributor Author

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.

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
Copy link
Contributor Author

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.
@geoffromer
Copy link
Contributor Author

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.

Copy link
Contributor

@josh11b josh11b left a 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.

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,
Copy link
Contributor

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.

Copy link
Contributor Author

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).

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
Copy link
Contributor

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?

Copy link
Contributor Author

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?

Comment on lines 581 to 586
> **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.
Copy link
Contributor

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.

Copy link
Contributor Author

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 initialize i when i is a var parameter.

That sounds to me like i is bound to an initializing outcome, not a variable.

Copy link
Contributor

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?

Copy link
Contributor Author

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.

@geoffromer geoffromer mentioned this pull request Sep 9, 2025
Copy link
Contributor Author

@geoffromer geoffromer left a 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.

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,
Copy link
Contributor Author

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).

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
Copy link
Contributor Author

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?

Comment on lines 581 to 586
> **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.
Copy link
Contributor Author

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 initialize i when i is a var parameter.

That sounds to me like i is bound to an initializing outcome, not a variable.

Comment on lines +204 to +206
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
Copy link
Contributor

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?).

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Comment on lines +234 to +238
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").
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Contributor

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.

Copy link
Contributor Author

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.

Comment on lines +880 to +906
- 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.
Copy link
Contributor

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?

Copy link
Contributor Author

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.

@josh11b
Copy link
Contributor

josh11b commented Sep 26, 2025

Sorry for the long delay!

@geoffromer geoffromer requested a review from josh11b September 29, 2025 21:30
Co-authored-by: Carbon Infra Bot <[email protected]>
Copy link
Contributor

@josh11b josh11b left a 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.

@geoffromer geoffromer requested review from a team as code owners October 2, 2025 22:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation An issue or proposed change to our documentation proposal rfc Proposal with request-for-comment sent out proposal A proposal

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants