Skip to content

MLC112: Default wait() freezes while a frame-varying one-argument updater is active

qual reports MLC112 when a default Scene.wait() is provably static while a one-argument updater that provably reads frame-varying state (ValueTracker.get_value, random, wall-clock time, iterator advancement) is active on a scene-family member. Manim decides whether a wait re-renders via should_update_mobjects: only scene updaters, stop_condition, always_update_mobjects, or a time-based family updater (one whose signature names a dt parameter) make it dynamic. A one-argument updater never does โ€” so the wait renders a single frozen frame and the updater's visual change never appears.

  • Default severity: warning
  • Minimum confidence: high
  • Implementation phase: 2
  • Fix: passing frozen_frame=False is suggested as an unsafe fix (declaring a dt parameter is the idiomatic alternative)

Only Yes-certainty dataflow facts fire: the updater body must provably read frame-varying state and provably not use dt, the registration must happen on every path, the host must be provably in the scene family with the updater still attached at the wait, and the wait must be provably static with no literal frozen_frame argument. Any Maybe โ€” an unresolvable callback body, a branch-dependent registration, a possible removal โ€” stays silent.

Wrong

class Demo(Scene):
    def construct(self):
        tracker = ValueTracker(0)
        dot = Dot()
        dot.add_updater(lambda m: m.set_x(tracker.get_value()))
        self.add(dot)
        self.wait(2)  # frozen frame: the tracker read never renders
class Demo(Scene):
    def construct(self):
        tracker = ValueTracker(0)
        dot = Dot()
        dot.add_updater(lambda m, dt: m.set_x(tracker.get_value()))
        self.add(dot)
        self.wait(2)  # the dt parameter makes the wait dynamic