terralang / terra

Terra is a low-level system programming language that is embedded in and meta-programmed by the Lua programming language.
terralang.org
Other
2.72k stars 201 forks source link

Parallel assignments aren't #500

Open aiverson opened 3 years ago

aiverson commented 3 years ago

There seems to be a miscompilation bug in parallel assignments, where the assignments are executed in sequence, with the effects of one assignment being visible in the RHS.

Known cases:

Using the geometric algebra from stdlib and a dynamic array type setup:

  local g = ga(float, dimension)
  local struct particle {
    pos: g.vector_t
    vel: g.vector_t
    mass: g.scalar_t
                        }

  local struct spring {
    conns: tuple(uint, uint)
    strength: g.scalar_t -- [0, 1]
    length: g.scalar_t -- [0, oo)
                      }

  local struct system {
    gravity: g.scalar_t -- [0, oo)
    drag: g.scalar_t -- [0, 1]
    particles: dynarray(particle)
    springs: dynarray(spring)
                      }

Reference implementation with no parallel assignment:

  terra system:update()
    -- position based dynamics
    -- initial velocity, gravity, and drag application.
    for i = 0, self.particles.size do
      var part = &self.particles(i)
      var vel = part.vel
      part.vel = part.pos
      part.pos = part.pos + vel * (1 - self.drag) --reuse velocity to store old position; will be recomputed at end
      part.pos = part.pos - self.gravity * part.pos:normalize()
    end
    -- rest of the physics omitted, the presence/absence doesn't affect the bug
    -- compute final velocity
    for i = 0, self.particles.size do
      var part = &self.particles(i)
      part.vel = part.pos - part.vel --velocity field stored old position during updates; recover velocity from position change
    end
  end

This variant using parallel assignment should behave identically to the reference implementation, but triggers a miscompilation bug and so instead completes the first part of the assignment updating the position before reading the position to update the velocity, resulting in all velocities carried between ticks being zeroed every tick

  terra system:update()
    -- position based dynamics
    -- initial velocity, gravity, and drag application.
    for i = 0, self.particles.size do
      var part = &self.particles(i)
      part.pos, part.vel = part.pos + part.vel * (1 - self.drag), part.pos --reuse velocity to store old position; will be recomputed at end
      part.pos = part.pos - self.gravity * part.pos:normalize()
    end
    -- rest of the physics omitted, the presence/absence doesn't affect the bug
    -- compute final velocity
    for i = 0, self.particles.size do
      var part = &self.particles(i)
      part.vel = part.pos - part.vel --velocity field stored old position during updates; recover velocity from position change
    end
  end

This variant using parallel assignment should behave identically to both the reference implementation and to how the other variant should behave. If it triggered the bug, the expected behavior would be to incorrectly use the old position as the velocity, resulting in everything flying away from the origin at high speed. This variant does not appear to trigger the bug, and so works identically to the reference implementation

  terra system:update()
    -- position based dynamics
    -- initial velocity, gravity, and drag application.
    for i = 0, self.particles.size do
      var part = &self.particles(i)
      part.vel, part.pos = part.pos, part.pos + part.vel * (1 - self.drag) --reuse velocity to store old position; will be recomputed at end
      part.pos = part.pos - self.gravity * part.pos:normalize()
    end
    -- rest of the physics omitted, the presence/absence doesn't affect the bug
    -- compute final velocity
    for i = 0, self.particles.size do
      var part = &self.particles(i)
      part.vel = part.pos - part.vel --velocity field stored old position during updates; recover velocity from position change
    end
  end

Some additional cases which don't trigger the bug:

local struct foo{
  a: int
  b: int
                }

local function printfoo(f)
  print(f.a, f.b)
end

local terra test1(a: int, b: int)
  a, b = b, a
  return [foo]{a, b}
end

printfoo(test1(1, 2))

local terra test2(a: int, b: int)
  var f = [foo]{a, b}
  f.a, f.b = f.b, f.a
  return f
end

printfoo(test2(1, 2))

local terra test3(a: int, b: int)
  var f = [foo]{a, b}
  var fp = &f
  fp.a, fp.b = fp.a + fp.b, fp.a
  fp.a = fp.a + 1
  fp.b = fp.a - fp.b
  return f
end

printfoo(test3(1, 2))

local terra test4(a: int, b: int, c: int)
  var f = [foo]{a, b}
  var fp = &f
  fp.a, fp.b = fp.a + fp.b * (1 - c), fp.a
  fp.a = fp.a + 1
  fp.b = fp.a - fp.b
  return f
end

printfoo(test4(1, 2, 0))