| # 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. |
| |
| """Functions for checking expression types.""" |
| |
| from compiler.front_end import attributes |
| from compiler.util import error |
| from compiler.util import ir_pb2 |
| from compiler.util import ir_util |
| from compiler.util import traverse_ir |
| |
| |
| def _type_check_expression(expression, source_file_name, ir, errors): |
| """Checks and annotates the type of an expression and all subexpressions.""" |
| if expression.type.WhichOneof("type"): |
| # This expression has already been type checked. |
| return |
| expression_variety = expression.WhichOneof("expression") |
| if expression_variety == "constant": |
| _type_check_integer_constant(expression) |
| elif expression_variety == "constant_reference": |
| _type_check_constant_reference(expression, source_file_name, ir, errors) |
| elif expression_variety == "function": |
| _type_check_operation(expression, source_file_name, ir, errors) |
| elif expression_variety == "field_reference": |
| _type_check_local_reference(expression, ir, errors) |
| elif expression_variety == "boolean_constant": |
| _type_check_boolean_constant(expression) |
| elif expression_variety == "builtin_reference": |
| _type_check_builtin_reference(expression) |
| else: |
| assert False, "Unknown expression variety {!r}".format(expression_variety) |
| |
| |
| def _annotate_as_integer(expression): |
| expression.type.integer.CopyFrom(ir_pb2.IntegerType()) |
| |
| |
| def _annotate_as_boolean(expression): |
| expression.type.boolean.CopyFrom(ir_pb2.BooleanType()) |
| |
| |
| def _type_check(expression, source_file_name, errors, type_oneof, type_name, |
| expression_name): |
| if expression.type.WhichOneof("type") != type_oneof: |
| errors.append([ |
| error.error(source_file_name, expression.source_location, |
| "{} must be {}.".format(expression_name, type_name)) |
| ]) |
| |
| |
| def _type_check_integer(expression, source_file_name, errors, expression_name): |
| _type_check(expression, source_file_name, errors, "integer", |
| "an integer", expression_name) |
| |
| |
| def _type_check_boolean(expression, source_file_name, errors, expression_name): |
| _type_check(expression, source_file_name, errors, "boolean", "a boolean", |
| expression_name) |
| |
| |
| def _kind_check_field_reference(expression, source_file_name, errors, |
| expression_name): |
| if expression.WhichOneof("expression") != "field_reference": |
| errors.append([ |
| error.error(source_file_name, expression.source_location, |
| "{} must be a field.".format(expression_name)) |
| ]) |
| |
| |
| def _type_check_integer_constant(expression): |
| _annotate_as_integer(expression) |
| |
| |
| 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 _type_check_operation(expression, source_file_name, ir, errors): |
| for arg in expression.function.args: |
| _type_check_expression(arg, source_file_name, ir, errors) |
| function = expression.function.function |
| if function in (ir_pb2.FunctionMapping.EQUALITY, ir_pb2.FunctionMapping.INEQUALITY, |
| ir_pb2.FunctionMapping.LESS, ir_pb2.FunctionMapping.LESS_OR_EQUAL, |
| ir_pb2.FunctionMapping.GREATER, ir_pb2.FunctionMapping.GREATER_OR_EQUAL): |
| _type_check_comparison_operator(expression, source_file_name, errors) |
| elif function == ir_pb2.FunctionMapping.CHOICE: |
| _type_check_choice_operator(expression, source_file_name, errors) |
| else: |
| _type_check_monomorphic_operator(expression, source_file_name, errors) |
| |
| |
| def _type_check_monomorphic_operator(expression, source_file_name, errors): |
| """Type checks an operator that accepts only one set of argument types.""" |
| args = expression.function.args |
| int_args = _type_check_integer |
| bool_args = _type_check_boolean |
| field_args = _kind_check_field_reference |
| int_result = _annotate_as_integer |
| bool_result = _annotate_as_boolean |
| binary = ("Left argument", "Right argument") |
| n_ary = ("Argument {}".format(n) for n in range(len(args))) |
| functions = { |
| ir_pb2.FunctionMapping.ADDITION: (int_result, int_args, binary, 2, 2, |
| "operator"), |
| ir_pb2.FunctionMapping.SUBTRACTION: (int_result, int_args, binary, 2, 2, |
| "operator"), |
| ir_pb2.FunctionMapping.MULTIPLICATION: (int_result, int_args, binary, 2, 2, |
| "operator"), |
| ir_pb2.FunctionMapping.AND: (bool_result, bool_args, binary, 2, 2, "operator"), |
| ir_pb2.FunctionMapping.OR: (bool_result, bool_args, binary, 2, 2, "operator"), |
| ir_pb2.FunctionMapping.MAXIMUM: (int_result, int_args, n_ary, 1, None, |
| "function"), |
| ir_pb2.FunctionMapping.PRESENCE: (bool_result, field_args, n_ary, 1, 1, |
| "function"), |
| ir_pb2.FunctionMapping.UPPER_BOUND: (int_result, int_args, n_ary, 1, 1, |
| "function"), |
| ir_pb2.FunctionMapping.LOWER_BOUND: (int_result, int_args, n_ary, 1, 1, |
| "function"), |
| } |
| function = expression.function.function |
| (set_result_type, check_arg, arg_names, min_args, max_args, |
| kind) = functions[function] |
| for argument, name in zip(args, arg_names): |
| assert name is not None, "Too many arguments to function!" |
| check_arg(argument, source_file_name, errors, |
| "{} of {} '{}'".format(name, kind, |
| expression.function.function_name.text)) |
| if len(args) < min_args: |
| errors.append([ |
| error.error(source_file_name, expression.source_location, |
| "{} '{}' requires {} {} argument{}.".format( |
| kind.title(), expression.function.function_name.text, |
| "exactly" if min_args == max_args else "at least", |
| min_args, "s" if min_args > 1 else "")) |
| ]) |
| if max_args is not None and len(args) > max_args: |
| errors.append([ |
| error.error(source_file_name, expression.source_location, |
| "{} '{}' requires {} {} argument{}.".format( |
| kind.title(), expression.function.function_name.text, |
| "exactly" if min_args == max_args else "at most", |
| max_args, "s" if max_args > 1 else "")) |
| ]) |
| set_result_type(expression) |
| |
| |
| def _type_check_local_reference(expression, ir, errors): |
| """Annotates the type of a local reference.""" |
| referrent = ir_util.find_object(expression.field_reference.path[-1], ir) |
| assert referrent, "Local reference should be non-None after name resolution." |
| if isinstance(referrent, ir_pb2.RuntimeParameter): |
| parameter = referrent |
| _set_expression_type_from_physical_type_reference( |
| expression, parameter.physical_type_alias.atomic_type.reference, ir) |
| return |
| field = referrent |
| if ir_util.field_is_virtual(field): |
| _type_check_expression(field.read_transform, |
| expression.field_reference.path[0], ir, errors) |
| expression.type.CopyFrom(field.read_transform.type) |
| return |
| if not field.type.HasField("atomic_type"): |
| expression.type.opaque.CopyFrom(ir_pb2.OpaqueType()) |
| else: |
| _set_expression_type_from_physical_type_reference( |
| expression, field.type.atomic_type.reference, ir) |
| |
| |
| def unbounded_expression_type_for_physical_type(type_definition): |
| """Gets the ExpressionType for a field of the given TypeDefinition. |
| |
| Arguments: |
| type_definition: an ir_pb2.TypeDefinition. |
| |
| Returns: |
| An ir_pb2.ExpressionType with the corresponding expression type filled in: |
| for example, [prelude].UInt will result in an ExpressionType with the |
| `integer` field filled in. |
| |
| The returned ExpressionType will not have any bounds set. |
| """ |
| # TODO(bolms): Add a `[value_type]` attribute for `external`s. |
| if ir_util.get_boolean_attribute(type_definition.attribute, |
| attributes.IS_INTEGER): |
| return ir_pb2.ExpressionType(integer=ir_pb2.IntegerType()) |
| elif tuple(type_definition.name.canonical_name.object_path) == ("Flag",): |
| # This is a hack: the Flag type should say that it is a boolean. |
| return ir_pb2.ExpressionType(boolean=ir_pb2.BooleanType()) |
| elif type_definition.HasField("enumeration"): |
| return ir_pb2.ExpressionType( |
| enumeration=ir_pb2.EnumType( |
| name=ir_pb2.Reference( |
| canonical_name=type_definition.name.canonical_name))) |
| else: |
| return ir_pb2.ExpressionType(opaque=ir_pb2.OpaqueType()) |
| |
| |
| def _set_expression_type_from_physical_type_reference(expression, |
| type_reference, ir): |
| """Sets the type of an expression to match a physical type.""" |
| field_type = ir_util.find_object(type_reference, ir) |
| assert field_type, "Field type should be non-None after name resolution." |
| expression.type.CopyFrom( |
| unbounded_expression_type_for_physical_type(field_type)) |
| |
| |
| def _annotate_parameter_type(parameter, ir, source_file_name, errors): |
| if parameter.physical_type_alias.WhichOneof("type") != "atomic_type": |
| errors.append([ |
| error.error( |
| source_file_name, parameter.physical_type_alias.source_location, |
| "Parameters cannot be arrays.") |
| ]) |
| return |
| _set_expression_type_from_physical_type_reference( |
| parameter, parameter.physical_type_alias.atomic_type.reference, ir) |
| |
| |
| def _types_are_compatible(a, b): |
| """Returns true if a and b have compatible types.""" |
| if a.type.WhichOneof("type") != b.type.WhichOneof("type"): |
| return False |
| elif a.type.WhichOneof("type") == "enumeration": |
| return (ir_util.hashable_form_of_reference(a.type.enumeration.name) == |
| ir_util.hashable_form_of_reference(b.type.enumeration.name)) |
| elif a.type.WhichOneof("type") in ("integer", "boolean"): |
| # All integers are compatible with integers; booleans are compatible with |
| # booleans |
| return True |
| else: |
| assert False, "_types_are_compatible works with enums, integers, booleans." |
| |
| |
| def _type_check_comparison_operator(expression, source_file_name, errors): |
| """Checks the type of a comparison operator (==, !=, <, >, >=, <=).""" |
| # Applying less than or greater than to a boolean is likely a mistake, so |
| # only equality and inequality are allowed for booleans. |
| if expression.function.function in (ir_pb2.FunctionMapping.EQUALITY, |
| ir_pb2.FunctionMapping.INEQUALITY): |
| acceptable_types = ("integer", "boolean", "enumeration") |
| acceptable_types_for_humans = "an integer, boolean, or enum" |
| else: |
| acceptable_types = ("integer", "enumeration") |
| acceptable_types_for_humans = "an integer or enum" |
| left = expression.function.args[0] |
| right = expression.function.args[1] |
| for (argument, name) in ((left, "Left"), (right, "Right")): |
| if argument.type.WhichOneof("type") not in acceptable_types: |
| errors.append([ |
| error.error(source_file_name, argument.source_location, |
| "{} argument of operator '{}' must be {}.".format( |
| name, expression.function.function_name.text, |
| acceptable_types_for_humans)) |
| ]) |
| return |
| if not _types_are_compatible(left, right): |
| errors.append([ |
| error.error(source_file_name, expression.source_location, |
| "Both arguments of operator '{}' must have the same " |
| "type.".format(expression.function.function_name.text)) |
| ]) |
| _annotate_as_boolean(expression) |
| |
| |
| def _type_check_choice_operator(expression, source_file_name, errors): |
| """Checks the type of the choice operator cond ? if_true : if_false.""" |
| condition = expression.function.args[0] |
| if condition.type.WhichOneof("type") != "boolean": |
| errors.append([ |
| error.error(source_file_name, condition.source_location, |
| "Condition of operator '?:' must be a boolean.") |
| ]) |
| if_true = expression.function.args[1] |
| if if_true.type.WhichOneof("type") not in ("integer", "boolean", |
| "enumeration"): |
| errors.append([ |
| error.error(source_file_name, if_true.source_location, |
| "If-true clause of operator '?:' must be an integer, " |
| "boolean, or enum.") |
| ]) |
| return |
| if_false = expression.function.args[2] |
| if not _types_are_compatible(if_true, if_false): |
| errors.append([ |
| error.error(source_file_name, expression.source_location, |
| "The if-true and if-false clauses of operator '?:' must " |
| "have the same type.") |
| ]) |
| if if_true.type.WhichOneof("type") == "integer": |
| _annotate_as_integer(expression) |
| elif if_true.type.WhichOneof("type") == "boolean": |
| _annotate_as_boolean(expression) |
| elif if_true.type.WhichOneof("type") == "enumeration": |
| expression.type.enumeration.name.CopyFrom(if_true.type.enumeration.name) |
| else: |
| assert False, "Unexpected type for if_true." |
| |
| |
| def _type_check_boolean_constant(expression): |
| _annotate_as_boolean(expression) |
| |
| |
| def _type_check_builtin_reference(expression): |
| name = expression.builtin_reference.canonical_name.object_path[0] |
| if name == "$is_statically_sized": |
| _annotate_as_boolean(expression) |
| elif name == "$static_size_in_bits": |
| _annotate_as_integer(expression) |
| else: |
| assert False, "Unknown builtin '{}'.".format(name) |
| |
| |
| def _type_check_array_size(expression, source_file_name, errors): |
| _type_check_integer(expression, source_file_name, errors, "Array size") |
| |
| |
| def _type_check_field_location(location, source_file_name, errors): |
| _type_check_integer(location.start, source_file_name, errors, |
| "Start of field") |
| _type_check_integer(location.size, source_file_name, errors, "Size of field") |
| |
| |
| def _type_check_field_existence_condition(field, source_file_name, errors): |
| _type_check_boolean(field.existence_condition, source_file_name, errors, |
| "Existence condition") |
| |
| |
| def _type_name_for_error_messages(expression_type): |
| if expression_type.WhichOneof("type") == "integer": |
| return "integer" |
| elif expression_type.WhichOneof("type") == "enumeration": |
| # TODO(bolms): Should this be the fully-qualified name? |
| return expression_type.enumeration.name.canonical_name.object_path[-1] |
| assert False, "Shouldn't be here." |
| |
| |
| 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_parameter(runtime_parameter, source_file_name, errors): |
| """Checks the type of a parameter to a physical type.""" |
| if runtime_parameter.type.WhichOneof("type") not in ("integer", |
| "enumeration"): |
| errors.append([ |
| error.error(source_file_name, |
| runtime_parameter.physical_type_alias.source_location, |
| "Runtime parameters must be integer or enum.") |
| ]) |
| |
| |
| def annotate_types(ir): |
| """Adds type annotations to all expressions in ir. |
| |
| annotate_types adds type information to all expressions (and subexpressions) |
| in the IR. Additionally, it checks expressions for internal type consistency: |
| it will generate an error for constructs like "1 + true", where the types of |
| the operands are not accepted by the operator. |
| |
| Arguments: |
| ir: an IR to which to add type annotations |
| |
| Returns: |
| A (possibly empty) list of errors. |
| """ |
| errors = [] |
| traverse_ir.fast_traverse_ir_top_down( |
| ir, [ir_pb2.Expression], _type_check_expression, |
| skip_descendants_of={ir_pb2.Expression}, |
| parameters={"errors": errors}) |
| traverse_ir.fast_traverse_ir_top_down( |
| ir, [ir_pb2.RuntimeParameter], _annotate_parameter_type, |
| parameters={"errors": errors}) |
| return errors |
| |
| |
| def check_types(ir): |
| """Checks that expressions within the IR have the correct top-level types. |
| |
| check_types ensures that expressions at the top level have correct types; in |
| particular, it ensures that array sizes are integers ("UInt[true]" is not a |
| valid array type) and that the starts and ends of ranges are integers. |
| |
| Arguments: |
| ir: an IR to type check. |
| |
| Returns: |
| A (possibly empty) list of errors. |
| """ |
| errors = [] |
| traverse_ir.fast_traverse_ir_top_down( |
| ir, [ir_pb2.FieldLocation], _type_check_field_location, |
| parameters={"errors": errors}) |
| traverse_ir.fast_traverse_ir_top_down( |
| ir, [ir_pb2.ArrayType, ir_pb2.Expression], _type_check_array_size, |
| skip_descendants_of={ir_pb2.AtomicType}, |
| parameters={"errors": errors}) |
| traverse_ir.fast_traverse_ir_top_down( |
| ir, [ir_pb2.Field], _type_check_field_existence_condition, |
| parameters={"errors": errors}) |
| traverse_ir.fast_traverse_ir_top_down( |
| ir, [ir_pb2.RuntimeParameter], _type_check_parameter, |
| parameters={"errors": errors}) |
| traverse_ir.fast_traverse_ir_top_down( |
| ir, [ir_pb2.AtomicType], _type_check_passed_parameters, |
| parameters={"errors": errors}) |
| return errors |