# 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. ######################################################################### # 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 = '' # 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