diff --git a/emulation/rocjitsu/docs/codegen.md b/emulation/rocjitsu/docs/codegen.md
index df561288aa68..f81918c181c9 100644
--- a/emulation/rocjitsu/docs/codegen.md
+++ b/emulation/rocjitsu/docs/codegen.md
@@ -39,6 +39,85 @@ while its installed dependencies come from the environment.
rocm-systems/shared/machine-readable-isa/isa/
```
+## ISA additions
+
+Repository-owned ISA additions can be supplied without modifying the public MR
+ISA XML. Each ISA additions document has this shape:
+
+```xml
+
+
+
+ ENC_EXAMPLE
+ 7
+ ...complete identifier...
+
+
+
+
+
+
+
+
+```
+
+Apply one or more additions documents with a logical ISA name. Repeated
+documents for one ISA are processed in command-line and document order:
+
+```bash
+python -m amdisa \
+ cdna5:/path/to/amdgpu_isa_cdna5.xml \
+ --isa-additions cdna5:/path/to/first-additions.xml \
+ --isa-additions cdna5:/path/to/second-additions.xml \
+ --isa-output /path/to/isa-output \
+ --dbt-output /path/to/dbt-output
+```
+
+The `EncodingIdentifierAdditions` element is optional. `InstructionAdditions`
+is required and may be empty. The additions contract is intentionally narrower
+than a general XML patch:
+
+- Only complete `` nodes and complete identifiers for an existing
+ encoding's `` list are accepted. An additions document
+ cannot replace or delete base definitions, identifier masks, microcode
+ formats, encoding conditions, operand types, or data formats.
+- Added instructions must reference encodings, encoding conditions, operand
+ types, and operand fields already defined by the applicable base encoding
+ layout. Implied-literal operands outside that layout are accepted only when
+ their normalized field, type, direction, and parser-role flags match an
+ implied-literal contract established by the base XML.
+- An identifier addition names its encoding and expected opcode. Its radix and
+ width must match the base encoding; its fixed bits must match an existing
+ identifier layout; and its decoded opcode must equal the declared opcode.
+ Duplicate and colliding decode slots are rejected. Each identifier must be
+ owned by an added instruction. An implied-literal identifier must have the
+ same instruction owner and opcode in its parent encoding.
+- Additions document IDs and instruction names must be valid and unique.
+ Encoding/opcode ownership collisions between different instructions,
+ repeated instruction forms, multiple active forms that generate the same C++
+ instruction symbol, out-of-range opcodes, missing references, unknown root
+ data, and base architecture/schema mismatches are errors. Repetition of one
+ instruction's slot under distinct base-established MR-ISA encoding conditions
+ is allowed only when profile filtering leaves at most one generated form
+ active. Every added instruction must retain at least one active form.
+- Every input document and contained addition validates before the parser-owned
+ tree is changed. Validated identifiers are merged before normal
+ encoding/decode-table parsing, then instructions are appended in deterministic
+ file/document order. A bad later file cannot leave a partially merged
+ specification, and an active added instruction whose final opcode pointer
+ would remain unpopulated is an error.
+- An empty additions document leaves generated output byte-identical to the base
+ XML.
+
+The parsed `IsaSpec.applied_additions` and each added
+`Instruction.source_addition` retain the source document's provenance for later
+generator stages.
+Provenance does not by itself define target availability: a separate variant
+model must also be able to restrict encoding forms already present in the base
+XML.
+
## Generated file locations
| Generated files | Location | Generator |
@@ -69,7 +148,7 @@ decode/encode functions and neutral field structs are auto-generated.
## CLI reference
```
-python -m amdisa [--gen-isas] [--gen-dbt]
+python -m amdisa [--isa-additions NAME:XML] [--gen-isas] [--gen-dbt]
[--isa-output DIR] [--include-root DIR]
[--dbt-output DIR] [NAME:]XML ...
```
@@ -77,6 +156,7 @@ python -m amdisa [--gen-isas] [--gen-dbt]
| Option | Description |
|---|---|
| `[NAME:]XML ...` | One or more ISA XMLs. A recognized name selects its semantic profile. An unrecognized explicit name is treated as a custom generated identity, with its semantic profile detected from the XML |
+| `--isa-additions NAME:XML` | Apply a validated ISA additions file to the named ISA; may be repeated |
| `--gen-isas` | Generate ISA C++ files (decoders, encodings, execute bodies) |
| `--gen-dbt` | Generate DBT legalization tables and encoding translators |
| `--isa-output DIR` | Output path for generated ISA C++ files |
diff --git a/emulation/rocjitsu/lib/python/amdisa/__init__.py b/emulation/rocjitsu/lib/python/amdisa/__init__.py
index 6e87305fcf4e..4b2285f03aab 100644
--- a/emulation/rocjitsu/lib/python/amdisa/__init__.py
+++ b/emulation/rocjitsu/lib/python/amdisa/__init__.py
@@ -7,9 +7,11 @@
from amdisa.gpuisa import (
InstEncoding,
Instruction,
+ IsaAdditionProvenance,
IsaSpec,
Operand,
)
+from amdisa.isa_additions import IsaAdditionError, apply_isa_additions
from amdisa.isa_profile import (
Cdna1Profile,
Cdna2Profile,
@@ -52,6 +54,8 @@
'Cdna5Profile',
'InstEncoding',
'Instruction',
+ 'IsaAdditionError',
+ 'IsaAdditionProvenance',
'IsaProfile',
'MemoryCoherencyModel',
'MnemonicRule',
@@ -63,6 +67,7 @@
'IsaSpec',
'Operand',
'Parser',
+ 'apply_isa_additions',
'SchemaValueError',
'SchemaVersion',
'InstructionSemantics',
diff --git a/emulation/rocjitsu/lib/python/amdisa/__main__.py b/emulation/rocjitsu/lib/python/amdisa/__main__.py
index 79886547e07f..a458b2258129 100644
--- a/emulation/rocjitsu/lib/python/amdisa/__main__.py
+++ b/emulation/rocjitsu/lib/python/amdisa/__main__.py
@@ -60,6 +60,23 @@
}
+def _group_isa_additions_args(entries: list[str] | None) -> dict[str, list[str]]:
+ """Group repeated ``NAME:XML`` additions arguments by logical ISA name."""
+ grouped: dict[str, list[str]] = {}
+ for entry in entries or []:
+ if ':' not in entry:
+ raise ValueError(
+ f'--isa-additions entry must be name:xml_path, got: {entry}'
+ )
+ name, xml_path = entry.split(':', 1)
+ if not name or not xml_path:
+ raise ValueError(
+ f'--isa-additions entry must have non-empty name and path, got: {entry}'
+ )
+ grouped.setdefault(name, []).append(xml_path)
+ return grouped
+
+
def _collect_shared_execute_body_variants(specs, plan):
"""Collect candidate shared execute bodies from each ISA.
@@ -193,11 +210,35 @@ def _codegen_config(
def _run(args) -> None:
"""Parse the input XMLs and generate the requested outputs."""
- specs = []
+ try:
+ addition_xmls = _group_isa_additions_args(getattr(args, 'isa_additions', None))
+ except ValueError as error:
+ print(f'error: {error}', file=sys.stderr)
+ sys.exit(1)
+
+ parsed_inputs = []
+ logical_names = set()
for entry in args.isafiles:
name, xml_path, profile_key = _parse_isa_arg(entry)
profile = _PROFILES[profile_key]()
- spec = Parser(xml_path, profile).parse()
+ logical_name = (
+ name or profile.generated_arch_name or profile_key.replace('.', '_')
+ )
+ parsed_inputs.append((name, logical_name, xml_path, profile))
+ logical_names.add(logical_name)
+
+ unknown_addition_names = set(addition_xmls) - logical_names
+ if unknown_addition_names:
+ names = ', '.join(sorted(unknown_addition_names))
+ print(
+ f'error: --isa-additions names not present in ISA inputs: {names}',
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ specs = []
+ for name, logical_name, xml_path, profile in parsed_inputs:
+ spec = Parser(xml_path, profile, addition_xmls.get(logical_name, ())).parse()
_apply_codegen_identity(spec, name)
sem = derive_all_semantics(spec)
specs.append((spec.arch_name, spec, sem))
@@ -312,6 +353,14 @@ def main() -> None:
help='Machine-readable AMD GPU ISA XML specifications. Each path may '
'have a generated identity prefix (e.g., cdna1:/path/to/cdna1.xml).',
)
+ arg_parser.add_argument(
+ '--isa-additions',
+ action='append',
+ default=[],
+ metavar='NAME:XML',
+ help='apply an ISA additions XML file to the named ISA. May be repeated; '
+ 'files are applied in command-line order.',
+ )
arg_parser.add_argument(
'--gen-isas',
action='store_true',
diff --git a/emulation/rocjitsu/lib/python/amdisa/codegen/_generator.py b/emulation/rocjitsu/lib/python/amdisa/codegen/_generator.py
index bde9d14ade62..1e07d579e29d 100644
--- a/emulation/rocjitsu/lib/python/amdisa/codegen/_generator.py
+++ b/emulation/rocjitsu/lib/python/amdisa/codegen/_generator.py
@@ -36,6 +36,8 @@
IsaSpec,
Operand,
OperandNamePattern,
+ opcode_constant_base_name,
+ opcode_name_fragment,
)
from amdisa.fieldless_policy import (
FieldlessCategory,
@@ -475,20 +477,11 @@ def _opcode_name_fragment(token: str) -> str:
meant to be read and used directly by handwritten DBT code, so split a
few packed ISA abbreviations that commonly appear inside one XML token.
"""
- token = token.lower()
- if token.endswith('exec') and len(token) > len('exec'):
- return f'{token[:-4].capitalize()}Exec'
- if token.endswith('pc') and len(token) > len('pc'):
- return f'{token[:-2].capitalize()}Pc'
- return token.capitalize()
+ return opcode_name_fragment(token)
@classmethod
def _opcode_const_base_name(cls, mnemonic: str) -> str:
- return 'k' + ''.join(
- cls._opcode_name_fragment(token)
- for token in mnemonic.lower().split('_')
- if token
- )
+ return opcode_constant_base_name(mnemonic)
@staticmethod
def _emit_opcode_constant(name: str, value: int) -> str:
diff --git a/emulation/rocjitsu/lib/python/amdisa/gpuisa.py b/emulation/rocjitsu/lib/python/amdisa/gpuisa.py
index d8b6af252f07..1abe82ad8920 100644
--- a/emulation/rocjitsu/lib/python/amdisa/gpuisa.py
+++ b/emulation/rocjitsu/lib/python/amdisa/gpuisa.py
@@ -50,6 +50,60 @@ class MicrocodeField:
}
+def format_encoding_name(enc_name: str) -> str:
+ """Format an MR-ISA encoding name as a C++ PascalCase fragment."""
+ parts = enc_name.split('_')
+ if parts[0] == 'ENC':
+ parts = parts[1:]
+ return ''.join(part.capitalize() for part in parts)
+
+
+def format_true_encoding_name(
+ enc_name: str, *, is_implied_literal_enc: bool = False
+) -> str:
+ """Format the concrete C++ encoding class used by an instruction."""
+ if is_implied_literal_enc:
+ return enc_name.split('_')[0].capitalize()
+ return format_encoding_name(enc_name)
+
+
+def format_instruction_name(
+ name: str, enc_name: str, *, is_implied_literal_enc: bool = False
+) -> str:
+ """Format an instruction's generated C++ class/decode-function name."""
+ mnemonic = ''.join(part.capitalize() for part in name.split('_'))
+ encoding = format_true_encoding_name(
+ enc_name, is_implied_literal_enc=is_implied_literal_enc
+ )
+ return f'{mnemonic}{encoding}'
+
+
+def opcode_name_fragment(token: str) -> str:
+ """Return one C++ opcode-constant fragment for a mnemonic token."""
+ token = token.lower()
+ if token.endswith('exec') and len(token) > len('exec'):
+ return f'{token[:-4].capitalize()}Exec'
+ if token.endswith('pc') and len(token) > len('pc'):
+ return f'{token[:-2].capitalize()}Pc'
+ return token.capitalize()
+
+
+def opcode_constant_base_name(mnemonic: str) -> str:
+ """Return the generated C++ opcode-constant base for a mnemonic."""
+ return 'k' + ''.join(
+ opcode_name_fragment(token) for token in mnemonic.lower().split('_') if token
+ )
+
+
+def opcode_constant_name(
+ mnemonic: str, enc_name: str, *, is_implied_literal_enc: bool = False
+) -> str:
+ """Return the concrete encoding-qualified C++ opcode-constant name."""
+ return opcode_constant_base_name(mnemonic) + format_true_encoding_name(
+ enc_name, is_implied_literal_enc=is_implied_literal_enc
+ )
+
+
def synthesize_fieldless_name(operand_type: str) -> str:
"""Return a stable, valid C++ base identifier for a fieldless operand.
@@ -162,9 +216,7 @@ def __init__(self, enc_name: str, is_implied_literal_enc: bool = False) -> None:
@cached_property
def fmt_enc_name(self) -> str:
"""Encoding name formatted to C++ PascalCase style."""
- if self.enc_name.split('_')[0] == 'ENC':
- return ''.join(x.capitalize() for x in self.enc_name.split('_')[1:])
- return ''.join(x.capitalize() for x in self.enc_name.split('_'))
+ return format_encoding_name(self.enc_name)
@cached_property
def fmt_true_enc_name(self) -> str:
@@ -174,9 +226,9 @@ def fmt_true_enc_name(self) -> str:
their parent encoding name (e.g., ``Vop2``) because the C++ class
hierarchy inherits from the parent encoding class.
"""
- if self.is_implied_literal_enc:
- return self.enc_name.split('_')[0].capitalize()
- return self.fmt_enc_name
+ return format_true_encoding_name(
+ self.enc_name, is_implied_literal_enc=self.is_implied_literal_enc
+ )
class InstEncoding(InstBase):
@@ -243,6 +295,8 @@ class Instruction(InstBase):
classes. ``None`` means encoding provenance is unknown; modifier
generation rejects that state, so synthetic VOP instructions must
provide an explicit set.
+ source_addition: Additions provenance for a repository-supplied instruction,
+ or ``None`` when the instruction came from the base MR ISA XML.
"""
def __init__(
@@ -253,19 +307,22 @@ def __init__(
operands: list[Operand],
is_implied_literal_enc: bool = False,
available_encodings: frozenset[str] | None = None,
+ source_addition: IsaAdditionProvenance | None = None,
) -> None:
super().__init__(enc_name, is_implied_literal_enc)
self.name = name
self.opcode = opcode
self.operands = operands
self.available_encodings = available_encodings
+ self.source_addition = source_addition
@cached_property
def fmt_name(self) -> str:
"""Instruction name formatted to C++ PascalCase style."""
- return (
- f'{"".join(x.capitalize() for x in self.name.split("_"))}'
- f'{self.fmt_true_enc_name}'
+ return format_instruction_name(
+ self.name,
+ self.enc_name,
+ is_implied_literal_enc=self.is_implied_literal_enc,
)
@cached_property
@@ -332,6 +389,14 @@ class DecodeTableEntry:
decode_func: str | None = None
+@dataclass(frozen=True)
+class IsaAdditionProvenance:
+ """Identity and source path for one applied ISA additions document."""
+
+ identifier: str
+ path: str
+
+
class IsaSpec:
"""Internal representation of a machine-readable ISA spec.
@@ -357,6 +422,8 @@ class IsaSpec:
encoding conditions indicate an implied literal DWORD.
Populated during parsing using the profile's
``is_implied_literal_encoding()`` method.
+ applied_additions: Ordered provenance for ISA additions documents
+ merged into this specification.
"""
def __init__(
@@ -380,6 +447,7 @@ def __init__(
2, profile.max_enc_bits
)
self.alt_encs_with_implied_literal: set[str] = set()
+ self.applied_additions: tuple[IsaAdditionProvenance, ...] = ()
# Every fieldless operand type observed while parsing this spec.
# Consumed by fieldless_policy.validate_fieldless_taxonomy.
self.fieldless_operand_types: set[str] = set()
diff --git a/emulation/rocjitsu/lib/python/amdisa/isa_additions.py b/emulation/rocjitsu/lib/python/amdisa/isa_additions.py
new file mode 100644
index 000000000000..e22276e04802
--- /dev/null
+++ b/emulation/rocjitsu/lib/python/amdisa/isa_additions.py
@@ -0,0 +1,1407 @@
+# Copyright (c) 2026 Advanced Micro Devices, Inc.
+# SPDX-License-Identifier: MIT
+
+"""Validated instruction and encoding-identifier additions for MR ISA XMLs.
+
+The vendor MR ISA XML remains immutable. An additions document may add complete
+```` nodes and complete identifiers to an existing encoding's
+```` list. It cannot replace or delete existing data.
+"""
+
+from __future__ import annotations
+
+from collections.abc import Mapping, Sequence
+from copy import deepcopy
+from dataclasses import dataclass
+import re
+import xml.etree.ElementTree as elem_tree
+
+from amdisa import xml_schema as xs
+from amdisa.gpuisa import (
+ IsaAdditionProvenance,
+ format_instruction_name,
+ opcode_constant_name,
+)
+from amdisa.isa_profile import IsaProfile
+
+ADDITIONS_ROOT = 'IsaAdditions'
+INSTRUCTION_ADDITIONS = 'InstructionAdditions'
+IDENTIFIER_ADDITIONS = 'EncodingIdentifierAdditions'
+IDENTIFIER_ADDITION = 'EncodingIdentifierAddition'
+ADDITIONS_ID_ATTR = 'Id'
+ADDITIONS_BASE_ARCH_ATTR = 'BaseArchitecture'
+ADDITIONS_BASE_SCHEMA_ATTR = 'BaseSchemaVersion'
+ADDITION_SOURCE_ATTR = '_amdisa_addition_id'
+
+_ADDITIONS_ATTRIBUTES = {
+ ADDITIONS_ID_ATTR,
+ ADDITIONS_BASE_ARCH_ATTR,
+ ADDITIONS_BASE_SCHEMA_ATTR,
+}
+_ID_PATTERN = re.compile(r'[A-Za-z0-9][A-Za-z0-9._-]*\Z')
+_INSTRUCTION_NAME_PATTERN = re.compile(r'[A-Za-z][A-Za-z0-9_]*\Z')
+
+
+class IsaAdditionError(ValueError):
+ """An ISA additions document is malformed or conflicts with its base XML."""
+
+
+@dataclass(frozen=True)
+class _ImpliedLiteralOperandContract:
+ """Identity and role of an implied-literal operand established by the base."""
+
+ field_name: str
+ operand_type: str
+ is_input: bool
+ is_output: bool
+ is_implicit: bool
+ is_binary_microcode_required: bool
+
+
+@dataclass(frozen=True)
+class _EncodingDefinition:
+ """Base-owned encoding information needed to validate ISA additions."""
+
+ name: str
+ identifiers_node: elem_tree.Element
+ bit_width: int
+ radix: int
+ flat_encoding_slice: tuple[int, int]
+ opcode_slice: tuple[int, int]
+ dont_care_bits: int
+ opcode_bit_count: int
+ opcode_modifier_bit_count: int
+ base_layout_signatures: frozenset[str]
+ base_encoding_values: frozenset[int]
+ identifier_texts: frozenset[str]
+ decoded_opcodes: frozenset[int]
+ condition_ids: dict[str, frozenset[str]]
+ operand_field_names: frozenset[str]
+ implied_literal_operand_contracts: set[_ImpliedLiteralOperandContract]
+ parent_name: str | None
+ is_implied_literal: bool
+
+ @property
+ def opcode_limit(self) -> int:
+ return 1 << (self.opcode_bit_count + self.opcode_modifier_bit_count)
+
+
+@dataclass(frozen=True)
+class _InstructionAddition:
+ node: elem_tree.Element
+ name: str
+ path: str
+ addition_id: str
+ forms: tuple[tuple[str, int, str], ...]
+
+
+@dataclass(frozen=True)
+class _IdentifierAddition:
+ node: elem_tree.Element
+ encoding: _EncodingDefinition
+ opcode: int
+
+
+def parse_encoding_identifier_mask(
+ enc_id_mask: str,
+ max_enc_bits: int,
+ enc_field_bit_cnt: int,
+ op_field_bit_cnt: int,
+) -> tuple[tuple[int, int], tuple[int, int], int]:
+ """Derive primary-encoding and opcode slices from an identifier mask."""
+ bit_masks = [
+ (match.start(), match.end()) for match in re.finditer(r'1+', enc_id_mask)
+ ]
+ if not bit_masks:
+ raise IsaAdditionError('encoding identifier mask contains no set bits')
+ flat_enc_mask = bit_masks[0]
+ if len(bit_masks) == 1:
+ op_mask = (
+ bit_masks[0][0] + enc_field_bit_cnt,
+ bit_masks[0][0] + enc_field_bit_cnt + op_field_bit_cnt,
+ )
+ if (flat_enc_mask[1] - flat_enc_mask[0]) > max_enc_bits:
+ flat_enc_mask = (
+ flat_enc_mask[0],
+ flat_enc_mask[0] + max_enc_bits,
+ )
+ else:
+ op_mask = (
+ bit_masks[1][0],
+ bit_masks[1][0] + op_field_bit_cnt,
+ )
+ dont_care_bits = max_enc_bits - (flat_enc_mask[1] - flat_enc_mask[0])
+ if dont_care_bits < 0:
+ raise IsaAdditionError(
+ 'encoding identifier mask exceeds the primary decode width'
+ )
+ return flat_enc_mask, op_mask, dont_care_bits
+
+
+def _required_text(parent: elem_tree.Element, tag: str, context: str) -> str:
+ node = parent.find(tag)
+ if node is None or node.text is None or not node.text.strip():
+ raise IsaAdditionError(f'{context}: missing non-empty <{tag}>')
+ return node.text.strip()
+
+
+def _required_decimal(text: str, context: str) -> int:
+ try:
+ value = int(text)
+ except ValueError as error:
+ raise IsaAdditionError(f'{context}: invalid decimal value {text!r}') from error
+ return value
+
+
+def _implied_literal_operand_contract(
+ operand: elem_tree.Element,
+ enc_name: str,
+ profile: IsaProfile,
+ context: str,
+) -> _ImpliedLiteralOperandContract:
+ raw_field_name = _required_text(operand, xs.FIELD_NAME, context).lower()
+ operand_type = _required_text(operand, xs.OPERAND_TYPE, context)
+ return _ImpliedLiteralOperandContract(
+ field_name=profile.normalize_operand_field_name(enc_name, raw_field_name),
+ operand_type=profile.normalize_operand_type(
+ enc_name, raw_field_name, operand_type
+ ),
+ is_input=operand.attrib[xs.OPERAND_ATTR_INPUT].lower() == 'true',
+ is_output=operand.attrib[xs.OPERAND_ATTR_OUTPUT].lower() == 'true',
+ is_implicit=operand.attrib[xs.OPERAND_ATTR_IS_IMPLICIT].lower() == 'true',
+ is_binary_microcode_required=(
+ operand.attrib[xs.OPERAND_ATTR_IS_BINARY_MICROCODE_REQUIRED].lower()
+ == 'true'
+ ),
+ )
+
+
+def _validate_node_shape(
+ node: elem_tree.Element,
+ context: str,
+ *,
+ allowed_attributes: frozenset[str] = frozenset(),
+ required_attributes: frozenset[str] = frozenset(),
+ child_counts: Mapping[str, tuple[int, int | None]] | None = None,
+) -> None:
+ unknown_attributes = set(node.attrib) - allowed_attributes
+ if unknown_attributes:
+ names = ', '.join(sorted(unknown_attributes))
+ raise IsaAdditionError(f'{context}: unknown attributes: {names}')
+ missing_attributes = required_attributes - set(node.attrib)
+ if missing_attributes:
+ names = ', '.join(sorted(missing_attributes))
+ raise IsaAdditionError(f'{context}: missing required attributes: {names}')
+
+ allowed_children = child_counts or {}
+ unknown_children = [
+ child.tag for child in node if child.tag not in allowed_children
+ ]
+ if unknown_children:
+ names = ', '.join(f'<{name}>' for name in unknown_children)
+ raise IsaAdditionError(f'{context}: unknown elements: {names}')
+ for tag, (minimum, maximum) in allowed_children.items():
+ count = sum(child.tag == tag for child in node)
+ if count < minimum or (maximum is not None and count > maximum):
+ if minimum == maximum:
+ expected = f'exactly {minimum}'
+ elif maximum is None:
+ expected = f'at least {minimum}'
+ else:
+ expected = f'between {minimum} and {maximum}'
+ raise IsaAdditionError(
+ f'{context}: expected {expected} <{tag}> element(s), found {count}'
+ )
+
+
+def _validate_text_leaf(
+ node: elem_tree.Element,
+ context: str,
+ *,
+ allowed_attributes: frozenset[str] = frozenset(),
+ required_attributes: frozenset[str] = frozenset(),
+) -> str:
+ _validate_node_shape(
+ node,
+ context,
+ allowed_attributes=allowed_attributes,
+ required_attributes=required_attributes,
+ )
+ if node.text is None or not node.text.strip():
+ raise IsaAdditionError(f'{context}: value must not be empty')
+ return node.text.strip()
+
+
+def _validate_boolean_text(node: elem_tree.Element, context: str) -> None:
+ value = _validate_text_leaf(node, context).lower()
+ if value not in {'true', 'false'}:
+ raise IsaAdditionError(
+ f'{context}: expected boolean true or false, found {value!r}'
+ )
+
+
+def _validate_instruction_structure(
+ instruction: elem_tree.Element, context: str
+) -> None:
+ """Validate one complete Instruction subtree accepted by Parser.parse_insts."""
+ _validate_node_shape(
+ instruction,
+ context,
+ child_counts={
+ xs.INST_FLAGS: (1, 1),
+ xs.INST_NAME: (1, 1),
+ 'AliasedInstructionNames': (0, 1),
+ 'Description': (1, 1),
+ xs.INST_ENCODINGS: (1, 1),
+ 'FunctionalGroup': (1, 1),
+ },
+ )
+
+ flags = instruction.find(xs.INST_FLAGS)
+ assert flags is not None
+ flag_names = (
+ 'IsBranch',
+ 'IsConditionalBranch',
+ 'IsIndirectBranch',
+ 'IsProgramTerminator',
+ 'IsImmediatelyExecuted',
+ )
+ _validate_node_shape(
+ flags,
+ f'{context}/<{xs.INST_FLAGS}>',
+ child_counts={name: (1, 1) for name in flag_names},
+ )
+ for name in flag_names:
+ node = flags.find(name)
+ assert node is not None
+ _validate_boolean_text(node, f'{context}/<{xs.INST_FLAGS}>/<{name}>')
+
+ name_node = instruction.find(xs.INST_NAME)
+ description = instruction.find('Description')
+ assert name_node is not None and description is not None
+ _validate_text_leaf(name_node, f'{context}/<{xs.INST_NAME}>')
+ _validate_text_leaf(description, f'{context}/')
+
+ aliases = instruction.find('AliasedInstructionNames')
+ if aliases is not None:
+ _validate_node_shape(
+ aliases,
+ f'{context}/',
+ child_counts={xs.INST_NAME: (1, None)},
+ )
+ for alias in aliases:
+ _validate_text_leaf(
+ alias, f'{context}//<{xs.INST_NAME}>'
+ )
+
+ encodings = instruction.find(xs.INST_ENCODINGS)
+ assert encodings is not None
+ _validate_node_shape(
+ encodings,
+ f'{context}/<{xs.INST_ENCODINGS}>',
+ child_counts={xs.INST_ENCODING: (1, None)},
+ )
+ for encoding in encodings:
+ encoding_context = f'{context}/<{xs.INST_ENCODING}>'
+ _validate_node_shape(
+ encoding,
+ encoding_context,
+ child_counts={
+ xs.ENCODING_NAME: (1, 1),
+ xs.ENCODING_COND: (1, 1),
+ xs.OPCODE: (1, 1),
+ xs.OPERANDS: (1, 1),
+ },
+ )
+ encoding_name = encoding.find(xs.ENCODING_NAME)
+ condition = encoding.find(xs.ENCODING_COND)
+ opcode = encoding.find(xs.OPCODE)
+ operands = encoding.find(xs.OPERANDS)
+ assert (
+ encoding_name is not None
+ and condition is not None
+ and opcode is not None
+ and operands is not None
+ )
+ _validate_text_leaf(encoding_name, f'{encoding_context}/<{xs.ENCODING_NAME}>')
+ _validate_text_leaf(
+ condition,
+ f'{encoding_context}/<{xs.ENCODING_COND}>',
+ allowed_attributes=frozenset({'Id'}),
+ )
+ _validate_text_leaf(
+ opcode,
+ f'{encoding_context}/<{xs.OPCODE}>',
+ allowed_attributes=frozenset({xs.ENC_IDENTIFER_ATTR_RADIX}),
+ required_attributes=frozenset({xs.ENC_IDENTIFER_ATTR_RADIX}),
+ )
+ if opcode.attrib[xs.ENC_IDENTIFER_ATTR_RADIX] != '10':
+ raise IsaAdditionError(
+ f'{encoding_context}/<{xs.OPCODE}>: Radix must be 10'
+ )
+ _validate_node_shape(
+ operands,
+ f'{encoding_context}/<{xs.OPERANDS}>',
+ child_counts={xs.OPERAND: (0, None)},
+ )
+ for operand in operands:
+ operand_context = f'{encoding_context}/<{xs.OPERANDS}>/<{xs.OPERAND}>'
+ operand_attributes = frozenset(
+ {
+ xs.OPERAND_ATTR_INPUT,
+ xs.OPERAND_ATTR_OUTPUT,
+ xs.OPERAND_ATTR_IS_IMPLICIT,
+ xs.OPERAND_ATTR_IS_BINARY_MICROCODE_REQUIRED,
+ xs.OPERAND_ATTR_ORDER,
+ }
+ )
+ _validate_node_shape(
+ operand,
+ operand_context,
+ allowed_attributes=operand_attributes,
+ required_attributes=operand_attributes,
+ child_counts={
+ xs.FIELD_NAME: (0, 1),
+ xs.DATA_FORMAT_NAME: (1, 1),
+ xs.OPERAND_TYPE: (1, 1),
+ xs.OPERAND_SIZE: (1, 1),
+ },
+ )
+ for attribute in (
+ xs.OPERAND_ATTR_INPUT,
+ xs.OPERAND_ATTR_OUTPUT,
+ xs.OPERAND_ATTR_IS_IMPLICIT,
+ xs.OPERAND_ATTR_IS_BINARY_MICROCODE_REQUIRED,
+ ):
+ value = operand.attrib[attribute].lower()
+ if value not in {'true', 'false'}:
+ raise IsaAdditionError(
+ f'{operand_context}: attribute {attribute} must be '
+ f'true or false, found {value!r}'
+ )
+ order = _required_decimal(
+ operand.attrib[xs.OPERAND_ATTR_ORDER],
+ f'{operand_context} Order',
+ )
+ if order < 1:
+ raise IsaAdditionError(f'{operand_context}: Order must be positive')
+ for tag in (xs.FIELD_NAME, xs.DATA_FORMAT_NAME, xs.OPERAND_TYPE):
+ node = operand.find(tag)
+ if node is not None:
+ _validate_text_leaf(node, f'{operand_context}/<{tag}>')
+ size_node = operand.find(xs.OPERAND_SIZE)
+ assert size_node is not None
+ size = _required_decimal(
+ _validate_text_leaf(
+ size_node, f'{operand_context}/<{xs.OPERAND_SIZE}>'
+ ),
+ f'{operand_context}/<{xs.OPERAND_SIZE}>',
+ )
+ if size < 1:
+ raise IsaAdditionError(
+ f'{operand_context}/<{xs.OPERAND_SIZE}>: value must be positive'
+ )
+
+ functional_group = instruction.find('FunctionalGroup')
+ assert functional_group is not None
+ functional_context = f'{context}/'
+ _validate_node_shape(
+ functional_group,
+ functional_context,
+ child_counts={'Name': (1, 1), 'FunctionalSubgroups': (1, 1)},
+ )
+ functional_name = functional_group.find('Name')
+ subgroups = functional_group.find('FunctionalSubgroups')
+ assert functional_name is not None and subgroups is not None
+ _validate_text_leaf(functional_name, f'{functional_context}/')
+ _validate_node_shape(
+ subgroups,
+ f'{functional_context}/',
+ child_counts={'Subgroup': (1, None)},
+ )
+ for subgroup in subgroups:
+ _validate_text_leaf(
+ subgroup, f'{functional_context}//'
+ )
+
+
+def _binary_text(node: elem_tree.Element, bit_width: int, context: str) -> str:
+ unknown_attributes = set(node.attrib) - {xs.ENC_IDENTIFER_ATTR_RADIX}
+ if unknown_attributes:
+ names = ', '.join(sorted(unknown_attributes))
+ raise IsaAdditionError(f'{context}: unknown attributes: {names}')
+ radix_text = node.attrib.get(xs.ENC_IDENTIFER_ATTR_RADIX)
+ if radix_text is None:
+ raise IsaAdditionError(f'{context}: missing Radix attribute')
+ radix = _required_decimal(radix_text, f'{context} Radix')
+ if radix != 2:
+ raise IsaAdditionError(
+ f'{context}: radix {radix} does not match binary MR ISA layout'
+ )
+ if node.text is None or not node.text.strip():
+ raise IsaAdditionError(f'{context}: missing identifier value')
+ value = node.text.strip()
+ if not re.fullmatch(r'[01]+', value):
+ raise IsaAdditionError(f'{context}: malformed radix-2 identifier {value!r}')
+ if len(value) != bit_width:
+ raise IsaAdditionError(
+ f'{context}: identifier width {len(value)} does not match encoding '
+ f'width {bit_width}'
+ )
+ return value
+
+
+def _field_info(
+ encoding: elem_tree.Element, profile: IsaProfile, context: str
+) -> tuple[int, int, int, frozenset[str]]:
+ enc_name = _required_text(encoding, xs.ENCODING_NAME, context)
+ renames = profile.field_renames(enc_name.upper())
+ enc_field_bits: int | None = None
+ op_field_bits = 0
+ opm_field_bits = 0
+ field_names: set[str] = set()
+ for field in encoding.findall(f'./{xs.UCODE_FMT}/{xs.BITMAP}/{xs.FIELD}'):
+ field_name = _required_text(field, xs.FIELD_NAME, context).lower()
+ field_name = renames.get(field_name, field_name)
+ field_names.add(field_name)
+ ranges = sorted(
+ field.findall(f'{xs.BIT_LAYOUT}/{xs.RANGE}'),
+ key=lambda node: int(node.attrib.get('Order', 0)),
+ )
+ for range_index, range_node in enumerate(ranges):
+ width = _required_decimal(
+ _required_text(range_node, xs.BIT_CNT, context), context
+ )
+ range_name = field_name
+ if range_index > 0:
+ range_name = 'opm' if field_name == 'op' and range_index == 1 else ''
+ if range_name == 'encoding':
+ enc_field_bits = width
+ elif range_name == 'op':
+ op_field_bits = width
+ elif range_name == 'opm':
+ opm_field_bits = width
+ if enc_field_bits is None:
+ raise IsaAdditionError(f'{context}: microcode format has no encoding field')
+ return enc_field_bits, op_field_bits, opm_field_bits, frozenset(field_names)
+
+
+def _condition_ids(
+ encoding: elem_tree.Element, context: str
+) -> dict[str, frozenset[str]]:
+ conditions: dict[str, set[str]] = {}
+ for condition in encoding.findall(f'./{xs.ENCODING_CONDS}/{xs.ENCODING_COND}'):
+ name = _required_text(condition, xs.COND_NAME, context)
+ condition_id = condition.findtext('ConditionId')
+ ids = conditions.setdefault(name, set())
+ if condition_id is not None and condition_id.strip():
+ ids.add(condition_id.strip())
+ return {name: frozenset(ids) for name, ids in conditions.items()}
+
+
+def _applicable_condition_ids(
+ encoding: _EncodingDefinition,
+ encodings: Mapping[str, _EncodingDefinition],
+) -> dict[str, frozenset[str]]:
+ """Return conditions accepted for an encoding by the parser's inheritance."""
+
+ applicable: dict[str, set[str]] = {
+ name: set(ids) for name, ids in encoding.condition_ids.items()
+ }
+
+ def include(conditions: Mapping[str, frozenset[str]]) -> None:
+ for name, ids in conditions.items():
+ applicable.setdefault(name, set()).update(ids)
+
+ if encoding.parent_name is not None:
+ parent = encodings.get(encoding.parent_name)
+ if parent is not None:
+ include(parent.condition_ids)
+ else:
+ for child in encodings.values():
+ if child.parent_name == encoding.name:
+ include(child.condition_ids)
+ return {name: frozenset(ids) for name, ids in applicable.items()}
+
+
+def _layout_signature(text: str, opcode_slice: tuple[int, int]) -> str:
+ start, end = opcode_slice
+ return f'{text[:start]}{"0" * (end - start)}{text[end:]}'
+
+
+def _decode_identifier(
+ text: str,
+ encoding_slice: tuple[int, int],
+ opcode_slice: tuple[int, int],
+ dont_care_bits: int,
+) -> tuple[int, int]:
+ encoding_value = int(text[encoding_slice[0] : encoding_slice[1]], 2)
+ encoding_value <<= dont_care_bits
+ opcode_text = text[opcode_slice[0] : opcode_slice[1]]
+ opcode = int(opcode_text, 2) if opcode_text else 0
+ return encoding_value, opcode
+
+
+def _base_metadata(
+ root: elem_tree.Element,
+) -> tuple[str, str, elem_tree.Element, elem_tree.Element]:
+ try:
+ isa_node = xs.get_node(root, xs.ISA)
+ arch_node = xs.get_node(isa_node, xs.ARCH)
+ arch_name = xs.get_node_text(xs.get_node(arch_node, xs.ARCH_NAME)).strip()
+ document = xs.get_node(root, xs.DOCUMENT)
+ schema_version = xs.get_node_text(
+ xs.get_node(document, xs.SCHEMA_VERSION)
+ ).strip()
+ instructions = xs.get_node(isa_node, xs.INSTS)
+ encodings = xs.get_node(isa_node, xs.ENCODINGS)
+ except (xs.SchemaValueError, AttributeError) as error:
+ raise IsaAdditionError(
+ f'base MR ISA XML is missing required metadata: {error}'
+ ) from error
+ return arch_name, schema_version, encodings, instructions
+
+
+def _base_definitions(root: elem_tree.Element, profile: IsaProfile) -> tuple[
+ dict[str, _EncodingDefinition],
+ set[str],
+ dict[tuple[str, int], str],
+ set[str],
+ set[str],
+ dict[str, str],
+ dict[str, str],
+]:
+ isa_node = xs.get_node(root, xs.ISA)
+ encodings_node = xs.get_node(isa_node, xs.ENCODINGS)
+ instructions_node = xs.get_node(isa_node, xs.INSTS)
+ operand_types_node = xs.get_node(isa_node, xs.OPERAND_TYPES)
+
+ encoding_names = {
+ _required_text(encoding, xs.ENCODING_NAME, 'base encoding')
+ for encoding in encodings_node
+ }
+ encoding_definitions: dict[str, _EncodingDefinition] = {}
+ for encoding in encodings_node:
+ context = 'base encoding'
+ name = _required_text(encoding, xs.ENCODING_NAME, context)
+ if name in profile.skip_encodings:
+ continue
+ if name in encoding_definitions:
+ raise IsaAdditionError(f'base encoding {name!r} is duplicated')
+ bit_width = _required_decimal(
+ _required_text(encoding, xs.BIT_CNT, f'base encoding {name!r}'),
+ f'base encoding {name!r} bit width',
+ )
+ mask_node = encoding.find(xs.ENCODING_IDENTIFIER_MASK)
+ if mask_node is None:
+ raise IsaAdditionError(
+ f'base encoding {name!r}: missing <{xs.ENCODING_IDENTIFIER_MASK}>'
+ )
+ mask = _binary_text(mask_node, bit_width, f'base encoding {name!r} mask')
+ identifiers_node = encoding.find(xs.ENCODING_IDENTIFERS)
+ if identifiers_node is None:
+ raise IsaAdditionError(
+ f'base encoding {name!r}: missing <{xs.ENCODING_IDENTIFERS}>'
+ )
+ enc_bits, op_bits, opm_bits, own_field_names = _field_info(
+ encoding, profile, f'base encoding {name!r}'
+ )
+ condition_ids = _condition_ids(encoding, f'base encoding {name!r}')
+ try:
+ encoding_slice, opcode_slice, dont_care_bits = (
+ parse_encoding_identifier_mask(
+ mask, profile.max_enc_bits, enc_bits, op_bits
+ )
+ )
+ except IsaAdditionError as error:
+ raise IsaAdditionError(f'base encoding {name!r}: {error}') from error
+ if encoding_slice[1] > bit_width or opcode_slice[1] > bit_width:
+ raise IsaAdditionError(
+ f'base encoding {name!r}: identifier mask slices exceed bit width '
+ f'{bit_width}'
+ )
+
+ texts: set[str] = set()
+ layout_signatures: set[str] = set()
+ encoding_values: set[int] = set()
+ decoded_opcodes: set[int] = set()
+ for identifier in identifiers_node:
+ text = _binary_text(
+ identifier, bit_width, f'base encoding {name!r} identifier'
+ )
+ encoding_value, opcode = _decode_identifier(
+ text, encoding_slice, opcode_slice, dont_care_bits
+ )
+ texts.add(text)
+ layout_signatures.add(_layout_signature(text, opcode_slice))
+ encoding_values.add(encoding_value)
+ decoded_opcodes.add(opcode)
+
+ parent_name: str | None = None
+ is_implied_literal = False
+ operand_field_names = own_field_names
+ if profile.is_alt_encoding(name):
+ parent_name = profile.derive_parent_enc_name(name)
+ parent = encoding_definitions.get(parent_name)
+ if parent is None:
+ raise IsaAdditionError(
+ f'base encoding {name!r}: parent {parent_name!r} must appear first'
+ )
+ condition_names = [(condition_name, '') for condition_name in condition_ids]
+ is_implied_literal = profile.is_implied_literal_encoding(
+ name, condition_names, bit_width, parent.bit_width
+ )
+ operand_field_names = parent.operand_field_names
+ if not is_implied_literal:
+ operand_field_names |= own_field_names
+
+ encoding_definitions[name] = _EncodingDefinition(
+ name=name,
+ identifiers_node=identifiers_node,
+ bit_width=bit_width,
+ radix=2,
+ flat_encoding_slice=encoding_slice,
+ opcode_slice=opcode_slice,
+ dont_care_bits=dont_care_bits,
+ opcode_bit_count=op_bits,
+ opcode_modifier_bit_count=opm_bits,
+ base_layout_signatures=frozenset(layout_signatures),
+ base_encoding_values=frozenset(encoding_values),
+ identifier_texts=frozenset(texts),
+ decoded_opcodes=frozenset(decoded_opcodes),
+ condition_ids=condition_ids,
+ operand_field_names=operand_field_names,
+ implied_literal_operand_contracts=set(),
+ parent_name=parent_name,
+ is_implied_literal=is_implied_literal,
+ )
+ operand_types = {
+ _required_text(node, xs.OPERAND_TYPE_NAME, 'base operand type')
+ for node in operand_types_node
+ }
+ instruction_names: set[str] = set()
+ opcode_owners: dict[tuple[str, int], str] = {}
+ instruction_symbol_owners: dict[str, str] = {}
+ opcode_symbol_owners: dict[str, str] = {}
+ for instruction in instructions_node:
+ name = _required_text(instruction, xs.INST_NAME, 'base instruction')
+ instruction_names.add(name)
+ encodings = instruction.find(xs.INST_ENCODINGS)
+ if encodings is None:
+ continue
+ for encoding in encodings:
+ enc_name = _required_text(
+ encoding, xs.ENCODING_NAME, f'base instruction {name}'
+ )
+ opcode_text = _required_text(
+ encoding, xs.OPCODE, f'base instruction {name}/{enc_name}'
+ )
+ try:
+ opcode = int(opcode_text)
+ except ValueError as error:
+ raise IsaAdditionError(
+ f'base instruction {name}/{enc_name}: invalid decimal opcode '
+ f'{opcode_text!r}'
+ ) from error
+ opcode_owners.setdefault((enc_name, opcode), name)
+ encoding_definition = encoding_definitions.get(enc_name)
+ condition = _required_text(
+ encoding,
+ xs.ENCODING_COND,
+ f'base instruction {name}/{enc_name}',
+ )
+ if encoding_definition is not None:
+ # Some older vendor XMLs reference compatibility conditions from
+ # instructions without declaring them under EncodingConditions.
+ # Treat those base-established spellings as authoritative while
+ # still rejecting novel addition-only typos.
+ ids = set(encoding_definition.condition_ids.get(condition, ()))
+ condition_node = encoding.find(xs.ENCODING_COND)
+ assert condition_node is not None
+ condition_id = condition_node.attrib.get('Id')
+ if condition_id is not None:
+ ids.add(condition_id)
+ encoding_definition.condition_ids[condition] = frozenset(ids)
+ if encoding_definition.is_implied_literal:
+ operands = encoding.find(xs.OPERANDS)
+ if operands is not None:
+ for operand in operands:
+ if operand.find(xs.FIELD_NAME) is None:
+ continue
+ contract = _implied_literal_operand_contract(
+ operand,
+ enc_name,
+ profile,
+ f'base instruction {name}/{enc_name} operand',
+ )
+ if (
+ contract.field_name
+ not in encoding_definition.operand_field_names
+ ):
+ encoding_definition.implied_literal_operand_contracts.add(
+ contract
+ )
+ if encoding_definition is None or profile.skip_inst_encoding(
+ enc_name, condition
+ ):
+ continue
+ is_implied_literal = encoding_definition.is_implied_literal
+ instruction_symbol_owners.setdefault(
+ format_instruction_name(
+ name,
+ enc_name,
+ is_implied_literal_enc=is_implied_literal,
+ ),
+ name,
+ )
+ opcode_symbol_owners.setdefault(
+ opcode_constant_name(
+ name,
+ enc_name,
+ is_implied_literal_enc=is_implied_literal,
+ ),
+ name,
+ )
+
+ for slot, owner in profile.compatibility_instruction_slots.items():
+ enc_name, _ = slot
+ encoding_definition = encoding_definitions.get(enc_name)
+ if encoding_definition is None:
+ continue
+ opcode_owners.setdefault(slot, owner)
+ instruction_names.add(owner)
+ is_implied_literal = encoding_definition.is_implied_literal
+ instruction_symbol_owners.setdefault(
+ format_instruction_name(
+ owner,
+ enc_name,
+ is_implied_literal_enc=is_implied_literal,
+ ),
+ owner,
+ )
+ opcode_symbol_owners.setdefault(
+ opcode_constant_name(
+ owner,
+ enc_name,
+ is_implied_literal_enc=is_implied_literal,
+ ),
+ owner,
+ )
+ return (
+ encoding_definitions,
+ encoding_names,
+ opcode_owners,
+ operand_types,
+ instruction_names,
+ instruction_symbol_owners,
+ opcode_symbol_owners,
+ )
+
+
+def _parse_additions(path: str) -> elem_tree.Element:
+ try:
+ return elem_tree.parse(path).getroot()
+ except (OSError, elem_tree.ParseError) as error:
+ raise IsaAdditionError(
+ f'{path}: cannot parse ISA additions document: {error}'
+ ) from error
+
+
+def _validate_additions_root(
+ root: elem_tree.Element, path: str, base_arch: str, base_schema: str
+) -> tuple[str, elem_tree.Element | None, elem_tree.Element]:
+ if root.tag != ADDITIONS_ROOT:
+ raise IsaAdditionError(
+ f'{path}: expected <{ADDITIONS_ROOT}> root, found <{root.tag}>'
+ )
+ unknown_attributes = set(root.attrib) - _ADDITIONS_ATTRIBUTES
+ missing_attributes = _ADDITIONS_ATTRIBUTES - set(root.attrib)
+ if unknown_attributes:
+ names = ', '.join(sorted(unknown_attributes))
+ raise IsaAdditionError(f'{path}: unknown additions root attributes: {names}')
+ if missing_attributes:
+ names = ', '.join(sorted(missing_attributes))
+ raise IsaAdditionError(f'{path}: missing additions root attributes: {names}')
+
+ identifier = root.attrib[ADDITIONS_ID_ATTR].strip()
+ if not _ID_PATTERN.fullmatch(identifier):
+ raise IsaAdditionError(
+ f'{path}: invalid additions document Id {identifier!r}; use letters, '
+ 'digits, dot, underscore, or hyphen'
+ )
+
+ declared_arch = root.attrib[ADDITIONS_BASE_ARCH_ATTR].strip()
+ if declared_arch != base_arch:
+ raise IsaAdditionError(
+ f'{path}: additions base architecture {declared_arch!r} does not match '
+ f'{base_arch!r}'
+ )
+ declared_schema = root.attrib[ADDITIONS_BASE_SCHEMA_ATTR].strip()
+ if declared_schema != base_schema:
+ raise IsaAdditionError(
+ f'{path}: additions base schema {declared_schema!r} does not match '
+ f'{base_schema!r}'
+ )
+
+ children = list(root)
+ allowed_children = {IDENTIFIER_ADDITIONS, INSTRUCTION_ADDITIONS}
+ unknown_children = [
+ child.tag for child in children if child.tag not in allowed_children
+ ]
+ if unknown_children:
+ names = ', '.join(f'<{name}>' for name in unknown_children)
+ raise IsaAdditionError(f'{path}: unknown additions root elements: {names}')
+ instructions = [child for child in children if child.tag == INSTRUCTION_ADDITIONS]
+ identifier_additions = [
+ child for child in children if child.tag == IDENTIFIER_ADDITIONS
+ ]
+ if len(instructions) != 1:
+ raise IsaAdditionError(
+ f'{path}: additions document must contain exactly one '
+ f'<{INSTRUCTION_ADDITIONS}> element'
+ )
+ if len(identifier_additions) > 1:
+ raise IsaAdditionError(
+ f'{path}: additions document may contain at most one '
+ f'<{IDENTIFIER_ADDITIONS}> element'
+ )
+ instructions_node = instructions[0]
+ if instructions_node.attrib:
+ names = ', '.join(sorted(instructions_node.attrib))
+ raise IsaAdditionError(
+ f'{path}: <{INSTRUCTION_ADDITIONS}> must not have attributes: {names}'
+ )
+ identifier_group = identifier_additions[0] if identifier_additions else None
+ if identifier_group is not None and identifier_group.attrib:
+ names = ', '.join(sorted(identifier_group.attrib))
+ raise IsaAdditionError(
+ f'{path}: <{IDENTIFIER_ADDITIONS}> must not have attributes: {names}'
+ )
+ if identifier_group is not None and not list(identifier_group):
+ raise IsaAdditionError(f'{path}: <{IDENTIFIER_ADDITIONS}> must not be empty')
+ return identifier, identifier_group, instructions_node
+
+
+def _validate_instruction_addition(
+ instruction: elem_tree.Element,
+ *,
+ path: str,
+ addition_id: str,
+ encodings: Mapping[str, _EncodingDefinition],
+ encoding_names: set[str],
+ opcode_owners: dict[tuple[str, int], str],
+ opcode_limits: Mapping[str, int],
+ operand_types: set[str],
+ instruction_names: set[str],
+ instruction_symbol_owners: dict[str, str],
+ opcode_symbol_owners: dict[str, str],
+ profile: IsaProfile,
+) -> _InstructionAddition:
+ if instruction.tag != xs.INST:
+ raise IsaAdditionError(
+ f'{path}: <{INSTRUCTION_ADDITIONS}> may contain only <{xs.INST}> '
+ f'elements, found <{instruction.tag}>'
+ )
+
+ context = f'{path}: additions document {addition_id!r}'
+ _validate_instruction_structure(instruction, f'{context}: <{xs.INST}>')
+ name = _required_text(instruction, xs.INST_NAME, context)
+ if not _INSTRUCTION_NAME_PATTERN.fullmatch(name):
+ raise IsaAdditionError(
+ f'{context}: invalid instruction name {name!r}; use ASCII letters, '
+ 'digits, and underscores, starting with a letter'
+ )
+ if name in instruction_names:
+ raise IsaAdditionError(f'{context}: instruction {name!r} already exists')
+
+ instruction_encodings = instruction.find(xs.INST_ENCODINGS)
+ if instruction_encodings is None or not list(instruction_encodings):
+ raise IsaAdditionError(
+ f'{context}: instruction {name!r} has no <{xs.INST_ENCODING}> entries'
+ )
+
+ forms_in_order: list[tuple[str, int, str]] = []
+ forms: set[tuple[str, int, str]] = set()
+ active_instruction_forms: dict[str, tuple[str, int, str]] = {}
+ for encoding in instruction_encodings:
+ if encoding.tag != xs.INST_ENCODING:
+ raise IsaAdditionError(
+ f'{context}: <{xs.INST_ENCODINGS}> may contain only '
+ f'<{xs.INST_ENCODING}> elements, found <{encoding.tag}>'
+ )
+ enc_name = _required_text(
+ encoding, xs.ENCODING_NAME, f'{context}: instruction {name!r}'
+ )
+ if enc_name not in encoding_names:
+ raise IsaAdditionError(
+ f'{context}: instruction {name!r} references unknown encoding '
+ f'{enc_name!r}'
+ )
+ opcode_text = _required_text(
+ encoding, xs.OPCODE, f'{context}: instruction {name!r}/{enc_name}'
+ )
+ try:
+ opcode = int(opcode_text)
+ except ValueError as error:
+ raise IsaAdditionError(
+ f'{context}: instruction {name!r}/{enc_name} has invalid decimal '
+ f'opcode {opcode_text!r}'
+ ) from error
+ if opcode < 0:
+ raise IsaAdditionError(
+ f'{context}: instruction {name!r}/{enc_name} has negative opcode '
+ f'{opcode}'
+ )
+ opcode_limit = opcode_limits.get(enc_name)
+ if opcode_limit is not None and opcode >= opcode_limit:
+ raise IsaAdditionError(
+ f'{context}: instruction {name!r}/{enc_name} opcode {opcode} is '
+ f'outside the encoding range [0, {opcode_limit})'
+ )
+
+ slot = (enc_name, opcode)
+ owner = opcode_owners.get(slot)
+ if owner is not None:
+ raise IsaAdditionError(
+ f'{context}: opcode {opcode} in encoding {enc_name!r} is already '
+ f'owned by instruction {owner!r}'
+ )
+ condition = _required_text(
+ encoding,
+ xs.ENCODING_COND,
+ f'{context}: instruction {name!r}/{enc_name}',
+ )
+ encoding_definition = encodings.get(enc_name)
+ if encoding_definition is not None:
+ applicable_conditions = _applicable_condition_ids(
+ encoding_definition, encodings
+ )
+ if condition not in applicable_conditions:
+ raise IsaAdditionError(
+ f'{context}: instruction {name!r}/{enc_name} references '
+ f'unknown encoding condition {condition!r}'
+ )
+ condition_node = encoding.find(xs.ENCODING_COND)
+ assert condition_node is not None
+ condition_id = condition_node.attrib.get('Id')
+ declared_ids = applicable_conditions[condition]
+ if (
+ condition_id is not None
+ and declared_ids
+ and condition_id not in declared_ids
+ ):
+ expected = ', '.join(sorted(declared_ids))
+ raise IsaAdditionError(
+ f'{context}: instruction {name!r}/{enc_name} condition '
+ f'{condition!r} has Id {condition_id!r}, expected {expected}'
+ )
+ form = (enc_name, opcode, condition)
+ if form in forms:
+ raise IsaAdditionError(
+ f'{context}: instruction {name!r} repeats opcode {opcode} in '
+ f'encoding {enc_name!r} with condition {condition!r}'
+ )
+ forms.add(form)
+ forms_in_order.append(form)
+
+ if encoding_definition is not None and not profile.skip_inst_encoding(
+ enc_name, condition
+ ):
+ is_implied_literal = encoding_definition.is_implied_literal
+ instruction_symbol = format_instruction_name(
+ name,
+ enc_name,
+ is_implied_literal_enc=is_implied_literal,
+ )
+ previous_form = active_instruction_forms.get(instruction_symbol)
+ if previous_form is not None:
+ previous_encoding, previous_opcode, previous_condition = previous_form
+ raise IsaAdditionError(
+ f'{context}: instruction {name!r} has multiple active forms '
+ f'for generated instruction symbol {instruction_symbol!r}: '
+ f'{previous_encoding!r} opcode {previous_opcode} condition '
+ f'{previous_condition!r}, and {enc_name!r} opcode {opcode} '
+ f'condition {condition!r}'
+ )
+ active_instruction_forms[instruction_symbol] = form
+ generated_symbols = (
+ (
+ 'instruction',
+ instruction_symbol,
+ instruction_symbol_owners,
+ ),
+ (
+ 'opcode constant',
+ opcode_constant_name(
+ name,
+ enc_name,
+ is_implied_literal_enc=is_implied_literal,
+ ),
+ opcode_symbol_owners,
+ ),
+ )
+ for symbol_kind, symbol, owners in generated_symbols:
+ symbol_owner = owners.get(symbol)
+ if symbol_owner is not None and symbol_owner != name:
+ raise IsaAdditionError(
+ f'{context}: instruction {name!r}/{enc_name} normalizes '
+ f'to {symbol_kind} symbol {symbol!r}, already owned by '
+ f'instruction {symbol_owner!r}'
+ )
+ owners[symbol] = name
+
+ operands = encoding.find(xs.OPERANDS)
+ if operands is None:
+ raise IsaAdditionError(
+ f'{context}: instruction {name!r}/{enc_name} is missing '
+ f'<{xs.OPERANDS}>'
+ )
+ for operand in operands:
+ operand_context = f'{context}: instruction {name!r}/{enc_name} operand'
+ op_type = _required_text(
+ operand,
+ xs.OPERAND_TYPE,
+ operand_context,
+ )
+ if op_type not in operand_types:
+ raise IsaAdditionError(
+ f'{context}: instruction {name!r}/{enc_name} references '
+ f'unknown operand type {op_type!r}'
+ )
+ field_node = operand.find(xs.FIELD_NAME)
+ if field_node is None or encoding_definition is None:
+ continue
+ field_name = _required_text(operand, xs.FIELD_NAME, operand_context)
+ contract = _implied_literal_operand_contract(
+ operand, enc_name, profile, operand_context
+ )
+ if contract.field_name in encoding_definition.operand_field_names:
+ continue
+ if contract in encoding_definition.implied_literal_operand_contracts:
+ continue
+ established_implied_literal_fields = {
+ item.field_name
+ for item in encoding_definition.implied_literal_operand_contracts
+ }
+ if contract.field_name in established_implied_literal_fields:
+ raise IsaAdditionError(
+ f'{context}: instruction {name!r}/{enc_name} references '
+ f'implied-literal operand field {field_name!r} with a type or '
+ 'role not established by the base XML'
+ )
+ raise IsaAdditionError(
+ f'{context}: instruction {name!r}/{enc_name} references '
+ f'unknown operand field {field_name!r}'
+ )
+
+ addition = deepcopy(instruction)
+ addition.attrib[ADDITION_SOURCE_ATTR] = addition_id
+ return _InstructionAddition(
+ node=addition,
+ name=name,
+ path=path,
+ addition_id=addition_id,
+ forms=tuple(forms_in_order),
+ )
+
+
+def _single_child(
+ parent: elem_tree.Element, tag: str, context: str
+) -> elem_tree.Element:
+ nodes = [child for child in parent if child.tag == tag]
+ if len(nodes) != 1:
+ raise IsaAdditionError(f'{context}: expected exactly one <{tag}> element')
+ return nodes[0]
+
+
+def _validate_identifier_addition(
+ addition: elem_tree.Element,
+ *,
+ path: str,
+ addition_id: str,
+ encodings: Mapping[str, _EncodingDefinition],
+ all_encoding_names: set[str],
+ occupied_texts: dict[str, set[str]],
+ occupied_opcodes: dict[str, set[int]],
+) -> _IdentifierAddition:
+ context = f'{path}: additions document {addition_id!r} identifier addition'
+ if addition.tag != IDENTIFIER_ADDITION:
+ raise IsaAdditionError(
+ f'{path}: <{IDENTIFIER_ADDITIONS}> may contain only '
+ f'<{IDENTIFIER_ADDITION}> elements, found <{addition.tag}>'
+ )
+ if addition.attrib:
+ names = ', '.join(sorted(addition.attrib))
+ raise IsaAdditionError(f'{context}: unknown attributes: {names}')
+ allowed_children = {xs.ENCODING_NAME, xs.OPCODE, xs.ENCODING_IDENTIFER_ALT}
+ unknown_children = [
+ child.tag for child in addition if child.tag not in allowed_children
+ ]
+ if unknown_children:
+ names = ', '.join(f'<{name}>' for name in unknown_children)
+ raise IsaAdditionError(f'{context}: unknown elements: {names}')
+
+ encoding_name_node = _single_child(addition, xs.ENCODING_NAME, context)
+ opcode_node = _single_child(addition, xs.OPCODE, context)
+ identifier_node = _single_child(addition, xs.ENCODING_IDENTIFER_ALT, context)
+ if encoding_name_node.attrib or opcode_node.attrib:
+ raise IsaAdditionError(
+ f'{context}: <{xs.ENCODING_NAME}> and <{xs.OPCODE}> must not '
+ 'have attributes'
+ )
+
+ encoding_name = _required_text(addition, xs.ENCODING_NAME, context)
+ if encoding_name not in all_encoding_names:
+ raise IsaAdditionError(f'{context}: unknown encoding {encoding_name!r}')
+ encoding = encodings.get(encoding_name)
+ if encoding is None:
+ raise IsaAdditionError(
+ f'{context}: encoding {encoding_name!r} is not active in this ISA profile'
+ )
+ opcode_text = _required_text(addition, xs.OPCODE, context)
+ opcode = _required_decimal(opcode_text, f'{context} opcode')
+ if opcode < 0 or opcode >= encoding.opcode_limit:
+ raise IsaAdditionError(
+ f'{context}: opcode {opcode} is outside encoding {encoding_name!r} '
+ f'range [0, {encoding.opcode_limit})'
+ )
+
+ radix_text = identifier_node.attrib.get(xs.ENC_IDENTIFER_ATTR_RADIX)
+ if radix_text is None:
+ raise IsaAdditionError(f'{context}: is missing Radix')
+ radix = _required_decimal(radix_text, f'{context} identifier Radix')
+ if radix != encoding.radix:
+ raise IsaAdditionError(
+ f'{context}: identifier radix {radix} does not match encoding '
+ f'radix {encoding.radix}'
+ )
+ text = _binary_text(identifier_node, encoding.bit_width, context)
+ encoding_value, decoded_opcode = _decode_identifier(
+ text,
+ encoding.flat_encoding_slice,
+ encoding.opcode_slice,
+ encoding.dont_care_bits,
+ )
+ if decoded_opcode != opcode:
+ raise IsaAdditionError(
+ f'{context}: identifier decodes opcode {decoded_opcode}, not declared '
+ f'opcode {opcode} for encoding {encoding_name!r}'
+ )
+ if (
+ encoding_value not in encoding.base_encoding_values
+ or _layout_signature(text, encoding.opcode_slice)
+ not in encoding.base_layout_signatures
+ ):
+ raise IsaAdditionError(
+ f'{context}: identifier for encoding {encoding_name!r} is '
+ 'incompatible with the base identifier mask/layout'
+ )
+ if text in occupied_texts[encoding_name]:
+ raise IsaAdditionError(
+ f'{context}: duplicate identifier for encoding {encoding_name!r}'
+ )
+ if opcode in occupied_opcodes[encoding_name]:
+ raise IsaAdditionError(
+ f'{context}: identifier collides with an existing decode slot for '
+ f'encoding {encoding_name!r} opcode {opcode}'
+ )
+
+ copied_identifier = deepcopy(identifier_node)
+ copied_identifier.attrib[ADDITION_SOURCE_ATTR] = addition_id
+ occupied_texts[encoding_name].add(text)
+ occupied_opcodes[encoding_name].add(opcode)
+ return _IdentifierAddition(
+ node=copied_identifier,
+ encoding=encoding,
+ opcode=opcode,
+ )
+
+
+def _expanded_opcodes(encoding: _EncodingDefinition, base_opcode: int) -> set[int]:
+ """Return all complete opcodes represented by a decoded identifier opcode."""
+ base_count = 1 << encoding.opcode_bit_count
+ return {
+ base_opcode + modifier * base_count
+ for modifier in range(1 << encoding.opcode_modifier_bit_count)
+ }
+
+
+def _identifier_owners(
+ addition: _IdentifierAddition,
+ opcode_owners: Mapping[tuple[str, int], str],
+) -> set[str]:
+ return {
+ owner
+ for opcode in _expanded_opcodes(addition.encoding, addition.opcode)
+ if (owner := opcode_owners.get((addition.encoding.name, opcode))) is not None
+ }
+
+
+def _reachable_opcodes(
+ encodings: Mapping[str, _EncodingDefinition],
+ identifiers: Sequence[_IdentifierAddition],
+) -> dict[str, set[int]]:
+ base_opcodes = {
+ name: set(encoding.decoded_opcodes) for name, encoding in encodings.items()
+ }
+ for addition in identifiers:
+ base_opcodes[addition.encoding.name].add(addition.opcode)
+ return {
+ name: {
+ expanded
+ for opcode in base_opcodes[name]
+ for expanded in _expanded_opcodes(encoding, opcode)
+ }
+ for name, encoding in encodings.items()
+ }
+
+
+def apply_isa_additions(
+ base_root: elem_tree.Element,
+ addition_paths: Sequence[str],
+ profile: IsaProfile,
+) -> tuple[IsaAdditionProvenance, ...]:
+ """Validate and atomically merge ISA additions into a base MR ISA tree.
+
+ Validation of every input completes before the base tree is modified. The
+ returned provenance and merge order follow ``addition_paths`` and document
+ order. Identifier additions are merged before the caller constructs normal
+ decode tables.
+ """
+ if not addition_paths:
+ return ()
+
+ base_arch, base_schema, _, base_instructions = _base_metadata(base_root)
+ (
+ encodings,
+ encoding_names,
+ opcode_owners,
+ operand_types,
+ instruction_names,
+ instruction_symbol_owners,
+ opcode_symbol_owners,
+ ) = _base_definitions(base_root, profile)
+ opcode_limits = {
+ name: encoding.opcode_limit for name, encoding in encodings.items()
+ }
+ pending_instructions: list[_InstructionAddition] = []
+ pending_identifier_groups: list[tuple[str, str, elem_tree.Element]] = []
+ pending_identifiers: list[_IdentifierAddition] = []
+ addition_opcode_owners: dict[tuple[str, int], str] = {}
+ provenance: list[IsaAdditionProvenance] = []
+ addition_ids: set[str] = set()
+
+ for raw_path in addition_paths:
+ path = str(raw_path)
+ addition_root = _parse_additions(path)
+ addition_id, identifier_additions, instruction_additions = (
+ _validate_additions_root(addition_root, path, base_arch, base_schema)
+ )
+ if addition_id in addition_ids:
+ raise IsaAdditionError(
+ f'{path}: duplicate additions document Id {addition_id!r}'
+ )
+
+ addition_ids.add(addition_id)
+ provenance.append(IsaAdditionProvenance(addition_id, path))
+ if identifier_additions is not None:
+ pending_identifier_groups.append((path, addition_id, identifier_additions))
+ for instruction in instruction_additions:
+ addition = _validate_instruction_addition(
+ instruction,
+ path=path,
+ addition_id=addition_id,
+ encodings=encodings,
+ encoding_names=encoding_names,
+ opcode_owners=opcode_owners,
+ opcode_limits=opcode_limits,
+ operand_types=operand_types,
+ instruction_names=instruction_names,
+ instruction_symbol_owners=instruction_symbol_owners,
+ opcode_symbol_owners=opcode_symbol_owners,
+ profile=profile,
+ )
+ instruction_names.add(addition.name)
+ for enc_name, opcode, _ in addition.forms:
+ slot = (enc_name, opcode)
+ opcode_owners[slot] = addition.name
+ addition_opcode_owners[slot] = addition.name
+ pending_instructions.append(addition)
+
+ occupied_texts = {
+ name: set(encoding.identifier_texts) for name, encoding in encodings.items()
+ }
+ occupied_opcodes = {
+ name: set(encoding.decoded_opcodes) for name, encoding in encodings.items()
+ }
+ for path, addition_id, identifier_group in pending_identifier_groups:
+ for identifier in identifier_group:
+ addition = _validate_identifier_addition(
+ identifier,
+ path=path,
+ addition_id=addition_id,
+ encodings=encodings,
+ all_encoding_names=encoding_names,
+ occupied_texts=occupied_texts,
+ occupied_opcodes=occupied_opcodes,
+ )
+ owners = _identifier_owners(addition, addition_opcode_owners)
+ if not owners:
+ raise IsaAdditionError(
+ f'{path}: additions document {addition_id!r} identifier for encoding '
+ f'{addition.encoding.name!r} opcode {addition.opcode} is '
+ 'unowned; it must correspond to an added instruction'
+ )
+ if addition.encoding.is_implied_literal:
+ parent_name = addition.encoding.parent_name
+ assert parent_name is not None
+ parent_encoding = encodings[parent_name]
+ parent_owners = {
+ owner
+ for opcode in _expanded_opcodes(parent_encoding, addition.opcode)
+ if (owner := addition_opcode_owners.get((parent_name, opcode)))
+ is not None
+ }
+ if parent_owners != owners:
+ owner_text = ', '.join(sorted(owners))
+ parent_owner_text = (
+ ', '.join(sorted(parent_owners))
+ if parent_owners
+ else 'no owner'
+ )
+ raise IsaAdditionError(
+ f'{path}: additions document {addition_id!r} '
+ 'implied-literal identifier '
+ f'for {addition.encoding.name!r} opcode {addition.opcode} '
+ f'is owned by {owner_text!r}, but parent encoding '
+ f'{parent_name!r} has {parent_owner_text}'
+ )
+ pending_identifiers.append(addition)
+
+ reachable = _reachable_opcodes(encodings, pending_identifiers)
+ for addition in pending_instructions:
+ active_forms = [
+ form
+ for form in addition.forms
+ if form[0] not in profile.skip_encodings
+ and not profile.skip_inst_encoding(form[0], form[2])
+ ]
+ if not active_forms:
+ raise IsaAdditionError(
+ f'{addition.path}: additions document {addition.addition_id!r} '
+ f'instruction {addition.name!r} has no encoding form active in '
+ 'the selected ISA profile'
+ )
+ for enc_name, opcode, _condition in active_forms:
+ if opcode not in reachable[enc_name]:
+ raise IsaAdditionError(
+ f'{addition.path}: additions document '
+ f'{addition.addition_id!r} instruction '
+ f'{addition.name!r} encoding {enc_name!r} opcode {opcode} is '
+ 'unreachable; add a validated encoding identifier'
+ )
+
+ for addition in pending_identifiers:
+ addition.encoding.identifiers_node.append(addition.node)
+ base_instructions.extend(addition.node for addition in pending_instructions)
+ return tuple(provenance)
diff --git a/emulation/rocjitsu/lib/python/amdisa/isa_profile.py b/emulation/rocjitsu/lib/python/amdisa/isa_profile.py
index 64d53abbc185..f6154f20cbd5 100644
--- a/emulation/rocjitsu/lib/python/amdisa/isa_profile.py
+++ b/emulation/rocjitsu/lib/python/amdisa/isa_profile.py
@@ -440,6 +440,11 @@ def inst_size_overrides(self) -> dict[str, int]:
"""Per-instruction size overrides in bytes."""
return {}
+ @property
+ def compatibility_instruction_slots(self) -> dict[tuple[str, int], str]:
+ """Opcode slots owned by instructions synthesized after XML parsing."""
+ return {}
+
@property
def vop3px2_prefix_opcode(self) -> int | None:
"""VOP3P opcode slot for a VOP3PX2 prefix decoder, if any."""
@@ -2146,6 +2151,10 @@ def waitcnt_decode(self) -> str:
"""
return Rdna3Profile.waitcnt_decode.fget(self)
+ @property
+ def compatibility_instruction_slots(self) -> dict[tuple[str, int], str]:
+ return {('ENC_SOPP', 9): 'S_WAITCNT'}
+
@property
def supported_versions(self) -> list[str]:
return ['1.1.0', '1.1.1']
@@ -2463,6 +2472,10 @@ def field_renames(self, enc_name: str) -> dict[str, str]:
renames['literal'] = 'simm32'
return renames
+ @property
+ def compatibility_instruction_slots(self) -> dict[tuple[str, int], str]:
+ return {('ENC_VOP1', 103): 'V_PERMLANE64_B32'}
+
def normalize_operand_field_name(self, enc_name: str, field_name: str) -> str:
# Keep the concrete gfx1250 operand identity distinct from earlier
# CDNA/RDNA specs; its local generator path maps it to the renamed
diff --git a/emulation/rocjitsu/lib/python/amdisa/parser.py b/emulation/rocjitsu/lib/python/amdisa/parser.py
index 13fd78fe35e5..21bf0dd4d279 100644
--- a/emulation/rocjitsu/lib/python/amdisa/parser.py
+++ b/emulation/rocjitsu/lib/python/amdisa/parser.py
@@ -33,10 +33,10 @@
by checking instruction semantics that require the old value.
"""
+from collections.abc import Sequence
import re
import xml.etree.ElementTree as elem_tree
-
from amdisa import xml_schema as xs
from amdisa.gpuisa import (
DecodeTableEntry,
@@ -49,6 +49,12 @@
OperandSelector,
synthesize_fieldless_name,
)
+from amdisa.isa_additions import (
+ ADDITION_SOURCE_ATTR,
+ IsaAdditionError,
+ apply_isa_additions,
+ parse_encoding_identifier_mask as _parse_enc_id_masks,
+)
from amdisa.fieldless_policy import validate_fieldless_taxonomy
from amdisa.isa_profile import IsaProfile
@@ -93,50 +99,6 @@ def _fill_padding_gaps(
return pads
-def _parse_enc_id_masks(
- enc_id_mask: str,
- max_enc_bits: int,
- enc_field_bit_cnt: int,
- op_field_bit_cnt: int,
-) -> tuple[tuple[int, int], tuple[int, int], int]:
- """Parse an encoding identifier mask into (flat_enc_mask, op_mask, dont_care_bits).
-
- The encoding identifier mask is a binary string where '1' bits mark the
- encoding field and '0' bits separate encoding from opcode. This function
- derives the slice positions for the encoding field and opcode field, plus
- the number of don't-care bits available in the primary decode table index.
-
- Args:
- enc_id_mask: Binary string identifier mask (e.g. '111111111000000000000000').
- max_enc_bits: Maximum bits for the primary decode table index.
- enc_field_bit_cnt: Bit width of the encoding identifier field.
- op_field_bit_cnt: Bit width of the opcode field.
-
- Returns:
- Tuple of (flat_enc_mask, op_mask, dont_care_bits) where each mask is
- a (start, end) slice pair into the identifier string.
- """
- bit_masks = [(x.start(), x.end()) for x in re.finditer(r'1+', enc_id_mask)]
- flat_enc_mask = bit_masks[0]
- if len(bit_masks) == 1:
- op_mask = (
- bit_masks[0][0] + enc_field_bit_cnt,
- bit_masks[0][0] + enc_field_bit_cnt + op_field_bit_cnt,
- )
- if (flat_enc_mask[1] - flat_enc_mask[0]) > max_enc_bits:
- flat_enc_mask = (
- flat_enc_mask[0],
- flat_enc_mask[0] + max_enc_bits,
- )
- else:
- op_mask = (
- bit_masks[1][0],
- bit_masks[1][0] + op_field_bit_cnt,
- )
- dont_care_bits = max_enc_bits - (flat_enc_mask[1] - flat_enc_mask[0])
- return flat_enc_mask, op_mask, dont_care_bits
-
-
def _uniquify_fieldless_names(opnds: list[Operand]) -> None:
"""Make fieldless operand names unique within one instruction, in place.
@@ -331,6 +293,8 @@ class Parser:
Attributes:
isa_xml: Path to XML file for the ISA specification.
profile: ISA-specific encoding rules used during parsing.
+ addition_xmls: Ordered ISA additions XML paths to validate and merge
+ before decode-table and instruction parsing.
tree: XML element tree obtained by parsing the XML file.
root: Root of the XML element tree.
isa_spec: ISA specification object.
@@ -339,7 +303,12 @@ class Parser:
operand_types_node: Element tree node pointing to the operand types.
"""
- def __init__(self, isa_xml: str, profile: IsaProfile) -> None:
+ def __init__(
+ self,
+ isa_xml: str,
+ profile: IsaProfile,
+ addition_xmls: Sequence[str] = (),
+ ) -> None:
self.isa_xml = isa_xml
self.profile = profile
self.tree = elem_tree.parse(self.isa_xml)
@@ -366,6 +335,8 @@ def __init__(self, isa_xml: str, profile: IsaProfile) -> None:
generated_dir_name,
cpp_namespace,
)
+ self.addition_xmls = tuple(addition_xmls)
+ self._addition_by_id = {}
self.encodings_node = xs.get_node(isa_node, xs.ENCODINGS)
self.insts_node = xs.get_node(isa_node, xs.INSTS)
@@ -377,14 +348,61 @@ def parse(self) -> IsaSpec:
Returns:
Populated IsaSpec object.
"""
+ self.isa_spec.applied_additions = apply_isa_additions(
+ self.root, self.addition_xmls, self.profile
+ )
+ self._addition_by_id = {
+ addition.identifier: addition
+ for addition in self.isa_spec.applied_additions
+ }
self.parse_encodings()
self.parse_insts()
+ self._validate_addition_decode_reachability()
self.parse_operand_types()
self._inject_compat_insts()
self._collect_fieldless_operand_types()
validate_fieldless_taxonomy(self.isa_spec)
return self.isa_spec
+ def _validate_addition_decode_reachability(self) -> None:
+ """Check the final decode pointers for every active added instruction form."""
+ for inst_node in self.insts_node:
+ addition_id = inst_node.attrib.get(ADDITION_SOURCE_ATTR)
+ if addition_id is None:
+ continue
+ inst_name = xs.get_node_text(xs.get_node(inst_node, xs.INST_NAME))
+ provenance = self._addition_by_id.get(addition_id)
+ if provenance is None:
+ raise IsaAdditionError(
+ f'{self.isa_xml}: instruction {inst_name!r} has unknown '
+ f'{ADDITION_SOURCE_ATTR} value {addition_id!r}; the attribute '
+ 'was not produced by a configured ISA additions document'
+ )
+ for inst_enc_node in xs.get_node(inst_node, xs.INST_ENCODINGS):
+ enc_name = xs.get_node_text(
+ xs.get_node(inst_enc_node, xs.ENCODING_NAME)
+ )
+ if enc_name in self.profile.skip_encodings:
+ continue
+ condition = xs.get_node_text(
+ xs.get_node(inst_enc_node, xs.ENCODING_COND)
+ )
+ if self.profile.skip_inst_encoding(enc_name, condition):
+ continue
+ opcode = int(xs.get_node_text(xs.get_node(inst_enc_node, xs.OPCODE)))
+ pointers = self.isa_spec.encoding_map[enc_name].primary_dt_ptrs
+ if (
+ pointers is None
+ or opcode >= len(pointers)
+ or pointers[opcode] == -1
+ ):
+ raise IsaAdditionError(
+ f'{provenance.path}: additions document {addition_id!r} '
+ f'instruction {inst_name!r} encoding {enc_name!r} '
+ f'opcode {opcode} is '
+ 'unreachable in the final primary decode table'
+ )
+
def implicit_operand_accesses(
self, operand_type: str
) -> dict[tuple[str, str], tuple[bool, bool]]:
@@ -444,6 +462,24 @@ def _inject_compat_insts(self) -> None:
self._inject_s_waitcnt_compat()
self._inject_cdna5_permlane64_compat()
+ def _compatibility_instruction_slot(
+ self, expected_instruction_name: str
+ ) -> tuple[str, int, str]:
+ """Return the profile-owned instruction triple for its injector."""
+ slots = self.profile.compatibility_instruction_slots
+ matches = [
+ (enc_name, opcode, instruction_name)
+ for (enc_name, opcode), instruction_name in slots.items()
+ if instruction_name == expected_instruction_name
+ ]
+ if len(matches) != 1:
+ raise ValueError(
+ f'{self.isa_spec.arch_name} profile must declare exactly one '
+ f'compatibility instruction named {expected_instruction_name}, '
+ f'found {matches}'
+ )
+ return matches[0]
+
def _inject_s_waitcnt_compat(self) -> None:
"""Add the legacy monolithic S_WAITCNT accepted by LLVM on GFX12."""
if self.isa_spec.arch_name != 'rdna4':
@@ -452,29 +488,35 @@ def _inject_s_waitcnt_compat(self) -> None:
# The RDNA4/GFX12 XML only lists split S_WAIT_* instructions, but LLVM
# still accepts and emits the monolithic SOPP opcode-9 S_WAITCNT
# compatibility form for sources such as "s_waitcnt lgkmcnt(0)".
- enc = self.isa_spec.encoding_map.get('ENC_SOPP')
+ enc_name, opcode, instruction_name = self._compatibility_instruction_slot(
+ 'S_WAITCNT'
+ )
+ enc = self.isa_spec.encoding_map.get(enc_name)
if enc is None:
raise ValueError(
- 'RDNA4 S_WAITCNT compatibility injection requires ENC_SOPP'
+ f'RDNA4 {instruction_name} compatibility injection requires '
+ f'{enc_name}'
)
if enc.primary_dt_ptrs is None:
raise ValueError(
- 'RDNA4 S_WAITCNT compatibility injection requires an ENC_SOPP '
- 'primary decode route table'
+ f'RDNA4 {instruction_name} compatibility injection requires an '
+ f'{enc_name} primary decode route table'
)
- opcode = 9
- if any(inst.name == 'S_WAITCNT' and inst.opcode == 9 for inst in enc.insts):
+ if any(
+ inst.name == instruction_name and inst.opcode == opcode
+ for inst in enc.insts
+ ):
return
occupied = next((inst for inst in enc.insts if inst.opcode == opcode), None)
if occupied is not None:
raise ValueError(
- 'RDNA4 S_WAITCNT compatibility opcode 9 is already occupied by '
- f'{occupied.name}'
+ f'RDNA4 {instruction_name} compatibility opcode {opcode} is '
+ f'already occupied by {occupied.name}'
)
if len(enc.primary_dt_ptrs) <= opcode:
raise ValueError(
- 'RDNA4 ENC_SOPP primary decode route table does not contain '
- 'S_WAITCNT opcode 9'
+ f'RDNA4 {enc_name} primary decode route table does not contain '
+ f'{instruction_name} opcode {opcode}'
)
dt_ptr = enc.primary_dt_ptrs[opcode]
@@ -491,31 +533,33 @@ def _inject_s_waitcnt_compat(self) -> None:
}
if not matching_dt_ptrs:
raise ValueError(
- 'RDNA4 S_WAITCNT opcode 9 has no ENC_SOPP primary decode route'
+ f'RDNA4 {instruction_name} opcode {opcode} has no {enc_name} '
+ 'primary decode route'
)
if len(matching_dt_ptrs) != 1:
raise ValueError(
- 'RDNA4 S_WAITCNT opcode 9 requires exactly one unique '
- 'ENC_SOPP primary decode route, found '
+ f'RDNA4 {instruction_name} opcode {opcode} requires exactly '
+ f'one unique {enc_name} primary decode route, found '
f'{sorted(matching_dt_ptrs)}'
)
dt_ptr = matching_dt_ptrs.pop()
if dt_ptr < 0 or dt_ptr >= len(self.isa_spec.primary_decode_table):
raise ValueError(
- 'RDNA4 S_WAITCNT opcode 9 resolves to invalid primary '
+ f'RDNA4 {instruction_name} opcode {opcode} resolves to invalid primary '
f'decode-table index {dt_ptr}'
)
dte = self.isa_spec.primary_decode_table[dt_ptr]
- decode_func = 'decodeSWaitcntSopp'
if dte.sub_decode_funcs is not None:
if opcode >= len(dte.sub_decode_funcs):
raise ValueError(
- 'RDNA4 S_WAITCNT opcode 9 is outside the selected subdecode table'
+ f'RDNA4 {instruction_name} opcode {opcode} is outside the '
+ 'selected subdecode table'
)
if dte.sub_decode_funcs[opcode] not in (None, 'decodeInvalid'):
raise ValueError(
- 'RDNA4 S_WAITCNT opcode 9 subdecode slot is already occupied by '
+ f'RDNA4 {instruction_name} opcode {opcode} subdecode slot is '
+ 'already occupied by '
f'{dte.sub_decode_funcs[opcode]}'
)
elif (
@@ -523,13 +567,13 @@ def _inject_s_waitcnt_compat(self) -> None:
or getattr(dte, 'inst_name', None) is not None
):
raise ValueError(
- 'RDNA4 S_WAITCNT terminal decode entry is already occupied'
+ f'RDNA4 {instruction_name} terminal decode entry is already occupied'
)
inst = Instruction(
- 'S_WAITCNT',
- 'ENC_SOPP',
- 9,
+ instruction_name,
+ enc_name,
+ opcode,
[
Operand(
'simm16',
@@ -542,7 +586,7 @@ def _inject_s_waitcnt_compat(self) -> None:
1,
)
],
- available_encodings=frozenset({'ENC_SOPP'}),
+ available_encodings=frozenset({enc_name}),
)
insert_idx = next(
(
@@ -576,32 +620,35 @@ def _inject_cdna5_permlane64_compat(self) -> None:
if self.isa_spec.arch_name != 'cdna5':
return
- enc = self.isa_spec.encoding_map.get('ENC_VOP1')
+ enc_name, opcode, instruction_name = self._compatibility_instruction_slot(
+ 'V_PERMLANE64_B32'
+ )
+ enc = self.isa_spec.encoding_map.get(enc_name)
if enc is None:
raise ValueError(
- 'CDNA5 V_PERMLANE64_B32 compatibility injection requires ENC_VOP1'
+ f'CDNA5 {instruction_name} compatibility injection requires '
+ f'{enc_name}'
)
if enc.primary_dt_ptrs is None:
raise ValueError(
- 'CDNA5 V_PERMLANE64_B32 compatibility injection requires '
- 'an ENC_VOP1 primary decode route table'
+ f'CDNA5 {instruction_name} compatibility injection requires '
+ f'an {enc_name} primary decode route table'
)
- opcode = 103
if any(
- inst.name == 'V_PERMLANE64_B32' and inst.opcode == opcode
+ inst.name == instruction_name and inst.opcode == opcode
for inst in enc.insts
):
return
occupied = next((inst for inst in enc.insts if inst.opcode == opcode), None)
if occupied is not None:
raise ValueError(
- 'CDNA5 V_PERMLANE64_B32 compatibility opcode 103 is already '
- f'occupied by {occupied.name}'
+ f'CDNA5 {instruction_name} compatibility opcode {opcode} is '
+ f'already occupied by {occupied.name}'
)
if len(enc.primary_dt_ptrs) <= opcode:
raise ValueError(
- 'CDNA5 ENC_VOP1 primary decode route table does not contain '
- 'V_PERMLANE64_B32 opcode 103'
+ f'CDNA5 {enc_name} primary decode route table does not contain '
+ f'{instruction_name} opcode {opcode}'
)
dt_ptr = enc.primary_dt_ptrs[opcode]
@@ -615,40 +662,39 @@ def _inject_cdna5_permlane64_compat(self) -> None:
}
if len(adjacent_routes) != 1:
raise ValueError(
- 'CDNA5 V_PERMLANE64_B32 opcode 103 requires exactly one '
- 'adjacent ENC_VOP1 decode route'
+ f'CDNA5 {instruction_name} opcode {opcode} requires exactly '
+ f'one adjacent {enc_name} decode route'
)
dt_ptr = adjacent_routes.pop()
if dt_ptr < 0 or dt_ptr >= len(self.isa_spec.primary_decode_table):
raise ValueError(
- 'CDNA5 V_PERMLANE64_B32 opcode 103 resolves to invalid primary '
+ f'CDNA5 {instruction_name} opcode {opcode} resolves to invalid primary '
f'decode-table index {dt_ptr}'
)
dte = self.isa_spec.primary_decode_table[dt_ptr]
- decode_func = 'decodeVPermlane64B32Vop1'
if dte.sub_decode_funcs is not None:
if opcode >= len(dte.sub_decode_funcs):
raise ValueError(
- 'CDNA5 V_PERMLANE64_B32 opcode 103 is outside the selected '
- 'subdecode table'
+ f'CDNA5 {instruction_name} opcode {opcode} is outside the '
+ 'selected subdecode table'
)
if dte.sub_decode_funcs[opcode] not in (None, 'decodeInvalid'):
raise ValueError(
- 'CDNA5 V_PERMLANE64_B32 opcode 103 subdecode slot is already '
- f'occupied by {dte.sub_decode_funcs[opcode]}'
+ f'CDNA5 {instruction_name} opcode {opcode} subdecode slot is '
+ f'already occupied by {dte.sub_decode_funcs[opcode]}'
)
elif (
getattr(dte, 'decode_func', None) is not None
or getattr(dte, 'inst_name', None) is not None
):
raise ValueError(
- 'CDNA5 V_PERMLANE64_B32 terminal decode entry is already occupied'
+ f'CDNA5 {instruction_name} terminal decode entry is already occupied'
)
inst = Instruction(
- 'V_PERMLANE64_B32',
- 'ENC_VOP1',
+ instruction_name,
+ enc_name,
opcode,
[
Operand(
@@ -674,7 +720,7 @@ def _inject_cdna5_permlane64_compat(self) -> None:
'FMT_NUM_B32',
),
],
- available_encodings=frozenset({'ENC_VOP1'}),
+ available_encodings=frozenset({enc_name}),
)
insert_idx = next(
(idx for idx, existing in enumerate(enc.insts) if existing.opcode > opcode),
@@ -684,6 +730,7 @@ def _inject_cdna5_permlane64_compat(self) -> None:
if patch_route:
enc.primary_dt_ptrs[opcode] = dt_ptr
+ decode_func = f'decode{inst.fmt_name}'
if dte.sub_decode_funcs is not None:
dte.sub_decode_funcs[opcode] = decode_func
else:
@@ -1176,6 +1223,10 @@ class with a literal constant).
inst_name_node = xs.get_node(inst_node, xs.INST_NAME)
inst_encs_node = xs.get_node(inst_node, xs.INST_ENCODINGS)
inst_name = inst_name_node.text
+ addition_id = inst_node.attrib.get(ADDITION_SOURCE_ATTR)
+ source_addition = (
+ self._addition_by_id.get(addition_id) if addition_id else None
+ )
available_encodings = frozenset(
xs.get_node_text(xs.get_node(inst_enc_node, xs.ENCODING_NAME))
for inst_enc_node in inst_encs_node
@@ -1261,6 +1312,7 @@ class with a literal constant).
opnds,
is_implied_literal,
available_encodings,
+ source_addition,
)
# Implied-literal instructions go to the parent encoding's
diff --git a/emulation/rocjitsu/lib/python/amdisa/tests/test_generator_profile_gates.py b/emulation/rocjitsu/lib/python/amdisa/tests/test_generator_profile_gates.py
index a9e9e62a51d1..9e2d5b77177a 100644
--- a/emulation/rocjitsu/lib/python/amdisa/tests/test_generator_profile_gates.py
+++ b/emulation/rocjitsu/lib/python/amdisa/tests/test_generator_profile_gates.py
@@ -1125,6 +1125,11 @@ def test_gfx1250_generated_inventory_omits_legacy_s_waitcnt(
def _fake_sopp_parser(arch_name: str, *, sub_decode_funcs: list[str | None] | None):
parser = object.__new__(Parser)
+ parser.profile = {
+ 'cdna5': Cdna5Profile(),
+ 'rdna3': Rdna3Profile(),
+ 'rdna4': Rdna4Profile(),
+ }[arch_name]
sopp = SimpleNamespace(
insts=[
Instruction('S_WAIT_ALU', 'ENC_SOPP', 8, []),
@@ -1180,6 +1185,21 @@ def test_rdna4_parser_injects_s_waitcnt_compat_once_in_opcode_order():
assert dte.sub_decode_funcs[9] == 'decodeSWaitcntSopp'
+def test_rdna4_parser_selects_its_compatibility_slot_by_instruction_name():
+ parser, sopp, dte = _fake_sopp_parser('rdna4', sub_decode_funcs=[None] * 16)
+ parser.profile = SimpleNamespace(
+ compatibility_instruction_slots={
+ ('ENC_OTHER', 4): 'OTHER_COMPAT',
+ ('ENC_SOPP', 9): 'S_WAITCNT',
+ }
+ )
+
+ parser._inject_s_waitcnt_compat()
+
+ assert any(inst.name == 'S_WAITCNT' and inst.opcode == 9 for inst in sopp.insts)
+ assert dte.sub_decode_funcs[9] == 'decodeSWaitcntSopp'
+
+
def test_gfx1250_parser_does_not_inject_legacy_s_waitcnt():
parser, sopp, dte = _fake_sopp_parser('cdna5', sub_decode_funcs=[None] * 16)
@@ -1194,6 +1214,7 @@ def test_gfx1250_parser_does_not_inject_legacy_s_waitcnt():
def test_gfx1250_parser_injects_permlane64_compat_once():
parser = object.__new__(Parser)
+ parser.profile = Cdna5Profile()
vop1 = SimpleNamespace(
insts=[
Instruction('V_SWAP_B16', 'ENC_VOP1', 102, []),
@@ -1230,6 +1251,7 @@ def test_gfx1250_parser_injects_permlane64_compat_once():
def test_gfx1250_parser_permlane64_requires_an_unambiguous_adjacent_route():
parser = object.__new__(Parser)
+ parser.profile = Cdna5Profile()
vop1 = SimpleNamespace(
insts=[Instruction('V_SWAP_B16', 'ENC_VOP1', 102, [])],
primary_dt_ptrs=[-1] * 128,
@@ -1256,6 +1278,7 @@ def test_gfx1250_parser_permlane64_requires_an_unambiguous_adjacent_route():
def test_gfx1250_parser_permlane64_rejects_opcode_collision_before_mutation():
parser = object.__new__(Parser)
+ parser.profile = Cdna5Profile()
collision = Instruction('V_OTHER_B32', 'ENC_VOP1', 103, [])
vop1 = SimpleNamespace(
insts=[collision],
@@ -1280,6 +1303,7 @@ def test_gfx1250_parser_permlane64_rejects_opcode_collision_before_mutation():
def test_gfx1250_parser_permlane64_rejects_invalid_decode_table_index_before_mutation():
parser = object.__new__(Parser)
+ parser.profile = Cdna5Profile()
vop1 = SimpleNamespace(
insts=[Instruction('V_SWAP_B16', 'ENC_VOP1', 102, [])],
primary_dt_ptrs=[-1] * 128,
@@ -1334,6 +1358,7 @@ def test_gfx1250_parser_permlane64_rejects_terminal_conflicts_before_mutation(
decode_entry, message
):
parser = object.__new__(Parser)
+ parser.profile = Cdna5Profile()
neighbor = Instruction('V_SWAP_B16', 'ENC_VOP1', 102, [])
vop1 = SimpleNamespace(
insts=[neighbor],
@@ -1371,6 +1396,7 @@ def test_gfx1250_parser_permlane64_rejects_missing_route_invariants(
encoding_map, message
):
parser = object.__new__(Parser)
+ parser.profile = Cdna5Profile()
parser.isa_spec = SimpleNamespace(
arch_name='cdna5',
encoding_map=encoding_map,
@@ -1418,6 +1444,7 @@ def test_rdna4_s_waitcnt_rejects_opcode_collision_before_mutation():
)
def test_rdna4_s_waitcnt_rejects_missing_route_invariants(encoding_map, message):
parser = object.__new__(Parser)
+ parser.profile = Rdna4Profile()
parser.isa_spec = SimpleNamespace(
arch_name='rdna4',
encoding_map=encoding_map,
diff --git a/emulation/rocjitsu/lib/python/amdisa/tests/test_isa_additions.py b/emulation/rocjitsu/lib/python/amdisa/tests/test_isa_additions.py
new file mode 100644
index 000000000000..cfd9019dfa93
--- /dev/null
+++ b/emulation/rocjitsu/lib/python/amdisa/tests/test_isa_additions.py
@@ -0,0 +1,1534 @@
+# Copyright (c) 2026 Advanced Micro Devices, Inc.
+# SPDX-License-Identifier: MIT
+
+from copy import deepcopy
+import re
+import subprocess
+import sys
+import xml.etree.ElementTree as elem_tree
+from pathlib import Path
+
+import pytest
+
+from amdisa.__main__ import _group_isa_additions_args
+from amdisa.isa_additions import (
+ ADDITION_SOURCE_ATTR,
+ IsaAdditionError,
+ apply_isa_additions,
+)
+from amdisa.isa_profile import Cdna1Profile, Cdna5Profile, Rdna4Profile
+from amdisa.parser import Parser
+
+_BASE_XML = '''\
+
+ 1.2.0
+
+
+ AMD TEST 1
+ 1
+
+
+
+ ENC_TEST
+ 32
+ 11110000111000000000000000000000
+
+ 10110000000000000000000000000000
+ 10100000001000000000000000000000
+ 10100000010000000000000000000000
+ 10100000010000000000000000000001
+
+
+
+ default
+ 0
+
+
+ alternate
+ 1
+
+
+
+
+
+ ENCODING
+ 428
+
+
+ OP
+ 321
+
+
+
+
+
+
+
+
+ false
+ false
+ false
+ false
+ false
+
+ BASE_INST
+ Base test instruction.
+
+
+ ENC_TEST
+ default
+ 1
+
+
+ FMT_NUM_B32
+ OPR_TEST
+ 32
+
+
+
+
+
+ TEST
+ TEST
+
+
+
+
+ OPR_TEST
+
+
+
+'''
+
+_PROFILE = Cdna5Profile()
+
+
+def _identifier_text(
+ opcode: int,
+ *,
+ alternate_layout: bool = False,
+ alternate_encoding_value: bool = False,
+) -> str:
+ encoding = '1011' if alternate_encoding_value else '1010'
+ return f'{encoding}0000{opcode:03b}{"0" * 20}{int(alternate_layout)}'
+
+
+def _identifier_addition(
+ opcode: int,
+ *,
+ encoding: str = 'ENC_TEST',
+ declared_opcode: int | str | None = None,
+ radix: int | str = 2,
+ text: str | None = None,
+) -> str:
+ if declared_opcode is None:
+ declared_opcode = opcode
+ if text is None:
+ text = _identifier_text(opcode)
+ return f'''\
+
+ {encoding}
+ {declared_opcode}
+ {text}
+
+'''
+
+
+def _instruction(
+ name: str = 'ADDITION_INST',
+ encoding: str = 'ENC_TEST',
+ opcode: int | str = 2,
+ operand_type: str = 'OPR_TEST',
+) -> str:
+ return f'''\
+
+
+ false
+ false
+ false
+ false
+ false
+
+ {name}
+ Added test instruction.
+
+
+ {encoding}
+ default
+ {opcode}
+
+
+ FMT_NUM_B32
+ {operand_type}
+ 32
+
+
+
+
+
+ TEST
+ TEST
+
+
+'''
+
+
+def _write_additions(
+ tmp_path: Path,
+ instructions: str = '',
+ *,
+ identifier: str = 'test-additions',
+ architecture: str = 'AMD TEST 1',
+ schema: str = '1.2.0',
+ filename: str = 'additions.xml',
+ extra_root: str = '',
+ extra_attributes: str = '',
+ identifier_additions: str | None = None,
+ instructions_attributes: str = '',
+ identifier_container_attributes: str = '',
+) -> Path:
+ identifiers = ''
+ if identifier_additions is not None:
+ identifiers = f'''\
+
+ {identifier_additions}
+ '''
+ path = tmp_path / filename
+ path.write_text(f'''\
+
+ {identifiers}
+
+ {instructions}
+
+ {extra_root}
+
+''')
+ return path
+
+
+def _base_root() -> elem_tree.Element:
+ return elem_tree.fromstring(_BASE_XML)
+
+
+def test_empty_additions_document_does_not_mutate_base_xml(tmp_path):
+ root = _base_root()
+ before = elem_tree.tostring(root)
+ addition = _write_additions(tmp_path)
+
+ provenance = apply_isa_additions(root, [str(addition)], _PROFILE)
+
+ assert elem_tree.tostring(root) == before
+ assert [(item.identifier, item.path) for item in provenance] == [
+ ('test-additions', str(addition))
+ ]
+
+
+def test_addition_is_appended_with_provenance(tmp_path):
+ root = _base_root()
+ addition = _write_additions(tmp_path, _instruction())
+
+ apply_isa_additions(root, [str(addition)], _PROFILE)
+
+ instructions = root.findall('./ISA/Instructions/Instruction')
+ assert [node.findtext('InstructionName') for node in instructions] == [
+ 'BASE_INST',
+ 'ADDITION_INST',
+ ]
+ assert instructions[0].get(ADDITION_SOURCE_ATTR) is None
+ assert instructions[1].get(ADDITION_SOURCE_ATTR) == 'test-additions'
+
+
+def test_instruction_using_existing_populated_identifier_succeeds(tmp_path):
+ root = _base_root()
+ addition = _write_additions(tmp_path, _instruction(opcode=2))
+
+ apply_isa_additions(root, [str(addition)], _PROFILE)
+
+ assert (
+ root.findall('./ISA/Instructions/Instruction')[-1].findtext('InstructionName')
+ == 'ADDITION_INST'
+ )
+
+
+def test_instruction_and_primary_identifier_are_merged_atomically(tmp_path):
+ root = _base_root()
+ addition = _write_additions(
+ tmp_path,
+ _instruction(opcode=3),
+ identifier_additions=_identifier_addition(3),
+ )
+
+ apply_isa_additions(root, [str(addition)], _PROFILE)
+
+ identifiers = root.findall(
+ './ISA/Encodings/Encoding/EncodingIdentifiers/EncodingIdentifier'
+ )
+ assert identifiers[-1].text == _identifier_text(3)
+ assert identifiers[-1].get(ADDITION_SOURCE_ATTR) == 'test-additions'
+ assert (
+ root.findall('./ISA/Instructions/Instruction')[-1].get(ADDITION_SOURCE_ATTR)
+ == 'test-additions'
+ )
+
+
+def test_multiple_additions_preserve_command_line_and_document_order(tmp_path):
+ first = _write_additions(
+ tmp_path,
+ _instruction('ADDITION_A', opcode=2) + _instruction('ADDITION_B', opcode=3),
+ identifier='first',
+ filename='first.xml',
+ identifier_additions=_identifier_addition(3),
+ )
+ second = _write_additions(
+ tmp_path,
+ _instruction('ADDITION_C', opcode=4),
+ identifier='second',
+ filename='second.xml',
+ identifier_additions=_identifier_addition(4),
+ )
+ left = _base_root()
+ right = _base_root()
+
+ left_provenance = apply_isa_additions(left, [str(first), str(second)], _PROFILE)
+ right_provenance = apply_isa_additions(right, [str(first), str(second)], _PROFILE)
+
+ assert elem_tree.tostring(left) == elem_tree.tostring(right)
+ assert [item.identifier for item in left_provenance] == ['first', 'second']
+ assert left_provenance == right_provenance
+ assert [
+ node.findtext('InstructionName')
+ for node in left.findall('./ISA/Instructions/Instruction')
+ ] == ['BASE_INST', 'ADDITION_A', 'ADDITION_B', 'ADDITION_C']
+ assert [
+ node.text
+ for node in left.findall(
+ './ISA/Encodings/Encoding/EncodingIdentifiers/EncodingIdentifier'
+ )[-2:]
+ ] == [_identifier_text(3), _identifier_text(4)]
+
+
+def test_all_additions_validate_before_base_is_modified(tmp_path):
+ valid = _write_additions(
+ tmp_path,
+ _instruction('VALID', opcode=2),
+ identifier='valid',
+ filename='valid.xml',
+ )
+ invalid = _write_additions(
+ tmp_path,
+ _instruction('INVALID', encoding='ENC_MISSING', opcode=3),
+ identifier='invalid',
+ filename='invalid.xml',
+ )
+ root = _base_root()
+ before = elem_tree.tostring(root)
+
+ with pytest.raises(IsaAdditionError, match='unknown encoding'):
+ apply_isa_additions(root, [str(valid), str(invalid)], _PROFILE)
+
+ assert elem_tree.tostring(root) == before
+
+
+def test_bad_later_identifier_addition_does_not_partially_mutate_base(tmp_path):
+ first = _write_additions(
+ tmp_path,
+ _instruction('VALID', opcode=3),
+ identifier='first',
+ filename='first.xml',
+ identifier_additions=_identifier_addition(3),
+ )
+ second = _write_additions(
+ tmp_path,
+ identifier='second',
+ filename='second.xml',
+ identifier_additions=_identifier_addition(4),
+ )
+ root = _base_root()
+ before = elem_tree.tostring(root)
+
+ with pytest.raises(IsaAdditionError, match=r'opcode 4 is unowned'):
+ apply_isa_additions(root, [str(first), str(second)], _PROFILE)
+
+ assert elem_tree.tostring(root) == before
+
+
+@pytest.mark.parametrize(
+ ('keyword', 'value', 'message'),
+ [
+ ('architecture', 'AMD OTHER 1', 'base architecture'),
+ ('schema', '1.1.1', 'base schema'),
+ ('identifier', 'bad id', 'invalid additions document Id'),
+ ],
+)
+def test_additions_metadata_must_match_base(tmp_path, keyword, value, message):
+ addition = _write_additions(tmp_path, _instruction(), **{keyword: value})
+
+ with pytest.raises(IsaAdditionError, match=message):
+ apply_isa_additions(_base_root(), [str(addition)], _PROFILE)
+
+
+def test_duplicate_additions_document_id_is_rejected(tmp_path):
+ first = _write_additions(tmp_path, identifier='same', filename='first.xml')
+ second = _write_additions(tmp_path, identifier='same', filename='second.xml')
+
+ with pytest.raises(IsaAdditionError, match='duplicate additions document Id'):
+ apply_isa_additions(_base_root(), [str(first), str(second)], _PROFILE)
+
+
+def test_duplicate_instruction_across_additions_is_rejected(tmp_path):
+ first = _write_additions(
+ tmp_path,
+ _instruction('SAME_NAME', opcode=2),
+ identifier='first',
+ filename='first.xml',
+ )
+ second = _write_additions(
+ tmp_path,
+ _instruction('SAME_NAME', opcode=3),
+ identifier='second',
+ filename='second.xml',
+ )
+
+ with pytest.raises(IsaAdditionError, match='already exists'):
+ apply_isa_additions(_base_root(), [str(first), str(second)], _PROFILE)
+
+
+def test_instruction_name_collision_after_codegen_normalization_is_rejected(tmp_path):
+ addition = _write_additions(tmp_path, _instruction('base_inst', opcode=2))
+
+ with pytest.raises(
+ IsaAdditionError,
+ match=r"base_inst.*/ENC_TEST.*symbol 'BaseInstTest'.*BASE_INST",
+ ):
+ apply_isa_additions(_base_root(), [str(addition)], _PROFILE)
+
+
+def test_invalid_instruction_name_is_rejected_before_merge(tmp_path):
+ root = _base_root()
+ before = elem_tree.tostring(root)
+ addition = _write_additions(tmp_path, _instruction('BAD-NAME', opcode=2))
+
+ with pytest.raises(IsaAdditionError, match=r"invalid instruction name 'BAD-NAME'"):
+ apply_isa_additions(root, [str(addition)], _PROFILE)
+
+ assert elem_tree.tostring(root) == before
+
+
+def test_opcode_constant_name_collision_after_normalization_is_rejected(tmp_path):
+ root = _base_root()
+ root.find('./ISA/Instructions/Instruction/InstructionName').text = 'S_GETPC_B64'
+ addition = _write_additions(tmp_path, _instruction('S_GET_PC_B64', opcode=2))
+
+ with pytest.raises(
+ IsaAdditionError,
+ match=r"S_GET_PC_B64.*/ENC_TEST.*opcode constant symbol "
+ r"'kSGetPcB64Test'.*S_GETPC_B64",
+ ):
+ apply_isa_additions(root, [str(addition)], _PROFILE)
+
+
+@pytest.mark.parametrize(
+ ('instruction', 'message'),
+ [
+ (
+ _instruction().replace('', ''),
+ 'unknown attributes: Unexpected',
+ ),
+ (
+ _instruction().replace(
+ '', ''
+ ),
+ r'unknown elements: ',
+ ),
+ (
+ _instruction().replace(' Input="true"', ''),
+ 'missing required attributes: Input',
+ ),
+ (
+ _instruction().replace('32', ''),
+ r'expected exactly 1 ',
+ ),
+ ],
+)
+def test_incomplete_or_unknown_instruction_structure_is_rejected_before_merge(
+ tmp_path, instruction, message
+):
+ root = _base_root()
+ before = elem_tree.tostring(root)
+ addition = _write_additions(tmp_path, instruction)
+
+ with pytest.raises(IsaAdditionError, match=message):
+ apply_isa_additions(root, [str(addition)], _PROFILE)
+
+ assert elem_tree.tostring(root) == before
+
+
+def test_same_instruction_may_repeat_slot_when_only_one_condition_is_active(tmp_path):
+ instructions = _instruction().replace(
+ ' ',
+ '''\
+
+ ENC_TEST
+ alternate
+ 2
+
+
+ FMT_NUM_B32
+ OPR_TEST
+ 32
+
+
+
+ ''',
+ )
+ addition = _write_additions(tmp_path, instructions)
+ root = _base_root()
+
+ apply_isa_additions(root, [str(addition)], _PROFILE)
+
+ added = root.findall('./ISA/Instructions/Instruction')[-1]
+ assert len(added.findall('./InstructionEncodings/InstructionEncoding')) == 2
+
+
+def test_instruction_requires_an_active_encoding_condition(tmp_path):
+ root = _base_root()
+ before = elem_tree.tostring(root)
+ instruction = _instruction().replace(
+ 'default',
+ 'alternate',
+ )
+ addition = _write_additions(tmp_path, instruction)
+
+ with pytest.raises(IsaAdditionError, match=r'no encoding form active'):
+ apply_isa_additions(root, [str(addition)], _PROFILE)
+
+ assert elem_tree.tostring(root) == before
+
+
+def test_unknown_encoding_condition_is_rejected_before_merge(tmp_path):
+ root = _base_root()
+ before = elem_tree.tostring(root)
+ instruction = _instruction().replace(
+ 'default',
+ 'defualt',
+ )
+ addition = _write_additions(tmp_path, instruction)
+
+ with pytest.raises(IsaAdditionError, match=r"unknown encoding condition 'defualt'"):
+ apply_isa_additions(root, [str(addition)], _PROFILE)
+
+ assert elem_tree.tostring(root) == before
+
+
+def test_encoding_condition_id_must_match_declared_condition(tmp_path):
+ instruction = _instruction().replace(
+ 'default',
+ 'default',
+ )
+ addition = _write_additions(tmp_path, instruction)
+
+ with pytest.raises(
+ IsaAdditionError, match=r"condition 'default' has Id '99', expected 0"
+ ):
+ apply_isa_additions(_base_root(), [str(addition)], _PROFILE)
+
+
+def test_same_instruction_cannot_repeat_slot_for_same_encoding_condition(tmp_path):
+ instruction = elem_tree.fromstring(_instruction())
+ encodings = instruction.find('InstructionEncodings')
+ encodings.append(deepcopy(encodings[0]))
+ addition = _write_additions(
+ tmp_path, elem_tree.tostring(instruction, encoding='unicode')
+ )
+
+ with pytest.raises(IsaAdditionError, match='repeats opcode.*condition'):
+ apply_isa_additions(_base_root(), [str(addition)], _PROFILE)
+
+
+@pytest.mark.parametrize(
+ ('instructions', 'message'),
+ [
+ (_instruction('BASE_INST', opcode=2), 'already exists'),
+ (_instruction('ADDITION_INST', opcode=1), 'already owned'),
+ (_instruction(encoding='ENC_MISSING'), 'unknown encoding'),
+ (_instruction(operand_type='OPR_MISSING'), 'unknown operand type'),
+ (_instruction(opcode='not-decimal'), 'invalid decimal opcode'),
+ (_instruction(opcode=-1), 'negative opcode'),
+ (_instruction(opcode=8), 'outside the encoding range'),
+ ],
+)
+def test_conflicting_or_invalid_additions_are_rejected(tmp_path, instructions, message):
+ addition = _write_additions(tmp_path, instructions)
+
+ with pytest.raises(IsaAdditionError, match=message):
+ apply_isa_additions(_base_root(), [str(addition)], _PROFILE)
+
+
+@pytest.mark.parametrize(
+ ('identifier_additions', 'instructions', 'message'),
+ [
+ (
+ _identifier_addition(3, encoding='ENC_MISSING'),
+ _instruction(opcode=3),
+ 'unknown encoding',
+ ),
+ (
+ _identifier_addition(3, text=f'{_identifier_text(3)[:-1]}x'),
+ _instruction(opcode=3),
+ 'malformed radix-2 identifier',
+ ),
+ (
+ _identifier_addition(3, text=_identifier_text(3)[:-1]),
+ _instruction(opcode=3),
+ 'identifier width 31.*encoding width 32',
+ ),
+ (
+ _identifier_addition(3, radix=16),
+ _instruction(opcode=3),
+ 'identifier radix 16.*encoding radix 2',
+ ),
+ (
+ _identifier_addition(3, text=f'0{_identifier_text(3)[1:]}'),
+ _instruction(opcode=3),
+ 'incompatible with the base identifier mask/layout',
+ ),
+ (
+ _identifier_addition(4, declared_opcode=3),
+ _instruction(opcode=3),
+ 'decodes opcode 4, not declared opcode 3',
+ ),
+ (_identifier_addition(3), '', 'opcode 3 is unowned'),
+ (_identifier_addition(1), '', 'duplicate identifier'),
+ ],
+)
+def test_invalid_identifier_additions_are_rejected(
+ tmp_path, identifier_additions, instructions, message
+):
+ addition = _write_additions(
+ tmp_path,
+ instructions,
+ identifier_additions=identifier_additions,
+ )
+
+ with pytest.raises(IsaAdditionError, match=message):
+ apply_isa_additions(_base_root(), [str(addition)], _PROFILE)
+
+
+@pytest.mark.parametrize(
+ ('fragment', 'message'),
+ [
+ (
+ '3'
+ f'{_identifier_text(3)}'
+ '',
+ 'exactly one ',
+ ),
+ (
+ 'ENC_TEST'
+ f'{_identifier_text(3)}'
+ '',
+ 'exactly one ',
+ ),
+ (
+ 'ENC_TEST'
+ '3',
+ 'exactly one ',
+ ),
+ (
+ _identifier_addition(3).replace('Radix="2"', 'Radix="2" Replace="true"'),
+ 'unknown attributes: Replace',
+ ),
+ ],
+)
+def test_missing_or_unknown_identifier_structure_is_rejected(
+ tmp_path, fragment, message
+):
+ addition = _write_additions(
+ tmp_path,
+ _instruction(opcode=3),
+ identifier_additions=fragment,
+ )
+
+ with pytest.raises(IsaAdditionError, match=message):
+ apply_isa_additions(_base_root(), [str(addition)], _PROFILE)
+
+
+def test_identifier_collision_against_earlier_addition_is_rejected(tmp_path):
+ first = _write_additions(
+ tmp_path,
+ _instruction(opcode=3),
+ identifier='first',
+ filename='first.xml',
+ identifier_additions=_identifier_addition(3),
+ )
+ second = _write_additions(
+ tmp_path,
+ identifier='second',
+ filename='second.xml',
+ identifier_additions=_identifier_addition(
+ 3, text=_identifier_text(3, alternate_layout=True)
+ ),
+ )
+
+ with pytest.raises(IsaAdditionError, match=r'collides.*opcode 3'):
+ apply_isa_additions(_base_root(), [str(first), str(second)], _PROFILE)
+
+
+def test_identifier_opcode_collision_with_different_encoding_value_is_rejected(
+ tmp_path,
+):
+ first = _write_additions(
+ tmp_path,
+ _instruction(opcode=3),
+ identifier='first',
+ filename='first.xml',
+ identifier_additions=_identifier_addition(3),
+ )
+ second = _write_additions(
+ tmp_path,
+ identifier='second',
+ filename='second.xml',
+ identifier_additions=_identifier_addition(
+ 3,
+ text=_identifier_text(3, alternate_encoding_value=True),
+ ),
+ )
+
+ with pytest.raises(IsaAdditionError, match=r'collides.*opcode 3'):
+ apply_isa_additions(_base_root(), [str(first), str(second)], _PROFILE)
+
+
+def test_identifier_collision_against_base_is_rejected(tmp_path):
+ addition = _write_additions(
+ tmp_path,
+ identifier_additions=_identifier_addition(
+ 1, text=_identifier_text(1, alternate_layout=True)
+ ),
+ )
+
+ with pytest.raises(IsaAdditionError, match=r'collides.*opcode 1'):
+ apply_isa_additions(_base_root(), [str(addition)], _PROFILE)
+
+
+def test_duplicate_identifier_against_earlier_addition_is_rejected(tmp_path):
+ first = _write_additions(
+ tmp_path,
+ _instruction(opcode=3),
+ identifier='first',
+ filename='first.xml',
+ identifier_additions=_identifier_addition(3),
+ )
+ second = _write_additions(
+ tmp_path,
+ identifier='second',
+ filename='second.xml',
+ identifier_additions=_identifier_addition(3),
+ )
+
+ with pytest.raises(IsaAdditionError, match='duplicate identifier'):
+ apply_isa_additions(_base_root(), [str(first), str(second)], _PROFILE)
+
+
+def test_empty_identifier_additions_group_is_rejected(tmp_path):
+ addition = _write_additions(tmp_path, identifier_additions='')
+
+ with pytest.raises(IsaAdditionError, match='must not be empty'):
+ apply_isa_additions(_base_root(), [str(addition)], _PROFILE)
+
+
+def test_unknown_additions_structure_is_rejected(tmp_path):
+ addition = _write_additions(tmp_path, extra_root='')
+
+ with pytest.raises(IsaAdditionError, match='unknown additions root elements'):
+ apply_isa_additions(_base_root(), [str(addition)], _PROFILE)
+
+
+def test_unknown_additions_attribute_is_rejected(tmp_path):
+ addition = _write_additions(tmp_path, extra_attributes=' Replace="true"')
+
+ with pytest.raises(IsaAdditionError, match='unknown additions root attributes'):
+ apply_isa_additions(_base_root(), [str(addition)], _PROFILE)
+
+
+@pytest.mark.parametrize(
+ ('container', 'kwargs'),
+ [
+ ('InstructionAdditions', {'instructions_attributes': ' Replace="true"'}),
+ (
+ 'EncodingIdentifierAdditions',
+ {
+ 'identifier_additions': _identifier_addition(3),
+ 'identifier_container_attributes': ' Replace="true"',
+ },
+ ),
+ ],
+)
+def test_additions_container_attributes_are_rejected(tmp_path, container, kwargs):
+ addition = _write_additions(tmp_path, **kwargs)
+
+ with pytest.raises(
+ IsaAdditionError, match=rf'<{container}> must not have attributes: Replace'
+ ):
+ apply_isa_additions(_base_root(), [str(addition)], _PROFILE)
+
+
+def test_cli_additions_arguments_are_grouped_in_order():
+ assert _group_isa_additions_args(
+ ['cdna5:first.xml', 'rdna4:other.xml', 'cdna5:second.xml']
+ ) == {
+ 'cdna5': ['first.xml', 'second.xml'],
+ 'rdna4': ['other.xml'],
+ }
+
+
+@pytest.mark.parametrize('entry', ['missing-colon', ':additions.xml', 'cdna5:'])
+def test_cli_additions_argument_requires_name_and_path(entry):
+ with pytest.raises(ValueError, match='name'):
+ _group_isa_additions_args([entry])
+
+
+def _write_encoding_clone_additions(
+ tmp_path: Path,
+ base_xml: Path,
+ *,
+ encoding_name: str,
+ instruction_name: str,
+ opcode: int,
+) -> Path:
+ base_root = elem_tree.parse(base_xml).getroot()
+ original = next(
+ instruction
+ for instruction in base_root.findall('./ISA/Instructions/Instruction')
+ if any(
+ encoding.findtext('EncodingName') == encoding_name
+ for encoding in instruction.findall(
+ './InstructionEncodings/InstructionEncoding'
+ )
+ )
+ )
+ addition = deepcopy(original)
+ addition.find('InstructionName').text = instruction_name
+ instruction_encodings = addition.find('InstructionEncodings')
+ for encoding in list(instruction_encodings):
+ if encoding.findtext('EncodingName') != encoding_name:
+ instruction_encodings.remove(encoding)
+ else:
+ encoding.find('Opcode').text = str(opcode)
+
+ addition_root = elem_tree.Element(
+ 'IsaAdditions',
+ {
+ 'Id': 'compat-slot-test',
+ 'BaseArchitecture': base_root.findtext(
+ './ISA/Architecture/ArchitectureName'
+ ),
+ 'BaseSchemaVersion': base_root.findtext('./Document/SchemaVersion'),
+ },
+ )
+ instructions = elem_tree.SubElement(addition_root, 'InstructionAdditions')
+ instructions.append(addition)
+ path = tmp_path / 'compat-slot-test.xml'
+ elem_tree.ElementTree(addition_root).write(path, encoding='unicode')
+ return path
+
+
+@pytest.mark.parametrize(
+ ('base_filename', 'profile', 'encoding_name', 'opcode', 'owner'),
+ [
+ (
+ 'amdgpu_isa_cdna5.xml',
+ Cdna5Profile(),
+ 'ENC_VOP1',
+ 103,
+ 'V_PERMLANE64_B32',
+ ),
+ ('amdgpu_isa_rdna4.xml', Rdna4Profile(), 'ENC_SOPP', 9, 'S_WAITCNT'),
+ ],
+)
+def test_compatibility_instruction_slots_are_reserved_before_merge(
+ tmp_path, base_filename, profile, encoding_name, opcode, owner
+):
+ base_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / base_filename
+ )
+ addition = _write_encoding_clone_additions(
+ tmp_path,
+ base_xml,
+ encoding_name=encoding_name,
+ instruction_name='COLLIDING_ADDITION',
+ opcode=opcode,
+ )
+ root = elem_tree.parse(base_xml).getroot()
+ before = elem_tree.tostring(root)
+
+ with pytest.raises(
+ IsaAdditionError,
+ match=rf'opcode {opcode}.*already owned by instruction {owner!r}',
+ ):
+ apply_isa_additions(root, [str(addition)], profile)
+
+ assert elem_tree.tostring(root) == before
+
+
+def test_instruction_in_fully_skipped_encoding_is_rejected_before_merge(tmp_path):
+ base_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / 'amdgpu_isa_cdna5.xml'
+ )
+ addition = _write_encoding_clone_additions(
+ tmp_path,
+ base_xml,
+ encoding_name='ENC_VOP3PX2',
+ instruction_name='SKIPPED_ADDITION',
+ opcode=52,
+ )
+ root = elem_tree.parse(base_xml).getroot()
+ before = elem_tree.tostring(root)
+
+ with pytest.raises(IsaAdditionError, match=r'no encoding form active'):
+ apply_isa_additions(root, [str(addition)], Cdna5Profile())
+
+ assert elem_tree.tostring(root) == before
+
+
+def _cdna1_mimg_identifier(opcode: int) -> str:
+ value = list('00000000000000000000000000000000' '11110000000000000000000000000000')
+ value[39:46] = f'{opcode:07b}'
+ return ''.join(value)
+
+
+def _write_cdna1_mimg_opm_additions(tmp_path: Path, base_xml: Path) -> Path:
+ base_root = elem_tree.parse(base_xml).getroot()
+ original = next(
+ instruction
+ for instruction in base_root.findall('./ISA/Instructions/Instruction')
+ if any(
+ encoding.findtext('EncodingName') == 'ENC_MIMG'
+ for encoding in instruction.findall(
+ './InstructionEncodings/InstructionEncoding'
+ )
+ )
+ )
+ addition = deepcopy(original)
+ addition.find('InstructionName').text = 'IMAGE_ADDITION_TEST'
+ instruction_encodings = addition.find('InstructionEncodings')
+ for encoding in list(instruction_encodings):
+ if encoding.findtext('EncodingName') != 'ENC_MIMG':
+ instruction_encodings.remove(encoding)
+ else:
+ encoding.find('Opcode').text = '134'
+
+ addition_root = elem_tree.Element(
+ 'IsaAdditions',
+ {
+ 'Id': 'cdna1-mimg-opm-test',
+ 'BaseArchitecture': 'AMD CDNA 1',
+ 'BaseSchemaVersion': '1.1.1',
+ },
+ )
+ identifier_additions = elem_tree.SubElement(
+ addition_root, 'EncodingIdentifierAdditions'
+ )
+ identifier_addition = elem_tree.SubElement(
+ identifier_additions, 'EncodingIdentifierAddition'
+ )
+ elem_tree.SubElement(identifier_addition, 'EncodingName').text = 'ENC_MIMG'
+ elem_tree.SubElement(identifier_addition, 'Opcode').text = '6'
+ identifier = elem_tree.SubElement(
+ identifier_addition, 'EncodingIdentifier', {'Radix': '2'}
+ )
+ identifier.text = _cdna1_mimg_identifier(6)
+ instructions = elem_tree.SubElement(addition_root, 'InstructionAdditions')
+ instructions.append(addition)
+ path = tmp_path / 'cdna1-mimg-opm-test.xml'
+ elem_tree.ElementTree(addition_root).write(path, encoding='unicode')
+ return path
+
+
+def test_identifier_ownership_uses_expanded_opm_opcode(tmp_path):
+ base_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / 'amdgpu_isa_cdna1.xml'
+ )
+ addition = _write_cdna1_mimg_opm_additions(tmp_path, base_xml)
+
+ spec = Parser(str(base_xml), Cdna1Profile(), [str(addition)]).parse()
+
+ added = next(
+ instruction
+ for instruction in spec.encoding_map['ENC_MIMG'].insts
+ if instruction.name == 'IMAGE_ADDITION_TEST'
+ )
+ assert added.opcode == 134
+ assert spec.encoding_map['ENC_MIMG'].primary_dt_ptrs[134] != -1
+
+
+def _cdna5_sop1_identifier(opcode: int, *, implied_literal: bool = False) -> str:
+ if implied_literal:
+ value = list(
+ '00000000000000000000000000000000' '10111110100000000000000000000000'
+ )
+ value[48:56] = f'{opcode:08b}'
+ else:
+ value = list('10111110100000000000000000000000')
+ value[16:24] = f'{opcode:08b}'
+ return ''.join(value)
+
+
+_CDNA5_SOP1_OPCODE7_BYTES = bytes.fromhex('00 07 80 be')
+_CDNA5_SOP1_LITERAL_OPCODE7_BYTES = bytes.fromhex('00 07 80 be 00 00 00 00')
+
+
+def _cdna5_primary_table_index(encoded: bytes) -> int:
+ return int.from_bytes(encoded[:4], byteorder='little') >> 23
+
+
+def _assert_sop1_opcode7_decode_mapping(spec) -> None:
+ table_index = _cdna5_primary_table_index(_CDNA5_SOP1_OPCODE7_BYTES)
+ assert table_index == 381
+ assert _cdna5_sop1_identifier(7) == (
+ f'{int.from_bytes(_CDNA5_SOP1_OPCODE7_BYTES, byteorder="little"):032b}'
+ )
+ assert spec.encoding_map['ENC_SOP1'].primary_dt_ptrs[7] == table_index
+
+ entry = spec.primary_decode_table[table_index]
+ assert entry.decode_func == 'subDecodeSop1'
+ assert entry.sub_decode_table == 'sub_decode_sop1'
+ assert entry.sub_decode_funcs[7] == 'decodeSAdditionTestB32Sop1'
+
+
+def _write_cdna5_clone_additions(
+ tmp_path: Path,
+ base_xml: Path,
+ *,
+ opcode: int = 7,
+ instruction_name: str = 'S_ADDITION_TEST_B32',
+ add_identifiers: bool = False,
+ include_implied_literal: bool = False,
+) -> Path:
+ base_root = elem_tree.parse(base_xml).getroot()
+ original = next(
+ instruction
+ for instruction in base_root.findall('./ISA/Instructions/Instruction')
+ if instruction.findtext('InstructionName') == 'S_MOV_B32'
+ )
+ addition = deepcopy(original)
+ addition.find('InstructionName').text = instruction_name
+ retained_encodings = {'ENC_SOP1'}
+ if include_implied_literal:
+ retained_encodings.add('SOP1_INST_LITERAL')
+ instruction_encodings = addition.find('InstructionEncodings')
+ for encoding in list(instruction_encodings):
+ if encoding.findtext('EncodingName') not in retained_encodings:
+ instruction_encodings.remove(encoding)
+ for opcode_node in addition.findall(
+ './InstructionEncodings/InstructionEncoding/Opcode'
+ ):
+ opcode_node.text = str(opcode)
+
+ addition_root = elem_tree.Element(
+ 'IsaAdditions',
+ {
+ 'Id': 'cdna5-test',
+ 'BaseArchitecture': 'AMD CDNA 5',
+ 'BaseSchemaVersion': '1.2.0',
+ },
+ )
+ if add_identifiers:
+ identifier_additions = elem_tree.SubElement(
+ addition_root, 'EncodingIdentifierAdditions'
+ )
+ for encoding_name, implied_literal in (
+ ('ENC_SOP1', False),
+ ('SOP1_INST_LITERAL', True),
+ ):
+ if implied_literal and not include_implied_literal:
+ continue
+ identifier_addition = elem_tree.SubElement(
+ identifier_additions, 'EncodingIdentifierAddition'
+ )
+ elem_tree.SubElement(identifier_addition, 'EncodingName').text = (
+ encoding_name
+ )
+ elem_tree.SubElement(identifier_addition, 'Opcode').text = str(opcode)
+ identifier = elem_tree.SubElement(
+ identifier_addition, 'EncodingIdentifier', {'Radix': '2'}
+ )
+ identifier.text = _cdna5_sop1_identifier(
+ opcode, implied_literal=implied_literal
+ )
+ instructions = elem_tree.SubElement(addition_root, 'InstructionAdditions')
+ instructions.append(addition)
+ path = tmp_path / 'cdna5-test.xml'
+ elem_tree.ElementTree(addition_root).write(path, encoding='unicode')
+ return path
+
+
+def test_cdna5_distinct_active_conditions_cannot_generate_the_same_class(tmp_path):
+ base_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / 'amdgpu_isa_cdna5.xml'
+ )
+ addition = _write_cdna5_clone_additions(tmp_path, base_xml)
+ tree = elem_tree.parse(addition)
+ encodings = tree.find('./InstructionAdditions/Instruction/InstructionEncodings')
+ primary = next(
+ encoding
+ for encoding in encodings
+ if encoding.findtext('EncodingName') == 'ENC_SOP1'
+ )
+ duplicate = deepcopy(primary)
+ condition = duplicate.find('EncodingCondition')
+ condition.text = 'default'
+ condition.set('Id', '0')
+ encodings.append(duplicate)
+ tree.write(addition, encoding='unicode')
+ root = elem_tree.parse(base_xml).getroot()
+ before = elem_tree.tostring(root)
+
+ with pytest.raises(
+ IsaAdditionError,
+ match=r"multiple active forms.*symbol 'SAdditionTestB32Sop1'",
+ ):
+ apply_isa_additions(root, [str(addition)], Cdna5Profile())
+
+ assert elem_tree.tostring(root) == before
+
+
+def test_cdna5_unknown_operand_field_is_rejected_before_merge(tmp_path):
+ base_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / 'amdgpu_isa_cdna5.xml'
+ )
+ addition = _write_cdna5_clone_additions(tmp_path, base_xml)
+ tree = elem_tree.parse(addition)
+ primary = next(
+ encoding
+ for encoding in tree.findall(
+ './InstructionAdditions/Instruction/InstructionEncodings/'
+ 'InstructionEncoding'
+ )
+ if encoding.findtext('EncodingName') == 'ENC_SOP1'
+ )
+ destination = next(
+ field
+ for field in primary.findall('./Operands/Operand/FieldName')
+ if field.text == 'SDST'
+ )
+ destination.text = 'BOGUS'
+ tree.write(addition, encoding='unicode')
+ root = elem_tree.parse(base_xml).getroot()
+ before = elem_tree.tostring(root)
+
+ with pytest.raises(IsaAdditionError, match=r"unknown operand field 'BOGUS'"):
+ apply_isa_additions(root, [str(addition)], Cdna5Profile())
+
+ assert elem_tree.tostring(root) == before
+
+
+@pytest.mark.parametrize(
+ ('field_name', 'message'),
+ [
+ ('literal64', r"unknown operand field 'literal64'"),
+ (
+ 'LITERAL',
+ r"implied-literal operand field 'LITERAL'.*type or role not established",
+ ),
+ ],
+)
+def test_cdna5_implied_literal_field_requires_an_established_contract(
+ tmp_path, field_name, message
+):
+ base_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / 'amdgpu_isa_cdna5.xml'
+ )
+ addition = _write_cdna5_clone_additions(
+ tmp_path, base_xml, include_implied_literal=True
+ )
+ tree = elem_tree.parse(addition)
+ implied_literal = next(
+ encoding
+ for encoding in tree.findall(
+ './InstructionAdditions/Instruction/InstructionEncodings/'
+ 'InstructionEncoding'
+ )
+ if encoding.findtext('EncodingName') == 'SOP1_INST_LITERAL'
+ )
+ destination = next(
+ operand
+ for operand in implied_literal.findall('./Operands/Operand')
+ if operand.findtext('FieldName') == 'SDST'
+ )
+ destination.find('FieldName').text = field_name
+ tree.write(addition, encoding='unicode')
+ root = elem_tree.parse(base_xml).getroot()
+ before = elem_tree.tostring(root)
+
+ with pytest.raises(IsaAdditionError, match=message):
+ apply_isa_additions(root, [str(addition)], Cdna5Profile())
+
+ assert elem_tree.tostring(root) == before
+
+
+def test_cdna5_implied_literal_contract_allows_a_new_value_shape(tmp_path):
+ base_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / 'amdgpu_isa_cdna5.xml'
+ )
+ addition = _write_cdna5_clone_additions(
+ tmp_path,
+ base_xml,
+ add_identifiers=True,
+ include_implied_literal=True,
+ )
+ tree = elem_tree.parse(addition)
+ literal_operands = [
+ operand
+ for encoding in tree.findall(
+ './InstructionAdditions/Instruction/InstructionEncodings/'
+ 'InstructionEncoding'
+ )
+ if encoding.findtext('EncodingName') == 'SOP1_INST_LITERAL'
+ for operand in encoding.findall('./Operands/Operand')
+ if operand.findtext('FieldName') == 'LITERAL'
+ ]
+ assert literal_operands
+ for operand in literal_operands:
+ operand.find('OperandSize').text = '128'
+ operand.find('DataFormatName').text = 'FMT_NUM_PK2_F64'
+ tree.write(addition, encoding='unicode')
+
+ spec = Parser(str(base_xml), Cdna5Profile(), [str(addition)]).parse()
+
+ added = next(
+ instruction
+ for instruction in spec.encoding_map['ENC_SOP1'].insts
+ if instruction.name == 'S_ADDITION_TEST_B32'
+ )
+ assert added.source_addition == spec.applied_additions[0]
+
+
+def test_cdna5_normalized_instruction_name_collision_is_rejected(tmp_path):
+ base_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / 'amdgpu_isa_cdna5.xml'
+ )
+ addition = _write_cdna5_clone_additions(
+ tmp_path,
+ base_xml,
+ instruction_name='s_mov_b32',
+ )
+
+ with pytest.raises(
+ IsaAdditionError,
+ match=r"s_mov_b32.*/ENC_SOP1.*symbol 'SMovB32Sop1'.*S_MOV_B32",
+ ):
+ apply_isa_additions(
+ elem_tree.parse(base_xml).getroot(), [str(addition)], Cdna5Profile()
+ )
+
+
+def test_parser_rejects_unknown_base_addition_provenance(tmp_path):
+ source_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / 'amdgpu_isa_cdna5.xml'
+ )
+ tree = elem_tree.parse(source_xml)
+ instruction = tree.find('./ISA/Instructions/Instruction')
+ instruction_name = instruction.findtext('InstructionName')
+ instruction.set(ADDITION_SOURCE_ATTR, 'unknown-addition')
+ base_xml = tmp_path / 'base-with-unknown-provenance.xml'
+ tree.write(base_xml, encoding='unicode')
+
+ with pytest.raises(
+ IsaAdditionError,
+ match=rf'{instruction_name}.*{ADDITION_SOURCE_ATTR}.*unknown-addition',
+ ):
+ Parser(str(base_xml), Cdna5Profile()).parse()
+
+
+def test_parser_retains_additions_provenance_on_added_instructions(tmp_path):
+ base_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / 'amdgpu_isa_cdna5.xml'
+ )
+ addition = _write_cdna5_clone_additions(tmp_path, base_xml, add_identifiers=True)
+
+ spec = Parser(str(base_xml), Cdna5Profile(), [str(addition)]).parse()
+
+ assert [(item.identifier, item.path) for item in spec.applied_additions] == [
+ ('cdna5-test', str(addition))
+ ]
+ added = [
+ instruction
+ for encoding in spec.inst_encodings
+ for instruction in encoding.insts
+ if instruction.name == 'S_ADDITION_TEST_B32'
+ ]
+ assert added
+ assert all(
+ instruction.source_addition == spec.applied_additions[0]
+ for instruction in added
+ )
+ _assert_sop1_opcode7_decode_mapping(spec)
+
+
+def test_primary_and_implied_literal_identifiers_build_decode_pointers(tmp_path):
+ base_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / 'amdgpu_isa_cdna5.xml'
+ )
+ addition = _write_cdna5_clone_additions(
+ tmp_path,
+ base_xml,
+ add_identifiers=True,
+ include_implied_literal=True,
+ )
+
+ spec = Parser(str(base_xml), Cdna5Profile(), [str(addition)]).parse()
+
+ _assert_sop1_opcode7_decode_mapping(spec)
+ literal_table_index = _cdna5_primary_table_index(_CDNA5_SOP1_LITERAL_OPCODE7_BYTES)
+ assert literal_table_index == 381
+ assert _cdna5_sop1_identifier(7, implied_literal=True) == (
+ f'{int.from_bytes(_CDNA5_SOP1_LITERAL_OPCODE7_BYTES, byteorder="little"):064b}'
+ )
+ assert (
+ spec.encoding_map['SOP1_INST_LITERAL'].primary_dt_ptrs[7] == literal_table_index
+ )
+ literal_entry = spec.primary_decode_table[literal_table_index]
+ assert literal_entry.decode_func == 'subDecodeSop1'
+ assert literal_entry.sub_decode_table == 'sub_decode_sop1'
+ assert literal_entry.sub_decode_funcs[7] == 'decodeSAdditionTestB32Sop1'
+
+
+def test_implied_literal_identifier_requires_matching_parent_owner(tmp_path):
+ base_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / 'amdgpu_isa_cdna5.xml'
+ )
+ addition = _write_cdna5_clone_additions(
+ tmp_path,
+ base_xml,
+ add_identifiers=True,
+ include_implied_literal=True,
+ )
+ tree = elem_tree.parse(addition)
+ for parent_path in (
+ './EncodingIdentifierAdditions',
+ './InstructionAdditions/Instruction/InstructionEncodings',
+ ):
+ parent = tree.find(parent_path)
+ for child in list(parent):
+ if child.findtext('EncodingName') == 'ENC_SOP1':
+ parent.remove(child)
+ tree.write(addition, encoding='unicode')
+
+ with pytest.raises(
+ IsaAdditionError, match=r'implied-literal identifier.*parent encoding.*owner'
+ ):
+ Parser(str(base_xml), Cdna5Profile(), [str(addition)]).parse()
+
+
+def test_parser_rejects_added_instruction_without_decode_identifier(tmp_path):
+ base_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / 'amdgpu_isa_cdna5.xml'
+ )
+ addition = _write_cdna5_clone_additions(tmp_path, base_xml)
+
+ with pytest.raises(
+ IsaAdditionError, match=r'S_ADDITION_TEST_B32.*opcode 7.*unreachable'
+ ):
+ Parser(str(base_xml), Cdna5Profile(), [str(addition)]).parse()
+
+
+def test_later_parse_without_additions_is_not_affected(tmp_path):
+ base_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / 'amdgpu_isa_cdna5.xml'
+ )
+ addition = _write_cdna5_clone_additions(tmp_path, base_xml, add_identifiers=True)
+
+ with_addition = Parser(str(base_xml), Cdna5Profile(), [str(addition)]).parse()
+ without_addition = Parser(str(base_xml), Cdna5Profile()).parse()
+
+ assert with_addition.encoding_map['ENC_SOP1'].primary_dt_ptrs[7] != -1
+ assert without_addition.encoding_map['ENC_SOP1'].primary_dt_ptrs[7] == -1
+ assert all(
+ instruction.name != 'S_ADDITION_TEST_B32'
+ for encoding in without_addition.inst_encodings
+ for instruction in encoding.insts
+ )
+
+
+def test_parser_rejects_added_opcode_outside_base_encoding_range(tmp_path):
+ base_xml = (
+ Path(__file__).resolve().parents[6]
+ / 'shared'
+ / 'machine-readable-isa'
+ / 'isa'
+ / 'amdgpu_isa_cdna5.xml'
+ )
+ addition = _write_cdna5_clone_additions(tmp_path, base_xml)
+ tree = elem_tree.parse(addition)
+ for opcode in tree.findall(
+ './InstructionAdditions/Instruction/InstructionEncodings/'
+ 'InstructionEncoding/Opcode'
+ ):
+ opcode.text = '999999'
+ tree.write(addition, encoding='unicode')
+
+ with pytest.raises(
+ IsaAdditionError,
+ match=r'S_ADDITION_TEST_B32.*/ENC_SOP1 opcode 999999.*outside the encoding range',
+ ):
+ Parser(str(base_xml), Cdna5Profile(), [str(addition)]).parse()
+
+
+def _generated_files(root: Path) -> dict[Path, bytes]:
+ return {
+ path.relative_to(root): path.read_bytes()
+ for path in root.rglob('*')
+ if path.is_file()
+ }
+
+
+def test_empty_additions_document_is_byte_identical_through_cli(tmp_path):
+ repo_root = Path(__file__).resolve().parents[6]
+ base_xml = (
+ repo_root / 'shared' / 'machine-readable-isa' / 'isa' / 'amdgpu_isa_cdna5.xml'
+ )
+ addition = _write_additions(
+ tmp_path,
+ identifier='empty-cdna5',
+ architecture='AMD CDNA 5',
+ filename='empty-cdna5.xml',
+ )
+ baseline_output = tmp_path / 'baseline'
+ additions_output = tmp_path / 'with-additions'
+ baseline_output.mkdir()
+ additions_output.mkdir()
+
+ subprocess.run(
+ [
+ sys.executable,
+ '-m',
+ 'amdisa',
+ f'cdna5:{base_xml}',
+ '--isa-output',
+ str(baseline_output / 'isa'),
+ '--include-root',
+ str(baseline_output),
+ '--dbt-output',
+ str(baseline_output / 'dbt'),
+ ],
+ check=True,
+ capture_output=True,
+ text=True,
+ )
+ subprocess.run(
+ [
+ sys.executable,
+ '-m',
+ 'amdisa',
+ f'cdna5:{base_xml}',
+ '--isa-additions',
+ f'cdna5:{addition}',
+ '--isa-output',
+ str(additions_output / 'isa'),
+ '--include-root',
+ str(additions_output),
+ '--dbt-output',
+ str(additions_output / 'dbt'),
+ ],
+ check=True,
+ capture_output=True,
+ text=True,
+ )
+
+ assert _generated_files(additions_output) == _generated_files(baseline_output)
+
+
+def test_nonempty_additions_document_generates_decoder_dispatch(tmp_path):
+ repo_root = Path(__file__).resolve().parents[6]
+ base_xml = (
+ repo_root / 'shared' / 'machine-readable-isa' / 'isa' / 'amdgpu_isa_cdna5.xml'
+ )
+ addition = _write_cdna5_clone_additions(tmp_path, base_xml, add_identifiers=True)
+ output = tmp_path / 'generated'
+ output.mkdir()
+
+ subprocess.run(
+ [
+ sys.executable,
+ '-m',
+ 'amdisa',
+ f'cdna5:{base_xml}',
+ '--isa-additions',
+ f'cdna5:{addition}',
+ '--isa-output',
+ str(output / 'isa'),
+ '--dbt-output',
+ str(output / 'dbt'),
+ ],
+ check=True,
+ capture_output=True,
+ text=True,
+ )
+
+ decoder = (output / 'isa' / 'cdna5' / 'decoder.cpp').read_text()
+ match = re.search(
+ r'DecoderImpl::sub_decode_sop1 = \{\s*(.*?)\s*\};', decoder, re.DOTALL
+ )
+ assert match is not None
+ entries = [entry.strip() for entry in match.group(1).split(',')]
+ assert entries[7] == '&detail::decodeSAdditionTestB32Sop1'