summaryrefslogtreecommitdiffstats
path: root/ipc/ipdl/ipdl/cxx/code.py
blob: 0b5019b6238f9ac9b352902a818b096dbdef3519 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
# 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<escaped>\$)                  | # '$$' is an escaped '$'
        (?P<list>[*,])?{(?P<expr>[^}]+)} | # ${expr}, $*{expr}, or $,{expr}
        (?P<invalid>)                      # 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)