Confusing evaluation order

The program below doesn't behave as I would have expected:

struct Whole {

  var parts: [String] = []

  mutating func insert(_ newPart: String) -> Int {
    let id = parts.count
    parts.append(newPart)
    return id
  }

  func describe(_ partID: Int) -> String {
    parts[partID]
  }

}

var whole = Whole()

// 1
let foo = whole.insert("Foo")
print(whole.describe(foo)) // OK

// 2
print(whole.describe(whole.insert("Bar"))) // Fail!?

Whole is a structure that contains some parts.Those are inserted with insert(_:), which returns some ID. Meanwhile describe(_:) returns a textural description of the part identified by the specified ID.

(1) works as expected. We're inserting a part and printing its description right after. To my surprise, though, (2) does not and crashes because of an out-of-bound error.

My instinct would lead me to believe it's a bug. Am I missing something?

4 Likes

What's the expected behaviour in this case? Work as in #1 or a compilation error?

Swift uses a strict left-to-right evaluation order in most situations. In this case, that causes the value of whole to be copied before the other arguments to describe are evaluated.

We’ve considered (CC @Andrew_Trick) changing the evaluation of this so that self is not evaluated by copy but instead by immutable borrow, which in this case would cause this code to not compile due to an exclusivity error when the variable is modified while being immutably borrowed.

12 Likes

Good mental model?

(whole.describe)(whole.insert("Foo"))

AKA

Whole.describe(whole)(whole.insert("Foo"))

(whole.insert)("Foo")

wouldn't work because you "Cannot reference 'mutating' method as function value".

That's what I was missing. That clears up my confusion, thanks!

Could parameters be executed first and then copy / borrow of the "whole" LHS variable done to call "description" on it? If that's possible it would match the behaviour of #1. Or is this bad for some reason?

BTW, this simplified version works ok in C++:

#include <assert.h>
#include <stdio.h>

struct Whole {
    int partCount = 0;
    int insert() {
        return partCount++;
    }
    void check(int index) {
        assert(index < partCount);
    }
};

int main() {
    Whole whole;
    whole.check(whole.insert());
    printf("done\n");
}

You can say that self arguments should be borrowed, and that would lead to something like what you’re saying. I think there’s a fair argument that that’s more like people’s naive mental model for self. It also would make mutating and non-mutating method calls more similar in their evaluation order. owned self (when allowed) would presumably have the current evaluation order.

If you go more complicated than that, it quickly becomes extremely difficult to reason about evaluation order, which would be very bad.

C++ always passes self (essentially) inout without an exclusivity rule, which is not something we would ever imitate.

2 Likes

I assume we wouldn't want to modify the evaluation order, but if that was on the table, then I believe there's no reason why we couldn't evaluate the arguments first, copy them, and only then borrow/copy the receiver.

A good argument for that evaluation order is that expressions of the form foo(bar()) do not compose well (or at least, intuitively) if foo evaluates before the bar(). Instead, we're compelled to store the result of the argument into a temporary.

2 Likes

Good catch!

How come class Whole behaves as expected :confused:

class Whole {

  var parts: [String] = []

  func insert(_ newPart: String) -> Int {
    let id = parts.count
    parts.append(newPart)
    return id
  }

  func describe(_ partID: Int) -> String {
    parts [partID]
  }
}
@main
struct ConfusingEvalOrder {
    static func main () async {
        print (type (of:Self.self), #function, "...")
               
        var whole = Whole()

        // 0
        print (whole.describe (whole.insert ("Jibber"))) // Ok

        // 1
        let foo = whole.insert ("Foo")
        print (whole.describe (foo)) // OK

        // 2
        print (whole.describe (whole.insert ("Bar"))) // Ok
    }
}

Due to struct being a value type, this surprising change in evaluation order feels very unnatural to say the least.

I hope that people who are maintaining the The Swift Programming Language Book will see @Alvae's original post and employ it as an example of pitfalls to avoid when using a struct with a mutating method.

1 Like

This is high on my list of things we should fix in Swift 6.
Three reasons that immutable methods should borrow self:

  • it matches typical programmer expectations that self refers to the caller's value, not a copy. Any (new) exclusivity violations are likely bugs in the original code

  • it lets the compiler remove copies in common cases where programmers don't expect copies

  • it supports method invocation on move-only values

24 Likes

I appreciate the clarity of this answer and the discussion, but wonder if it's conclusory in this case.

Parentheses establish precedence, and should here with method call, no?

One might say: well, the target/callee is treated as a argument (somewhat explicitly: "before the other arguments") - indeed, as the first argument.

What prevents us from treating it as the last argument, for evaluation purposes, since treating it as an argument is a compiler convention? Put another way, since it's first not because it's lexically first, but because the compiler has to pass it with other arguments, then still: why treat it as the first argument?

Yes, changing this order could cause unacceptable behavior changes, or it could mess with how arguments are handled in a call, assuming evaluation time is bound to memory/register position (?). But is there anything in Swift semantics/syntax that prohibits it?

If not, it seems better, no? Aside from matching the programmer's mental model better, it might offer optimization opportunities, since more would be known about the (other) parameters at the point the callee is evaluated, and we may have more luck with callee/target optimizations than (other) arguments.

Thanks (And sorry if I'm misunderstanding the situation.)

The self expression being evaluated before the argument expressions is part of Swift semantics.

1 Like

Sorry, I know this is an old thread, but it's really the right place to point this out: the “strict left-to-right evaluation order” of Swift is a nice story we tell, but it isn't true. There's an inconsistency between mutating and non-mutating methods. All I have to do is add mutating before describe and the program works just as expected.

The fact that users don't notice the difference and aren't confused is a pretty strong indicator that “strict left-to-right” shouldn't be considered sacred, and the fact that we had to treat inout differently to make lots of useful programs work is a strong indicator that what we call “strict left-to-right” is in fact the wrong order.

This program demonstrates the differences in one place:

struct Y {
  func f<T, U>(_ x: T, _ y: U) -> (T, U) {
    print("-", (x, y))
    return (x, y)
  }

  mutating func g<T, U>(_ x: T, _ y: U) -> (T, U) {
    print("-", (x, y))
    return (x, y)
  }

  var me: Y {
    get {
      print("get access")
      return self
    }
    set {
      print("set access")
    }
  }
}

var x = Y()
let a = x.me.f(x.f(1, 2), x.f(3, 4))
print("----------")
let b = x.me.g(x.g(5, 6), x.g(7, 8))

The use of the me property is to show where acesses are evaluated. The output is

get access
- (1, 2)
- (3, 4)
- ((1, 2), (3, 4))
----------
- (5, 6)
- (7, 8)
get access
- ((5, 6), (7, 8))
set access

(if you use a _modify accessor it rums where the get accessor runs below the dashed line. And if you make Y noncopyable and replace get with _read, it runs where the get accessors run. So it's clearly not using the weird order in the first case due to x.f being interpreted as creating a closure that contains a copy of x.)

It's likely the ship has sailed, but IMO the right order is the one you get when everything is mutating. I'd characterize it as left-to-right, inner-to-outer. A left-to-right postorder traversal of the expression tree. It's consistent and non-confusing, and FWIW it's what I intuitively expect a language with specified evaluation order to do.

8 Likes

I want to add that a non-copyable receiver will give you the semantics of the inout case, which is further evidence that it might have been the right choice all along. Or, if strict left-to-right must be the rule, then it shouldn't be possible to call r.read(r.modify()) when r is non-copyable.

Demo
struct R1: ~Copyable {
  var b = false
  func read(_ b: Bool) {
    assert(b == self.b)
  }
  mutating func modify() -> Bool {
    self.b = !self.b
    return self.b
  }
}

struct R2 {
  var b = false
  func read(_ b: Bool) {
    assert(b == self.b)
  }
  mutating func modify() -> Bool {
    self.b = !self.b
    return self.b
  }
}

var r1 = R1()
r1.read(r1.modify()) // OK

var r2 = R2()
r2.read(r2.modify()) // assertion failure
1 Like

I found Dave's code example a little hard to read, so here's a simpler example that demonstrates what he's saying:

struct S {
  func f(_: Self...) {
    print(#function)
  }
  mutating func fMut(_: Self...) {
    print(#function)
  }
}

struct Provider {
  subscript(name: String) -> S {
    get {
      print("get \(name)")
      return S()
    }
    set {
      print("set \(name)")
    }
  }
}

var p = Provider()
p["arg-self"].f(p["arg-0"], p["arg-1"])
print("---")
p["arg-self"].fMut(p["arg-0"], p["arg-1"])

The output of this program is:

get arg-self
get arg-0
get arg-1
f(_:)
---
get arg-0
get arg-1
get arg-self
fMut(_:)
set arg-self

As you can see, when the called function is nonmutating, self is evaluated before the arguments. But when the function is mutating, self is evaluated after the arguments.