Skip to content

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

Open
wants to merge 32 commits into
base: trunk
Choose a base branch
from

Conversation

josh11b
Copy link
Contributor

@josh11b josh11b commented May 6, 2025

  • A parameter binding can be marked ref instead of var or the default. It will bind to reference argument expressions in the caller and produces a reference expression in the callee.
    • Unlike pointers, a ref binding can't be rebound to a different object.
    • This replaces addr, and is not restricted to the self parameter.
    • A ref binding, like a let binding, can't be used in fields of classes or structs.
    • When calling functions, arguments to non-self ref parameters are also marked with ref.
  • The return of a function can optionally be marked ref, let, or var. These control the category of the call expression invoking the function, and how the return expression is returned.
    • These may be mixed for functions returning parens or brace forms.
  • Any parameters whose lifetime needs to contain the lifetime of the return must be marked bound.
  • The address of a ref binding is nocapture and noalias.
  • We mark parameters of a function that may be referenced by the return value with bound.

@josh11b josh11b added proposal A proposal proposal draft Proposal in draft, not ready for review labels May 6, 2025
@josh11b josh11b marked this pull request as ready for review May 20, 2025 05:47
@github-actions github-actions bot requested a review from zygoloid May 20, 2025 05:48
@github-actions github-actions bot added proposal rfc Proposal with request-for-comment sent out and removed proposal draft Proposal in draft, not ready for review labels May 20, 2025
@josh11b josh11b requested a review from geoffromer May 20, 2025 16:12
```carbon
fn F(ptr: i32*) {
// A reference binding `x`.
let ref x: i32 = *ptr;
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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.

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

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!

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 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) `.`;
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, this isn't modeled as a function call, so it doesn't take parameters at all, ref or otherwise.

Copy link
Contributor Author

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.

chandlerc
chandlerc previously approved these changes May 21, 2025
Comment on lines 752 to 753
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 `->`.
Copy link
Contributor

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)

Copy link
Contributor Author

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.

Copy link
Contributor

@zygoloid zygoloid May 22, 2025

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.

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

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

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 updated the proposal to remove -> var T, and added it instead to the alternatives considered.

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.

(please ignore the "approve" ... misclick and high github latency...)

@chandlerc chandlerc self-requested a review May 21, 2025 01:51
@chandlerc chandlerc dismissed their stale review May 21, 2025 01:52

(misclicked the button)

We can revisit and expand this later if this does not handle use cases we would
like to support.

### Use case: `Deref` interface
Copy link
Contributor

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.

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

@josh11b josh11b requested a review from geoffromer May 21, 2025 22:08
@josh11b josh11b changed the title ref ref parameters, arguments, returns and let returns May 22, 2025
Copy link
Contributor

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

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

Copy link
Contributor Author

@josh11b josh11b May 22, 2025

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

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 or val, which causes the function call to be a reference or value expression (respectively).
  • A binding pattern can optionally be marked with ref or val, 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 a var.

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

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, whereas val 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.

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

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
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.

7 participants