| # Copyright 2019 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. |
| |
| """Main driver for the Emboss front-end. |
| |
| The parse_emboss_file function performs a complete parse of the specified file, |
| and returns an IR or formatted error message. |
| """ |
| |
| import collections |
| |
| from compiler.front_end import attribute_checker |
| from compiler.front_end import constraints |
| from compiler.front_end import dependency_checker |
| from compiler.front_end import expression_bounds |
| from compiler.front_end import lr1 |
| from compiler.front_end import module_ir |
| from compiler.front_end import parser |
| from compiler.front_end import symbol_resolver |
| from compiler.front_end import synthetics |
| from compiler.front_end import tokenizer |
| from compiler.front_end import type_check |
| from compiler.front_end import write_inference |
| from compiler.util import error |
| from compiler.util import ir_data |
| from compiler.util import ir_data_utils |
| from compiler.util import parser_types |
| from compiler.util import resources |
| |
| _IrDebugInfo = collections.namedtuple("IrDebugInfo", ["ir", "debug_info", |
| "errors"]) |
| |
| |
| class DebugInfo(object): |
| """Debug information about Emboss parsing.""" |
| __slots__ = ("modules") |
| |
| def __init__(self): |
| self.modules = {} |
| |
| def __eq__(self, other): |
| return self.modules == other.modules |
| |
| def __ne__(self, other): |
| return not self == other |
| |
| |
| class ModuleDebugInfo(object): |
| """Debug information about the parse of a single file. |
| |
| Attributes: |
| file_name: The name of the file from which this module came. |
| tokens: The tokenization of this module's source text. |
| parse_tree: The raw parse tree for this module. |
| ir: The intermediate representation of this module, before additional |
| processing such as symbol resolution. |
| used_productions: The set of grammar productions used when parsing this |
| module. |
| source_code: The source text of the module. |
| """ |
| __slots__ = ("file_name", "tokens", "parse_tree", "ir", "used_productions", |
| "source_code") |
| |
| def __init__(self, file_name): |
| self.file_name = file_name |
| self.tokens = None |
| self.parse_tree = None |
| self.ir = None |
| self.used_productions = None |
| self.source_code = None |
| |
| def __eq__(self, other): |
| return (self.file_name == other.file_name and self.tokens == other.tokens |
| and self.parse_tree == other.parse_tree and self.ir == other.ir and |
| self.used_productions == other.used_productions and |
| self.source_code == other.source_code) |
| |
| def __ne__(self, other): |
| return not self == other |
| |
| def format_tokenization(self): |
| """Renders self.tokens in a human-readable format.""" |
| return "\n".join([str(token) for token in self.tokens]) |
| |
| def format_parse_tree(self, parse_tree=None, indent=""): |
| """Renders self.parse_tree in a human-readable format.""" |
| if parse_tree is None: |
| parse_tree = self.parse_tree |
| result = [] |
| if isinstance(parse_tree, lr1.Reduction): |
| result.append(indent + parse_tree.symbol) |
| if parse_tree.children: |
| result.append(":\n") |
| for child in parse_tree.children: |
| result.append(self.format_parse_tree(child, indent + " ")) |
| else: |
| result.append("\n") |
| else: |
| result.append("{}{}\n".format(indent, parse_tree)) |
| return "".join(result) |
| |
| def format_module_ir(self): |
| """Renders self.ir in a human-readable format.""" |
| return ir_data_utils.IrDataSerializer(self.ir).to_json(indent=2) |
| |
| |
| def format_production_set(productions): |
| """Renders a set of productions in a human-readable format.""" |
| return "\n".join([str(production) for production in sorted(productions)]) |
| |
| |
| _cached_modules = {} |
| |
| |
| def parse_module_text(source_code, file_name): |
| """Parses the text of a module, returning a module-level IR. |
| |
| Arguments: |
| source_code: The text of the module to parse. |
| file_name: The name of the module's source file (will be included in the |
| resulting IR). |
| |
| Returns: |
| A module-level intermediate representation (IR), prior to import and symbol |
| resolution, and a corresponding ModuleDebugInfo, for debugging the parser. |
| |
| Raises: |
| FrontEndFailure: An error occurred while parsing the module. str(error) |
| will give a human-readable error message. |
| """ |
| # This is strictly an optimization to speed up tests, mostly by avoiding the |
| # need to re-parse the prelude for every test .emb. |
| if (source_code, file_name) in _cached_modules: |
| debug_info = _cached_modules[source_code, file_name] |
| ir = ir_data.Module() |
| ir.CopyFrom(debug_info.ir) |
| else: |
| debug_info = ModuleDebugInfo(file_name) |
| debug_info.source_code = source_code |
| tokens, errors = tokenizer.tokenize(source_code, file_name) |
| if errors: |
| return _IrDebugInfo(None, debug_info, errors) |
| debug_info.tokens = tokens |
| parse_result = parser.parse_module(tokens) |
| if parse_result.error: |
| return _IrDebugInfo( |
| None, |
| debug_info, |
| [error.make_error_from_parse_error(file_name, parse_result.error)]) |
| debug_info.parse_tree = parse_result.parse_tree |
| used_productions = set() |
| ir = module_ir.build_ir(parse_result.parse_tree, used_productions) |
| ir.source_text = source_code |
| debug_info.used_productions = used_productions |
| debug_info.ir = ir_data.Module() |
| debug_info.ir.CopyFrom(ir) |
| _cached_modules[source_code, file_name] = debug_info |
| ir.source_file_name = file_name |
| return _IrDebugInfo(ir, debug_info, []) |
| |
| |
| def parse_module(file_name, file_reader): |
| """Parses a module, returning a module-level IR. |
| |
| Arguments: |
| file_name: The name of the module's source file. |
| file_reader: A callable that returns either: |
| (file_contents, None) or |
| (None, list_of_error_detail_strings) |
| |
| Returns: |
| (ir, debug_info, errors), where ir is a module-level intermediate |
| representation (IR), debug_info is a ModuleDebugInfo containing the |
| tokenization, parse tree, and original source text of all modules, and |
| errors is a list of tokenization or parse errors. If errors is not an empty |
| list, ir will be None. |
| |
| Raises: |
| FrontEndFailure: An error occurred while reading or parsing the module. |
| str(error) will give a human-readable error message. |
| """ |
| source_code, errors = file_reader(file_name) |
| if errors: |
| location = parser_types.make_location((1, 1), (1, 1)) |
| return None, None, [ |
| [error.error(file_name, location, "Unable to read file.")] + |
| [error.note(file_name, location, e) for e in errors] |
| ] |
| return parse_module_text(source_code, file_name) |
| |
| |
| def get_prelude(): |
| """Returns the module IR and debug info of the Emboss Prelude.""" |
| return parse_module_text( |
| resources.load("compiler.front_end", "prelude.emb"), "") |
| |
| |
| def parse_emboss_file(file_name, file_reader, stop_before_step=None): |
| """Fully parses an .emb, and returns an IR suitable for passing to a back end. |
| |
| parse_emboss_file is a convenience function which calls only_parse_emboss_file |
| and process_ir. |
| |
| Arguments: |
| file_name: The name of the module's source file. |
| file_reader: A callable that returns the contents of files, or raises |
| IOError. |
| stop_before_step: If set, parse_emboss_file will stop normalizing the IR |
| just before the specified step. This parameter should be None for |
| non-test code. |
| |
| Returns: |
| (ir, debug_info, errors), where ir is a complete IR, ready for consumption |
| by an Emboss back end, debug_info is a DebugInfo containing the |
| tokenization, parse tree, and original source text of all modules, and |
| errors is a list of tokenization or parse errors. If errors is not an empty |
| list, ir will be None. |
| """ |
| ir, debug_info, errors = only_parse_emboss_file(file_name, file_reader) |
| if errors: |
| return _IrDebugInfo(None, debug_info, errors) |
| ir, errors = process_ir(ir, stop_before_step) |
| if errors: |
| return _IrDebugInfo(None, debug_info, errors) |
| return _IrDebugInfo(ir, debug_info, errors) |
| |
| |
| def only_parse_emboss_file(file_name, file_reader): |
| """Parses an .emb, and returns an IR suitable for process_ir. |
| |
| only_parse_emboss_file parses the given file and all of its transitive |
| imports, and returns a first-stage intermediate representation, which can be |
| passed to process_ir. |
| |
| Arguments: |
| file_name: The name of the module's source file. |
| file_reader: A callable that returns the contents of files, or raises |
| IOError. |
| |
| Returns: |
| (ir, debug_info, errors), where ir is an intermediate representation (IR), |
| debug_info is a DebugInfo containing the tokenization, parse tree, and |
| original source text of all modules, and errors is a list of tokenization or |
| parse errors. If errors is not an empty list, ir will be None. |
| """ |
| file_queue = [file_name] |
| files = {file_name} |
| debug_info = DebugInfo() |
| ir = ir_data.EmbossIr(module=[]) |
| while file_queue: |
| file_to_parse = file_queue[0] |
| del file_queue[0] |
| if file_to_parse: |
| module, module_debug_info, errors = parse_module(file_to_parse, |
| file_reader) |
| else: |
| module, module_debug_info, errors = get_prelude() |
| if module_debug_info: |
| debug_info.modules[file_to_parse] = module_debug_info |
| if errors: |
| return _IrDebugInfo(None, debug_info, errors) |
| ir.module.extend([module]) # Proto supports extend but not append here. |
| for import_ in module.foreign_import: |
| if import_.file_name.text not in files: |
| file_queue.append(import_.file_name.text) |
| files.add(import_.file_name.text) |
| return _IrDebugInfo(ir, debug_info, []) |
| |
| |
| def process_ir(ir, stop_before_step): |
| """Turns a first-stage IR into a fully-processed IR. |
| |
| process_ir performs all of the semantic processing steps on `ir`: resolving |
| symbols, checking dependencies, adding type annotations, normalizing |
| attributes, etc. process_ir is generally meant to be called with the result |
| of parse_emboss_file(), but in theory could be called with a first-stage |
| intermediate representation (IR) from another source. |
| |
| Arguments: |
| ir: The IR to process. This structure will be modified during processing. |
| stop_before_step: If set, process_ir will stop normalizing the IR just |
| before the specified step. This parameter should be None for non-test |
| code. |
| |
| Returns: |
| (ir, errors), where ir is a complete IR, ready for consumption by an Emboss |
| back end, and errors is a list of compilation errors. If errors is not an |
| empty list, ir will be None. |
| """ |
| passes = (synthetics.desugar, |
| symbol_resolver.resolve_symbols, |
| dependency_checker.find_dependency_cycles, |
| dependency_checker.set_dependency_order, |
| symbol_resolver.resolve_field_references, |
| type_check.annotate_types, |
| type_check.check_types, |
| expression_bounds.compute_constants, |
| attribute_checker.normalize_and_verify, |
| constraints.check_constraints, |
| write_inference.set_write_methods) |
| assert stop_before_step in [None] + [f.__name__ for f in passes], ( |
| "Bad value for stop_before_step.") |
| # Some parts of the IR are synthesized from "natural" parts of the IR, before |
| # the natural parts have been fully error checked. Because of this, the |
| # synthesized parts can have errors; in a couple of cases, they can have |
| # errors that show up in an earlier pass than the errors in the natural parts |
| # of the IR. As an example: |
| # |
| # struct Foo: |
| # 0 [+1] bits: |
| # 0 [+1] Flag flag |
| # 1 [+flag] UInt:8 field |
| # |
| # In this case, the use of `flag` as the size of `field` is incorrect, because |
| # `flag` is a boolean, but the size of a field must be an integer. |
| # |
| # Type checking occurs in two passes: in the first pass, expressions are |
| # checked for internal consistency. In the second pass, expression types are |
| # checked against their location. The use of `flag` would be caught in the |
| # second pass. |
| # |
| # However, the generated_fields pass will synthesize a $size_in_bytes virtual |
| # field that would look like: |
| # |
| # struct Foo: |
| # 0 [+1] bits: |
| # 0 [+1] Flag flag |
| # 1 [+flag] UInt:8 field |
| # let $size_in_bytes = $max(true ? 0 + 1 : 0, true ? 1 + flag : 0) |
| # |
| # Since `1 + flag` is not internally consistent, this type error would be |
| # caught in the first pass, and the user would see a very strange error |
| # message that "the right-hand argument of operator `+` must be an integer." |
| # |
| # In order to avoid showing these kinds of errors to the user, we defer any |
| # errors in synthetic parts of the IR. Unless there is a compiler bug, those |
| # errors will show up as errors in the natural parts of the IR, which should |
| # be much more comprehensible to end users. |
| # |
| # If, for some reason, there is an error in the synthetic IR, but no error in |
| # the natural IR, the synthetic errors will be shown. In this case, the |
| # formatting for the synthetic errors will show '[compiler bug]' for the |
| # error location, which (hopefully) will provide the end user with a cue that |
| # the error is a compiler bug. |
| deferred_errors = [] |
| for function in passes: |
| if stop_before_step == function.__name__: |
| return (ir, []) |
| errors, hidden_errors = error.split_errors(function(ir)) |
| if errors: |
| return (None, errors) |
| deferred_errors.extend(hidden_errors) |
| |
| if deferred_errors: |
| return (None, deferred_errors) |
| |
| assert stop_before_step is None, "Bad value for stop_before_step." |
| return (ir, []) |