Handle very large `.emb` files. (#215)
This change switches parse tree handling to use iteration (with an
explicit stack) instead of recursion, which:
* Allows large (>~1000 entity) `.emb` files to be formatted.
* Allows very large (>~16k entity) `.emb` files to be compiled.
The difference in sizes in the previous code was due to `module_ir.py`
hackily increasing the recursion limit: while this more or less worked,
it was a little dangerous (it ran the risk of blowing out the C stack,
depending on platform) and only increased the limit. This change
removes the limit entirely (at least, up to the available memory on the
system).
diff --git a/compiler/front_end/BUILD b/compiler/front_end/BUILD
index efcec3e..5128a77 100644
--- a/compiler/front_end/BUILD
+++ b/compiler/front_end/BUILD
@@ -73,6 +73,7 @@
"//compiler/util:ir_data",
"//compiler/util:name_conversion",
"//compiler/util:parser_types",
+ "//compiler/util:parser_util",
],
)
@@ -436,6 +437,7 @@
":module_ir",
":tokenizer",
"//compiler/util:parser_types",
+ "//compiler/util:parser_util",
],
)
diff --git a/compiler/front_end/format_emb.py b/compiler/front_end/format_emb.py
index fc7bf94..f0b0137 100644
--- a/compiler/front_end/format_emb.py
+++ b/compiler/front_end/format_emb.py
@@ -26,6 +26,7 @@
from compiler.front_end import module_ir
from compiler.front_end import tokenizer
from compiler.util import parser_types
+from compiler.util import parser_util
class Config(collections.namedtuple("Config", ["indent_width", "show_line_types"])):
@@ -67,18 +68,17 @@
Returns:
A string of the reformatted source text.
"""
- if hasattr(parse_tree, "children"):
- parsed_children = [
- format_emboss_parse_tree(child, config, used_productions)
- for child in parse_tree.children
- ]
- args = parsed_children + [config]
- if used_productions is not None:
- used_productions.add(parse_tree.production)
- return _formatters[parse_tree.production](*args)
- else:
- assert isinstance(parse_tree, parser_types.Token), str(parse_tree)
- return parse_tree.text
+ formatters = {}
+ for production, handler in _formatters.items():
+ # An extra layer of indirection is required here so that the resulting
+ # lambda does not capture the local variable `handler`.
+ def wrapped_handler(handler):
+ return lambda _, *args: handler(*(args + (config,)))
+
+ formatters[production] = wrapped_handler(handler)
+ return parser_util.transform_parse_tree(
+ parse_tree, lambda n: n.text, formatters, used_productions
+ )
def sanity_check_format_result(formatted_text, original_text):
diff --git a/compiler/front_end/format_emb_test.py b/compiler/front_end/format_emb_test.py
index 3d5331e..42d8a26 100644
--- a/compiler/front_end/format_emb_test.py
+++ b/compiler/front_end/format_emb_test.py
@@ -92,7 +92,21 @@
class FormatEmbTest(unittest.TestCase):
- pass
+
+ def test_very_long_emb(self):
+ """Checks that very long inputs do not hit the Python recursion limit."""
+ emb = ["enum Test:\n"]
+ # Enough entities to blow through the default recursion limit and the
+ # bumped limit that was previously in place.
+ for i in range(max(sys.getrecursionlimit(), 16 * 1024) * 2):
+ emb.append(f" VALUE_{i} = {i}\n")
+ parsed_unformatted = parser.parse_module(
+ tokenizer.tokenize("".join(emb), "long.emb")[0]
+ )
+ formatted_text = format_emb.format_emboss_parse_tree(
+ parsed_unformatted.parse_tree,
+ format_emb.Config(indent_width=2),
+ )
def _make_golden_file_tests():
diff --git a/compiler/front_end/module_ir.py b/compiler/front_end/module_ir.py
index 145bba3..633c1b8 100644
--- a/compiler/front_end/module_ir.py
+++ b/compiler/front_end/module_ir.py
@@ -29,6 +29,7 @@
from compiler.util import ir_data_utils
from compiler.util import name_conversion
from compiler.util import parser_types
+from compiler.util import parser_util
# Intermediate types; should not be found in the final IR.
@@ -82,88 +83,78 @@
def build_ir(parse_tree, used_productions=None):
r"""Builds a module-level intermediate representation from a valid parse tree.
- The parse tree is precisely dictated by the exact productions in the grammar
- used by the parser, with no semantic information. _really_build_ir transforms
- this "raw" form into a stable, cooked representation, thereby isolating
- subsequent steps from the exact details of the grammar.
-
- (Probably incomplete) list of transformations:
-
- * ParseResult and Token nodes are replaced with Module, Attribute, Struct,
- Type, etc. objects.
-
- * Purely syntactic tokens ('"["', '"struct"', etc.) are discarded.
-
- * Repeated elements are transformed from tree form to list form:
-
- a*
- / \
- b a*
+ The parse tree is precisely dictated by the exact productions in the grammar
+ used by the parser, with no semantic information. _really_build_ir
+ transforms this "raw" form into a stable, cooked representation, thereby
+ isolating subsequent steps from the exact details of the grammar.
+
+ (Probably incomplete) list of transformations:
+
+ * ParseResult and Token nodes are replaced with Module, Attribute, Struct,
+ Type, etc. objects.
+
+ * Purely syntactic tokens ('"["', '"struct"', etc.) are discarded.
+
+ * Repeated elements are transformed from tree form to list form:
+
+ a*
/ \
- c a*
+ b a*
/ \
- d a*
+ c a*
+ / \
+ d a*
+
+ (where b, c, and d are nodes of type "a") becomes [b, c, d].
+
+ * The values of numeric constants (Number, etc. tokens) are parsed.
+
+ * Different classes of names (snake_names, CamelNames, ShoutyNames) are
+ folded into a single "Name" type, since they are guaranteed to appear in
+ the correct places in the parse tree.
+
+
+ Arguments:
+ parse_tree: A parse tree. Each leaf node should be a parser_types.Token
+ object, and each non-leaf node should have a 'symbol' attribute
+ specifying which grammar symbol it represents, and a 'children'
+ attribute containing a list of child nodes. This is the format
+ returned by the parsers produced by the lr1 module, when run
+ against tokens from the tokenizer module.
+ used_productions: If specified, used_productions.add() will be called
+ with each production actually used in parsing. This can be useful
+ when developing the grammar and writing tests; in particular, it
+ can be used to figure out which productions are *not* used when
+ parsing a particular file.
+
+ Returns:
+ A module-level intermediate representation (module IR) for an Emboss
+ module (source file). This IR will not have symbols resolved,
+ constraints checked, fields synthesized, etc.; it will only be a
+ representation of the syntactic elements of the source.
+ """
+ handlers = {}
+ for production, handler in _handlers.items():
+ # An extra layer of indirection is required here so that the resulting
+ # lambda does not capture the local variable `handler`.
+ def wrapped_handler(handler):
+ def wrapped_handler(node, *args):
+ module_node = handler(*args)
+ if node.source_location:
+ if isinstance(module_node, tuple):
+ module_node = module_node._replace(
+ source_location=node.source_location
+ )
+ else:
+ module_node.source_location = node.source_location
+ return module_node
- (where b, c, and d are nodes of type "a") becomes [b, c, d].
+ return wrapped_handler
- * The values of numeric constants (Number, etc. tokens) are parsed.
-
- * Different classes of names (snake_names, CamelNames, ShoutyNames) are
- folded into a single "Name" type, since they are guaranteed to appear in
- the correct places in the parse tree.
-
-
- Arguments:
- parse_tree: A parse tree. Each leaf node should be a parser_types.Token
- object, and each non-leaf node should have a 'symbol' attribute specifying
- which grammar symbol it represents, and a 'children' attribute containing
- a list of child nodes. This is the format returned by the parsers
- produced by the lr1 module, when run against tokens from the tokenizer
- module.
- used_productions: If specified, used_productions.add() will be called with
- each production actually used in parsing. This can be useful when
- developing the grammar and writing tests; in particular, it can be used to
- figure out which productions are *not* used when parsing a particular
- file.
-
- Returns:
- A module-level intermediate representation (module IR) for an Emboss module
- (source file). This IR will not have symbols resolved; that must be done on
- a forest of module IRs so that names from other modules can be resolved.
- """
-
- # TODO(b/140259131): Refactor _really_build_ir to be less recursive/use an
- # explicit stack.
- old_recursion_limit = sys.getrecursionlimit()
- sys.setrecursionlimit(16 * 1024) # ~8000 top-level entities in one module.
- try:
- result = _really_build_ir(parse_tree, used_productions)
- finally:
- sys.setrecursionlimit(old_recursion_limit)
- return result
-
-
-def _really_build_ir(parse_tree, used_productions):
- """Real implementation of build_ir()."""
- if used_productions is None:
- used_productions = set()
- if hasattr(parse_tree, "children"):
- parsed_children = [
- _really_build_ir(child, used_productions) for child in parse_tree.children
- ]
- used_productions.add(parse_tree.production)
- result = _handlers[parse_tree.production](*parsed_children)
- if parse_tree.source_location:
- if isinstance(result, tuple):
- result = result._replace(source_location=parse_tree.source_location)
- else:
- result.source_location = parse_tree.source_location
- return result
- else:
- # For leaf nodes, the temporary "IR" is just the token. Higher-level rules
- # will translate it to a real IR.
- assert isinstance(parse_tree, parser_types.Token), str(parse_tree)
- return parse_tree
+ handlers[production] = wrapped_handler(handler)
+ return parser_util.transform_parse_tree(
+ parse_tree, lambda n: n, handlers, used_productions
+ )
# Map of productions to their handlers.
diff --git a/compiler/front_end/module_ir_test.py b/compiler/front_end/module_ir_test.py
index 1931bab..fa8db9c 100644
--- a/compiler/front_end/module_ir_test.py
+++ b/compiler/front_end/module_ir_test.py
@@ -18,6 +18,7 @@
import collections
import pkgutil
+import sys
import unittest
from compiler.front_end import module_ir
@@ -4057,6 +4058,18 @@
parse_result = parser.parse_module(tokenizer.tokenize(example, "")[0])
self.assertFalse(parse_result.error)
+ def test_long_input(self):
+ """Checks that very long inputs do not hit the Python recursion limit."""
+ emb = ["enum Test:\n"]
+ # Enough entities to blow through the default recursion limit and the
+ # bumped limit that was previously in place.
+ for i in range(max(sys.getrecursionlimit(), 16 * 1024) * 2):
+ emb.append(f" VALUE_{i} = {i}\n")
+ parse_result = parser.parse_module(
+ tokenizer.tokenize("".join(emb), "long.emb")[0]
+ )
+ module_ir.build_ir(parse_result.parse_tree)
+
def _make_superset_tests():
diff --git a/compiler/util/BUILD b/compiler/util/BUILD
index b0a29a3..4a444fd 100644
--- a/compiler/util/BUILD
+++ b/compiler/util/BUILD
@@ -155,6 +155,14 @@
)
py_library(
+ name = "parser_util",
+ srcs = ["parser_util.py"],
+ deps = [
+ ":parser_types",
+ ],
+)
+
+py_library(
name = "error",
srcs = [
"error.py",
diff --git a/compiler/util/parser_util.py b/compiler/util/parser_util.py
new file mode 100644
index 0000000..c95f70a
--- /dev/null
+++ b/compiler/util/parser_util.py
@@ -0,0 +1,138 @@
+# Copyright 2024 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Utilities for working with parse trees."""
+
+from compiler.util import parser_types
+
+
+def transform_parse_tree(
+ parse_tree, token_handler, production_handlers, used_productions=None
+):
+ """Walks the provided parse_tree, calling handlers for each node.
+
+ This function uses provided handlers to transform a parse tree into a new
+ structure. From the bottom up, calls a handler on each node, passing the
+ node itself and the results of calling handlers on each child of the node
+ (if any).
+
+ This function avoids recursion, so it is suitable for very deep parse
+ trees.
+
+ Arguments:
+ parse_tree: the tree to process
+ token_handler: the handler to use for Token nodes.
+ production_handlers: a map from productions to handlers for those
+ productions.
+ used_productions: an optional set; all encountered productions will be
+ added to used_productions.
+
+ Returns:
+ The result of the production_handler for the top-level parse_tree node.
+ """
+ # The stack of entries to process. Each entry is in one of two states,
+ # depending on the 3rd element (children_completed):
+ #
+ # If False, the node's children have not been added, and the action is to
+ # push the same node with children_completed=True, then push each an entry
+ # for each of node's children with their children_completed=False.
+ #
+ # If True, the node's children have been added and (by the time the entry
+ # is back on top of the stack) all of them have been processed. In this
+ # case, the action is to call the appropriate handler (token_handler() or
+ # production_handlers[node.production]()) with the node and its transformed
+ # children, then store the result in the node's parent's
+ # transformed_children list.
+ #
+ # As an example, if we have:
+ #
+ # A
+ # / \
+ # B C
+ # / \
+ # D E
+ #
+ # Then the steps are:
+ #
+ # Initialize:
+ # 1. Push (A, children_completed=False)
+ #
+ # Start handling A:
+ # 2. Pop (A, children_completed=False)
+ # 3. Push (A, children_completed=True)
+ # 4. Push (B, children_completed=False)
+ # 5. Push (C, children_completed=False)
+ #
+ # Start handling C:
+ # 6. Pop (C, children_completed=False)
+ # 7. Push (C, children_completed=True)
+ # 8. Push (D, children_completed=False)
+ # 9. Push (E, children_completed=False)
+ #
+ # Start handling E:
+ # 10. Pop (E, children_completed=False)
+ # 11. Push (E, children_completed=True)
+ #
+ # Finish handling E:
+ # 12. Pop (E, children_completed=True)
+ # 13. Insert token_handler(E) into C.transformed_children
+ #
+ # Start handling D:
+ # 14. Pop (D, children_completed=False)
+ # 15. Push (D, children_completed=True)
+ #
+ # Finish handling D:
+ # 16. Pop (D, children_completed=True)
+ # 17. Insert token_handler(D) into C.transformed_children
+ #
+ # Finish handling C:
+ # 18. Pop (C, children_completed=True)
+ # 19. Insert production_handlers[C.production](C, *C.transformed_children)
+ # into A.transformed_children
+ #
+ # Start handling B:
+ # 20. Pop (B, children_completed=False)
+ # 21. Push (B, children_completed=True)
+ #
+ # Finish handling B:
+ # 22. Pop (B, children_completed=True)
+ # 23. Insert token_handler(B) into A.transformed_children
+ #
+ # Finish handling A:
+ # 24. Pop (A, children_completed=True)
+ # 25. Return production_handlers[A.production](A, *A.transformed_children)
+ #
+ # It takes quite a few steps to handle even a small tree!
+ stack = [(parse_tree, None, False, None)]
+ while True:
+ node, parent, children_completed, transformed_children = stack.pop()
+ if not children_completed:
+ parent_entry = []
+ stack.append((node, parent, True, parent_entry))
+ if hasattr(node, "children"):
+ for child in node.children:
+ stack.append((child, parent_entry, False, None))
+ if used_productions is not None:
+ used_productions.add(node.production)
+ else:
+ if isinstance(node, parser_types.Token):
+ transformed_node = token_handler(node)
+ else:
+ transformed_node = production_handlers[node.production](
+ *([node] + transformed_children)
+ )
+ if parent is None:
+ return transformed_node
+ else:
+ parent.insert(0, transformed_node)