-
Notifications
You must be signed in to change notification settings - Fork 1.5k
ref
parameters, arguments, returns and let
returns
#5434
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?
Conversation
```carbon | ||
fn F(ptr: i32*) { | ||
// A reference binding `x`. | ||
let ref x: i32 = *ptr; |
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.
At a high level, let
can be thought of as a const thing, and var
as a mutable thing, though there are of course storage implications. But this breaks that mental model for developers, so maybe we could preserve that by using ref x:
instead?
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 is the "Top-level ref
introducer" alternative. I have taken the conservative approach that we can always add this later, but I'd be interested in hearing if this is widely believed to be an issue.
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.
That mental model breaks in a lot of places already. For example, fn F(x: i32)
declares an immutable x
despite the absence of let
, and let (var x: i32, y: i32) = ...
declares a mutable x
despite the presence of let
. We can't make that model coherent (without much deeper changes), so I don't think we should try to accommodate it.
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 realizing that -> let
really encourages the mental model where let
means immutable (or at least by-value), but as noted elsewhere, I think that means we should choose a different syntax. If we don't, then I don't know, maybe allowing ref
as a statement introducer would be desirable as well.
|
||
This is a "try it and see how well it works" sort of decision. | ||
|
||
### Top-level `ref` introducer |
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.
Just got finished reading! Really excited about this direction.
One question comes to mind:
Top-level ref introducer
For now, we don't believe let ref to be so common as to need a shorter way to write, unlike what we do for var.
There doesn't appear to be much written on this. Why was this decision made? I personally find it surprising that the following code is invalid:
fn F(ptr: T*) {
ref t: T = *ptr;
}
Is this something that we have strong consensus on? Or is it possible to reconsider before landing the proposal?
Thanks!
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 don't know how strong the consensus, but I think the reason here is enough for my personal opinion to be to go forward with the approach in the proposal.
- [the address-of operator](/docs/design/expressions/pointer_operators.md) | ||
`&`; | ||
- [the indexing operator](/docs/design/expressions/indexing.md) `[`...`]`; | ||
- [the member access operator](/docs/design/expressions/member_access.md) `.`; |
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, this isn't modeled as a function call, so it doesn't take parameters at all, ref
or otherwise.
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.
See #3720 . Looking that over, looks like I should included an update to the BindToRef
interface in this proposal.
proposals/p5434.md
Outdated
The rule is: `returned var` may only be used when there is a single component to | ||
the return form, and it is either `->var` or default `->`. |
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 wonder -- could we instead simply say that returned ...
must match the structure of the return pattern, and any non-->var
or non-->
components must be initialized in the declaration?
(also OK deferring this if folks prefer, it just seems a clean generalization)
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 was the first option in the "return var
with compound return forms" alternative considered. How would you expect that example to be written?
fn F(...) -> (->ref R, ->let L, -> V) {
returned (ref r: R, let l: L, var v: V) = (...);
// or?
returned let (ref r: R, let l: L, var v: V) = (...);
// What goes here?
return ???;
}
I also don't see a way to avoid initializing all of them together using this approach.
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.
We could shorten return var;
to simply return;
when a returned ...
declaration is in scope. I'm not sure how much clarity we're getting from including the var
keyword there. If we do that, we might also want to consider allowing flowing off the end of a function with a returned
declaration in its outermost block scope.
proposals/p5434.md
Outdated
- A call to a function declared `-> var T` is an initializing expression. The | ||
caller provides the address of storage to initialize with an object of type | ||
`T`, that the caller owns upon return. The object will never be returned in | ||
registers. |
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.
After chatting with @zygoloid, I think I convinced him that we could fold together -> T
and -> var T
. I'm happy to write up more fully the idea, or discuss it / take notes on it, but leaving a quick comment here so I don't forget.
For my own memory, the most convincing thing was to start from thinking about var
parameters, and how we would want those to work for an owning pointer like std::unique_ptr
.
More details when I have a bit more time at a keyboard...
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 updated the proposal to remove -> var T
, and added it instead to the alternatives considered.
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.
(please ignore the "approve" ... misclick and high github latency...)
Co-authored-by: Geoff Romer <[email protected]>
We can revisit and expand this later if this does not handle use cases we would | ||
like to support. | ||
|
||
### Use case: `Deref` interface |
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, I forgot to include this comment in my first batch.)
When a proposal is making changes to an already-documented design, I think we should err pretty strongly on the side of making those documentation changes as part of the proposal. Seeing the changes in context should make it substantially easier to review, and may surface important issues that should be considered as part of the proposal (plus, it eliminates the time window where the design docs no longer reflect the adopted design).
This section and the next one seem like they would particularly benefit from this, but I think it applies to this proposal as a whole.
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 have updated expressions/indexing.md
with the changes from the next section, but Deref
is not currently in the design.
As for updating the rest of the design, I think it is important to get consensus on the proposal first, otherwise iteration will be significantly more expensive. Updating all of the design docs to reflect this change is going to be a big effort which we will need to prioritize against other work, and may need to be done only partially at first.
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 still reviewing, but I wanted to get a few of these comments out early in case they're worth discussing this afternoon.
optional `ref` or `let` between the `->` and `auto`. `-> auto` continues to | ||
return an initializing expression, `-> let auto` returns a value expression, | ||
and `-> ref auto` returns a durable reference expression. | ||
- Using `=>` to specify a return continues to return an 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.
I think there's a strong case to be made for having =>
deduce the expression form, because that makes(fn => expr)()
a drop-in replacement for expr
. I'd be fine with leaving this as an open question, but I'd rather not resolve it (particularly not in this way) without considering that alternative.
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.
There is an issue that if we return something other than an initializing expression, we probably need the parameters to be marked bound
. I added some text saying that.
```carbon | ||
fn F(ptr: i32*) { | ||
// A reference binding `x`. | ||
let ref x: i32 = *ptr; |
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 realizing that -> let
really encourages the mental model where let
means immutable (or at least by-value), but as noted elsewhere, I think that means we should choose a different syntax. If we don't, then I don't know, maybe allowing ref
as a statement introducer would be desirable as well.
|
||
### `ref`, `let`, and `var` returns | ||
|
||
The return of a function can optionally be marked `ref` or `let`. These control |
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'd like to suggest an alternative:
- The return of a function can optionally marked with
ref
orval
, which causes the function call to be a reference or value expression (respectively). - A binding pattern can optionally be marked with
ref
orval
, which causes name expressions that name the binding to be reference or value expressions (respectively). Note that this gives us a way to declare a value binding that's nested inside avar
.
That way there's a clear and consistent parallel between ref
and val
:
- Each can be used as a modifier on a binding pattern or function return declaration, and nowhere else.
- Each causes uses of the declared entity to have a particular category.
- Each is an abbreviation of the name of that category.
- One is the implicit default for bindings inside
var
, and the other is the implicit default for bindings outsidevar
.
Even the ways the parallels break down are illuminating, because they reflect the asymmetries of the underlying model of expression categories:
ref
on a binding pattern constrains the category of the scrutinee, whereasval
does not, but that reflects the fact that there are no conversions from other categories to durable reference.var
is a modifier on->
but acts as an independent operator rather than a binding modifier in patterns, but that reflects the fact that matching an initializing expression has side effects, and does not propagate its category to the subpattern's scrutinee.
This also preserves the current simplicity of let
: it's solely a pattern introducer, with no connection to expression categories or mutability.
Mirroring the [paren](/docs/design/pattern_matching.md#tuple-patterns) and | ||
[brace](/docs/design/pattern_matching.md#struct-patterns) pattern forms, we also | ||
support paren and brace return forms. Every element of these forms starts with |
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.
But where does that leave "tuple literal" and "tuple pattern", which are neither types nor forms? Tuple literals in particular seem easy to confuse with tuple types, and possibly also with paren forms. Also, why would "tuple forms" and "tuple types" not be sufficiently distinguished by the fact that they have different head nouns?
ref
instead ofvar
or the default. It will bind to reference argument expressions in the caller and produces a reference expression in the callee.ref
binding can't be rebound to a different object.addr
, and is not restricted to theself
parameter.ref
binding, like alet
binding, can't be used in fields of classes or structs.self
ref
parameters are also marked withref
.ref
,let
, orvar
. These control the category of the call expression invoking the function, and how the return expression is returned.bound
.ref
binding isnocapture
andnoalias
.bound
.