# This Source Code Form is subject to the terms of the Mozilla Public # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. # This module contains functionality for adding formatted, opaque "code" blocks # into the IPDL ast. These code objects follow IPDL C++ ast patterns, and # perform lowering in much the same way. # In general it is recommended to use these for blocks of code which would # otherwise be specified by building a hardcoded IPDL-C++ AST, as users of this # API are often easier to read than users of the AST APIs in these cases. import re import math import textwrap from ipdl.cxx.ast import Node, Whitespace, GroupNode, VerbatimNode # ----------------------------------------------------------------------------- # Public API. def StmtCode(tmpl, **kwargs): """Perform template substitution to build opaque C++ AST nodes. See the module documentation for more information on the templating syntax. StmtCode nodes should be used where Stmt* nodes are used. They are placed on their own line and indented.""" return _code(tmpl, False, kwargs) def ExprCode(tmpl, **kwargs): """Perform template substitution to build opaque C++ AST nodes. See the module documentation for more information on the templating syntax. ExprCode nodes should be used where Expr* nodes are used. They are placed inline, and no trailing newline is added.""" return _code(tmpl, True, kwargs) def StmtVerbatim(text): """Build an opaque C++ AST node which emits input text verbatim. StmtVerbatim nodes should be used where Stmt* nodes are used. They are placed on their own line and indented.""" return _verbatim(text, False) def ExprVerbatim(text): """Build an opaque C++ AST node which emits input text verbatim. ExprVerbatim nodes should be used where Expr* nodes are used. They are placed inline, and no trailing newline is added.""" return _verbatim(text, True) # ----------------------------------------------------------------------------- # Implementation def _code(tmpl, inline, context): # Remove common indentation, and strip the preceding newline from # '''-quoting, because we usually don't want it. if tmpl.startswith("\n"): tmpl = tmpl[1:] tmpl = textwrap.dedent(tmpl) # Process each line in turn, building up a list of nodes. nodes = [] for idx, line in enumerate(tmpl.splitlines()): # Place newline tokens between lines in the input. if idx > 0: nodes.append(Whitespace.NL) # Don't indent the first line if `inline` is set. skip_indent = inline and idx == 0 nodes.append(_line(line.rstrip(), skip_indent, idx + 1, context)) # If we're inline, don't add the final trailing newline. if not inline: nodes.append(Whitespace.NL) return GroupNode(nodes) def _verbatim(text, inline): # For simplicitly, _verbatim is implemented using the same logic as _code, # but with '$' characters escaped. This ensures we only need to worry about # a single, albeit complex, codepath. return _code(text.replace("$", "$$"), inline, {}) # Pattern used to identify substitutions. _substPat = re.compile( r""" \$(?: (?P\$) | # '$$' is an escaped '$' (?P[*,])?{(?P[^}]+)} | # ${expr}, $*{expr}, or $,{expr} (?P) # For error reporting ) """, re.IGNORECASE | re.VERBOSE, ) def _line(raw, skip_indent, lineno, context): assert "\n" not in raw # Determine the level of indentation used for this line line = raw.lstrip() offset = int(math.ceil((len(raw) - len(line)) / 4)) # If line starts with a directive, don't indent it. if line.startswith("#"): skip_indent = True column = 0 children = [] for match in _substPat.finditer(line): if match.group("invalid") is not None: raise ValueError("Invalid substitution on line %d" % lineno) # Any text from before the current entry should be written, and column # advanced. if match.start() > column: before = line[column : match.start()] children.append(VerbatimNode(before)) column = match.end() # If we have an escaped group, emit a '$' node. if match.group("escaped") is not None: children.append(VerbatimNode("$")) continue # At this point we should have an expression. list_chr = match.group("list") expr = match.group("expr") assert expr is not None # Evaluate our expression in the context to get the values. try: values = eval(expr, context, {}) except Exception as e: msg = "%s in substitution on line %d" % (repr(e), lineno) raise ValueError(msg) from e # If we aren't dealing with lists, wrap the result into a # single-element list. if list_chr is None: values = [values] # Check if this substitution is inline, or the entire line. inline = match.span() != (0, len(line)) for idx, value in enumerate(values): # If we're using ',' as list mode, put a comma between each node. if idx > 0 and list_chr == ",": children.append(VerbatimNode(", ")) # If our value isn't a node, turn it into one. Verbatim should be # inline unless indent isn't being skipped, and the match isn't # inline. if not isinstance(value, Node): value = _verbatim(str(value), skip_indent or inline) children.append(value) # If we were the entire line, indentation is handled by the added child # nodes. Do this after the above loop such that created verbatims have # the correct inline-ness. if not inline: skip_indent = True # Add any remaining text in the line. if len(line) > column: children.append(VerbatimNode(line[column:])) # If we have no children, just emit the empty string. This will become a # blank line. if len(children) == 0: return VerbatimNode("") # Add the initial indent if we aren't skipping it. if not skip_indent: children.insert(0, VerbatimNode("", indent=True)) # Wrap ourselves into a group node with the correct indent offset return GroupNode(children, offset=offset)