CampBuddy/Camp.Buddy v2.2.1/Camp_Buddy-2.2.1-pc/renpy/sl2/slast.py
2025-03-03 23:00:33 +01:00

2212 lines
61 KiB
Python

# Copyright 2004-2019 Tom Rothamel <pytom@bishoujo.us>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#########################################################################
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
#
# When adding fields to a class in an __init__ method, we need to ensure that
# field is copied in the copy() method.
from __future__ import print_function
import ast
import collections
import linecache
from cPickle import loads, dumps
import zlib
import weakref
import renpy.display
import renpy.pyanalysis
import renpy.sl2
from renpy.display.transform import Transform, ATLTransform
from renpy.display.layout import Fixed
from renpy.display.predict import displayable as predict_displayable
from renpy.python import py_eval_bytecode
from renpy.pyanalysis import Analysis, NOT_CONST, LOCAL_CONST, GLOBAL_CONST, ccache
import hashlib
import time
# This file contains the abstract syntax tree for a screen language
# screen.
# A serial number that makes each SLNode unique.
serial = int(time.time() * 1000000)
# A sentinel used to indicate we should use the value found in the
# expression.
use_expression = renpy.object.Sentinel("use_expression")
# The filename that's currently being compiled.
filename = '<screen language>'
# A log that's used for profiling information.
profile_log = renpy.log.open("profile_screen", developer=True, append=False, flush=False)
def compile_expr(node):
"""
Wraps the node in a python AST, and compiles it.
"""
expr = ast.Expression(body=node)
ast.fix_missing_locations(expr)
return compile(expr, filename, "eval")
class SLContext(renpy.ui.Addable):
"""
A context object that can be passed to the execute methods, and can also
be placed in renpy.ui.stack.
"""
def __init__(self, parent=None):
if parent is not None:
self.__dict__.update(parent.__dict__)
return
# The local scope that python code is evaluated in.
self.scope = { }
# The global scope that python code is evaluated in.
self.globals = { }
# A list of child displayables that will be added to an outer
# displayable.
self.children = [ ]
# A map from keyword arguments to their values.
self.keywords = { }
# The style prefix that is given to children of this displayable.
self.style_prefix = None
# A cache associated with this context. The cache maps from
# statement serial to information associated with the statement.
self.new_cache = { }
# The old cache, used to take information from the old version of
# this displayable.
self.old_cache = { }
# The miss cache, used to take information that isn't present in
# old_cache.
self.miss_cache = { }
# The number of times a particular use statement has been called
# in the current screen. We use this to generate a unique name for
# each call site.
self.use_index = collections.defaultdict(int)
# When a constant node uses the scope, we add it to this list, so
# it may be reused. (If None, no list is used.)
self.uses_scope = None
# When a constant node has an id, we added it to this dict, so it
# may be reused. (If None, no dict is used.)
self.widgets = None
# True if we should dump debug information to the profile log.
self.debug = False
# True if we're predicting the screen.
self.predicting = False
# True if we're updating the screen.
self.updating = False
# A list of nodes we've predicted, for cases where predicting more than
# once could be a performance problem.
self.predicted = set()
# True if we're in a true showif block, False if we're in a false showif
# block, or None if we're not in a showif block.
self.showif = None
# True if there was a failure in this statement or any of its children.
# Fails can only occur when predicting, as otherwise an exception
# would be thrown.
self.fail = False
# The parent context of a use statement with a block.
self.parent = None
# The use statement containing the transcluded block.
self.transclude = None
# True if it's unlikely this node will run. This is used in prediction
# to speed things up.
self.unlikely = False
# The old and new generations of the use_cache.
self.new_use_cache = { }
self.old_use_cache = { }
def add(self, d, key):
self.children.append(d)
def close(self, d):
raise Exception("Spurious ui.close().")
class SLNode(object):
"""
The base class for screen language nodes.
"""
# The type of constant this node is.
constant = GLOBAL_CONST
# True if this node has at least one keyword that applies to its
# parent. False otherwise.
has_keyword = False
# True if this node should be the last keyword parsed.
last_keyword = False
def __init__(self, loc):
global serial
serial += 1
# A unique serial number assigned to this node.
self.serial = serial
# The location of this node, a (file, line) tuple.
self.location = loc
def instantiate(self, transclude):
"""
Instantiates a new instance of this class, copying the global
attributes of this class onto the new instance.
"""
cls = type(self)
rv = cls.__new__(cls)
rv.serial = self.serial
rv.location = self.location
return rv
def copy(self, transclude):
"""
Makes a copy of this node.
`transclude`
The constness of transclude statements.
"""
raise Exception("copy not implemented by " + type(self).__name__)
def report_traceback(self, name, last):
if last:
return None
filename, line = self.location
return [ (filename, line, name, None) ]
def analyze(self, analysis):
"""
Performs static analysis on Python code used in this statement.
"""
# By default, does nothing.
def prepare(self, analysis):
"""
This should be called before the execute code is called, and again
after init-level code (like the code in a .rpym module or an init
python block) is called.
`analysis`
A pyanalysis.Analysis object containing the analysis of this screen.
"""
# By default, does nothing.
def execute(self, context):
"""
Execute this node, updating context as appropriate.
"""
raise Exception("execute not implemented by " + type(self).__name__)
def keywords(self, context):
"""
Execute this node, updating context.keywords as appropriate.
"""
# By default, does nothing.
return
def copy_on_change(self, cache):
"""
Flags the displayables that are created by this node and its children
as copy-on-change.
"""
return
def debug_line(self):
"""
Writes information about the line we're on to the debug log.
"""
filename, lineno = self.location
full_filename = renpy.exports.unelide_filename(filename)
line = linecache.getline(full_filename, lineno) or ""
line = line.decode("utf-8")
profile_log.write(" %s:%d %s", filename, lineno, line.rstrip())
if self.constant:
profile_log.write(" potentially constant")
def used_screens(self, callback):
"""
Calls callback with the name of each screen this node and its
children use.
"""
return
def has_transclude(self):
"""
Returns true if this node is a transclude or has a transclude as a child.
"""
return False
def has_python(self):
"""
Returns true if this node is Python or has a python node as a child.
"""
return False
# A sentinel used to indicate a keyword argument was not given.
NotGiven = renpy.object.Sentinel("NotGiven")
class SLBlock(SLNode):
"""
Represents a screen language block that can contain keyword arguments
and child displayables.
"""
# RawBlock from parse or None if not present.
atl_transform = None
def __init__(self, loc):
SLNode.__init__(self, loc)
# A list of keyword argument, expr tuples.
self.keyword = [ ]
# A list of child SLNodes.
self.children = [ ]
def instantiate(self, transclude):
rv = SLNode.instantiate(self, transclude)
rv.keyword = self.keyword
rv.children = [ i.copy(transclude) for i in self.children ]
return rv
def copy(self, transclude):
return self.instantiate(transclude)
def analyze(self, analysis):
for i in self.children:
i.analyze(analysis)
def prepare(self, analysis):
for i in self.children:
i.prepare(analysis)
self.constant = min(self.constant, i.constant)
# Compile the keywords.
keyword_values = { }
keyword_keys = [ ]
keyword_exprs = [ ]
for k, expr in self.keyword:
node = ccache.ast_eval(expr)
const = analysis.is_constant(node)
if const == GLOBAL_CONST:
keyword_values[k] = py_eval_bytecode(compile_expr(node))
else:
keyword_keys.append(ast.Str(s=k))
keyword_exprs.append(node) # Will be compiled as part of ast.Dict below.
self.constant = min(self.constant, const)
if keyword_values:
self.keyword_values = keyword_values
else:
self.keyword_values = None
if keyword_keys:
node = ast.Dict(keys=keyword_keys, values=keyword_exprs)
ast.copy_location(node, keyword_exprs[0])
self.keyword_exprs = compile_expr(node)
else:
self.keyword_exprs = None
self.has_keyword = bool(self.keyword)
self.keyword_children = [ ]
if self.atl_transform is not None:
self.has_keyword = True
self.atl_transform.mark_constant()
const = self.atl_transform.constant
self.constant = min(self.constant, const)
for i in self.children:
if i.has_keyword:
self.keyword_children.append(i)
self.has_keyword = True
if i.last_keyword:
self.last_keyword = True
break
def execute(self, context):
# Note: SLBlock.execute() is inlined in various locations for performance
# reasons.
for i in self.children:
try:
i.execute(context)
except:
if not context.predicting:
raise
def keywords(self, context):
keyword_values = self.keyword_values
if keyword_values is not None:
context.keywords.update(keyword_values)
keyword_exprs = self.keyword_exprs
if keyword_exprs is not None:
context.keywords.update(eval(keyword_exprs, context.globals, context.scope))
for i in self.keyword_children:
i.keywords(context)
if self.atl_transform is not None:
transform = ATLTransform(self.atl_transform, context=context.scope)
context.keywords["at"] = transform
style_prefix = context.keywords.pop("style_prefix", NotGiven)
if style_prefix is NotGiven:
style_prefix = context.keywords.pop("style_group", NotGiven)
if style_prefix is not NotGiven:
context.style_prefix = style_prefix
def copy_on_change(self, cache):
for i in self.children:
i.copy_on_change(cache)
def used_screens(self, callback):
for i in self.children:
i.used_screens(callback)
def has_transclude(self):
for i in self.children:
if i.has_transclude():
return True
return False
def has_python(self):
return any(i.has_python() for i in self.children)
def has_noncondition_child(self):
"""
Returns true if this block has a child that is not an SLIf statement,
or false otherwise.
"""
worklist = list(self.children)
while worklist:
n = worklist.pop(0)
if type(n) is SLBlock:
worklist.extend(n.children)
elif isinstance(n, SLIf):
for _, block in n.entries:
worklist.append(block)
else:
return True
return False
list_or_tuple = (list, tuple)
class SLCache(object):
"""
The type of cache associated with an SLDisplayable.
"""
def __init__(self):
# The displayable object created.
self.displayable = None
# The positional arguments that were used to create the displayable.
self.positional = None
# The keyword arguments that were used to created the displayable.
self.keywords = None
# A list of the children that were added to self.displayable.
self.children = None
# The old transform created.
self.transform = None
# The transform that was used to create self.transform.
self.raw_transform = None
# The imagemap stack entry we reuse.
self.imagemap = None
# If this can be represented as a single constant displayable,
# do so.
self.constant = None
# For a constant statement, a list of our children that use
# the scope.
self.constant_uses_scope = [ ]
# For a constant statement, a map from children to widgets.
self.constant_widgets = { }
# True if the displayable should be re-created if its arguments
# or children are changed.
self.copy_on_change = False
# The ShowIf this statement was wrapped in the last time it was wrapped.
self.old_showif = None
# The SLUse that was transcluded by this SLCache statement.
self.transclude = None
# The style prefix used when this statement was first created.
self.style_prefix = None
# A magic value that, if returned by a displayable function, is not added to
# the parent.
NO_DISPLAYABLE = renpy.display.layout.Null()
class SLDisplayable(SLBlock):
"""
A screen language AST node that corresponds to a displayable being
added to the tree.
"""
hotspot = False
variable = None
# A list of variables that are locally constant.
local_constant = [ ]
def __init__(self, loc, displayable, scope=False, child_or_fixed=False, style=None, text_style=None, pass_context=False, imagemap=False, replaces=False, default_keywords={}, hotspot=False, variable=None):
"""
`displayable`
A function that, when called with the positional and keyword
arguments, causes the displayable to be displayed.
`scope`
If true, the scope is supplied as an argument to the displayable.
`child_or_fixed`
If true and the number of children of this displayable is not one,
the children are added to a Fixed, and the Fixed is added to the
displayable.
`style`
The base name of the main style.
`pass_context`
If given, the context is passed in as the first positonal argument
of the displayable.
`imagemap`
True if this is an imagemap, and should be handled as one.
`hotspot`
True if this is a hotspot that depends on the imagemap it was
first displayed with.
`replaces`
True if the object this displayable replaces should be
passed to it.
`default_keywords`
The default keyword arguments to supply to the displayable.
`variable`
A variable that the main displayable is assigned to.
"""
SLBlock.__init__(self, loc)
self.displayable = displayable
self.scope = scope
self.child_or_fixed = child_or_fixed
self.style = style
self.pass_context = pass_context
self.imagemap = imagemap
self.hotspot = hotspot
self.replaces = replaces
self.default_keywords = default_keywords
self.variable = variable
# Positional argument expressions.
self.positional = [ ]
def copy(self, transclude):
rv = self.instantiate(transclude)
rv.displayable = self.displayable
rv.scope = self.scope
rv.child_or_fixed = self.child_or_fixed
rv.style = self.style
rv.pass_context = self.pass_context
rv.imagemap = self.imagemap
rv.hotspot = self.hotspot
rv.replaces = self.replaces
rv.default_keywords = self.default_keywords
rv.variable = self.variable
rv.positional = self.positional
return rv
def analyze(self, analysis):
if self.imagemap:
const = GLOBAL_CONST
for _k, expr in self.keyword:
const = min(const, analysis.is_constant_expr(expr))
analysis.push_control(imagemap=(const != GLOBAL_CONST))
if self.hotspot:
self.constant = min(analysis.imagemap(), self.constant)
SLBlock.analyze(self, analysis)
if self.imagemap:
analysis.pop_control()
# If we use a scope, store the local constants that need to be
# kept and placed into the scope.
if self.scope:
self.local_constant = list(analysis.local_constant)
if self.variable is not None:
const = self.constant
for i in self.positional:
const = min(self.constant, analysis.is_constant_expr(i))
for k, v in self.keyword:
const = min(self.constant, analysis.is_constant_expr(v))
if k == "id":
const = NOT_CONST
if const == LOCAL_CONST:
analysis.mark_constant(self.variable)
elif const == NOT_CONST:
analysis.mark_not_constant(self.variable)
def prepare(self, analysis):
SLBlock.prepare(self, analysis)
# Prepare the positional arguments.
exprs = [ ]
values = [ ]
has_exprs = False
has_values = False
for a in self.positional:
node = ccache.ast_eval(a)
const = analysis.is_constant(node)
if const == GLOBAL_CONST:
values.append(py_eval_bytecode(compile_expr(node)))
exprs.append(ast.Num(n=0))
has_values = True
else:
values.append(use_expression)
exprs.append(node) # Will be compiled as part of the tuple.
has_exprs = True
self.constant = min(self.constant, const)
if has_values:
self.positional_values = values
else:
self.positional_values = None
if has_exprs:
t = ast.Tuple(elts=exprs, ctx=ast.Load())
ast.copy_location(t, exprs[0])
self.positional_exprs = compile_expr(t)
else:
self.positional_exprs = None
# We do not pass keywords to our parents.
self.has_keyword = False
# We want to preserve last_keyword, however, in case we run a
# python block.
# If we have the id property, we're not constant - since we may get
# additional keywords via id. (It's unlikely, but id should be pretty
# rare.)
for k, _expr in self.keyword:
if k == "id":
self.constant = NOT_CONST
if self.variable is not None:
self.constant = NOT_CONST
def keywords(self, context):
# We do not want to pass keywords to our parents, so just return.
return
def execute(self, context):
debug = context.debug
screen = renpy.ui.screen
cache = context.old_cache.get(self.serial, None) or context.miss_cache.get(self.serial, None)
if not isinstance(cache, SLCache):
cache = SLCache()
context.new_cache[self.serial] = cache
copy_on_change = cache.copy_on_change
if debug:
self.debug_line()
if cache.constant and (cache.style_prefix == context.style_prefix):
for i, local_scope in cache.constant_uses_scope:
if local_scope:
scope = dict(context.scope)
scope.update(local_scope)
else:
scope = context.scope
if copy_on_change:
if i._scope(scope, False):
cache.constant = None
break
else:
i._scope(scope, True)
else:
d = cache.constant
if d is not NO_DISPLAYABLE:
if context.showif is not None:
d = self.wrap_in_showif(d, context, cache)
context.children.append(d)
if context.uses_scope is not None:
context.uses_scope.extend(cache.constant_uses_scope)
if debug:
profile_log.write(" reused constant displayable")
return
# Create the context.
ctx = SLContext(context)
# True if we encountered an exception that we're recovering from
# due to being in prediction mode.
fail = False
# The main displayable we're predicting.
main = None
# True if we've pushed something onto the imagemap stack.
imagemap = False
try:
# Evaluate the positional arguments.
positional_values = self.positional_values
positional_exprs = self.positional_exprs
if positional_values and positional_exprs:
values = eval(positional_exprs, context.globals, context.scope)
positional = [ b if (a is use_expression) else a for a, b in zip(positional_values, values) ]
elif positional_values:
positional = positional_values
elif positional_exprs:
positional = eval(positional_exprs, context.globals, context.scope)
else:
positional = [ ]
keywords = ctx.keywords = self.default_keywords.copy()
if self.constant:
ctx.uses_scope = [ ]
SLBlock.keywords(self, ctx)
# Get the widget id and transform, if any.
widget_id = keywords.pop("id", None)
transform = keywords.pop("at", None)
arguments = keywords.pop("arguments", None)
properties = keywords.pop("properties", None)
style_suffix = keywords.pop("style_suffix", None) or self.style
if arguments:
positional += arguments
if properties:
keywords.update(properties)
# If we don't know the style, figure it out.
if ("style" not in keywords) and style_suffix:
if ctx.style_prefix is None:
keywords["style"] = style_suffix
else:
keywords["style"] = ctx.style_prefix + "_" + style_suffix
if widget_id and (widget_id in screen.widget_properties):
keywords.update(screen.widget_properties[widget_id])
old_d = cache.displayable
if old_d:
old_main = old_d._main or old_d
else:
old_main = None
reused = False
if debug:
self.report_arguments(cache, positional, keywords, transform)
can_reuse = (old_d is not None) and (positional == cache.positional) and (keywords == cache.keywords) and (context.style_prefix == cache.style_prefix)
if (self.variable is not None) and copy_on_change:
can_reuse = False
# A hotspot can only be reused if the imagemap it belongs to has
# not changed.
if self.hotspot:
imc = renpy.ui.imagemap_stack[-1]
if cache.imagemap is not imc:
can_reuse = False
cache.imagemap = imc
if can_reuse:
reused = True
d = old_d
# The main displayable, if d is a composite displayable. (This is
# the one that gets the scope, and gets children added to it.)
main = old_main
if widget_id and not ctx.unlikely:
screen.widgets[widget_id] = main
if self.scope and main._uses_scope:
if copy_on_change:
if main._scope(ctx.scope, False):
reused = False
else:
main._scope(ctx.scope, True)
if reused and self.imagemap:
imagemap = True
cache.imagemap.reuse()
renpy.ui.imagemap_stack.append(cache.imagemap)
if not reused:
cache.positional = positional
cache.keywords = keywords.copy()
# This child creation code is copied below, for the copy_on_change
# case.
if self.scope:
keywords["scope"] = ctx.scope
if self.replaces and ctx.updating:
keywords['replaces'] = old_main
# Pass the context
if self.pass_context:
keywords['context'] = ctx
d = self.displayable(*positional, **keywords)
main = d._main or d
main._location = self.location
if widget_id and not ctx.unlikely:
screen.widgets[widget_id] = main
# End child creation code.
imagemap = self.imagemap
cache.copy_on_change = False # We no longer need to copy on change.
cache.children = None # Re-add the children.
if debug:
if reused:
profile_log.write(" reused displayable")
elif self.constant:
profile_log.write(" created constant displayable")
else:
profile_log.write(" created displayable")
except:
if not context.predicting:
raise
fail = True
if self.variable is not None:
context.scope[self.variable] = main
ctx.children = [ ]
ctx.showif = None
stack = renpy.ui.stack
stack.append(ctx)
try:
# Evaluate children. (Inlined SLBlock.execute)
for i in self.children:
try:
i.execute(ctx)
except:
if not context.predicting:
raise
fail = True
finally:
ctx.keywords = None
stack.pop()
if imagemap:
cache.imagemap = renpy.ui.imagemap_stack.pop()
cache.imagemap.cache.finish()
# If a failure occurred during prediction, predict main (if known),
# and ctx.children, and return.
if fail:
predict_displayable(main)
for i in ctx.children:
predict_displayable(i)
context.fail = True
return
if ctx.children != cache.children:
if reused and copy_on_change:
# This is a copy of the child creation code from above.
if self.scope:
keywords["scope"] = ctx.scope
if self.replaces and context.updating:
keywords['replaces'] = old_main
if self.pass_context:
keywords['context'] = ctx
d = self.displayable(*positional, **keywords)
main = d._main or d
main._location = self.location
if widget_id:
screen.widgets[widget_id] = main
# End child creation code.
cache.copy_on_change = False
reused = False
if reused:
main._clear()
if self.child_or_fixed and len(ctx.children) != 1:
f = Fixed()
for i in ctx.children:
f.add(i)
main.add(f)
else:
for i in ctx.children:
main.add(i)
# Inform the focus system about replacement displayables.
if (not context.predicting) and (old_d is not None):
replaced_by = renpy.display.focus.replaced_by
replaced_by[id(old_d)] = d
if d is not main:
for old_part, new_part in zip(old_d._composite_parts, d._composite_parts):
replaced_by[id(old_part)] = new_part
cache.displayable = d
cache.children = ctx.children
cache.style_prefix = context.style_prefix
if (transform is not None) and (d is not NO_DISPLAYABLE):
if reused and (transform == cache.raw_transform):
if isinstance(cache.transform, renpy.display.transform.Transform):
if cache.transform.child is not d:
cache.transform.set_child(d, duplicate=False)
d = cache.transform
else:
cache.raw_transform = transform
if isinstance(transform, Transform):
d = transform(child=d)
d._unique()
elif isinstance(transform, list_or_tuple):
for t in transform:
if isinstance(t, Transform):
d = t(child=d)
else:
d = t(d)
d._unique()
else:
d = transform(d)
d._unique()
if isinstance(d, Transform):
old_transform = cache.transform
if not context.updating:
old_transform = None
d.take_state(old_transform)
d.take_execution_state(old_transform)
cache.transform = d
else:
cache.transform = None
cache.raw_transform = None
if ctx.fail:
context.fail = True
else:
if self.constant:
cache.constant = d
if self.scope and main._uses_scope:
local_scope = { }
for i in self.local_constant:
if i in ctx.scope:
local_scope[i] = ctx.scope[i]
ctx.uses_scope.append((main, local_scope))
cache.constant_uses_scope = ctx.uses_scope
if context.uses_scope is not None:
context.uses_scope.extend(ctx.uses_scope)
if d is not NO_DISPLAYABLE:
if context.showif is not None:
d = self.wrap_in_showif(d, context, cache)
context.children.append(d)
def wrap_in_showif(self, d, context, cache):
"""
Wraps `d` in a ShowIf displayable.
"""
rv = renpy.sl2.sldisplayables.ShowIf(context.showif, cache.old_showif)
rv.add(d)
if not context.predicting:
cache.old_showif = rv
return rv
def report_arguments(self, cache, positional, keywords, transform):
if positional:
report = [ ]
values = self.positional_values or ([ use_expression ] * len(positional))
for i in range(len(positional)):
if values[i] is not use_expression:
report.append("const")
elif cache.positional is None:
report.append("new")
elif cache.positional[i] == positional[i]:
report.append("equal")
else:
report.append("not-equal")
profile_log.write(" args: %s", " ".join(report))
values = self.keyword_values or { }
if keywords:
report = { }
if cache.keywords is None:
for k in keywords:
if k in values:
report[k] = "const"
continue
report[k] = "new"
else:
for k in keywords:
k = str(k)
if k in values:
report[k] = "const"
continue
if k not in cache.keywords:
report[k] = "new-only"
continue
if keywords[k] == cache.keywords[k]:
report[k] = "equal"
else:
report[k] = "not-equal"
for k in cache.keywords:
if k not in keywords:
report[k] = "old-only"
profile_log.write(" kwargs: %r", report)
if transform is not None:
if "at" in values:
profile_log.write(" at: const")
elif cache.raw_transform is None:
profile_log.write(" at: new")
elif cache.raw_transform == transform:
profile_log.write(" at: equal")
else:
profile_log.write(" at: not-equal")
def copy_on_change(self, cache):
c = cache.get(self.serial, None)
if isinstance(c, SLCache):
c.copy_on_change = True
for i in self.children:
i.copy_on_change(cache)
class SLIf(SLNode):
"""
A screen language AST node that corresponds to an If/Elif/Else statement.
"""
def __init__(self, loc):
"""
An AST node that represents an if statement.
"""
SLNode.__init__(self, loc)
# A list of entries, with each consisting of an expression (or
# None, for the else block) and a SLBlock.
self.entries = [ ]
def copy(self, transclude):
rv = self.instantiate(transclude)
rv.entries = [ (expr, block.copy(transclude)) for expr, block in self.entries ]
return rv
def analyze(self, analysis):
const = GLOBAL_CONST
for cond, _block in self.entries:
if cond is not None:
const = min(const, analysis.is_constant_expr(cond))
analysis.push_control(const)
for _cond, block in self.entries:
block.analyze(analysis)
analysis.pop_control()
def prepare(self, analysis):
# A list of prepared entries, with each consisting of expression
# bytecode and a SLBlock.
self.prepared_entries = [ ]
for cond, block in self.entries:
if cond is not None:
node = ccache.ast_eval(cond)
self.constant = min(self.constant, analysis.is_constant(node))
cond = compile_expr(node)
block.prepare(analysis)
self.constant = min(self.constant, block.constant)
self.prepared_entries.append((cond, block))
self.has_keyword = self.has_keyword or block.has_keyword
self.last_keyword = self.last_keyword or block.last_keyword
def execute(self, context):
if context.predicting:
self.execute_predicting(context)
return
for cond, block in self.prepared_entries:
if cond is None or eval(cond, context.globals, context.scope):
for i in block.children:
i.execute(context)
return
def execute_predicting(self, context):
# A variant of the this code that runs while predicting, executing
# all paths of the if.
# True if no block has been the main choice yet.
first = True
# Has any instance of this node been predicted? We only predict
# once per node, for performance reasons.
predicted = self.serial in context.predicted
if not predicted:
context.predicted.add(self.serial)
for cond, block in self.prepared_entries:
try:
cond_value = (cond is None) or eval(cond, context.globals, context.scope)
except:
cond_value = False
# The taken branch.
if first and cond_value:
first = False
for i in block.children:
try:
i.execute(context)
except:
pass
# Not-taken branches, only if not already predicted.
elif not predicted:
ctx = SLContext(context)
ctx.children = [ ]
ctx.unlikely = True
for i in block.children:
try:
i.execute(ctx)
except:
pass
for i in ctx.children:
predict_displayable(i)
def keywords(self, context):
for cond, block in self.prepared_entries:
if cond is None or eval(cond, context.globals, context.scope):
block.keywords(context)
return
def copy_on_change(self, cache):
for _cond, block in self.entries:
block.copy_on_change(cache)
def used_screens(self, callback):
for _cond, block in self.entries:
block.used_screens(callback)
def has_transclude(self):
for _cond, block in self.entries:
if block.has_transclude():
return True
return False
def has_python(self):
return any(i[1].has_python() for i in self.entries)
class SLShowIf(SLNode):
"""
The AST node that corresponds to the showif statement.
"""
def __init__(self, loc):
"""
An AST node that represents an if statement.
"""
SLNode.__init__(self, loc)
# A list of entries, with each consisting of an expression (or
# None, for the else block) and a SLBlock.
self.entries = [ ]
def copy(self, transclude):
rv = self.instantiate(transclude)
rv.entries = [ (expr, block.copy(transclude)) for expr, block in self.entries ]
return rv
def analyze(self, analysis):
for _cond, block in self.entries:
block.analyze(analysis)
def prepare(self, analysis):
# A list of prepared entries, with each consisting of expression
# bytecode and a SLBlock.
self.prepared_entries = [ ]
for cond, block in self.entries:
if cond is not None:
node = ccache.ast_eval(cond)
self.constant = min(self.constant, analysis.is_constant(node))
cond = compile_expr(node)
block.prepare(analysis)
self.constant = min(self.constant, block.constant)
self.prepared_entries.append((cond, block))
self.last_keyword = True
def execute(self, context):
# This is true when the block should be executed - when no outer
# showif is False, and when no prior block in this showif has
# executed.
first_true = context.showif is not False
for cond, block in self.prepared_entries:
ctx = SLContext(context)
if not first_true:
ctx.showif = False
else:
if cond is None or eval(cond, context.globals, context.scope):
ctx.showif = True
first_true = False
else:
ctx.showif = False
for i in block.children:
i.execute(ctx)
if ctx.fail:
context.fail = True
def copy_on_change(self, cache):
for _cond, block in self.entries:
block.copy_on_change(cache)
def used_screens(self, callback):
for _cond, block in self.entries:
block.used_screens(callback)
def has_transclude(self):
for _cond, block in self.entries:
if block.has_transclude():
return True
return False
def has_python(self):
return any(i[1].has_python() for i in self.entries)
class SLFor(SLBlock):
"""
The AST node that corresponds to a for statement. This only supports
simple for loops that assign a single variable.
"""
index_expression = None
def __init__(self, loc, variable, expression, index_expression):
SLBlock.__init__(self, loc)
self.variable = variable
self.expression = expression
self.index_expression = index_expression
def copy(self, transclude):
rv = self.instantiate(transclude)
rv.variable = self.variable
rv.expression = self.expression
return rv
def analyze(self, analysis):
if analysis.is_constant_expr(self.expression) == GLOBAL_CONST:
analysis.push_control(True)
analysis.mark_constant(self.variable)
else:
analysis.push_control(False)
analysis.mark_not_constant(self.variable)
SLBlock.analyze(self, analysis)
analysis.pop_control()
def prepare(self, analysis):
node = ccache.ast_eval(self.expression)
const = analysis.is_constant(node)
if const == GLOBAL_CONST:
self.expression_value = py_eval_bytecode(compile_expr(node))
self.expression_expr = None
else:
self.expression_value = None
self.expression_expr = compile_expr(node)
self.constant = min(self.constant, const)
SLBlock.prepare(self, analysis)
self.last_keyword = True
def execute(self, context):
variable = self.variable
expr = self.expression_expr
try:
if expr is not None:
value = eval(expr, context.globals, context.scope)
else:
value = self.expression_value
except:
if not context.predicting:
raise
value = [ 0 ]
newcaches = { }
oldcaches = context.old_cache.get(self.serial, newcaches) or { }
if not isinstance(oldcaches, dict):
oldcaches = { }
misscaches = context.miss_cache.get(self.serial, newcaches) or { }
if not isinstance(misscaches, dict):
misscaches = { }
ctx = SLContext(context)
for index, v in enumerate(value):
ctx.scope[variable] = v
if self.index_expression is not None:
index = eval(self.index_expression, ctx.globals, ctx.scope)
ctx.old_cache = oldcaches.get(index, None) or { }
if not isinstance(ctx.old_cache, dict):
ctx.old_cache = {}
ctx.miss_cache = misscaches.get(index, None) or { }
if not isinstance(ctx.miss_cache, dict):
ctx.miss_cache = {}
newcaches[index] = ctx.new_cache = { }
# Inline of SLBlock.execute.
for i in self.children:
try:
i.execute(ctx)
except:
if not context.predicting:
raise
if context.unlikely:
break
context.new_cache[self.serial] = newcaches
if ctx.fail:
context.fail = True
def keywords(self, context):
return
def copy_on_change(self, cache):
c = cache.get(self.serial, None)
if not isinstance(c, dict):
return
for child_cache in c.values():
for i in self.children:
i.copy_on_change(child_cache)
class SLPython(SLNode):
def __init__(self, loc, code):
SLNode.__init__(self, loc)
# A pycode object.
self.code = code
def copy(self, transclude):
rv = self.instantiate(transclude)
rv.code = self.code
return rv
def analyze(self, analysis):
analysis.python(self.code.source)
def execute(self, context):
exec self.code.bytecode in context.globals, context.scope
def prepare(self, analysis):
self.constant = NOT_CONST
self.last_keyword = True
def has_python(self):
return True
class SLPass(SLNode):
def execute(self, context):
return
def copy(self, transclude):
rv = self.instantiate(transclude)
return rv
class SLDefault(SLNode):
def __init__(self, loc, variable, expression):
SLNode.__init__(self, loc)
self.variable = variable
self.expression = expression
def copy(self, transclude):
rv = self.instantiate(transclude)
rv.variable = self.variable
rv.expression = self.expression
return rv
def analyze(self, analysis):
analysis.mark_not_constant(self.variable)
def prepare(self, analysis):
self.expr = compile_expr(ccache.ast_eval(self.expression))
self.constant = NOT_CONST
self.last_keyword = True
def execute(self, context):
scope = context.scope
variable = self.variable
if variable in scope:
return
scope[variable] = eval(self.expr, context.globals, scope)
def has_python(self):
return True
class SLUse(SLNode):
id = None
block = None
def __init__(self, loc, target, args, id_expr, block):
SLNode.__init__(self, loc)
# The name of the screen we're accessing.
self.target = target
# If the target is an SL2 screen, the SLScreen node at the root of
# the ast for that screen.
self.ast = None
# If arguments are given, those arguments.
self.args = args
# An expression, if the id property is given.
self.id = id_expr
# A block for transclusion, or None if the statement does not have a
# block.
self.block = block
def copy(self, transclude):
rv = self.instantiate(transclude)
rv.target = self.target
rv.args = self.args
rv.id = self.id
if self.block is not None:
rv.block = self.block.copy(transclude)
else:
rv.block = None
rv.ast = None
return rv
def analyze(self, analysis):
self.last_keyword = True
if self.id:
self.constant = NOT_CONST
if self.block:
self.block.analyze(analysis)
def prepare(self, analysis):
self.ast = None
if self.block:
self.block.prepare(analysis)
if self.block.constant == GLOBAL_CONST:
const = True
else:
const = False
else:
const = False
if isinstance(self.target, renpy.ast.PyExpr):
self.constant = NOT_CONST
const = False
self.ast = None
else:
target = renpy.display.screen.get_screen_variant(self.target)
if target is None:
self.constant = NOT_CONST
if renpy.config.developer:
raise Exception("A screen named {} does not exist.".format(self.target))
else:
return
if target.ast is None:
self.constant = NOT_CONST
return
if const:
self.ast = target.ast.const_ast
else:
self.ast = target.ast.not_const_ast
self.constant = min(self.constant, self.ast.constant)
def execute_use_screen(self, context):
# Create an old-style displayable name for this call site.
serial = context.use_index[self.serial]
context.use_index[self.serial] = serial + 1
name = (
context.scope.get("_name", ()),
self.serial,
serial)
if self.args:
args, kwargs = self.args.evaluate(context.scope)
else:
args = [ ]
kwargs = { }
renpy.display.screen.use_screen(self.target, _name=name, _scope=context.scope, *args, **kwargs)
def execute(self, context):
if isinstance(self.target, renpy.ast.PyExpr):
target_name = eval(self.target, context.globals, context.scope)
target = renpy.display.screen.get_screen_variant(target_name)
if target is None:
raise Exception("A screen named {} does not exist.".format(target_name))
ast = target.ast.not_const_ast
id_prefix = "_use_expression"
else:
id_prefix = self.target
ast = self.ast
# If self.ast is not an SL2 screen, run it using renpy.display.screen.use_screen.
if ast is None:
self.execute_use_screen(context)
return
# Otherwise, run the use statement directly.
# Figure out the cache to use.
ctx = SLContext(context)
ctx.new_cache = context.new_cache[self.serial] = { }
ctx.miss_cache = context.miss_cache.get(self.serial, None) or { }
ctx.uses_scope = [ ]
if self.id:
use_id = (id_prefix, eval(self.id, context.globals, context.scope))
ctx.old_cache = context.old_use_cache.get(use_id, None) or context.old_cache.get(self.serial, None) or { }
if use_id in ctx.old_use_cache:
ctx.updating = True
ctx.new_use_cache[use_id] = ctx.new_cache
else:
ctx.old_cache = context.old_cache.get(self.serial, None) or { }
if not isinstance(ctx.old_cache, dict):
ctx.old_cache = { }
if not isinstance(ctx.miss_cache, dict):
ctx.miss_cache = { }
# Evaluate the arguments.
try:
if self.args:
args, kwargs = self.args.evaluate(context.scope)
else:
args = [ ]
kwargs = { }
except:
if not context.predicting:
raise
args = [ ]
kwargs = { }
# Apply the arguments to the parameters (if present) or to the scope of the used screen.
if ast.parameters is not None:
new_scope = ast.parameters.apply(args, kwargs, ignore_errors=context.predicting)
scope = ctx.old_cache.get("scope", None) or ctx.miss_cache.get("scope", None) or { }
scope.update(new_scope)
else:
if args:
raise Exception("Screen {} does not take positional arguments. ({} given)".format(self.target, len(args)))
scope = context.scope.copy()
scope.update(kwargs)
scope["_scope"] = scope
ctx.new_cache["scope"] = scope
# Run the child screen.
ctx.scope = scope
ctx.parent = weakref.ref(context)
ctx.transclude = self.block
try:
ast.execute(ctx)
finally:
del scope["_scope"]
if ctx.fail:
context.fail = True
def copy_on_change(self, cache):
c = cache.get(self.serial, None)
if c is None:
return
if self.ast is not None:
self.ast.copy_on_change(c)
def used_screens(self, callback):
callback(self.target)
class SLTransclude(SLNode):
def __init__(self, loc):
SLNode.__init__(self, loc)
def copy(self, transclude):
rv = self.instantiate(transclude)
rv.constant = transclude
return rv
def execute(self, context):
if not context.transclude:
return
parent = context.parent
if parent is not None:
parent = parent()
ctx = SLContext(parent)
ctx.new_cache = context.new_cache[self.serial] = { }
ctx.old_cache = context.old_cache.get(self.serial, None) or { }
ctx.miss_cache = context.miss_cache.get(self.serial, None) or { }
if not isinstance(ctx.old_cache, dict):
ctx.old_cache = { }
if not isinstance(ctx.miss_cache, dict):
ctx.miss_cache = { }
ctx.new_cache["transclude"] = context.transclude
ctx.children = context.children
ctx.showif = context.showif
try:
renpy.ui.stack.append(ctx)
context.transclude.keywords(ctx)
context.transclude.execute(ctx)
finally:
renpy.ui.stack.pop()
if ctx.fail:
context.fail = True
def copy_on_change(self, cache):
c = cache.get(self.serial, None)
if c is None or "transclude" not in c:
return
SLBlock.copy_on_change(c["transclude"], c)
def has_transclude(self):
return True
class SLScreen(SLBlock):
"""
This represents a screen defined in the screen language 2.
"""
version = 0
# This screen's AST when the transcluded block is entirely
# constant (or there is no transcluded block at all). This may be
# the actual AST, or a copy.
const_ast = None
# A copy of this screen's AST when the transcluded block is not
# constant.
not_const_ast = None
# The analysis
analysis = None
layer = "'screens'"
sensitive = "True"
def __init__(self, loc):
SLBlock.__init__(self, loc)
# The name of the screen.
self.name = None
# Should this screen be declared as modal?
self.modal = "False"
# The screen's zorder.
self.zorder = "0"
# The screen's tag.
self.tag = None
# The variant of screen we're defining.
self.variant = "None" # expr.
# Should we predict this screen?
self.predict = "None" # expr.
# Should this screen be sensitive.
self.sensitive = "True"
# The parameters this screen takes.
self.parameters = None
# The analysis object used for this screen, if the screen has
# already been analyzed.
self.analysis = None
# True if this screen has been prepared.
self.prepared = False
def copy(self, transclude):
rv = self.instantiate(transclude)
rv.name = self.name
rv.modal = self.modal
rv.zorder = self.zorder
rv.tag = self.tag
rv.variant = self.variant
rv.predict = self.predict
rv.parameters = self.parameters
rv.sensitive = self.sensitive
rv.prepared = False
rv.analysis = None
rv.ast = None
return rv
def define(self, location):
"""
Defines a screen.
"""
renpy.display.screen.define_screen(
self.name,
self,
modal=self.modal,
zorder=self.zorder,
tag=self.tag,
variant=renpy.python.py_eval(self.variant),
predict=renpy.python.py_eval(self.predict),
parameters=self.parameters,
location=self.location,
layer=renpy.python.py_eval(self.layer),
sensitive=self.sensitive,
)
def analyze(self, analysis):
SLBlock.analyze(self, analysis)
def analyze_screen(self):
# Have we already been analyzed?
if self.const_ast:
return
key = (self.name, self.variant)
if key in scache.const_analyzed:
self.const_ast = scache.const_analyzed[key]
self.not_const_ast = scache.not_const_analyzed[key]
return
self.const_ast = self
if self.has_transclude():
self.not_const_ast = self.copy(NOT_CONST)
targets = [ self.const_ast, self.not_const_ast ]
else:
self.not_const_ast = self.const_ast
targets = [ self.const_ast ]
for ast in targets:
analysis = ast.analysis = Analysis(None)
if ast.parameters:
analysis.parameters(ast.parameters)
ast.analyze(analysis)
while not analysis.at_fixed_point():
ast.analyze(analysis)
scache.const_analyzed[key] = self.const_ast
scache.not_const_analyzed[key] = self.not_const_ast
scache.updated = True
def unprepare_screen(self):
self.prepared = False
def prepare_screen(self):
if self.prepared:
return
self.analyze_screen()
# This version ensures we're not using the cache from an old
# version of the screen.
self.version += 1
self.const_ast.prepare(self.const_ast.analysis)
if self.not_const_ast is not self.const_ast:
self.not_const_ast.prepare(self.not_const_ast.analysis)
self.prepared = True
if renpy.display.screen.get_profile(self.name).const:
profile_log.write("CONST ANALYSIS %s", self.name)
new_constants = [ i for i in self.const_ast.analysis.global_constant if i not in renpy.pyanalysis.constants ]
new_constants.sort()
profile_log.write(' global_const: %s', " ".join(new_constants))
local_constants = list(self.const_ast.analysis.local_constant)
local_constants.sort()
profile_log.write(' local_const: %s', " ".join(local_constants))
not_constants = list(self.const_ast.analysis.not_constant)
not_constants.sort()
profile_log.write(' not_const: %s', " ".join(not_constants))
def execute(self, context):
self.keywords(context)
SLBlock.execute(self, context)
def report_traceback(self, name, last):
if last:
return None
if name == "__call__":
return [ ]
return SLBlock.report_traceback(self, name, last)
def __call__(self, *args, **kwargs):
scope = kwargs["_scope"]
debug = kwargs.get("_debug", False)
if self.parameters:
args = scope.get("_args", ())
kwargs = scope.get("_kwargs", { })
values = renpy.ast.apply_arguments(self.parameters, args, kwargs, ignore_errors=renpy.display.predict.predicting)
scope.update(values)
if not self.prepared:
self.prepare_screen()
current_screen = renpy.display.screen.current_screen()
if current_screen.screen_name[0] in renpy.config.profile_screens:
debug = True
context = SLContext()
context.scope = scope
context.globals = renpy.python.store_dicts["store"]
context.debug = debug
context.predicting = renpy.display.predict.predicting
context.updating = (current_screen.phase == renpy.display.screen.UPDATE)
name = scope["_name"]
def get_cache(d):
rv = d.get(name, None)
if (not isinstance(rv, dict)) or (rv.get("version", None) != self.version):
rv = { "version" : self.version }
d[name] = rv
return rv
context.old_cache = get_cache(current_screen.cache)
context.miss_cache = get_cache(current_screen.miss_cache)
context.new_cache = { "version" : self.version }
context.old_use_cache = current_screen.use_cache
context.new_use_cache = { }
self.const_ast.execute(context)
for i in context.children:
renpy.ui.implicit_add(i)
current_screen.cache[name] = context.new_cache
current_screen.use_cache = context.new_use_cache
class ScreenCache(object):
def __init__(self):
self.version = 1
self.const_analyzed = { }
self.not_const_analyzed = { }
self.updated = False
scache = ScreenCache()
CACHE_FILENAME = "cache/screens.rpyb"
def load_cache():
if renpy.game.args.compile: # @UndefinedVariable
return
try:
f = renpy.loader.load(CACHE_FILENAME)
digest = f.read(hashlib.md5().digest_size)
if digest != renpy.game.script.digest.digest():
return
s = loads(zlib.decompress(f.read()))
f.close()
if s.version == scache.version:
renpy.game.script.update_bytecode()
scache.const_analyzed.update(s.const_analyzed)
scache.not_const_analyzed.update(s.not_const_analyzed)
except:
pass
def save_cache():
if not scache.updated:
return
if renpy.macapp:
return
try:
data = zlib.compress(dumps(scache, 2), 9)
with open(renpy.loader.get_path(CACHE_FILENAME), "wb") as f:
f.write(renpy.game.script.digest.digest())
f.write(data)
except:
pass