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,dtbeing 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