blob: ed2ce7a278bdb2495e40409ad565dd1bb4e19d5e [file] [log] [blame]
# 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