blob: 603c0c0ccf42b214b9a2e7c898930a7ebdd2dc3a [file] [log] [blame]
# 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.
"""Utility functions for reading and manipulating Emboss IR."""
import operator
from compiler.util import ir_data
from compiler.util import ir_data_utils
_FIXED_SIZE_ATTRIBUTE = "fixed_size_in_bits"
def get_attribute(attribute_list, name):
"""Finds name in attribute_list and returns a AttributeValue or None."""
if not attribute_list:
return None
attribute_value = None
for attr in attribute_list:
if attr.name.text == name and not attr.is_default:
assert attribute_value is None, 'Duplicate attribute "{}".'.format(name)
attribute_value = attr.value
return attribute_value
def get_boolean_attribute(attribute_list, name, default_value=None):
"""Returns the boolean value of an attribute, if any, or default_value.
Arguments:
attribute_list: A list of attributes to search.
name: The name of the desired attribute.
default_value: A value to return if name is not found in attribute_list,
or the attribute does not have a boolean value.
Returns:
The boolean value of the requested attribute, or default_value if the
requested attribute is not found or has a non-boolean value.
"""
attribute_value = get_attribute(attribute_list, name)
if (not attribute_value or
not attribute_value.expression.HasField("boolean_constant")):
return default_value
return attribute_value.expression.boolean_constant.value
def get_integer_attribute(attribute_list, name, default_value=None):
"""Returns the integer value of an attribute, if any, or default_value.
Arguments:
attribute_list: A list of attributes to search.
name: The name of the desired attribute.
default_value: A value to return if name is not found in attribute_list,
or the attribute does not have an integer value.
Returns:
The integer value of the requested attribute, or default_value if the
requested attribute is not found or has a non-integer value.
"""
attribute_value = get_attribute(attribute_list, name)
if (not attribute_value or
attribute_value.expression.type.WhichOneof("type") != "integer" or
not is_constant(attribute_value.expression)):
return default_value
return constant_value(attribute_value.expression)
def is_constant(expression, bindings=None):
return constant_value(expression, bindings) is not None
def is_constant_type(expression_type):
"""Returns True if expression_type is inhabited by a single value."""
expression_type = ir_data_utils.reader(expression_type)
return (expression_type.integer.modulus == "infinity" or
expression_type.boolean.HasField("value") or
expression_type.enumeration.HasField("value"))
def constant_value(expression, bindings=None):
"""Evaluates expression with the given bindings."""
if expression is None:
return None
expression = ir_data_utils.reader(expression)
if expression.WhichOneof("expression") == "constant":
return int(expression.constant.value or 0)
elif expression.WhichOneof("expression") == "constant_reference":
# We can't look up the constant reference without the IR, but by the time
# constant_value is called, the actual values should have been propagated to
# the type information.
if expression.type.WhichOneof("type") == "integer":
assert expression.type.integer.modulus == "infinity"
return int(expression.type.integer.modular_value)
elif expression.type.WhichOneof("type") == "boolean":
assert expression.type.boolean.HasField("value")
return expression.type.boolean.value
elif expression.type.WhichOneof("type") == "enumeration":
assert expression.type.enumeration.HasField("value")
return int(expression.type.enumeration.value)
else:
assert False, "Unexpected expression type {}".format(
expression.type.WhichOneof("type"))
elif expression.WhichOneof("expression") == "function":
return _constant_value_of_function(expression.function, bindings)
elif expression.WhichOneof("expression") == "field_reference":
return None
elif expression.WhichOneof("expression") == "boolean_constant":
return expression.boolean_constant.value
elif expression.WhichOneof("expression") == "builtin_reference":
name = expression.builtin_reference.canonical_name.object_path[0]
if bindings and name in bindings:
return bindings[name]
else:
return None
elif expression.WhichOneof("expression") is None:
return None
else:
assert False, "Unexpected expression kind {}".format(
expression.WhichOneof("expression"))
def _constant_value_of_function(function, bindings):
"""Returns the constant value of evaluating `function`, or None."""
values = [constant_value(arg, bindings) for arg in function.args]
# Expressions like `$is_statically_sized && 1 <= $static_size_in_bits <= 64`
# should return False, not None, if `$is_statically_sized` is false, even
# though `$static_size_in_bits` is unknown.
#
# The easiest way to allow this is to use a three-way logic chart for each;
# specifically:
#
# AND: True False Unknown
# +--------------------------
# True | True False Unknown
# False | False False False
# Unknown | Unknown False Unknown
#
# OR: True False Unknown
# +--------------------------
# True | True True True
# False | True False Unknown
# Unknown | True Unknown Unknown
#
# This raises the question of just how many constant-from-nonconstant
# expressions Emboss should support. There are, after all, a vast number of
# constant expression patterns built from non-constant subexpressions, such as
# `0 * X` or `X == X` or `3 * X == X + X + X`. I (bolms@) am not implementing
# any further special cases because I do not see any practical use for them.
if function.function == ir_data.FunctionMapping.UNKNOWN:
return None
if function.function == ir_data.FunctionMapping.AND:
if any(value is False for value in values):
return False
elif any(value is None for value in values):
return None
else:
return True
elif function.function == ir_data.FunctionMapping.OR:
if any(value is True for value in values):
return True
elif any(value is None for value in values):
return None
else:
return False
elif function.function == ir_data.FunctionMapping.CHOICE:
if values[0] is None:
return None
else:
return values[1] if values[0] else values[2]
# Other than the logical operators and choice operator, the result of any
# function on an unknown value is, itself, considered unknown.
if any(value is None for value in values):
return None
functions = {
ir_data.FunctionMapping.ADDITION: operator.add,
ir_data.FunctionMapping.SUBTRACTION: operator.sub,
ir_data.FunctionMapping.MULTIPLICATION: operator.mul,
ir_data.FunctionMapping.EQUALITY: operator.eq,
ir_data.FunctionMapping.INEQUALITY: operator.ne,
ir_data.FunctionMapping.LESS: operator.lt,
ir_data.FunctionMapping.LESS_OR_EQUAL: operator.le,
ir_data.FunctionMapping.GREATER: operator.gt,
ir_data.FunctionMapping.GREATER_OR_EQUAL: operator.ge,
# Python's max([1, 2]) == 2; max(1, 2) == 2; max([1]) == 1; but max(1)
# throws a TypeError ("'int' object is not iterable").
ir_data.FunctionMapping.MAXIMUM: lambda *x: max(x),
}
return functions[function.function](*values)
def _hashable_form_of_name(name):
return (name.module_file,) + tuple(name.object_path)
def hashable_form_of_reference(reference):
"""Returns a representation of reference that can be used as a dict key.
Arguments:
reference: An ir_data.Reference or ir_data.NameDefinition.
Returns:
A tuple of the module_file and object_path.
"""
return _hashable_form_of_name(reference.canonical_name)
def hashable_form_of_field_reference(field_reference):
"""Returns a representation of field_reference that can be used as a dict key.
Arguments:
field_reference: An ir_data.FieldReference
Returns:
A tuple of tuples of the module_files and object_paths.
"""
return tuple(_hashable_form_of_name(reference.canonical_name)
for reference in field_reference.path)
def is_array(type_ir):
"""Returns true if type_ir is an array type."""
return type_ir.HasField("array_type")
def _find_path_in_structure_field(path, field):
if not path:
return field
return None
def _find_path_in_structure(path, type_definition):
for field in type_definition.structure.field:
if field.name.name.text == path[0]:
return _find_path_in_structure_field(path[1:], field)
return None
def _find_path_in_enumeration(path, type_definition):
if len(path) != 1:
return None
for value in type_definition.enumeration.value:
if value.name.name.text == path[0]:
return value
return None
def _find_path_in_parameters(path, type_definition):
if len(path) > 1 or not type_definition.HasField("runtime_parameter"):
return None
for parameter in type_definition.runtime_parameter:
if ir_data_utils.reader(parameter).name.name.text == path[0]:
return parameter
return None
def _find_path_in_type_definition(path, type_definition):
"""Finds the object with the given path in the given type_definition."""
if not path:
return type_definition
obj = _find_path_in_parameters(path, type_definition)
if obj:
return obj
if type_definition.HasField("structure"):
obj = _find_path_in_structure(path, type_definition)
elif type_definition.HasField("enumeration"):
obj = _find_path_in_enumeration(path, type_definition)
if obj:
return obj
else:
return _find_path_in_type_list(path, type_definition.subtype or [])
def _find_path_in_type_list(path, type_list):
for type_definition in type_list:
if type_definition.name.name.text == path[0]:
return _find_path_in_type_definition(path[1:], type_definition)
return None
def _find_path_in_module(path, module_ir):
if not path:
return module_ir
return _find_path_in_type_list(path, module_ir.type)
def find_object_or_none(name, ir):
"""Finds the object with the given canonical name, if it exists.."""
if (isinstance(name, ir_data.Reference) or
isinstance(name, ir_data.NameDefinition)):
path = _hashable_form_of_name(name.canonical_name)
elif isinstance(name, ir_data.CanonicalName):
path = _hashable_form_of_name(name)
else:
path = name
for module in ir.module:
if module.source_file_name == path[0]:
return _find_path_in_module(path[1:], module)
return None
def find_object(name, ir):
"""Finds the IR of the type, field, or value with the given canonical name."""
result = find_object_or_none(name, ir)
assert result is not None, "Bad reference {}".format(name)
return result
def find_parent_object(name, ir):
"""Finds the parent object of the object with the given canonical name."""
if (isinstance(name, ir_data.Reference) or
isinstance(name, ir_data.NameDefinition)):
path = _hashable_form_of_name(name.canonical_name)
elif isinstance(name, ir_data.CanonicalName):
path = _hashable_form_of_name(name)
else:
path = name
return find_object(path[:-1], ir)
def get_base_type(type_ir):
"""Returns the base type of the given type.
Arguments:
type_ir: IR of a type reference.
Returns:
If type_ir corresponds to an atomic type (like "UInt"), returns type_ir. If
type_ir corresponds to an array type (like "UInt:8[12]" or "Square[8][8]"),
returns the type after stripping off the array types ("UInt" or "Square").
"""
while type_ir.HasField("array_type"):
type_ir = type_ir.array_type.base_type
assert type_ir.HasField("atomic_type"), (
"Unknown kind of type {}".format(type_ir))
return type_ir
def fixed_size_of_type_in_bits(type_ir, ir):
"""Returns the fixed, known size for the given type, in bits, or None.
Arguments:
type_ir: The IR of a type.
ir: A complete IR, used to resolve references to types.
Returns:
size if the size of the type can be determined, otherwise None.
"""
array_multiplier = 1
while type_ir.HasField("array_type"):
if type_ir.array_type.WhichOneof("size") == "automatic":
return None
else:
assert type_ir.array_type.WhichOneof("size") == "element_count", (
'Expected array size to be "automatic" or "element_count".')
element_count = type_ir.array_type.element_count
if not is_constant(element_count):
return None
else:
array_multiplier *= constant_value(element_count)
assert not type_ir.HasField("size_in_bits"), (
"TODO(bolms): implement explicitly-sized arrays")
type_ir = type_ir.array_type.base_type
assert type_ir.HasField("atomic_type"), "Unexpected type!"
if type_ir.HasField("size_in_bits"):
size = constant_value(type_ir.size_in_bits)
else:
type_definition = find_object(type_ir.atomic_type.reference, ir)
size_attr = get_attribute(type_definition.attribute, _FIXED_SIZE_ATTRIBUTE)
if not size_attr:
return None
size = constant_value(size_attr.expression)
return size * array_multiplier
def field_is_virtual(field_ir):
"""Returns true if the field is virtual."""
# TODO(bolms): Should there be a more explicit indicator that a field is
# virtual?
return not field_ir.HasField("location")
def field_is_read_only(field_ir):
"""Returns true if the field is read-only."""
# For now, all virtual fields are read-only, and no non-virtual fields are
# read-only.
return ir_data_utils.reader(field_ir).write_method.read_only