Hi,
This is a very much a very rare edge case...
I'm not sure a fix would be worth it at all depending on complexity/your design preferences, but still wanted to document it for anyone else, just in case!
Context
ATL deep comparison was introduced in Ren'Py 8.1.2 to determine more thoroughly if an ATL transform has changed or not. See commits 468733b but also especially 9c7b8a6. Previously, ATL comparison was only performed via loaded variables comparisons (cf. b2a49cc for the removed code of loaded variables comparisons via find_loaded_variables).
It works, but when porting an old project to recent Ren'Py versions, I found an edge case where string interpolation, when used in a show expression statement, combined with an ATL interpolation that references a dynamic label variable, will make deep comparison fail, even though the code is valid Ren'Py script.
Minimal example to reproduce the anomaly
To be tested with any version starting from Ren'Py 8.1.2, which introduces ATL deep comparison. No anomaly with Ren'Py <= 8.1.1.
label display_date_somewhere(new_date, fade_duration=10.0):
$ interpolated_text = Text("[new_date]")
$ my_window = Window(child=interpolated_text, background='#000', padding=(10, 10), style="default")
show expression my_window onlayer screens:
alpha 0.0
linear fade_duration alpha 1.0
return
label start:
"Let's start it."
call display_date_somewhere("June 18")
pause 1.0
show bg uni:
linear 2.0 zoom 2.0
with dissolve
Traceback (line numbers within Ren'Py core may be off; I debugged directly in the files with statements of mine):
Compiling ATL code at game/script.rpy:23
File "game/script.rpy", line 34, in script
with dissolve
File "game/script.rpy", line 23, in <module>
linear fade_duration alpha 1.0
^^^^^^^^^^^^^
NameError: name 'fade_duration' is not defined
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "game/script.rpy", line 34, in script
with dissolve
File "game/script.rpy", line 34, in script
with dissolve
File "game/script.rpy", line 23, in <module>
linear fade_duration alpha 1.0
^^^^^^^^^^^^^
NameError: name 'fade_duration' is not defined
-- Full Traceback ------------------------------------------------------------
Traceback (most recent call last):
File "game/script.rpy", line 34, in script
with dissolve
File "renpy/ast.py", line 1591, in execute
renpy.exports.with_statement(trans, paired=paired)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^
File "renpy/exports/statementexports.py", line 260, in with_statement
return renpy.game.interface.do_with(trans, paired, clear=clear)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "renpy/display/core.py", line 1579, in do_with
return self.interact(
~~~~~~~~~~~~~^
trans_pause=True, suppress_overlay=not renpy.config.overlay_during_with, mouse="with", clear=clear
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
)
^
File "renpy/display/core.py", line 2219, in interact
repeat, rv = self.interact_core(
~~~~~~~~~~~~~~~~~~^
preloads=preloads,
^^^^^^^^^^^^^^^^^^
...<4 lines>...
**kwargs,
^^^^^^^^^
) # type: ignore
^
File "renpy/display/core.py", line 2473, in interact_core
self.transition_from[k] = self.old_scene[k]._in_current_store()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
File "renpy/display/layout.py", line 736, in _in_current_store
new_d = old_d._in_current_store()
~~~~~~~~~~~~~~~~~~~~~~~^^
File "renpy/display/layout.py", line 709, in _in_current_store
d = new_sle.displayable._in_current_store()
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
File "renpy/display/transform.py", line 1234, in _in_current_store
rv.take_execution_state(self)
~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
File "renpy/atl.py", line 569, in take_execution_state
block = self.compile()
~~~~~~~~~~~~^^
File "renpy/atl.py", line 757, in compile
block = self.atl.compile(self.context)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
File "renpy/atl.py", line 951, in compile
statements = [i.compile(ctx) for i in self.statements]
~~~~~~~~~^^^^^
File "renpy/atl.py", line 1325, in compile
duration = ctx.eval(self.duration)
~~~~~~~~^^^^^^^^^^^^^^^
File "renpy/atl.py", line 403, in eval
return renpy.python.py_eval(expr, locals=self.context)
~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "renpy/python.py", line 1331, in py_eval
return py_eval_bytecode(code, globals, locals)
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "renpy/python.py", line 1320, in py_eval_bytecode
value = eval(bytecode, globals, locals)
~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "game/script.rpy", line 23, in <module>
linear fade_duration alpha 1.0
^^^^^^^^^^^^^
NameError: name 'fade_duration' is not defined
The anomaly immediately disappears if you:
- Use a Text element without string interpolation like
Text(new_date), quite simply, which is really the thing to do here!
- Or if you do away with the dynamic label variable in the ATL block:
linear 1.0
- Or, better still, if you move over the
linear fade_duration to a transform to isolate it properly, like this:
transform my_transform(fade_duration):
alpha 0.0
linear fade_duration alpha 1.0
label display_date_somewhere(new_date, fade_duration=10.0):
$ interpolated_text = Text("[new_date]")
$ my_window = Window(child=interpolated_text, background='#000', padding=(10, 10), style="default")
show expression my_window at my_transform(fade_duration) onlayer screens
return
Minimal analysis
From what I understand after a lot of brute-force debugging in the dark:
- The execution of the
display_date_somewhere label works perfectly. The fade_duration dynamic variable is in scope during execution.
- After the execution of the label, though, when the core needs to check if the ATL has changed for the next transition, the
show expression with an interpolated text element is I assume forced to reevaluate that string interpolation.
Found out while debugging that the Window displayable considers that the new Text element child is different from the original one, e.g old is not new here:
def _in_current_store(self):
children = []
changed = False
for old in self.children:
new = old._in_current_store() # This is now a different object, I assume because of the string interpolation?
changed |= old is not new
children.append(new)
if not changed:
return self
As a result, down the stack, the ATLTransform object is forced to take_execution_state() and to be recompiled here, in the context of ATL deep comparison:
if t.atl.constant != GLOBAL_CONST:
block = self.get_block()
if block is None:
block = self.compile() # That's the line introduced in commit 9c7b8a6
if not deep_compare(self.block, t.block):
return
self.done = t.done
self.block = t.block
self.atl_state = t.atl_state
self.transform_event = t.transform_event
self.last_transform_event = t.last_transform_event
self.last_child_transform_event = t.last_child_transform_event
But since we're now outside the scope of the label where the the dynamic fade_duration variable used to exist, it can't be found in the store dictionaries, and bytecode compilation fails with a variable that can't be found.
A dirty fix to ignore?
I'm just going to use a proper and clean Transform in my code, I think that's the cleanest solution here!
But otherwise, the following fix in atl.py does seem to work:
if t.atl.constant != GLOBAL_CONST:
block = self.get_block()
#if block is None: I'm removing the compilation prior to deep comparison entirely
# block = self.compile()
if block is not None and not deep_compare(self.block, t.block): #I perform deep comparison and return early only if the block is already compiled
return
Deep comparison of ATL blocks is now performed only if the current block is already compiled.
If the block is still uncompiled, we march forward to the self.block = t.block assignment without even performing deep comparison: we just take the value of the new t.block and that's it.
But I'm not sure at all of the implications of such a fix for deep comparison more widely, it seems to sidestep its point entirely. I don't mind at all if this remains unfixable given how strange the conditions to reproduce it are.
Hi,
This is a very much a very rare edge case...
I'm not sure a fix would be worth it at all depending on complexity/your design preferences, but still wanted to document it for anyone else, just in case!
Context
ATL deep comparison was introduced in Ren'Py 8.1.2 to determine more thoroughly if an ATL transform has changed or not. See commits 468733b but also especially 9c7b8a6. Previously, ATL comparison was only performed via loaded variables comparisons (cf. b2a49cc for the removed code of loaded variables comparisons via
find_loaded_variables).It works, but when porting an old project to recent Ren'Py versions, I found an edge case where string interpolation, when used in a
show expressionstatement, combined with an ATL interpolation that references a dynamic label variable, will make deep comparison fail, even though the code is valid Ren'Py script.Minimal example to reproduce the anomaly
To be tested with any version starting from Ren'Py 8.1.2, which introduces ATL deep comparison. No anomaly with Ren'Py <= 8.1.1.
Traceback (line numbers within Ren'Py core may be off; I debugged directly in the files with statements of mine):
The anomaly immediately disappears if you:
Text(new_date), quite simply, which is really the thing to do here!linear 1.0linear fade_durationto a transform to isolate it properly, like this:Minimal analysis
From what I understand after a lot of brute-force debugging in the dark:
display_date_somewherelabel works perfectly. Thefade_durationdynamic variable is in scope during execution.show expressionwith an interpolated text element is I assume forced to reevaluate that string interpolation.Found out while debugging that the
Windowdisplayable considers that the new Text element child is different from the original one, e.gold is not newhere:As a result, down the stack, the ATLTransform object is forced to
take_execution_state()and to be recompiled here, in the context of ATL deep comparison:But since we're now outside the scope of the label where the the dynamic
fade_durationvariable used to exist, it can't be found in the store dictionaries, and bytecode compilation fails with a variable that can't be found.A dirty fix to ignore?
I'm just going to use a proper and clean
Transformin my code, I think that's the cleanest solution here!But otherwise, the following fix in
atl.pydoes seem to work:Deep comparison of ATL blocks is now performed only if the current block is already compiled.
If the block is still uncompiled, we march forward to the
self.block = t.blockassignment without even performing deep comparison: we just take the value of the newt.blockand that's it.But I'm not sure at all of the implications of such a fix for deep comparison more widely, it seems to sidestep its point entirely. I don't mind at all if this remains unfixable given how strange the conditions to reproduce it are.