# 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. """Adds auto-generated virtual fields to the IR.""" from compiler.front_end import attributes from compiler.util import error from compiler.util import expression_parser from compiler.util import ir_data from compiler.util import ir_data_utils from compiler.util import ir_util from compiler.util import traverse_ir def _mark_as_synthetic(proto): """Marks all source_locations in proto with is_synthetic=True.""" if not isinstance(proto, ir_data.Message): return if hasattr(proto, "source_location"): ir_data_utils.builder(proto).source_location.is_synthetic = True for spec, value in ir_data_utils.get_set_fields(proto): if spec.name != "source_location" and spec.is_dataclass: if spec.is_sequence: for i in value: _mark_as_synthetic(i) else: _mark_as_synthetic(value) def _skip_text_output_attribute(): """Returns the IR for a [text_output: "Skip"] attribute.""" result = ir_data.Attribute( name=ir_data.Word(text=attributes.TEXT_OUTPUT), value=ir_data.AttributeValue(string_constant=ir_data.String(text="Skip"))) _mark_as_synthetic(result) return result # The existence condition for an alias for an anonymous bits' field is the union # of the existence condition for the anonymous bits and the existence condition # for the field within. The 'x' and 'x.y' are placeholders here; they'll be # overwritten in _add_anonymous_aliases. _ANONYMOUS_BITS_ALIAS_EXISTENCE_SKELETON = expression_parser.parse( "$present(x) && $present(x.y)") def _add_anonymous_aliases(structure, type_definition): """Adds synthetic alias fields for all fields in anonymous fields. This essentially completes the rewrite of this: struct Foo: 0 [+4] bits: 0 [+1] Flag low 31 [+1] Flag high Into this: struct Foo: bits EmbossReservedAnonymous0: [text_output: "Skip"] 0 [+1] Flag low 31 [+1] Flag high 0 [+4] EmbossReservedAnonymous0 emboss_reserved_anonymous_1 let low = emboss_reserved_anonymous_1.low let high = emboss_reserved_anonymous_1.high Note that this pass runs very, very early -- even before symbols have been resolved -- so very little in ir_util will work at this point. Arguments: structure: The ir_data.Structure on which to synthesize fields. type_definition: The ir_data.TypeDefinition containing structure. Returns: None """ new_fields = [] for field in structure.field: new_fields.append(field) if not field.name.is_anonymous: continue field.attribute.extend([_skip_text_output_attribute()]) for subtype in type_definition.subtype: if (subtype.name.name.text == field.type.atomic_type.reference.source_name[-1].text): field_type = subtype break else: assert False, ("Unable to find corresponding type {} for anonymous field " "in {}.".format( field.type.atomic_type.reference, type_definition)) anonymous_reference = ir_data.Reference(source_name=[field.name.name]) anonymous_field_reference = ir_data.FieldReference( path=[anonymous_reference]) for subfield in field_type.structure.field: alias_field_reference = ir_data.FieldReference( path=[ anonymous_reference, ir_data.Reference(source_name=[subfield.name.name]), ] ) new_existence_condition = ir_data_utils.copy(_ANONYMOUS_BITS_ALIAS_EXISTENCE_SKELETON) existence_clauses = ir_data_utils.builder(new_existence_condition).function.args existence_clauses[0].function.args[0].field_reference.CopyFrom( anonymous_field_reference) existence_clauses[1].function.args[0].field_reference.CopyFrom( alias_field_reference) new_read_transform = ir_data.Expression( field_reference=ir_data_utils.copy(alias_field_reference)) # This treats *most* of the alias field as synthetic, but not its name(s): # leaving the name(s) as "real" means that symbol collisions with the # surrounding structure will be properly reported to the user. _mark_as_synthetic(new_existence_condition) _mark_as_synthetic(new_read_transform) new_alias = ir_data.Field( read_transform=new_read_transform, existence_condition=new_existence_condition, name=ir_data_utils.copy(subfield.name)) if subfield.HasField("abbreviation"): ir_data_utils.builder(new_alias).abbreviation.CopyFrom(subfield.abbreviation) _mark_as_synthetic(new_alias.existence_condition) _mark_as_synthetic(new_alias.read_transform) new_fields.append(new_alias) # Since the alias field's name(s) are "real," it is important to mark the # original field's name(s) as synthetic, to avoid duplicate error # messages. _mark_as_synthetic(subfield.name) if subfield.HasField("abbreviation"): _mark_as_synthetic(subfield.abbreviation) del structure.field[:] structure.field.extend(new_fields) _SIZE_BOUNDS = { "$max_size_in_bits": expression_parser.parse("$upper_bound($size_in_bits)"), "$min_size_in_bits": expression_parser.parse("$lower_bound($size_in_bits)"), "$max_size_in_bytes": expression_parser.parse( "$upper_bound($size_in_bytes)"), "$min_size_in_bytes": expression_parser.parse( "$lower_bound($size_in_bytes)"), } def _add_size_bound_virtuals(structure, type_definition): """Adds ${min,max}_size_in_{bits,bytes} virtual fields to structure.""" names = { ir_data.AddressableUnit.BIT: ("$max_size_in_bits", "$min_size_in_bits"), ir_data.AddressableUnit.BYTE: ("$max_size_in_bytes", "$min_size_in_bytes"), } for name in names[type_definition.addressable_unit]: bound_field = ir_data.Field( read_transform=_SIZE_BOUNDS[name], name=ir_data.NameDefinition(name=ir_data.Word(text=name)), existence_condition=expression_parser.parse("true"), attribute=[_skip_text_output_attribute()] ) _mark_as_synthetic(bound_field.read_transform) structure.field.extend([bound_field]) # Each non-virtual field in a structure generates a clause that is passed to # `$max()` in the definition of `$size_in_bits`/`$size_in_bytes`. Additionally, # the `$max()` call is seeded with a `0` argument: this ensures that # `$size_in_units` is never negative, and ensures that structures with no # physical fields don't end up with a zero-argument `$max()` call, which would # fail type checking. _SIZE_CLAUSE_SKELETON = expression_parser.parse( "existence_condition ? start + size : 0") _SIZE_SKELETON = expression_parser.parse("$max(0)") def _add_size_virtuals(structure, type_definition): """Adds a $size_in_bits or $size_in_bytes virtual field to structure.""" names = { ir_data.AddressableUnit.BIT: "$size_in_bits", ir_data.AddressableUnit.BYTE: "$size_in_bytes", } size_field_name = names[type_definition.addressable_unit] size_clauses = [] for field in structure.field: # Virtual fields do not have a physical location, and thus do not contribute # to the size of the structure. if ir_util.field_is_virtual(field): continue size_clause_ir = ir_data_utils.copy(_SIZE_CLAUSE_SKELETON) size_clause = ir_data_utils.builder(size_clause_ir) # Copy the appropriate clauses into `existence_condition ? start + size : 0` size_clause.function.args[0].CopyFrom(field.existence_condition) size_clause.function.args[1].function.args[0].CopyFrom(field.location.start) size_clause.function.args[1].function.args[1].CopyFrom(field.location.size) size_clauses.append(size_clause_ir) size_expression = ir_data_utils.copy(_SIZE_SKELETON) size_expression.function.args.extend(size_clauses) _mark_as_synthetic(size_expression) size_field = ir_data.Field( read_transform=size_expression, name=ir_data.NameDefinition(name=ir_data.Word(text=size_field_name)), existence_condition=ir_data.Expression( boolean_constant=ir_data.BooleanConstant(value=True) ), attribute=[_skip_text_output_attribute()] ) structure.field.extend([size_field]) # The replacement for the "$next" keyword is a simple "start + size" expression. # 'x' and 'y' are placeholders, to be replaced. _NEXT_KEYWORD_REPLACEMENT_EXPRESSION = expression_parser.parse("x + y") def _maybe_replace_next_keyword_in_expression(expression_ir, last_location, source_file_name, errors): if not expression_ir.HasField("builtin_reference"): return if ir_data_utils.reader(expression_ir).builtin_reference.canonical_name.object_path[0] != "$next": return if not last_location: errors.append([ error.error(source_file_name, expression_ir.source_location, "`$next` may not be used in the first physical field of a " + "structure; perhaps you meant `0`?") ]) return original_location = expression_ir.source_location expression = ir_data_utils.builder(expression_ir) expression.CopyFrom(_NEXT_KEYWORD_REPLACEMENT_EXPRESSION) expression.function.args[0].CopyFrom(last_location.start) expression.function.args[1].CopyFrom(last_location.size) expression.source_location.CopyFrom(original_location) _mark_as_synthetic(expression.function) def _check_for_bad_next_keyword_in_size(expression, source_file_name, errors): if not expression.HasField("builtin_reference"): return if expression.builtin_reference.canonical_name.object_path[0] != "$next": return errors.append([ error.error(source_file_name, expression.source_location, "`$next` may only be used in the start expression of a " + "physical field.") ]) def _replace_next_keyword(structure, source_file_name, errors): last_physical_field_location = None new_errors = [] for field in structure.field: if ir_util.field_is_virtual(field): # TODO(bolms): It could be useful to allow `$next` in a virtual field, in # order to reuse the value (say, to allow overlapping fields in a # mostly-packed structure), but it seems better to add `$end_of(field)`, # `$offset_of(field)`, and `$size_of(field)` constructs of some sort, # instead. continue traverse_ir.fast_traverse_node_top_down( field.location.size, [ir_data.Expression], _check_for_bad_next_keyword_in_size, parameters={ "errors": new_errors, "source_file_name": source_file_name, }) # If `$next` is misused in a field size, it can end up causing a # `RecursionError` in fast_traverse_node_top_down. (When the `$next` node # in the next field is replaced, its replacement gets traversed, but the # replacement also contains a `$next` node, leading to infinite recursion.) # # Technically, we could scan all of the sizes instead of bailing early, but # it seems relatively unlikely that someone will have `$next` in multiple # sizes and not figure out what is going on relatively quickly. if new_errors: errors.extend(new_errors) return traverse_ir.fast_traverse_node_top_down( field.location.start, [ir_data.Expression], _maybe_replace_next_keyword_in_expression, parameters={ "last_location": last_physical_field_location, "errors": new_errors, "source_file_name": source_file_name, }) # The only possible error from _maybe_replace_next_keyword_in_expression is # `$next` occurring in the start expression of the first physical field, # which leads to similar recursion issue if `$next` is used in the start # expression of the next physical field. if new_errors: errors.extend(new_errors) return last_physical_field_location = field.location def _add_virtuals_to_structure(structure, type_definition): _add_anonymous_aliases(structure, type_definition) _add_size_virtuals(structure, type_definition) _add_size_bound_virtuals(structure, type_definition) def desugar(ir): """Translates pure syntactic sugar to its desugared form. Replaces `$next` symbols with the start+length of the previous physical field. Adds aliases for all fields in anonymous `bits` to the enclosing structure. Arguments: ir: The IR to desugar. Returns: A list of errors, or an empty list. """ errors = [] traverse_ir.fast_traverse_ir_top_down( ir, [ir_data.Structure], _replace_next_keyword, parameters={"errors": errors}) if errors: return errors traverse_ir.fast_traverse_ir_top_down( ir, [ir_data.Structure], _add_virtuals_to_structure) return []