def test_return_value_requires_different_signedness_from_arguments(self): ir = _make_ir_from_emb( '[$default byte_order: "LittleEndian"]\n' "struct Foo:\n" " 0 [+1] UInt x\n" # Both arguments require uint64; result fits in int64. " if (x + 0x7fff_ffff_ffff_ffff) - 0x8000_0000_0000_0000 < 10:\n" " 1 [+1] UInt y\n") condition = ir.module[0].type[0].structure.field[1].existence_condition error_expression = condition.function.args[0] error_location = error_expression.source_location arg0_location = error_expression.function.args[0].source_location arg1_location = error_expression.function.args[1].source_location self.assertEqual([ [error.error( "m.emb", error_location, "Either all arguments to '-' and its result must fit in a 64-bit " "unsigned integer, or all must fit in a 64-bit signed integer."), error.note("m.emb", arg0_location, "Requires unsigned 64-bit integer."), error.note("m.emb", arg1_location, "Requires unsigned 64-bit integer."), error.note("m.emb", error_location, "Requires signed 64-bit integer.")] ], error.filter_errors(constraints.check_constraints(ir)))
def test_error_on_triple_field_cycle(self): ir = _parse_snippet("struct Foo:\n" " 0 [+field2] UInt field1\n" " 0 [+field3] UInt field2\n" " 0 [+field1] UInt field3\n") struct = ir.module[0].type[0].structure self.assertEqual([[ error.error("m.emb", struct.field[0].source_location, "Dependency cycle\nfield1"), error.note("m.emb", struct.field[1].source_location, "field2"), error.note("m.emb", struct.field[2].source_location, "field3"), ]], dependency_checker.find_dependency_cycles(ir))
def _type_check_passed_parameters(atomic_type, ir, source_file_name, errors): """Checks the types of parameters to a parameterized physical type.""" referenced_type = ir_util.find_object(atomic_type.reference.canonical_name, ir) if (len(referenced_type.runtime_parameter) != len( atomic_type.runtime_parameter)): errors.append([ error.error( source_file_name, atomic_type.source_location, "Type {} requires {} parameter{}; {} parameter{} given.". format( referenced_type.name.name.text, len(referenced_type.runtime_parameter), "" if len(referenced_type.runtime_parameter) == 1 else "s", len(atomic_type.runtime_parameter), "" if len(atomic_type.runtime_parameter) == 1 else "s")), error.note( atomic_type.reference.canonical_name.module_file, referenced_type.source_location, "Definition of type {}.".format( referenced_type.name.name.text)) ]) return for i in range(len(referenced_type.runtime_parameter)): if referenced_type.runtime_parameter[i].type.WhichOneof( "type") not in ("integer", "boolean", "enumeration"): # _type_check_parameter will catch invalid parameter types at the # definition site; no need for another, probably-confusing error at any # usage sites. continue if (atomic_type.runtime_parameter[i].type.WhichOneof("type") != referenced_type.runtime_parameter[i].type.WhichOneof("type")): errors.append([ error.error( source_file_name, atomic_type.runtime_parameter[i].source_location, "Parameter {} of type {} must be {}, not {}.".format( i, referenced_type.name.name.text, _type_name_for_error_messages( referenced_type.runtime_parameter[i].type), _type_name_for_error_messages( atomic_type.runtime_parameter[i].type))), error.note( atomic_type.reference.canonical_name.module_file, referenced_type.runtime_parameter[i].source_location, "Parameter {} of {}.".format( i, referenced_type.name.name.text)) ])
def _type_check_constant_reference(expression, source_file_name, ir, errors): """Annotates the type of a constant reference.""" referred_name = expression.constant_reference.canonical_name referred_object = ir_util.find_object(referred_name, ir) if isinstance(referred_object, ir_pb2.EnumValue): expression.type.enumeration.name.CopyFrom( expression.constant_reference) del expression.type.enumeration.name.canonical_name.object_path[-1] elif isinstance(referred_object, ir_pb2.Field): if not ir_util.field_is_virtual(referred_object): errors.append([ error.error( source_file_name, expression.source_location, "Static references to physical fields are not allowed."), error.note( referred_name.module_file, referred_object.source_location, "{} is a physical field.".format( referred_name.object_path[-1])), ]) return _type_check_expression(referred_object.read_transform, referred_name.module_file, ir, errors) expression.type.CopyFrom(referred_object.read_transform.type) else: assert False, "Unexpected constant reference type."
def _verify_requires_attribute_on_field(field, source_file_name, ir, errors): """Verifies that [requires] is valid on the given field.""" requires_attr = ir_util.get_attribute(field.attribute, attributes.REQUIRES) if not requires_attr: return if ir_util.field_is_virtual(field): field_expression_type = field.read_transform.type else: if not field.type.HasField("atomic_type"): errors.append([ error.error(source_file_name, requires_attr.source_location, "Attribute 'requires' is only allowed on integer, " "enumeration, or boolean fields, not arrays."), error.note(source_file_name, field.type.source_location, "Field type."), ]) return field_type = ir_util.find_object(field.type.atomic_type.reference, ir) assert field_type, "Field type should be non-None after name resolution." field_expression_type = ( type_check.unbounded_expression_type_for_physical_type(field_type)) if field_expression_type.WhichOneof("type") not in ( "integer", "enumeration", "boolean"): errors.append([error.error( source_file_name, requires_attr.source_location, "Attribute 'requires' is only allowed on integer, enumeration, or " "boolean fields.")])
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 duplicate_name_error(file_name, location, name, original_location): """A name is defined two or more times.""" return [ error.error(file_name, location, "Duplicate name '{}'".format(name)), error.note(original_location.file, original_location.location, "Original definition") ]
def _find_object_dependency_cycles(ir): """Finds dependency cycles in types in the ir.""" dependencies, find_dependency_errors = _find_dependencies(ir) if find_dependency_errors: return find_dependency_errors errors = [] cycles = _find_cycles(dict(dependencies)) for cycle in cycles: # TODO(bolms): This lists the entire strongly-connected component in a # fairly arbitrary order. This is simple, and handles components that # aren't simple cycles, but may not be the most user-friendly way to # present this information. cycle_list = sorted(list(cycle)) node_object = ir_util.find_object(cycle_list[0], ir) error_group = [ error.error(cycle_list[0][0], node_object.source_location, "Dependency cycle\n" + node_object.name.name.text) ] for node in cycle_list[1:]: node_object = ir_util.find_object(node, ir) error_group.append( error.note(node[0], node_object.source_location, node_object.name.name.text)) errors.append(error_group) return errors
def test_error_on_simple_enum_value_cycle(self): ir = _parse_snippet("enum Foo:\n" " XX = YY\n" " YY = XX\n") enum = ir.module[0].type[0].enumeration self.assertEqual([[ error.error("m.emb", enum.value[0].source_location, "Dependency cycle\nXX"), error.note("m.emb", enum.value[1].source_location, "YY") ]], dependency_checker.find_dependency_cycles(ir))
def test_error_on_virtual_field_cycle(self): ir = _parse_snippet("struct Foo:\n" " let x = y\n" " let y = x\n") struct = ir.module[0].type[0].structure self.assertEqual([[ error.error("m.emb", struct.field[0].source_location, "Dependency cycle\nx"), error.note("m.emb", struct.field[1].source_location, "y") ]], dependency_checker.find_dependency_cycles(ir))
def test_rejects_duplicate_default_attribute(self): ir = _make_ir_from_emb('[$default byte_order: "LittleEndian"]\n' '[$default byte_order: "LittleEndian"]\n') self.assertEqual([[ error.error("m.emb", ir.module[0].attribute[1].source_location, "Duplicate attribute 'byte_order'."), error.note("m.emb", ir.module[0].attribute[0].source_location, "Original attribute"), ]], attribute_checker.normalize_and_verify(ir))
def ambiguous_name_error(file_name, location, name, candidate_locations): """A name cannot be resolved because there are two or more candidates.""" result = [ error.error(file_name, location, "Ambiguous name '{}'".format(name)) ] for location in sorted(candidate_locations): result.append( error.note(location.file, location.location, "Possible resolution")) return result
def _check_attributes(attribute_list, attribute_specs, context_name, module_source_file): """Performs basic checks on the given list of attributes. Checks the given attribute_list for duplicates, unknown attributes, attributes with incorrect type, and attributes whose values are not constant. Arguments: attribute_list: An iterable of ir_pb2.Attribute. attribute_specs: A dict of attribute names to _Attribute structures specifying the allowed attributes. context_name: A name for the context of these attributes, such as "struct 'Foo'" or "module 'm.emb'". Used in error messages. module_source_file: The value of module.source_file_name from the module containing 'attribute_list'. Used in error messages. Returns: A list of lists of error.Errors. An empty list indicates no errors were found. """ errors = [] already_seen_attributes = {} for attr in attribute_list: if attr.back_end.text: attribute_name = "({}) {}".format(attr.back_end.text, attr.name.text) else: attribute_name = attr.name.text if (attr.name.text, attr.is_default) in already_seen_attributes: original_attr = already_seen_attributes[attr.name.text, attr.is_default] errors.append([ error.error(module_source_file, attr.source_location, "Duplicate attribute '{}'.".format(attribute_name)), error.note(module_source_file, original_attr.source_location, "Original attribute")]) continue already_seen_attributes[attr.name.text, attr.is_default] = attr if ((attr.back_end.text, attr.name.text, attr.is_default) not in attribute_specs): if attr.is_default: error_message = "Attribute '{}' may not be defaulted on {}.".format( attribute_name, context_name) else: error_message = "Unknown attribute '{}' on {}.".format(attribute_name, context_name) errors.append([error.error(module_source_file, attr.name.source_location, error_message)]) else: attribute_check = _ATTRIBUTE_TYPES[attr.back_end.text, attr.name.text] errors.extend(attribute_check(attr, module_source_file)) return errors
def test_parse_module_no_such_file(self): file_name = "nonexistent.emb" ir, debug_info, errors = glue.parse_emboss_file( file_name, test_util.dict_file_reader({})) self.assertEqual([[ error.error("nonexistent.emb", _location((1, 1), (1, 1)), "Unable to read file."), error.note("nonexistent.emb", _location((1, 1), (1, 1)), "File 'nonexistent.emb' not found."), ]], errors) self.assertFalse(file_name in debug_info.modules) self.assertFalse(ir)
def test_error_on_cycle_involving_subfield(self): ir = _parse_snippet("struct Bar:\n" " foo_b.x [+4] Foo foo_a\n" " foo_a.x [+4] Foo foo_b\n" "struct Foo:\n" " 0 [+4] UInt x\n") struct = ir.module[0].type[0].structure self.assertEqual([[ error.error("m.emb", struct.field[0].source_location, "Dependency cycle\nfoo_a"), error.note("m.emb", struct.field[1].source_location, "foo_b") ]], dependency_checker.find_dependency_cycles(ir))
def test_error_on_field_existence_cycle(self): ir = _parse_snippet("struct Foo:\n" " if y == 1:\n" " 0 [+1] UInt x\n" " if x == 0:\n" " 1 [+1] UInt y\n") struct = ir.module[0].type[0].structure self.assertEqual([[ error.error("m.emb", struct.field[0].source_location, "Dependency cycle\nx"), error.note("m.emb", struct.field[1].source_location, "y") ]], dependency_checker.find_dependency_cycles(ir))
def test_rejects_requires_on_array(self): ir = _make_ir_from_emb("struct Foo:\n" " 0 [+4] UInt:8[] array\n" " [requires: false]\n") field_ir = ir.module[0].type[0].structure.field[0] self.assertEqual([[ error.error( "m.emb", field_ir.attribute[0].value.source_location, "Attribute 'requires' is only allowed on integer, " "enumeration, or boolean fields, not arrays."), error.note("m.emb", field_ir.type.source_location, "Field type."), ]], error.filter_errors(attribute_checker.normalize_and_verify(ir)))
def test_error_on_passing_unneeded_parameter(self): ir = self._make_ir("struct Foo:\n" " 0 [+1] Bar(1) b\n" "struct Bar:\n" " 0 [+1] UInt:8[] x\n") type_ir = ir.module[0].type[0].structure.field[0].type bar = ir.module[0].type[1] self.assertEqual([[ error.error("m.emb", type_ir.source_location, "Type Bar requires 0 parameters; 1 parameter given."), error.note("m.emb", bar.source_location, "Definition of type Bar.") ]], type_check.check_types(ir))
def test_rejects_duplicate_attribute(self): ir = _make_ir_from_emb("external Foo:\n" " [is_integer: true]\n" " [is_integer: true]\n") self.assertEqual([[ error.error("m.emb", ir.module[0].type[0].attribute[1].source_location, "Duplicate attribute 'is_integer'."), error.note("m.emb", ir.module[0].type[0].attribute[0].source_location, "Original attribute"), ]], attribute_checker.normalize_and_verify(ir))
def test_split_errors(self): user_error = [ error.error("foo.emb", parser_types.make_location((1, 2), (3, 4)), "Bad thing"), error.note("foo.emb", parser_types.make_location((3, 4), (5, 6)), "Note: bad thing referrent") ] user_error_2 = [ error.error("foo.emb", parser_types.make_location( (8, 9), (10, 11)), "Bad thing"), error.note("foo.emb", parser_types.make_location( (10, 11), (12, 13)), "Note: bad thing referrent") ] synthetic_error = [ error.error("foo.emb", parser_types.make_location((1, 2), (3, 4)), "Bad thing"), error.note("foo.emb", parser_types.make_location((3, 4), (5, 6), True), "Note: bad thing referrent") ] synthetic_error_2 = [ error.error("foo.emb", parser_types.make_location((8, 9), (10, 11), True), "Bad thing"), error.note("foo.emb", parser_types.make_location( (10, 11), (12, 13)), "Note: bad thing referrent") ] user_errors, synthetic_errors = error.split_errors( [user_error, synthetic_error]) self.assertEqual([user_error], user_errors) self.assertEqual([synthetic_error], synthetic_errors) user_errors, synthetic_errors = error.split_errors( [synthetic_error, user_error]) self.assertEqual([user_error], user_errors) self.assertEqual([synthetic_error], synthetic_errors) user_errors, synthetic_errors = error.split_errors( [synthetic_error, user_error, synthetic_error_2, user_error_2]) self.assertEqual([user_error, user_error_2], user_errors) self.assertEqual([synthetic_error, synthetic_error_2], synthetic_errors)
def test_explicit_size_too_small(self): ir = _make_ir_from_emb("bits Foo:\n" " 0 [+0] UInt:0 zero_bit\n") error_field = ir.module[0].type[0].structure.field[0] uint_type = ir_util.find_object(error_field.type.atomic_type.reference, ir) uint_requirements = ir_util.get_attribute(uint_type.attribute, attributes.STATIC_REQUIREMENTS) self.assertEqual([[ error.error("m.emb", error_field.source_location, "Requirements of UInt not met."), error.note("", uint_requirements.source_location, "Requirements specified here."), ]], error.filter_errors(constraints.check_constraints(ir)))
def test_error_static_reference_to_physical_field(self): ir = self._make_ir("struct Foo:\n" " 0 [+1] UInt x\n" " let y = Foo.x\n") static_ref = ir.module[0].type[0].structure.field[1].read_transform physical_field = ir.module[0].type[0].structure.field[0] self.assertEqual([[ error.error( "m.emb", static_ref.source_location, "Static references to physical fields are not allowed."), error.note("m.emb", physical_field.source_location, "x is a physical field.") ]], type_check.annotate_types(ir))
def test_error_on_import_cycle(self): ir, unused_debug_info, errors = glue.parse_emboss_file( "m.emb", test_util.dict_file_reader({ "m.emb": 'import "n.emb" as n\n', "n.emb": 'import "m.emb" as m\n' }), stop_before_step="find_dependency_cycles") assert not errors self.assertEqual([[ error.error("m.emb", ir.module[0].source_location, "Import dependency cycle\nm.emb"), error.note("n.emb", ir.module[2].source_location, "n.emb") ]], dependency_checker.find_dependency_cycles(ir))
def test_checks_constancy_of_constant_references(self): ir = _make_ir_from_emb("struct Foo:\n" " 0 [+1] UInt x\n" " let y = x\n" " let z = Foo.y\n") error_expression = ir.module[0].type[0].structure.field[2].read_transform error_location = error_expression.source_location note_field = ir.module[0].type[0].structure.field[1] note_location = note_field.source_location self.assertEqual([ [error.error("m.emb", error_location, "Static references must refer to constants."), error.note("m.emb", note_location, "y is not constant.")] ], error.filter_errors(constraints.check_constraints(ir)))
def test_explicit_size_too_small_on_fixed_size_type(self): ir = _make_ir_from_emb("struct Foo:\n" " 0 [+0] Byte:0 null_byte\n" "struct Byte:\n" " 0 [+1] UInt b\n") error_type = ir.module[0].type[0].structure.field[0].type self.assertEqual([[ error.error( "m.emb", error_type.size_in_bits.source_location, "Explicit size of 0 bits does not match fixed size (8 bits) of " "type 'Byte'."), error.note("m.emb", ir.module[0].type[1].source_location, "Size specified here."), ]], error.filter_errors(constraints.check_constraints(ir)))
def test_explicit_size_too_big(self): ir = _make_ir_from_emb("struct Foo:\n" " 0 [+16] UInt:128 one_twenty_eight_bit\n" ' [byte_order: "LittleEndian"]\n') error_field = ir.module[0].type[0].structure.field[0] uint_type = ir_util.find_object(error_field.type.atomic_type.reference, ir) uint_requirements = ir_util.get_attribute(uint_type.attribute, attributes.STATIC_REQUIREMENTS) self.assertEqual([[ error.error("m.emb", error_field.source_location, "Requirements of UInt not met."), error.note("", uint_requirements.source_location, "Requirements specified here."), ]], error.filter_errors(constraints.check_constraints(ir)))
def _integer_bounds_errors_for_expression(expression, source_file_name): """Checks that `expression` is in range for int64_t or uint64_t.""" # Only check non-constant subexpressions. if (expression.WhichOneof("expression") == "function" and not ir_util.is_constant_type(expression.type)): errors = [] for arg in expression.function.args: errors += _integer_bounds_errors_for_expression( arg, source_file_name) if errors: # Don't cascade bounds errors: report them at the lowest level they # appear. return errors if expression.type.WhichOneof("type") == "integer": errors = _integer_bounds_errors(expression.type.integer, "expression", source_file_name, expression.source_location) if errors: return errors if (expression.WhichOneof("expression") == "function" and not ir_util.is_constant_type(expression.type)): int64_only_clauses = [] uint64_only_clauses = [] for clause in [expression] + list(expression.function.args): if clause.type.WhichOneof("type") == "integer": arg_minimum = int(clause.type.integer.minimum_value) arg_maximum = int(clause.type.integer.maximum_value) if not _bounds_can_fit_64_bit_signed(arg_minimum, arg_maximum): uint64_only_clauses.append(clause) elif not _bounds_can_fit_64_bit_unsigned( arg_minimum, arg_maximum): int64_only_clauses.append(clause) if int64_only_clauses and uint64_only_clauses: error_set = [ error.error( source_file_name, expression.source_location, "Either all arguments to '{}' and its result must fit in a " "64-bit unsigned integer, or all must fit in a 64-bit signed " "integer.".format(expression.function.function_name.text)) ] for signedness, clause_list in (("unsigned", uint64_only_clauses), ("signed", int64_only_clauses)): for clause in clause_list: error_set.append( error.note( source_file_name, clause.source_location, "Requires {} 64-bit integer.".format(signedness))) return [error_set] return []
def _check_physical_type_requirements(type_ir, usage_source_location, size, ir, source_file_name): """Checks that the given atomic `type_ir` is allowed to be `size` bits.""" referenced_type_definition = ir_util.find_object( type_ir.atomic_type.reference, ir) # TODO(bolms): replace this with a check against an automatically-generated # `static_requirements` attribute on enum types. (The main problem is that # the generated attribute would have no source text, so there would be a crash # when trying to display the error.) if referenced_type_definition.HasField("enumeration"): if size is None: return [[ error.error( source_file_name, type_ir.source_location, "Enumeration {} cannot be placed in a dynamically-sized " "field.".format(_render_type(type_ir, ir))) ]] elif size < 1 or size > 64: return [[ error.error( source_file_name, type_ir.source_location, "Enumeration {} cannot be {} bits; enumerations must be between " "1 and 64 bits, inclusive.".format( _render_atomic_type_name(type_ir, ir), size)) ]] if size is None: bindings = {"$is_statically_sized": False} else: bindings = {"$is_statically_sized": True, "$static_size_in_bits": size} requires_attr = ir_util.get_attribute(referenced_type_definition.attribute, attributes.STATIC_REQUIREMENTS) if requires_attr and not ir_util.constant_value(requires_attr.expression, bindings): # TODO(bolms): Figure out a better way to build this error message. # The "Requirements specified here." message should print out the actual # source text of the requires attribute, so that should help, but it's still # a bit generic and unfriendly. return [[ error.error( source_file_name, usage_source_location, "Requirements of {} not met.".format( type_ir.atomic_type.reference.canonical_name. object_path[-1])), error.note( type_ir.atomic_type.reference.canonical_name.module_file, requires_attr.source_location, "Requirements specified here.") ]] return []
def test_equality(self): note_message = error.note("foo.emb", parser_types.make_location((3, 4), (3, 6)), "thing") self.assertEqual( note_message, error.note("foo.emb", parser_types.make_location((3, 4), (3, 6)), "thing")) self.assertNotEqual( note_message, error.warn("foo.emb", parser_types.make_location((3, 4), (3, 6)), "thing")) self.assertNotEqual( note_message, error.note("foo2.emb", parser_types.make_location((3, 4), (3, 6)), "thing")) self.assertNotEqual( note_message, error.note("foo.emb", parser_types.make_location((2, 4), (3, 6)), "thing")) self.assertNotEqual( note_message, error.note("foo.emb", parser_types.make_location((3, 4), (3, 6)), "thing2"))
def test_error_on_passing_wrong_parameter_type(self): ir = self._make_ir("struct Foo:\n" " 0 [+1] Bar(1) b\n" "enum Baz:\n" " QUX = 1\n" "struct Bar(n: Baz):\n" " 0 [+1] UInt:8[] x\n") type_ir = ir.module[0].type[0].structure.field[0].type usage_parameter_ir = type_ir.atomic_type.runtime_parameter[0] source_parameter_ir = ir.module[0].type[2].runtime_parameter[0] self.assertEqual([[ error.error("m.emb", usage_parameter_ir.source_location, "Parameter 0 of type Bar must be Baz, not integer."), error.note("m.emb", source_parameter_ir.source_location, "Parameter 0 of Bar.") ]], type_check.check_types(ir))