# Copyright 2004-2019 Tom Rothamel # # 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. from __future__ import print_function from __future__ import unicode_literals from __future__ import division from __future__ import absolute_import import renpy # @UnusedImport from renpy.python import py_compile # Import the Python AST module, instead of the Ren'Py ast module. import ast import zlib from cPickle import loads, dumps # The set of names that should be treated as constants. always_constants = { 'True', 'False', 'None' } # The set of names that should be treated as pure functions. pure_functions = { # Python builtins. "abs", "all", "any", "apply", "bin", "bool", "bytes", "callable", "chr", "cmp", "dict", "divmod", "filter", "float", "frozenset", "getattr", "globals", "hasattr", "hash", "hex", "int", "isinstance", "len", "list", "long", "map", "max", "min", "oct", "ord", "pow", "range", "reduce", "repr", "round", "set", "sorted", "str", "sum", "tuple", "unichr", "unicode", "vars", "zip", # enumerator and reversed return iterators at the moment. # minstore.py "_", "_p", "absolute", "__renpy__list__", "__renpy__dict__", "__renpy__set__", # defaultstore.py "ImageReference", "Image", "Frame", "Solid", "LiveComposite", "LiveCrop", "LiveTile", "Flatten", "Null", "Window", "Viewport", "DynamicDisplayable", "ConditionSwitch", "ShowingSwitch", "Transform", "Animation", "Movie", "Particles", "SnowBlossom", "Text", "ParameterizedText", "FontGroup", "Drag", "Alpha", "AlphaMask", "Position", "Pan", "Move", "Motion", "Revolve", "Zoom", "RotoZoom", "FactorZoom", "SizeZoom", "Fade", "Dissolve", "ImageDissolve", "AlphaDissolve", "CropMove", "PushMove", "Pixellate", "OldMoveTransition", "MoveTransition", "MoveFactory", "MoveIn", "MoveOut", "ZoomInOut", "RevolveInOut", "MultipleTransition", "ComposeTransition", "Pause", "SubTransition", "ADVSpeaker", "ADVCharacter", "Speaker", "Character", "DynamicCharacter", "Fixed", "HBox", "VBox", "Grid", "AlphaBlend", "At", "color", "Color", # ui.py "ui.returns", "ui.jumps", "ui.jumpsoutofcontext", "ui.callsinnewcontext", "ui.invokesinnewcontext", "ui.gamemenus", # renpy.py "renpy.version_string", "renpy.version_only", "renpy.version_tuple", "renpy.version_name", "renpy.license", } constants = { "config", "style" } | always_constants | pure_functions # A set of names that should not be treated as global constants. not_constants = set() # The base set for the local constants. local_constants = set() def const(name): """ :doc: const Declares a variable in the store to be constant. A variable is constant if nothing can change its value, or any value reached by indexing it or accessing its attributes. Variables must remain constant out of define, init, and translate python blocks. `name` A string giving the name of the variable to declare constant. """ if name not in not_constants: constants.add(name) def not_const(name): """ :doc: const Declares a name in the store to be not constant. This undoes the effect of calls to :func:`renpy.const` and :func:`renpy.pure`. `name` The name to declare not constant. """ constants.discard(name) pure_functions.discard(name) not_constants.add(name) def pure(fn): """ :doc: const Declares a function as pure. A pure function must always return the same value when it is called with the same arguments, outside of define, init, and translate python blocks. `fn` The name of the function to declare pure. This may either be a string containing the name of the function, or the function itself. Returns `fn`, allowing this function to be used as a decorator. """ name = fn if not isinstance(name, basestring): name = fn.__name__ if name not in not_constants: pure_functions.add(name) constants.add(name) return fn class Control(object): """ Represents control flow. `const` True if this statement always executes. `loop` True if this corresponds to a loop. `imagemap` True if this control is in a non-constant imagemap. """ def __init__(self, const, loop, imagemap): self.const = const self.loop = loop self.imagemap = imagemap # Three levels of constness. GLOBAL_CONST = 2 # Expressions that are const everywhere. LOCAL_CONST = 1 # Expressions that are const with regard to a screen + parameters. NOT_CONST = 0 # Expressions that are not const. class DeltaSet(object): def __init__(self, base, copy=None): """ Represents a set that stores its contents as differences from a base set. """ self.base = base if copy is not None: self.added = set(copy.added) self.removed = set(copy.removed) else: self.added = set() self.removed = set() self.changed = False def add(self, v): if v in self.removed: self.removed.discard(v) self.changed = True elif v not in self.base and v not in self.added: self.added.add(v) self.changed = True def discard(self, v): if v in self.added: self.added.discard(v) self.changed = True elif v in self.base and v not in self.removed: self.removed.add(v) self.changed = True def __contains__(self, v): return (v in self.added) or ((v in self.base) and (v not in self.removed)) def copy(self): return DeltaSet(self.base, self) def __iter__(self): for i in self.base: if i not in self.removed: yield i for i in self.added: yield i class Analysis(object): """ Represents the result of code analysis, and provides tools to perform code analysis. """ def __init__(self, parent=None): # The parent context transcludes run in, or None if there is no parent # context. self.parent = parent # Analyses of children, such a screens we use. self.children = { } # The variables we consider to be not-constant. self.not_constant = DeltaSet(not_constants) # Variables we consider to be locally constant. self.local_constant = DeltaSet(local_constants) # Variables we consider to be globally constant. self.global_constant = DeltaSet(always_constants) # The functions we consider to be pure. self.pure_functions = DeltaSet(pure_functions) # Represents what we know about the current control. self.control = Control(True, False, False) # The stack of const_flow values. self.control_stack = [ self.control ] def get_child(self, identifier): if identifier in self.children: return self.children[identifier] rv = Analysis(self) self.children[identifier] = rv return rv def push_control(self, const=True, loop=False, imagemap=False): self.control = Control(self.control.const and const, loop, self.imagemap or imagemap) self.control_stack.append(self.control) def pop_control(self): rv = self.control_stack.pop() self.control = self.control_stack[-1] return rv def imagemap(self): """ Returns NOT_CONST if we're in a non-constant imagemap. """ if self.control.imagemap: return NOT_CONST else: return GLOBAL_CONST def exit_loop(self): """ Call this to indicate the current loop is being exited by the continue or break statements. """ l = list(self.control_stack) l.reverse() for i in l: i.const = False if i.loop: break def at_fixed_point(self): """ Returns True if we've reached a fixed point, where the analysis has not changed since the last time we called this function. """ for i in self.children.values(): if not i.at_fixed_point(): return False if (self.not_constant.changed or self.global_constant.changed or self.local_constant.changed or self.pure_functions.changed): self.not_constant.changed = False self.global_constant.changed = False self.local_constant.changed = False self.pure_functions.changed = False return False return True def mark_constant(self, name): """ Marks `name` as a potential local constant. """ if not name in self.not_constant: self.local_constant.add(name) self.global_constant.discard(name) self.pure_functions.discard(name) def mark_not_constant(self, name): """ Marks `name` as definitely not-constant. """ self.not_constant.add(name) self.pure_functions.discard(name) self.local_constant.discard(name) self.global_constant.discard(name) def is_constant(self, node): """ Returns true if `node` is constant for the purpose of screen language. Node should be a python AST node. Screen language ignores object identity for the purposes of object equality. """ def check_slice(slice): # @ReservedAssignment if isinstance(slice, ast.Index): return check_node(slice.value) elif isinstance(slice, ast.Slice): consts = [ ] if slice.lower: consts.append(check_node(slice.lower)) if slice.upper: consts.append(check_node(slice.upper)) if slice.step: consts.append(check_node(slice.step)) if not consts: return GLOBAL_CONST else: return min(consts) return NOT_CONST def check_name(node): """ Check nodes that make up a name. This returns a pair: * The first element is True if the node is constant, and False otherwise. * The second element is None if the node is constant or the name is not known, and the name otherwise. """ if isinstance(node, ast.Name): const = NOT_CONST name = node.id elif isinstance(node, ast.Attribute): const, name = check_name(node.value) if name is not None: name = name + "." + node.attr else: return check_node(node), None if name in self.not_constant: return NOT_CONST, name elif name in self.global_constant: return GLOBAL_CONST, name elif name in self.local_constant: return LOCAL_CONST, name else: return const, name def check_nodes(nodes): """ Checks a list of nodes for constness. """ nodes = list(nodes) if not nodes: return GLOBAL_CONST return min(check_node(i) for i in nodes) def check_node(node): """ Returns true if the ast node `node` is constant. """ # This handles children that do not exist. if node is None: return GLOBAL_CONST #PY3: see if there are new node types. if isinstance(node, (ast.Num, ast.Str)): return GLOBAL_CONST elif isinstance(node, (ast.List, ast.Tuple)): return check_nodes(node.elts) elif isinstance(node, (ast.Attribute, ast.Name)): return check_name(node)[0] elif isinstance(node, ast.BoolOp): return check_nodes(node.values) elif isinstance(node, ast.BinOp): return min( check_node(node.left), check_node(node.right), ) elif isinstance(node, ast.UnaryOp): return check_node(node.operand) elif isinstance(node, ast.Call): const, name = check_name(node.func) # The function must have a name, and must be declared pure. if (const != GLOBAL_CONST) or (name not in self.pure_functions): return NOT_CONST consts = [ ] # Arguments and keyword arguments must be pure. consts.append(check_nodes(node.args)) consts.append(check_nodes(i.value for i in node.keywords)) if node.starargs is not None: consts.append(check_node(node.starargs)) if node.kwargs is not None: consts.append(check_node(node.kwargs)) return min(consts) elif isinstance(node, ast.IfExp): return min( check_node(node.test), check_node(node.body), check_node(node.orelse), ) elif isinstance(node, ast.Dict): return min( check_nodes(node.keys), check_nodes(node.values) ) elif isinstance(node, ast.Set): return check_nodes(node.elts) elif isinstance(node, ast.Compare): return min( check_node(node.left), check_nodes(node.comparators), ) elif isinstance(node, ast.Repr): return check_node(node.value) elif isinstance(node, ast.Subscript): return min( check_node(node.value), check_slice(node.slice), ) return NOT_CONST return check_node(node) def is_constant_expr(self, expr): """ Compiles `expr` into an AST node, then returns the result of self.is_constant called on that node. """ node, literal = ccache.ast_eval_literal(expr) if literal: return GLOBAL_CONST else: return self.is_constant(node) def python(self, code): """ Performs analysis on a block of python code. """ nodes = ccache.ast_exec(code) a = PyAnalysis(self) for i in nodes: a.visit(i) def parameters(self, parameters): """ Analyzes the parameters to the screen. """ self.global_constant = DeltaSet(constants) # As we have parameters, analyze with those parameters. for name, _default in parameters.parameters: self.mark_not_constant(name) if parameters.extrapos is not None: self.mark_not_constant(parameters.extrapos) if parameters.extrakw is not None: self.mark_not_constant(parameters.extrakw) class PyAnalysis(ast.NodeVisitor): """ This analyzes Python code to determine which variables should be marked const, and which should be marked non-const. """ def __init__(self, analysis): self.analysis = analysis def visit_Name(self, node): if isinstance(node, ast.AugStore): self.analysis.mark_not_constant(node.id) elif isinstance(node.ctx, ast.Store): if self.analysis.control.const: self.analysis.mark_constant(node.id) else: self.analysis.mark_not_constant(node.id) def visit_Assign(self, node): const = self.analysis.is_constant(node.value) self.analysis.push_control(const, False) self.generic_visit(node) self.analysis.pop_control() def visit_AugAssign(self, node): self.analysis.push_control(False, False) self.generic_visit(node) self.analysis.pop_control() def visit_For(self, node): const = self.analysis.is_constant(node.iter) self.analysis.push_control(const=const, loop=True) old_const = self.analysis.control.const self.generic_visit(node) if self.analysis.control.const != old_const: self.generic_visit(node) self.analysis.pop_control() def visit_While(self, node): const = self.analysis.is_constant(node.test) self.analysis.push_control(const=const, loop=True) old_const = self.analysis.control.const self.generic_visit(node) if self.analysis.control.const != old_const: self.generic_visit(node) self.analysis.pop_control() def visit_If(self, node): const = self.analysis.is_constant(node.test) self.analysis.push_control(const, False) self.generic_visit(node) self.analysis.pop_control() # The continue and break statements should be pretty rare, so if they # occur, we mark everything later in the loop as non-const. def visit_Break(self, node): self.analysis.exit_loop() def visit_Continue(self, node): self.analysis.exit_loop() class CompilerCache(object): """ Objects of this class are used to cache the compiliation of Python code. """ def __init__(self): self.ast_eval_cache = { } self.ast_exec_cache = { } # True if we've changed the caches. self.updated = False # The version of this object. self.version = 1 def ast_eval_literal(self, expr): """ Compiles an expression into an AST. """ if isinstance(expr, renpy.ast.PyExpr): filename = expr.filename linenumber = expr.linenumber else: filename = None linenumber = None key = (expr, filename, linenumber) rv = self.ast_eval_cache.get(key, None) if rv is None: expr = py_compile(expr, 'eval', ast_node=True) try: ast.literal_eval(expr) literal = True except: literal = False rv = (expr, literal) self.ast_eval_cache[key] = rv self.updated = True return rv def ast_eval(self, expr): return self.ast_eval_literal(expr)[0] def ast_exec(self, code): """ Compiles a block into an AST. """ if isinstance(code, renpy.ast.PyExpr): key = (code, code.filename, code.linenumber) else: key = (code, None, None) rv = self.ast_exec_cache.get(key, None) if rv is None: rv = py_compile(code, 'exec', ast_node=True) self.ast_exec_cache[key] = rv self.updated = True return rv ccache = CompilerCache() CACHE_FILENAME = "cache/pyanalysis.rpyb" def load_cache(): if renpy.game.args.compile: # @UndefinedVariable return try: f = renpy.loader.load(CACHE_FILENAME) c = loads(zlib.decompress(f.read())) f.close() if c.version == ccache.version: ccache.ast_eval_cache.update(c.ast_eval_cache) ccache.ast_exec_cache.update(c.ast_exec_cache) except: pass def save_cache(): if not ccache.updated: return if renpy.macapp: return try: data = zlib.compress(dumps(ccache, 2), 9) with open(renpy.loader.get_path(CACHE_FILENAME), "wb") as f: f.write(data) except: pass