Make legalization actions configurable.

When an instruction doesn't have a valid encoding for the target ISA, it
needs to be legalized. Different legalization strategies can be
expressed as separate XFormGroup objects.

Make the choice of XFormGroup configurable per CPU mode, rather than
depending on a hard-coded default.

Add a CPUMode.legalize_type() method which assigns an XFormGroup to
controlling type variables and lets you set a default.

Add a `legalize` field to Level1Entry so the first-level hash table
lookup gives us the configured default legalization action for the
instruction's controlling type variable.
This commit is contained in:
Jakob Stoklund Olesen
2017-07-24 11:21:12 -07:00
parent a06964fc0e
commit 127b22af5f
10 changed files with 229 additions and 41 deletions

View File

@@ -27,6 +27,13 @@ narrow = XFormGroup('narrow', """
operations are expressed in terms of smaller integer types.
""")
widen = XFormGroup('widen', """
Legalize instructions by widening.
The transformations in the 'widen' group work by expressing
instructions in terms of larger types.
""")
expand = XFormGroup('expand', """
Legalize instructions by expansion.

View File

@@ -1,8 +1,10 @@
"""Defining instruction set architectures."""
from __future__ import absolute_import
from collections import OrderedDict
from .predicates import And
from .registers import RegClass, Register, Stack
from .ast import Apply
from .types import ValueType
# The typing module is only required by mypy, and we don't use these imports
# outside type comments.
@@ -12,8 +14,8 @@ try:
from .instructions import MaybeBoundInst, InstructionGroup, InstructionFormat # noqa
from .predicates import PredNode # noqa
from .settings import SettingGroup # noqa
from .types import ValueType # noqa
from .registers import RegBank # noqa
from .xform import XFormGroup # noqa
OperandConstraint = Union[RegClass, Register, int, Stack]
ConstraintSeq = Union[OperandConstraint, Tuple[OperandConstraint, ...]]
# Instruction specification for encodings. Allows for predicated
@@ -43,6 +45,11 @@ class TargetISA(object):
self.cpumodes = list() # type: List[CPUMode]
self.regbanks = list() # type: List[RegBank]
self.regclasses = list() # type: List[RegClass]
self.legalize_codes = OrderedDict() # type: OrderedDict[XFormGroup, int] # noqa
def __str__(self):
# type: () -> str
return self.name
def finish(self):
# type: () -> TargetISA
@@ -138,6 +145,25 @@ class TargetISA(object):
# `isa/registers.rs`.
assert len(self.regclasses) <= 32, "Too many register classes"
def legalize_code(self, xgrp):
# type: (XFormGroup) -> int
"""
Get the legalization code for the transform group `xgrp`. Assign one if
necessary.
Each target ISA has its own list of legalization actions with
associated legalize codes that appear in the encoding tables.
This method is used to maintain the registry of legalization actions
and their table codes.
"""
if xgrp in self.legalize_codes:
code = self.legalize_codes[xgrp]
else:
code = len(self.legalize_codes)
self.legalize_codes[xgrp] = code
return code
class CPUMode(object):
"""
@@ -157,6 +183,11 @@ class CPUMode(object):
self.encodings = [] # type: List[Encoding]
isa.cpumodes.append(self)
# Tables for configuring legalization actions when no valid encoding
# exists for an instruction.
self.default_legalize = None # type: XFormGroup
self.type_legalize = dict() # type: Dict[ValueType, XFormGroup]
def __str__(self):
# type: () -> str
return self.name
@@ -171,6 +202,36 @@ class CPUMode(object):
"""
self.encodings.append(Encoding(self, *args, **kwargs))
def legalize_type(self, default=None, **kwargs):
# type: (XFormGroup, **XFormGroup) -> None
"""
Configure the legalization action per controlling type variable.
Instructions that have a controlling type variable mentioned in one of
the arguments will be legalized according to the action specified here
instead of using the `legalize_default` action.
The keyword arguments are value type names:
mode.legalize_type(i8=widen, i16=widen, i32=expand)
The `default` argument specifies the action to take for controlling
type variables that don't have an explicitly configured action.
"""
if default is not None:
self.default_legalize = default
for name, xgrp in kwargs.items():
ty = ValueType.by_name(name)
self.type_legalize[ty] = xgrp
def get_legalize_action(self, ty):
# type: (ValueType) -> XFormGroup
"""
Get the legalization action to use for `ty`.
"""
return self.type_legalize.get(ty, self.default_legalize)
class EncRecipe(object):
"""

View File

@@ -241,6 +241,10 @@ class XFormGroup(object):
self.name = name
self.__doc__ = doc
def __str__(self):
# type: () -> str
return self.name
def legalize(self, src, dst):
# type: (Union[Def, Apply], Rtl) -> None
"""

View File

@@ -66,6 +66,7 @@ try:
from cdsl.predicates import PredNode, PredLeaf # noqa
from cdsl.types import ValueType # noqa
from cdsl.instructions import Instruction # noqa
from cdsl.xform import XFormGroup # noqa
except ImportError:
pass
@@ -261,12 +262,17 @@ class Level2Table(object):
"""
Level 2 table mapping instruction opcodes to `EncList` objects.
A level 2 table can be completely empty if it only holds a custom
legalization action for `ty`.
:param ty: Controlling type variable of all entries, or `None`.
:param legalize: Default legalize action for `ty`.
"""
def __init__(self, ty):
# type: (ValueType) -> None
def __init__(self, ty, legalize):
# type: (ValueType, XFormGroup) -> None
self.ty = ty
self.legalize = legalize
# Maps inst -> EncList
self.lists = OrderedDict() # type: OrderedDict[Instruction, EncList]
@@ -278,6 +284,16 @@ class Level2Table(object):
self.lists[inst] = ls
return ls
def is_empty(self):
# type: () -> bool
"""
Check if this level 2 table is completely empty.
This can happen if the associated type simply has an overridden
legalize action.
"""
return len(self.lists) == 0
def enclists(self):
# type: () -> Iterable[EncList]
return iter(self.lists.values())
@@ -310,21 +326,32 @@ class Level1Table(object):
Level 1 table mapping types to `Level2` objects.
"""
def __init__(self):
# type: () -> None
def __init__(self, cpumode):
# type: (CPUMode) -> None
self.cpumode = cpumode
self.tables = OrderedDict() # type: OrderedDict[ValueType, Level2Table] # noqa
if cpumode.default_legalize is None:
raise AssertionError(
'CPU mode {}.{} needs a default legalize action'
.format(cpumode.isa, cpumode))
self.legalize_code = cpumode.isa.legalize_code(
cpumode.default_legalize)
def __getitem__(self, ty):
# type: (ValueType) -> Level2Table
tbl = self.tables.get(ty)
if not tbl:
tbl = Level2Table(ty)
legalize = self.cpumode.get_legalize_action(ty)
# Allocate a legalization code in a predictable order.
self.cpumode.isa.legalize_code(legalize)
tbl = Level2Table(ty, legalize)
self.tables[ty] = tbl
return tbl
def l2tables(self):
# type: () -> Iterable[Level2Table]
return iter(self.tables.values())
return (l2 for l2 in self.tables.values() if not l2.is_empty())
def make_tables(cpumode):
@@ -332,11 +359,17 @@ def make_tables(cpumode):
"""
Generate tables for `cpumode` as described above.
"""
table = Level1Table()
table = Level1Table(cpumode)
for enc in cpumode.encodings:
ty = enc.ctrl_typevar()
inst = enc.inst
table[ty][inst].encodings.append(enc)
# Ensure there are level 1 table entries for all types with a custom
# legalize action. Try to be stable relative to dict ordering.
for ty in sorted(cpumode.type_legalize.keys(), key=str):
table[ty]
return table
@@ -412,22 +445,42 @@ def emit_level1_hashtable(cpumode, level1, offt, fmt):
'pub static LEVEL1_{}: [Level1Entry<{}>; {}] = ['
.format(cpumode.name.upper(), offt, len(hash_table)), '];'):
for level2 in hash_table:
if level2:
l2l = int(math.log(level2.hash_table_len, 2))
assert l2l > 0, "Hash table too small"
tyname = level2.ty.name if level2.ty is not None else 'void'
fmt.line(
'Level1Entry ' +
'{{ ty: types::{}, log2len: {}, offset: {:#08x} }},'
.format(
tyname.upper(),
l2l,
level2.hash_table_offset))
# Empty hash table entry. Include the default legalization action.
if not level2:
fmt.format(
'Level1Entry {{ ty: types::VOID, log2len: !0, '
'offset: 0, legalize: {} }},',
level1.legalize_code)
continue
if level2.ty is not None:
tyname = level2.ty.rust_name()
else:
# Empty entry.
fmt.line(
'Level1Entry ' +
'{ ty: types::VOID, log2len: 0, offset: 0 },')
tyname = 'types::VOID'
lcode = cpumode.isa.legalize_code(level2.legalize)
# Empty level 2 table: Only a specialized legalization action, no
# actual table.
# Set an offset that is out of bounds, but make sure it doesn't
# overflow its type when adding `1<<log2len`.
if level2.is_empty():
fmt.format(
'Level1Entry {{ '
'ty: {}, log2len: 0, offset: !0 - 1, '
'legalize: {} }}, // {}',
tyname, lcode, level2.legalize)
continue
# Proper level 2 hash table.
l2l = int(math.log(level2.hash_table_len, 2))
assert l2l > 0, "Level2 hash table too small"
fmt.format(
'Level1Entry {{ '
'ty: {}, log2len: {}, offset: {:#08x}, '
'legalize: {} }}, // {}',
tyname, l2l, level2.hash_table_offset,
lcode, level2.legalize)
def offset_type(length):

View File

@@ -6,9 +6,14 @@ Commonly used definitions.
from __future__ import absolute_import
from cdsl.isa import TargetISA, CPUMode
import base.instructions
from base.legalize import narrow
ISA = TargetISA('arm32', [base.instructions.GROUP])
# CPU modes for 32-bit ARM and Thumb2.
A32 = CPUMode('A32', ISA)
T32 = CPUMode('T32', ISA)
# TODO: Refine these.
A32.legalize_type(narrow)
T32.legalize_type(narrow)

View File

@@ -6,6 +6,10 @@ Commonly used definitions.
from __future__ import absolute_import
from cdsl.isa import TargetISA, CPUMode
import base.instructions
from base.legalize import narrow
ISA = TargetISA('arm64', [base.instructions.GROUP])
A64 = CPUMode('A64', ISA)
# TODO: Refine these
A64.legalize_type(narrow)

View File

@@ -9,6 +9,20 @@ from .defs import I32, I64
from . import recipes as r
from . import settings as cfg
from . import instructions as x86
from base.legalize import narrow, expand
I32.legalize_type(
default=narrow,
i32=expand,
f32=expand,
f64=expand)
I64.legalize_type(
default=narrow,
i32=expand,
i64=expand,
f32=expand,
f64=expand)
for inst, opc in [
(base.iadd, 0x01),

View File

@@ -11,6 +11,20 @@ from .recipes import R, Rshamt, Ricmp, I, Iz, Iicmp, Iret, Icall, Icopy
from .recipes import U, UJ, UJcall, SB, SBzero, GPsp, GPfi, Irmov
from .settings import use_m
from cdsl.ast import Var
from base.legalize import narrow, expand
RV32.legalize_type(
default=narrow,
i32=expand,
f32=expand,
f64=expand)
RV64.legalize_type(
default=narrow,
i32=expand,
i64=expand,
f32=expand,
f64=expand)
# Dummies for instruction predicates.
x = Var('x')

View File

@@ -9,6 +9,11 @@ use isa::{Encoding, Legalize};
use settings::PredicateView;
use std::ops::Range;
/// Legalization action to perform when no encoding can be found for an instruction.
///
/// This is an index into an ISA-specific table of legalization actions.
pub type LegalizeCode = u8;
/// Level 1 hash table entry.
///
/// One level 1 hash table is generated per CPU mode. This table is keyed by the controlling type
@@ -19,14 +24,15 @@ use std::ops::Range;
/// have a power-of-two size.
///
/// Entries are generic over the offset type. It will typically be `u32` or `u16`, depending on the
/// size of the `LEVEL2` table. A `u16` offset allows entries to shrink to 32 bits each, but some
/// ISAs may have tables so large that `u32` offsets are needed.
/// size of the `LEVEL2` table.
///
/// Empty entries are encoded with a 0 `log2len`. This is on the assumption that no level 2 tables
/// have only a single entry.
/// Empty entries are encoded with a `!0` value for `log2len` which will always be out of range.
/// Entries that have a `legalize` value but no level 2 table have an `offset` field that is out f
/// bounds.
pub struct Level1Entry<OffT: Into<u32> + Copy> {
pub ty: Type,
pub log2len: u8,
pub legalize: LegalizeCode,
pub offset: OffT,
}
@@ -44,7 +50,7 @@ impl<OffT: Into<u32> + Copy> Table<Type> for [Level1Entry<OffT>] {
}
fn key(&self, idx: usize) -> Option<Type> {
if self[idx].log2len != 0 {
if self[idx].log2len != !0 {
Some(self[idx].ty)
} else {
None
@@ -55,7 +61,7 @@ impl<OffT: Into<u32> + Copy> Table<Type> for [Level1Entry<OffT>] {
/// Level 2 hash table entry.
///
/// The second level hash tables are keyed by `Opcode`, and contain an offset into the `ENCLISTS`
/// table where the encoding recipes for the instrution are stored.
/// table where the encoding recipes for the instruction are stored.
///
/// Entries are generic over the offset type which depends on the size of `ENCLISTS`. A `u16`
/// offset allows the entries to be only 32 bits each. There is no benefit to dropping down to `u8`
@@ -93,22 +99,28 @@ pub fn lookup_enclist<OffT1, OffT2>(ctrl_typevar: Type,
where OffT1: Into<u32> + Copy,
OffT2: Into<u32> + Copy
{
// TODO: The choice of legalization actions here is naive. This needs to be configurable.
match probe(level1_table, ctrl_typevar, ctrl_typevar.index()) {
Err(_) => {
// No level 1 entry for the type.
Err(if ctrl_typevar.lane_type().bits() > 32 {
Legalize::Narrow
} else {
Legalize::Expand
})
Err(l1idx) => {
// No level 1 entry found for the type.
// We have a sentinel entry with the default legalization code.
let l1ent = &level1_table[l1idx];
Err(l1ent.legalize.into())
}
Ok(l1idx) => {
// We have a valid level 1 entry for this type.
let l1ent = &level1_table[l1idx];
let l2tab = &level2_table[l1ent.range()];
match level2_table.get(l1ent.range()) {
Some(l2tab) => {
probe(l2tab, opcode, opcode as usize)
.map(|l2idx| l2tab[l2idx].offset.into() as usize)
.map_err(|_| Legalize::Expand)
.map_err(|_| l1ent.legalize.into())
}
None => {
// The l1ent range is invalid. This means that we just have a customized
// legalization code for this type. The level 2 table is empty.
Err(l1ent.legalize.into())
}
}
}
}
}

View File

@@ -126,6 +126,20 @@ pub enum Legalize {
Expand,
}
/// Translate a legalization code into a `Legalize` enum.
///
/// This mapping is going away soon. It depends on matching the `TargetISA.legalize_code()`
/// mapping.
impl From<u8> for Legalize {
fn from(x: u8) -> Legalize {
match x {
0 => Legalize::Narrow,
1 => Legalize::Expand,
_ => panic!("Unknown legalization code {}"),
}
}
}
/// Methods that are specialized to a target ISA.
pub trait TargetIsa {
/// Get the name of this ISA.