Skip to content

Order of operations when initializing from a compound literal #6456

@geoffromer

Description

@geoffromer

Summary of issue:

When a tuple or struct literal is used as an initializer, how do we order operations within vs. across elements?

Details:

Consider the following code, written in terms of #5389 form generics:

fn MakeA() -> var A;
fn MakeB() -> var B;

// var A -> val C and var B -> val D
impl form(var A) as Core.ImplicitAsPrimitive(C) where .ResultForm = Form(val C);
impl form(var B) as Core.ImplicitAsPrimitive(D) where .ResultForm = Form(val D);

var cd: (C, D) = (MakeA(), MakeB());

Evaluation of that last line requires 6 separate function calls: for each tuple element, we have to call a Make function, an implementation of Core.ImplicitAsPrimitive.Convert type conversion, and an implementation of Core.Copy.Op (which can't be elided because in this example, the conversions happen to be value expressions, not initializing expressions).

What order should these function calls happen in?

I assume that we want to go left-to-right, don't want to have different rules for different elements, and don't want to violate causality, but that still leaves several possible orders. The two extremes are:

  • Breadth-first: evaluate both the factory calls, then both type conversions, and then both copies.
  • Depth-first: evaluate all operations involving the first element, then all operations involving the second.

There are also a couple of hybrid options, where we combine some steps depth-first within an overall breadth-first order.

Depth-first traversal has the appealing property that it doesn't need any temporaries to remain live across a function call: the result of each call is immediately consumed by the next call, or written directly to a durable variable. This may enable us to generate more efficient machine code. However, it poses some substantial challenges:

The implementation of pattern matching seems like it will require major architectural changes to support interleaving scrutinee evaluation with pattern matching. We can avoid this problem by evaluating the scrutinee breadth-first, and limiting the depth-first order to conversion and pattern matching, but it's not clear if that's meaningfully better than fully breadth-first.

It seems like we would need major changes to #5389 and/or other parts of the language design if we want to model a tuple-to-tuple type conversion in terms of a call to Core.ImplicitAsPrimitive.Convert while retaining depth-first evaluation. Note that there are at least two separate problems here: interleaving the Convert call with its arguments, and with the code that uses its outcome.

Finally, this question applies to structs as well as tuples, and in that context a depth-first order reopens the question of how to handle cases where the field order differs:

var cd: {.c: C, .d: D} = {.d = MakeB(), .c = MakeA()};

We had previously decided to evaluate the initializer in the order it's written, and then evaluate the conversions and copies in the order specified by the destination type, but that only works if the order of operations is at least partially breadth-first. In a fully depth-first world, we have to choose a single field order for the entire initialization.

Any other information that you want to share?

This question came up here during the review of #5545, and there was some preliminary discussion at the 12-02 open discussion.

Metadata

Metadata

Assignees

No one assigned

    Labels

    leads questionA question for the leads team

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions