| # Copyright lowRISC contributors. |
| # Licensed under the Apache License, Version 2.0, see LICENSE for details. |
| # SPDX-License-Identifier: Apache-2.0 |
| |
| '''Code for handling instruction encoding schemes''' |
| |
| import re |
| from typing import Dict, List, Optional, Set |
| |
| from serialize.parse_helpers import (check_keys, |
| check_str, check_list, index_list) |
| |
| from .bit_ranges import BitRanges |
| from .bool_literal import BoolLiteral |
| |
| |
| class EncSchemeField: |
| '''Represents a single field in an encoding scheme''' |
| def __init__(self, |
| bits: BitRanges, |
| value: Optional[BoolLiteral]) -> None: |
| self.bits = bits |
| self.value = value |
| |
| @staticmethod |
| def from_yaml(yml: object, what: str) -> 'EncSchemeField': |
| # This is either represented as a dict in the YAML or as a bare string. |
| bits_what = 'bits for {}'.format(what) |
| value_what = 'value for {}'.format(what) |
| |
| if isinstance(yml, dict): |
| yd = check_keys(yml, what, ['bits'], ['value']) |
| |
| bits_yml = yd['bits'] |
| if not (isinstance(bits_yml, str) or isinstance(bits_yml, int)): |
| raise ValueError('{} is of type {}, not a string or int.' |
| .format(bits_what, type(bits_yml).__name__)) |
| |
| # We require value to be given as a string because it's supposed to |
| # be in base 2, and PyYAML will parse 111 as one-hundred and |
| # eleven, 011 as 9 and 0x11 as 17. Aargh! |
| raw_value = None |
| val_yml = yd.get('value') |
| if val_yml is not None: |
| if not isinstance(val_yml, str): |
| raise ValueError("{} is of type {}, but must be a string " |
| "(we don't allow automatic conversion " |
| "because YAML's int conversion assumes " |
| "base 10 and value should be in base 2)." |
| .format(value_what, |
| type(val_yml).__name__)) |
| raw_value = val_yml |
| |
| elif isinstance(yml, str) or isinstance(yml, int): |
| bits_yml = yml |
| raw_value = None |
| else: |
| raise ValueError('{} is a {}, but should be a ' |
| 'dict, string or integer.' |
| .format(what, type(yml).__name__)) |
| |
| # The bits field is usually parsed as a string ("10-4", or similar). |
| # But if it's a bare integer then YAML will parse it as an int. That's |
| # fine, but we turn it back into a string to be re-parsed by BitRanges. |
| assert isinstance(bits_yml, str) or isinstance(bits_yml, int) |
| |
| bits = BitRanges.from_yaml(str(bits_yml), bits_what) |
| value = None |
| if raw_value is not None: |
| value = BoolLiteral.from_string(raw_value, value_what) |
| if bits.width != value.width: |
| raise ValueError('{} has bits that imply a width of {}, but ' |
| 'a value with width {}.' |
| .format(what, bits.width, value.width)) |
| |
| return EncSchemeField(bits, value) |
| |
| |
| class EncSchemeImport: |
| '''An object representing inheritance of a parent scheme |
| |
| When importing a parent scheme, we can set some of its fields with |
| immediate values. These are stored in the settings field. |
| |
| ''' |
| def __init__(self, yml: object, importer_name: str) -> None: |
| as_str = check_str(yml, |
| 'value for import in encoding scheme {!r}' |
| .format(importer_name)) |
| |
| # The supported syntax is |
| # |
| # - parent0(field0=b111, field1=b10) |
| # - parent1() |
| # - parent2 |
| |
| match = re.match(r'([^ (]+)[ ]*(?:\(([^)]+)\))?$', as_str) |
| if not match: |
| raise ValueError('Malformed encoding scheme ' |
| 'inheritance by scheme {!r}: {!r}.' |
| .format(importer_name, as_str)) |
| |
| self.parent = match.group(1) |
| self.settings = {} # type: Dict[str, BoolLiteral] |
| |
| when = ('When inheriting from {!r} in encoding scheme {!r}' |
| .format(self.parent, importer_name)) |
| |
| if match.group(2) is not None: |
| args = match.group(2).split(',') |
| for arg in args: |
| arg = arg.strip() |
| arg_parts = arg.split('=') |
| if len(arg_parts) != 2: |
| raise ValueError('{}, found an argument with {} ' |
| 'equals signs (should have exactly one).' |
| .format(when, len(arg_parts) - 1)) |
| |
| field_name = arg_parts[0] |
| field_what = ('literal value for field {!r} when inheriting ' |
| 'from {!r} in encoding scheme {!r}' |
| .format(arg_parts[0], |
| self.parent, importer_name)) |
| field_value = BoolLiteral.from_string(arg_parts[1], field_what) |
| |
| if field_name in self.settings: |
| raise ValueError('{}, found multiple arguments assigning ' |
| 'values to the field {!r}.' |
| .format(when, field_name)) |
| |
| self.settings[field_name] = field_value |
| |
| def apply_settings(self, |
| esf: 'EncSchemeFields', what: str) -> 'EncSchemeFields': |
| # Copy and set values in anything that has a setting |
| fields = {} |
| for name, literal in self.settings.items(): |
| old_field = esf.fields.get(name) |
| if old_field is None: |
| raise ValueError('{} sets unknown field {!r} from {!r}.' |
| .format(what, name, self.parent)) |
| |
| if old_field.bits.width != literal.width: |
| raise ValueError('{} sets field {!r} from {!r} with a literal ' |
| 'of width {}, but the field has width {}.' |
| .format(what, name, self.parent, |
| literal.width, old_field.bits.width)) |
| |
| fields[name] = EncSchemeField(old_field.bits, literal) |
| |
| # Copy anything else |
| op_fields = set() |
| for name, old_field in esf.fields.items(): |
| if name in fields: |
| continue |
| op_fields.add(name) |
| fields[name] = old_field |
| |
| return EncSchemeFields(fields, op_fields, esf.mask) |
| |
| |
| class EncSchemeFields: |
| '''An object representing some fields in an encoding scheme''' |
| def __init__(self, |
| fields: Dict[str, EncSchemeField], |
| op_fields: Set[str], |
| mask: int) -> None: |
| self.fields = fields |
| self.op_fields = op_fields |
| self.mask = mask |
| |
| @staticmethod |
| def empty() -> 'EncSchemeFields': |
| return EncSchemeFields({}, set(), 0) |
| |
| @staticmethod |
| def from_yaml(yml: object, name: str) -> 'EncSchemeFields': |
| if not isinstance(yml, dict): |
| raise ValueError('fields for encoding scheme {!r} should be a ' |
| 'dict, but we saw a {}.' |
| .format(name, type(yml).__name__)) |
| |
| fields = {} |
| op_fields = set() # type: Set[str] |
| mask = 0 |
| |
| overlaps = 0 |
| |
| for key, val in yml.items(): |
| if not isinstance(key, str): |
| raise ValueError('{!r} is a bad key for a field name of ' |
| 'encoding scheme {} (should be str, not {}).' |
| .format(key, name, type(key).__name__)) |
| |
| fld_what = 'field {!r} of encoding scheme {}'.format(key, name) |
| field = EncSchemeField.from_yaml(val, fld_what) |
| |
| overlaps |= mask & field.bits.mask |
| mask |= field.bits.mask |
| |
| fields[key] = field |
| if field.value is None: |
| op_fields.add(key) |
| |
| if overlaps: |
| raise ValueError('Direct fields for encoding scheme {} have ' |
| 'overlapping ranges (mask: {:#08x})' |
| .format(name, overlaps)) |
| |
| return EncSchemeFields(fields, op_fields, mask) |
| |
| def merge_in(self, right: 'EncSchemeFields', when: str) -> None: |
| for name, field in right.fields.items(): |
| if name in self.fields: |
| raise ValueError('Duplicate field name: {!r} {}.' |
| .format(name, when)) |
| |
| overlap = self.mask & field.bits.mask |
| if overlap: |
| raise ValueError('Overlapping bit ranges ' |
| '(masks: {:08x} and {:08x} have ' |
| 'intersection {:08x}) {}.' |
| .format(self.mask, |
| field.bits.mask, overlap, when)) |
| |
| self.fields[name] = field |
| self.mask |= field.bits.mask |
| if field.value is None: |
| assert name not in self.op_fields |
| self.op_fields.add(name) |
| |
| |
| class EncScheme: |
| def __init__(self, yml: object, name: str) -> None: |
| what = 'encoding scheme {!r}'.format(name) |
| yd = check_keys(yml, what, [], ['parents', 'fields']) |
| |
| if not yd: |
| raise ValueError('{} has no parents or fields.'.format(what)) |
| |
| fields_yml = yd.get('fields') |
| self.direct_fields = (EncSchemeFields.from_yaml(fields_yml, name) |
| if fields_yml is not None |
| else EncSchemeFields.empty()) |
| |
| parents_yml = yd.get('parents') |
| parents_what = 'parents of {}'.format(what) |
| parents = ([EncSchemeImport(y, name) |
| for y in check_list(parents_yml, parents_what)] |
| if parents_yml is not None |
| else []) |
| self.parents = index_list(parents_what, |
| parents, |
| lambda imp: imp.parent) |
| |
| |
| class EncSchemes: |
| def __init__(self, yml: object) -> None: |
| if not isinstance(yml, dict): |
| raise ValueError("value for encoding-schemes is expected to be " |
| "a dict, but was actually a {}." |
| .format(type(yml).__name__)) |
| |
| self.schemes = {} # type: Dict[str, EncScheme] |
| self.resolved = {} # type: Dict[str, EncSchemeFields] |
| |
| for key, val in yml.items(): |
| if not isinstance(key, str): |
| raise ValueError('{!r} is a bad key for an encoding scheme ' |
| 'name (should be str, not {}).' |
| .format(key, type(key).__name__)) |
| self.schemes[key] = EncScheme(val, key) |
| |
| def _resolve(self, |
| name: str, |
| user: str, |
| stack: List[str]) -> EncSchemeFields: |
| # Have we resolved this before? |
| resolved = self.resolved.get(name) |
| if resolved is not None: |
| return resolved |
| |
| # Spot any circular inheritance |
| if name in stack: |
| raise RuntimeError('Circular inheritance of encoding ' |
| 'schemes: {}' |
| .format(' -> '.join(stack + [name]))) |
| |
| # Does the scheme actually exist? |
| scheme = self.schemes.get(name) |
| if scheme is None: |
| raise ValueError('{} requires undefined encoding scheme {!r}.' |
| .format(user, name)) |
| |
| # Recursively try to resolve each parent scheme, applying any import |
| # settings |
| resolved_parents = {} |
| new_stack = stack + [name] |
| what = 'Import list of encoding scheme {!r}'.format(name) |
| for pname, pimport in scheme.parents.items(): |
| resolved = self._resolve(pimport.parent, what, new_stack) |
| resolved_parents[pname] = pimport.apply_settings(resolved, what) |
| |
| # Now try to merge the resolved imports |
| merged = EncSchemeFields.empty() |
| parent_names_so_far = [] # type: List[str] |
| for pname, pfields in resolved_parents.items(): |
| when = ('merging fields of scheme {} into ' |
| 'already merged fields of {}' |
| .format(pname, ', '.join(parent_names_so_far))) |
| merged.merge_in(pfields, when) |
| parent_names_so_far.append(repr(pname)) |
| |
| # Now try to merge in any direct fields |
| when = ('merging direct fields of scheme {} into fields from parents' |
| .format(name)) |
| merged.merge_in(scheme.direct_fields, when) |
| |
| return merged |
| |
| def resolve(self, name: str, mnemonic: str) -> EncSchemeFields: |
| fields = self._resolve(name, 'Instruction {!r}'.format(mnemonic), []) |
| |
| # Check completeness |
| missing = ((1 << 32) - 1) & ~fields.mask |
| if missing: |
| raise ValueError('Fields for encoding scheme {} miss some bits ' |
| '(mask: {:#08x})' |
| .format(name, missing)) |
| |
| return fields |