Zulip Chat Archive

Stream: lean4

Topic: csimp can violate linear access of data


Jeremy Avigad (May 08 2021 at 12:31):

When you found that csimp did a swap that resulted in a shared reference, what did you do to fix it?

Mario Carneiro (May 08 2021 at 12:33):

The issue was that simp was basically inlining portions of the result of the function, but without actually deleting the call to the function and resulting in two versions of the value lying around. I marked the function as @[inline] so simp didn't have to deal with it

Mario Carneiro (May 08 2021 at 12:33):

I'll make a MWE

Mario Carneiro (May 08 2021 at 13:05):

structure Foo where
  a : String
  b : Unit
  c : Unit

def withA (f : String  String) (s : Foo) : Foo :=
  { s with a := f s.a }

def doStuffLinearly : String  String :=
dbgTraceIfShared "oh no"

set_option trace.compiler.ir.result true in
def bar (s : Foo) : Foo := do
  let s' := withA doStuffLinearly s
  -- let s' := dbgTraceIfShared "this would fail" s'
  pure { s' with c := () }

#eval show IO Unit from do
  let n  IO.rand 0 1 -- to make sure lean doesn't put s in a global variable
  let s : Foo := toString n, (), ()⟩
  let s := dbgTraceIfShared "ok here" s
  let a, b, c  bar s
  b

Mario Carneiro (May 08 2021 at 13:10):

The interesting part here is bar. It performs two linear operations on the state Foo, first it modifies a with doStuffLinearly, then we get the resulting value s' and resets c in it. However, the generated IR makes it clear that what lean is actually compiling is

def bar (s : Foo) : Foo := do
  let s' := withA doStuffLinearly s
  pure { a := s'.a, b := s.b, c := () }

where field b is taken from the previous value of s. At first I had these named the same thing and I thought it was some kind of name confusion, but what's actually happening is that lean looks at the definition of withA and determines that s'.b = s.b so the replacement is permitted. Which is true, but causes a reference to s to hang around when withA doStuffLinearly s is called, meaning that doStuffLinearly will not have linear access to s.a.

Mario Carneiro (May 08 2021 at 13:11):

def bar (x_1 : obj) : obj :=
  let x_2 : obj := bar._closed_1;
  inc x_1;
  let x_3 : obj := withA x_2 x_1;
  let x_4 : obj := proj[0] x_3;
  inc x_4;
  dec x_3;
  let x_5 : u8 := isShared x_1;
  case x_5 : u8 of
  Bool.false 
    let x_6 : obj := proj[2] x_1;
    dec x_6;
    let x_7 : obj := proj[0] x_1;
    dec x_7;
    let x_8 : obj := ctor_0[PUnit.unit];
    set x_1[2] := x_8;
    set x_1[0] := x_4;
    ret x_1
  Bool.true 
    let x_9 : obj := proj[1] x_1;
    inc x_9;
    dec x_1;
    let x_10 : obj := ctor_0[PUnit.unit];
    let x_11 : obj := ctor_0[Foo.mk] x_4 x_9 x_10;
    ret x_11

Here x_1 is s, and x_3 is s'. You can see references to x_1 appearing later in the function after the withA call, as well as inc x_1 before calling withA.

Mario Carneiro (May 08 2021 at 13:13):

Putting @[inline] on withA causes it to actually be inlined, instead of just partially inlined like this, meaning that all the destructuring happens only at the beginning of the function and the individual projections are used linearly.

Jeremy Avigad (May 08 2021 at 13:46):

Thanks, Mario. The example is clear, and the explanations are especially interesting and helpful.


Last updated: Dec 20 2023 at 11:08 UTC