Skip to content

MLP218: frame-invariant updater dynamicizes a plain wait

qual reports MLP218 on a wait that renders dynamically solely because of an updater whose body is proven frame-invariant: the dt parameter is never read, no frame-varying state (trackers, random, wall clock, iterators) is read, no unknown call exists, and — verified against the callback AST — every effect is an absolute setter (set_*, move_to, next_to, to_edge, to_corner, align_to, center) on the parameter whose arguments never mention the parameter. Every rendered frame of the wait is then identical, and the full frame pipeline (update pass, interpolation, raster) repeats for nothing.

  • Default severity: info
  • Minimum confidence: high (per DESIGN 7.3, dt being unused is not enough on its own — frame-invariance must be proven)
  • Implementation phase: 3
  • Fix: none

Relative accumulators (shift, rotate, scale) are deliberately outside the proof: applying them every frame changes state (that pattern is MLD301 territory when unscaled), so freezing such a wait would change visuals. A body that mutates nothing at all is MLP215's no-op defect instead — this rule requires a real (mutates_target == Yes) but invariant write. The wait attribution uses the same sole-dynamicizer proof as MLP215.

Wrong

class Demo(Scene):
    def construct(self):
        square = Square()
        self.add(square)
        square.add_updater(lambda m, dt: m.set_fill(RED))  # same result every frame
        self.wait(4)                                       # ~240 identical frames
class Demo(Scene):
    def construct(self):
        square = Square()
        square.set_fill(RED)     # apply the final state once
        self.add(square)
        self.wait(4)             # frozen wait