hsutter / cppfront

A personal experimental C++ Syntax 2 -> Syntax 1 compiler
Other
5.23k stars 224 forks source link

[BUG] Single `out` return should be allowed #1119

Closed DyXel closed 1 week ago

DyXel commented 2 weeks ago

Describe the bug Having a function that returns a single value should be allowed to be out (just like forward and move).

To Reproduce

main: () -> int = {
    return my_call();
}
my_call: () -> out result: int = {
    result = EXIT_SUCCESS;
}

Outputs:

test.cpp2...
test.cpp2(4,16): error: only 'forward' and 'move' return passing style are allowed from functions (at 'out')

Additional context Do note that having parentheses make the example work, but I don't see why having a single out parameter shouldn't work:

main: () -> int = {
    return my_call();
}
my_call: () -> (out result: int) = {
    result = EXIT_SUCCESS;
}
gregmarr commented 2 weeks ago

What are you looking to do by marking it as out that doesn't happen with just the undecorated type? Are you just looking to explicitly specify the default behavior, as you are doing with the out result: int?

@hsutter This is the only mention I see of passing type on returns in the docs: https://hsutter.github.io/cppfront/cpp2/functions/

(2) -> ( / parameter list / ) to return a list of named return parameters using the same parameters syntax, but where the only passing styles are out (the default, which moves where possible) or forward.

There is no mention of passing types in the unnamed section.

DyXel commented 2 weeks ago

Three things come to mind:

  1. As you mentioned, I want to explicitly specify the default behavior (seemed more readable to me)
  2. I want to be able to name the return value directly, without the parentheses (they seem redundant to me, since a 1 return value cannot possibly be other thing)
  3. I don't want to repeat myself by declaring a variable with the same type that will be returned

Ideally, this would work:

my_call: () -> result: int = {
    result = EXIT_SUCCESS;
}

but what I really want to avoid in the end is doing this:

my_call: () -> int = { // <-- return type declared once
    result: int = EXIT_SUCCESS; // <-- return type declared again
    return result; 
}

and since parentheses allow me to do that, this issue is low priority. (unfortunately I can't add such label)

gregmarr commented 2 weeks ago

I could see a desire to keep named and unnamed return values having distinct syntax indicated by the () because the named return value uses a different body form.

my_call: () -> int = { 
    return EXIT_SUCCESS; 
}
my_call: () -> result: int = {
    result = EXIT_SUCCESS;
}

I was thinking that it also returned a struct instead of the actual type, and so the caller had to access it by name or by structured bindings, but then I remembered that there was a change to named return values that if there was only a single one, it would return the actual type instead of wrapping it in a struct.

@hsutter The documentation at https://hsutter.github.io/cppfront/cpp2/functions/#return-values doesn't call out in (2) that if there is only a single return value in the parameter list, then it is returned as in (1).

Maybe the () could be used to differentiate between "return this as a struct and the caller accesses by name" and "just name the value for internal function use". What about this?

(1) -> [passing-style] X to return a single unnamed value of type X, which can be void to signify the function has no return value, and the only passing styles are out (the default, which moves where possible) or forward. If X is not void, the function body must have a return value; statement that returns a value of type X on every path that exits the function. (2) -> [passing-style] ret: X to return a single unnamed value of type X, where the only passing styles are out (the default, which moves where possible) or forward. That return value is referred to in the body of the function by ret. The function body must initialize the value of the return-parameter ret in its body the same way as any other local variable. An explicit return statement is written just return; and returns the named value; the function has an implicit return; at the end. (3) -> ( /* parameter list */ ) to return a list of named return parameters using the same parameters syntax, but where the only passing styles are out (the default, which moves where possible) or forward. The function body must initialize the value of each return-parameter ret in its body the same way as any other local variable. An explicit return statement is written just return; and returns the named values; the function has an implicit return; at the end.

DyXel commented 2 weeks ago

Maybe the () could be used to differentiate between "return this as a struct and the caller accesses by name" and "just name the value for internal function use". What about this?

Yes! This would be ideal.

Side note: I really like the way Python 3 deals with multiple return values (multiple values in a single statement, in general), and I wish we could get closer to that. Essentially, a tuple of 1 value and the a value itself are the same case/type. However I of course understand that std::tuple<int> and int is not the same in C++, even if their layout is identical, probably for good reasons.

hsutter commented 1 week ago

Thanks! Although I'm not going to go this way right now (sorry!) I appreciate the feedback so I want to take time to explain why...

Having a function that returns a single value should be allowed to be out (just like forward and move).

Yes, and the way I view it is that's the default for the unnamed return value (Cpp1) syntax.

A single out parameter does work, it's the default. I think what you're actually asking for is that you want to also name it, and in that case omit the two ( ) tokens that are required today... as you say:

Do note that having parentheses make the example work, but I don't see why having a single out parameter shouldn't work:

Repeating the example, it's that this works:

my_call: () -> (out result: int)  // works today

and you would like this to work:

my_call: () -> out result: int    // proposed here

The current design is that if we want named return values, we write them in ( ) as an ordinary return parameter list.

The single-unnamed-return syntax is supported primarily for familiarity with Cpp1, and can still be design-justified as being a subset of the more general syntax with defaults applied for the simplest case of "return one unnamed object" so that you don't have to type the extra parts if you're not using them. But if you name something, it's in an ordinary (return) parameter list.

(Consider: If the ( ) were optional here for one return value, should they be optional for a single input parameter too?)

Even though I'm not going to go this way right now, I appreciate the input, and thanks for understanding!

hsutter commented 1 week ago

@hsutter The documentation at https://hsutter.github.io/cppfront/cpp2/functions/#return-values doesn't call out in (2) that if there is only a single return value in the parameter list, then it is returned as in (1).

Fixed, thanks: ddcf0fd3aeeb117e72fd1e9063b800e17626091f