Skip to content

result location mechanism (previously: well defined copy eliding semantics) #287

Closed
@thejoshwolfe

Description

@thejoshwolfe

Accepted Proposal


Old Proposal:

Have well-defined rules for copy eliding, and we sometimes allow what looks like copying non-copyable objects.

  • During the semantic analysis of every expression, there is an additional field of provided context, which is the location to put the expression's result value.
  • A function can declare a non-copyable return type. In this case, the function gets an additional, secret parameter that is a writable pointer to where it should write its return value.
  • Here's how specific language constructs handle the result location:
    • A var or const declaration creates a location, and passes that location to the initializer expression, if any.
    • An assignment statement uses the address of the left hand side as the result location for the right hand side.
    • A function call or an operator that acts like a function (e.g. +, ~) creates a temporary storage location for each of its parameters/operands and provides that temporary storage as the result location when evaluating each parameter/operand expression.
    • The body of a function whose return type is copyable uses a special result location, such as a platform-specific register.
    • The body of a function whose return type is non-copyable uses the secret result location pointer parameter as the result location.
    • A return statement provides the function body's result location to the return expression.
    • For a function call where the function's return type is non-copyable, the function call expression's result location is passed as the function's secret return value pointer parameter.
    • For a function call where the function's return type is copyable, the result of the function is copied from where the function puts it (such as a platform-specific register) to the function call expression's result location.
    • A block or branching control structure forwards its result location to whatever sub-expression determines its result value.
    • A statement followed by a ; in a block gets a void result location.
    • A defer statement provides a void result location to its expression.
    • A struct, array, or enum initialization expression uses its result location in-place.
    • Automatic error and maybe coercion happen in-place.

Examples:

fn foo() -> u32 { // result location for function body is a special register
    bar(); // function call gets a void result location, so bar() must not return anything (see #219).
    var // varaible declaration gets a void result location.
        a : u32 // creates location for a u32
        = 1; // integer literal gets &a as result location
    const b // creates a location for a TBD type
        = baz(); // baz() gets &b as result location, and baz() determines the type of b
    a = ( // result location is &a
            b // result location is a temporary location of a TBD type provided by the + operator
        + // checks left and right types and produces the sum into &a, possibly doing automatic type coorsion first
            baz() // result location is a temporary location of a TBD type provided by the + operator
    );
    var array // creates a location for a TBD type
        = []u32 { // result location is &array, and now type of array is [TBD]u32.
            1, // result location is &array[0], and array.len is at least 1
            5, // result location is &array[1], and array.len is at least 2
        };
    1 // result location is the special register for the function body
}

struct BigStruct {
    a: [2]SubStruct,
    pub fn init(offset: u32) -> BigStruct { // result location is the secret parameter; let's call it result_location
        BigStruct { // result location is still result_location
            .a = []SubStruct { // result location is &result_location.a
                SubStruct { // result location is &result_location.a[0]
                    .x = offset + 0, // result location is &result_location.a[0].x
                    // (note elaboration on the + operator is omitted here. see above.)
                },
                SubStruct { // result location is &result_location.a[1]
                    .x = offset + 1, // result location is &result_location.a[1].x
                },
            },
        }
    }
}
struct SubStruct {
    x: u32,
}
fn main() {
    var a // creates a location for a TBD type
        = BigStruct.init(10); // result location secret parameter is &a
    // equivalent to:
    var b : BigStruct = undefined;
    b.a[0].x = 10 + 0;
    b.a[1].x = 10 + 1;

    var c // creates a location for a TBD type
        = if // result location is &c
        (
            something() // result location is a temporary location created by the if
        ) {
            BigStruct.init(100) // result location is &c
        } else {
            BigStruct.init(200) // result location is &c
        };
    // equivalent to:
    var d : BigStruct = undefined;
    if (something()) {
        d.a[0].x = 100 + 0;
        d.a[1].x = 100 + 1;
    } else {
        d.a[0].x = 200 + 0;
        d.a[1].x = 200 + 1;
    }

    var e // creates a location for a TBD type
        = if (something()) {
            a // ERROR: can't copy type BigStruct
        } else {
            b // ERROR: can't copy type BigStruct
        };
}

Relative to what #83 originally proposed, we've got relaxed restrictions on returning non-copyable types from a function. Previously returning non-copyable types required use of a named return value. So do we still need named return values?

Here's a usecase for named return values:

struct PluginRegistry {
    id_to_plugin: Hashtable(Id, &Plugin), // non-copyable
    pub fn init() -> (result: PluginRegistry) {
        result.id_to_plugin = Hashtable(Id, &Plugin).init();
        result.register(base_plugin.id, &base_plugin);
    }
    pub fn register(self: &PluginRegistry, id: Id, plugin: &Plugin) {
        self.id_to_plugin.put(id, plugin);
        plugin.on_register();
    }
}

We want to design PluginRegistry to use the constructor-like pattern where you can assign from init(), and we want to do something non-trivial with the object before we return it. In order to refer to the object, it has to be named; we wouldn't be able to call register() if we did a return PluginRegistry { ... } expression.

Metadata

Metadata

Assignees

No one assigned

    Labels

    acceptedThis proposal is planned.proposalThis issue suggests modifications. If it also has the "accepted" label then it is planned.

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions