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 44 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 value 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, val, 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.
  • 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
@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.

Copy link
Contributor

Choose a reason for hiding this comment

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

I forgot to update here but meant to -- I think roughly this idea is what I wrote up in the leads issue here: #5522 (comment)

(I think I misunderstood it at first to be something different, but in a live discussion Geoff corrected me.)

Flagging that here to connect all the dots.

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?

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.

Made a moderately thorough pass over this to get a good background for the relevant leads issues and wanted to leave the somewhat minor editorial comments as I went.

Overall, just wanted to also say this is really awesome to pull together considering how far reaching, and how much ambiguity we need to leave to break off even this "small" of a chunk.

I do have some meatier thoughts tied up in theeads issues, but maybe best for a live discussion as I suspect somewhat I am still missing clarity and context on some parts.

Comment on lines +908 to +914
### Type completeness

Not a change by this proposal, but note that our existing rules will require the
type in a `ref` binding to be complete in situations where it would not need to
be if you were using a value binding with a pointer type instead. We may need to
change this in the future to match C++ which treats reference types like pointer
types for completeness purposes.
Copy link
Contributor

Choose a reason for hiding this comment

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

My understanding is that while we'll need completeness before we would for a pointer, much like a value binding for a type, declarations of the binding still wouldn't require completeness? Worth mentioning that the result here is expected to have the same level of incompleteness support in ref bindings as value bindings?

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 the text is technically correct as written. I can understand that it might not be as clear as it could be, but I'm having trouble figuring out how to address the points you make without introducing more confusion.

The situation is:

  • The type in bindings is sometimes required to be complete, for example in definitions but some cases for declarations as well.
  • T* is considered complete even when T is not.
  • It is true that T in a value binding will generally be required to be complete in the same situations as a ref binding. However, a value binding for T is not a substitute for a reference binding for T, while a value binding for T* is much more so.

josh11b and others added 2 commits June 13, 2025 17:54
Co-authored-by: Chandler Carruth <chandlerc@gmail.com>
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.

Opening up a more substantial syntax discussion. If this ends up contentious, happy to pop it into an issue for leads, but figured it was worth checking if there is alignment here first.

Comment on lines 366 to 383
A function may have multiple returns, each with their own marker, by using a
paren or brace compound return form.

```carbon
fn ParenReturn()
-> (->let bool, ->ref i32, -> C) {
return (true, global, {.x = 3});
}

fn BraceReturn()
-> {->let .a: bool,
->ref .b: i32,
-> .c: C} {
return {.a = true,
.b = global,
.c = {.x = 3}};
}
```
Copy link
Contributor

Choose a reason for hiding this comment

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

I was struggling a bit with the heavy usage of the -> symbol in these, and I discussed an alternative syntax with @josh11b in the open discussion that seemed promising.

Discussion: https://docs.google.com/document/d/1Yt-i5AmF76LSvD4TrWRIAE_92kii6j5yFiW-S7ahzlg/edit?tab=t.0#heading=h.52ru7ner80b4

Rough sketch of the syntax adjustment from that discussion:

// `->` begins a "return form", whatever the right word
fn F()     -> <return-form>;

// Within any return form, there are N syntactic overrides where the thing following
// the arrow *does not* form a type expression
fn Let()   -> let <type-expr>
fn Ref()   -> ref <type-expr>
fn Var()   -> var <type-expr>
fn Paren() -> ( <return-form>, ... )
fn Brace() -> { .<id>: <return-form>, ... }

// Otherwise, implicit `var`:
fn Other() -> <type-expr>

Basically, ( and { are always compound return forms unless they are explicitly placed in a type expression using let, ref, or var. This would make compound returns the default, which is a not-small change. But as I have thought about this proposal and the consequences, I think it is a reasonable if not preferable default, and it provides a nice way to organize the syntax.

There is also a minor change to put the modifier let or ref after the : in the brace form. I don't feel strongly about that either way, but seemed slightly more consistent this way as this : is a very different kind of :.

With this, I think these examples would change as follows:

Suggested change
A function may have multiple returns, each with their own marker, by using a
paren or brace compound return form.
```carbon
fn ParenReturn()
-> (->let bool, ->ref i32, -> C) {
return (true, global, {.x = 3});
}
fn BraceReturn()
-> {->let .a: bool,
->ref .b: i32,
-> .c: C} {
return {.a = true,
.b = global,
.c = {.x = 3}};
}
```
A function may have multiple returns, each with their own marker, by using a
paren or brace compound return form.
```carbon
fn ParenReturn() -> (let bool, ref i32, C) {
return (true, global, {.x = 3});
}
fn BraceReturn()
-> {.a: let bool,
.b: ref i32,
.c: C} {
return {.a = true,
.b = global,
.c = {.x = 3}};
}
```

Copy link
Contributor

Choose a reason for hiding this comment

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

A slight amendment to this from open discussion with @zygoloid and @josh11b --

Whenever we have a paren form or brace form with all implicit (IE, no let, ref, or var keywords), we don't form a compound return, the paren or brace form itself is just a <type-expr> and the var goes outside of it. This basically pushes the implicit var to the outer most layer it can be.

This gives it the important property that a type T that is maybe an alias for a tuple type and an explicitly written tuple type as (i32, i32) have the same behavior -- they both are var, neither is (var i32, var i32). If the user wants explicitly separate return forms despite all the forms being var, they can use the explicit var keyword to achieve this.

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