Generate value type constraints.

Add an Opcode::constraints() method which returns an OpcodeConstraints object.
This object provides information on instruction polymorphism and how many
results is produced.

Generate a list of TypeSet objects for checking free type variables. The type
sets are parametrized rather than being represented as fully general sets.

Add UniqueTable and UniqueSeqTable classes to the meta code generator. Use for
compressing tabular data by removing duplicates.
This commit is contained in:
Jakob Stoklund Olesen
2016-05-20 10:45:02 -07:00
parent 1e631fdbd6
commit 692a85d720
6 changed files with 413 additions and 10 deletions

View File

@@ -7,6 +7,7 @@ instructions.
import re import re
import importlib import importlib
from collections import namedtuple
camel_re = re.compile('(^|_)([a-z])') camel_re = re.compile('(^|_)([a-z])')
@@ -148,6 +149,9 @@ class ScalarType(ValueType):
def __repr__(self): def __repr__(self):
return 'ScalarType({})'.format(self.name) return 'ScalarType({})'.format(self.name)
def rust_name(self):
return 'types::' + self.name.upper()
def by(self, lanes): def by(self, lanes):
""" """
Get a vector type with this type as the lane type. Get a vector type with this type as the lane type.
@@ -232,6 +236,19 @@ class BoolType(ScalarType):
# Parametric polymorphism. # Parametric polymorphism.
#: A `TypeSet` represents a set of types. We don't allow arbitrary subsets of
#: types, but use a parametrized approach instead.
#: This is represented as a named tuple so it can be used as a dictionary key.
TypeSet = namedtuple(
'TypeSet', [
'allow_scalars',
'allow_simd',
'base',
'all_ints',
'all_floats',
'all_bools'])
class TypeVar(object): class TypeVar(object):
""" """
Type variables can be used in place of concrete types when defining Type variables can be used in place of concrete types when defining
@@ -257,10 +274,23 @@ class TypeVar(object):
def __init__( def __init__(
self, name, doc, base=None, self, name, doc, base=None,
ints=False, floats=False, bools=False, ints=False, floats=False, bools=False,
scalars=True, simd=False): scalars=True, simd=False,
derived_func=None):
self.name = name self.name = name
self.__doc__ = doc self.__doc__ = doc
self.base = base self.base = base
self.is_derived = isinstance(base, TypeVar)
if self.is_derived:
assert derived_func
self.derived_func = derived_func
else:
self.type_set = TypeSet(
allow_scalars=scalars,
allow_simd=simd,
base=base,
all_ints=ints,
all_floats=floats,
all_bools=bools)
def __str__(self): def __str__(self):
return "`{}`".format(self.name) return "`{}`".format(self.name)
@@ -273,14 +303,18 @@ class TypeVar(object):
When this type variable assumes a scalar type, the derived type will be When this type variable assumes a scalar type, the derived type will be
the same scalar type. the same scalar type.
""" """
return TypeVar("Lane type of " + self.name, '', base=self, simd=False) return TypeVar(
"Lane type of " + self.name, '',
base=self, derived_func='Lane')
def as_bool(self): def as_bool(self):
""" """
Return a derived type variable that has the same vector geometry as Return a derived type variable that has the same vector geometry as
this type variable, but with boolean lanes. Scalar types map to `b1`. this type variable, but with boolean lanes. Scalar types map to `b1`.
""" """
return TypeVar(self.name + " as boolean", '', base=self, bools=True) return TypeVar(
self.name + " as boolean", '',
base=self, derived_func='AsBool')
def operand_kind(self): def operand_kind(self):
# When a `TypeVar` object is used to describe the type of an `Operand` # When a `TypeVar` object is used to describe the type of an `Operand`

View File

@@ -61,9 +61,9 @@ br_table = Instruction(
'br_table', r""" 'br_table', r"""
Indirect branch via jump table. Indirect branch via jump table.
Use ``x`` as an unsigned index into the jump table ``JT``. If a jump table Use ``x`` as an unsigned index into the jump table ``JT``. If a jump
entry is found, branch to the corresponding EBB. If no entry was found fall table entry is found, branch to the corresponding EBB. If no entry was
through to the next instruction. found fall through to the next instruction.
Note that this branch instruction can't pass arguments to the targeted Note that this branch instruction can't pass arguments to the targeted
blocks. Split critical edges as needed to work around this. blocks. Split critical edges as needed to work around this.

View File

@@ -4,6 +4,7 @@ Generate sources with instruction info.
import srcgen import srcgen
import constant_hash import constant_hash
from unique_table import UniqueTable, UniqueSeqTable
import cretonne import cretonne
@@ -97,7 +98,8 @@ def gen_instruction_data_impl(fmt):
fmt.doc_comment('Mutable reference to second result value, if any.') fmt.doc_comment('Mutable reference to second result value, if any.')
with fmt.indented( with fmt.indented(
"pub fn second_result_mut<'a>(&'a mut self) -> Option<&'a mut Value> {", '}'): "pub fn second_result_mut<'a>(&'a mut self)" +
" -> Option<&'a mut Value> {", '}'):
with fmt.indented('match *self {', '}'): with fmt.indented('match *self {', '}'):
for f in cretonne.InstructionFormat.all_formats: for f in cretonne.InstructionFormat.all_formats:
if not f.multiple_results: if not f.multiple_results:
@@ -131,7 +133,11 @@ def collect_instr_groups(targets):
def gen_opcodes(groups, fmt): def gen_opcodes(groups, fmt):
"""Generate opcode enumerations.""" """
Generate opcode enumerations.
Return a list of all instructions.
"""
fmt.doc_comment('An instruction opcode.') fmt.doc_comment('An instruction opcode.')
fmt.doc_comment('') fmt.doc_comment('')
@@ -193,6 +199,125 @@ def gen_opcodes(groups, fmt):
else: else:
fmt.format('Opcode::{},', i.camel_name) fmt.format('Opcode::{},', i.camel_name)
fmt.line() fmt.line()
return instrs
def get_constraint(op, ctrl_typevar, type_sets):
"""
Get the value type constraint for an SSA value operand, where
`ctrl_typevar` is the controlling type variable.
Each operand constraint is represented as a string, one of:
- `Concrete(vt)`, where `vt` is a value type name.
- `Free(idx)` where `idx` is an index into `type_sets`.
- `Same`, `Lane`, `AsBool` for controlling typevar-derived constraints.
"""
t = op.typ
assert t.operand_kind() is cretonne.value
# A concrete value type.
if isinstance(t, cretonne.ValueType):
return 'Concrete({})'.format(t.rust_name())
if t.free_typevar() is not ctrl_typevar:
assert not t.is_derived
return 'Free({})'.format(type_sets.add(t.type_set))
if t.is_derived:
assert t.base is ctrl_typevar, "Not derived directly from ctrl_typevar"
return t.derived_func
assert t is ctrl_typevar
return 'Same'
def gen_type_constraints(fmt, instrs):
"""
Generate value type constraints for all instructions.
- Emit a compact constant table of ValueTypeSet objects.
- Emit a compact constant table of OperandConstraint objects.
- Emit an opcode-indexed table of instruction constraints.
"""
# Table of TypeSet instances.
type_sets = UniqueTable()
# Table of operand constraint sequences (as tuples). Each operand
# constraint is represented as a string, one of:
# - `Concrete(vt)`, where `vt` is a value type name.
# - `Free(idx)` where `idx` isan index into `type_sets`.
# - `Same`, `Lane`, `AsBool` for controlling typevar-derived constraints.
operand_seqs = UniqueSeqTable()
# Preload table with constraints for typical binops.
operand_seqs.add(['Same'] * 3)
# TypeSet indexes are encoded in 3 bits, with `111` reserved.
typeset_limit = 7
fmt.comment('Table of opcode constraints.')
with fmt.indented(
'const OPCODE_CONSTRAINTS : [OpcodeConstraints; {}] = ['
.format(len(instrs)), '];'):
for i in instrs:
# Collect constraints for the value results, not including
# `variable_args` results which are always special cased.
constraints = list()
ctrl_typevar = None
ctrl_typeset = typeset_limit
if i.is_polymorphic:
ctrl_typevar = i.ctrl_typevar
ctrl_typeset = type_sets.add(ctrl_typevar.type_set)
for idx in i.value_results:
constraints.append(
get_constraint(i.outs[idx], ctrl_typevar, type_sets))
for idx in i.format.value_operands:
constraints.append(
get_constraint(i.ins[idx], ctrl_typevar, type_sets))
offset = operand_seqs.add(constraints)
fixed_results = len(i.value_results)
use_typevar_operand = i.is_polymorphic and i.use_typevar_operand
fmt.comment(
'{}: fixed_results={}, use_typevar_operand={}'
.format(i.camel_name, fixed_results, use_typevar_operand))
fmt.comment('Constraints={}'.format(constraints))
if i.is_polymorphic:
fmt.comment(
'Polymorphic over {}'.format(ctrl_typevar.type_set))
# Compute the bit field encoding, c.f. instructions.rs.
assert fixed_results < 8, "Bit field encoding too tight"
bits = (offset << 8) | (ctrl_typeset << 4) | fixed_results
if use_typevar_operand:
bits |= 8
assert bits < 0x10000, "Constraint table too large for bit field"
fmt.line('OpcodeConstraints({:#06x}),'.format(bits))
fmt.comment('Table of value type sets.')
assert len(type_sets.table) <= typeset_limit, "Too many type sets"
with fmt.indented(
'const TYPE_SETS : [ValueTypeSet; {}] = ['
.format(len(type_sets.table)), '];'):
for ts in type_sets.table:
with fmt.indented('ValueTypeSet {', '},'):
if ts.base:
fmt.line('base: {},'.format(ts.base.rust_name()))
else:
fmt.line('base: types::VOID,')
for field in ts._fields:
if field == 'base':
continue
fmt.line('{}: {},'.format(
field, str(getattr(ts, field)).lower()))
fmt.comment('Table of operand constraint sequences.')
with fmt.indented(
'const OPERAND_CONSTRAINTS : [OperandConstraint; {}] = ['
.format(len(operand_seqs.table)), '];'):
for c in operand_seqs.table:
fmt.line('OperandConstraint::{},'.format(c))
def generate(targets, out_dir): def generate(targets, out_dir):
@@ -202,5 +327,6 @@ def generate(targets, out_dir):
fmt = srcgen.Formatter() fmt = srcgen.Formatter()
gen_formats(fmt) gen_formats(fmt)
gen_instruction_data_impl(fmt) gen_instruction_data_impl(fmt)
gen_opcodes(groups, fmt) instrs = gen_opcodes(groups, fmt)
gen_type_constraints(fmt, instrs)
fmt.update_file('opcodes.rs', out_dir) fmt.update_file('opcodes.rs', out_dir)

68
meta/unique_table.py Normal file
View File

@@ -0,0 +1,68 @@
"""
Generate a table of unique items.
The `UniqueTable` class collects items into an array, removing duplicates. Each
item is mapped to its offset in the final array.
This is a compression technique for compile-time generated tables.
"""
class UniqueTable:
"""
Collect items into the `table` list, removing duplicates.
"""
def __init__(self):
# List of items added in order.
self.table = list()
# Map item -> index.
self.index = dict()
def add(self, item):
"""
Add a single item to the table if it isn't already there.
Return the offset into `self.table` of the item.
"""
if item in self.index:
return self.index[item]
idx = len(self.table)
self.index[item] = idx
self.table.append(item)
return idx
class UniqueSeqTable:
"""
Collect sequences into the `table` list, removing duplicates.
Sequences don't have to be of the same length.
"""
def __init__(self):
self.table = list()
# Map seq -> index.
self.index = dict()
def add(self, seq):
"""
Add a sequence of items to the table. If the table already contains the
items in `seq` in the same order, use those instead.
Return the offset into `self.table` of the beginning of `seq`.
"""
if len(seq) == 0:
return 0
seq = tuple(seq)
if seq in self.index:
return self.index[seq]
idx = len(self.table)
self.table.extend(seq)
# Add seq and all sub-sequences to `index`.
for length in range(1, len(seq) + 1):
for offset in range(len(seq) - length + 1):
self.index[seq[offset:offset+length]] = idx + offset
return idx

View File

@@ -11,7 +11,7 @@ use std::str::FromStr;
use entities::*; use entities::*;
use immediates::*; use immediates::*;
use types::Type; use types::{self, Type};
// Include code generated by `meta/gen_instr.py`. This file contains: // Include code generated by `meta/gen_instr.py`. This file contains:
// //
@@ -21,6 +21,12 @@ use types::Type;
// - The private `fn opcode_name(Opcode) -> &'static str` function, and // - The private `fn opcode_name(Opcode) -> &'static str` function, and
// - The hash table `const OPCODE_HASH_TABLE: [Opcode; N]`. // - The hash table `const OPCODE_HASH_TABLE: [Opcode; N]`.
// //
// For value type constraints:
//
// - The `const OPCODE_CONSTRAINTS : [OpcodeConstraints; N]` table.
// - The `const TYPE_SETS : [ValueTypeSet; N]` table.
// - The `const OPERAND_CONSTRAINTS : [OperandConstraint; N]` table.
//
include!(concat!(env!("OUT_DIR"), "/opcodes.rs")); include!(concat!(env!("OUT_DIR"), "/opcodes.rs"));
impl Display for Opcode { impl Display for Opcode {
@@ -38,6 +44,12 @@ impl Opcode {
Some(OPCODE_FORMAT[self as usize - 1]) Some(OPCODE_FORMAT[self as usize - 1])
} }
} }
/// Get the constraint descriptor for this opcode.
/// Panic if this is called on `NotAnOpcode`.
pub fn constraints(self) -> OpcodeConstraints {
OPCODE_CONSTRAINTS[self as usize - 1]
}
} }
// A primitive hash function for matching opcodes. // A primitive hash function for matching opcodes.
@@ -259,6 +271,155 @@ impl InstructionData {
} }
} }
/// Value type constraints for a given opcode.
///
/// The `InstructionFormat` determines the constraints on most operands, but `Value` operands and
/// results are not determined by the format. Every `Opcode` has an associated
/// `OpcodeConstraints` object that provides the missing details.
///
/// Since there can be a lot of opcodes, the `OpcodeConstraints` object is encoded as a bit field
/// by the `meta/gen_instr.py` script.
///
/// The bit field bits are:
///
/// Bits 0-2:
/// Number of fixed result values. This does not include `variable_args` results as are
/// produced by call instructions.
///
/// Bit 3:
/// This opcode is polymorphic and the controlling type variable can be inferred from the
/// designated input operand. This is the `typevar_operand` index given to the
/// `InstructionFormat` meta language object. When bit 0 is not set, the controlling type
/// variable must be the first output value instead.
///
/// Bits 4-7:
/// Permitted set of types for the controlling type variable as an index into `TYPE_SETS`.
///
/// Bits 8-15:
/// Offset into `OPERAND_CONSTRAINT` table of the descriptors for this opcode. The first
/// `fixed_results()` entries describe the result constraints, then follows constraints for the
/// fixed `Value` input operands. The number of `Value` inputs isdetermined by the instruction
/// format.
///
#[derive(Clone, Copy)]
pub struct OpcodeConstraints(u16);
impl OpcodeConstraints {
/// Can the controlling type variable for this opcode be inferred from the designated value
/// input operand?
/// This also implies that this opcode is polymorphic.
pub fn use_typevar_operand(self) -> bool {
(self.0 & 0x8) != 0
}
/// Get the number of *fixed* result values produced by this opcode.
/// This does not include `variable_args` produced by calls.
pub fn fixed_results(self) -> usize {
(self.0 & 0x7) as usize
}
/// Get the offset into `TYPE_SETS` for the controlling type variable.
/// Returns `None` if the instruction is not polymorphic.
fn typeset_offset(self) -> Option<usize> {
let offset = ((self.0 & 0xff) >> 4) as usize;
if offset < TYPE_SETS.len() {
Some(offset)
} else {
None
}
}
/// Get the offset into OPERAND_CONSTRAINTS where the descriptors for this opcode begin.
fn constraint_offset(self) -> usize {
(self.0 >> 8) as usize
}
/// Get the value type of result number `n`, having resolved the controlling type variable to
/// `ctrl_type`.
pub fn result_type(self, n: usize, ctrl_type: Type) -> Type {
assert!(n < self.fixed_results(), "Invalid result index");
OPERAND_CONSTRAINTS[self.constraint_offset() + n]
.resolve(ctrl_type)
.expect("Result constraints can't be free")
}
/// Get the typeset of allowed types for the controlling type variable in a polymorphic
/// instruction.
pub fn ctrl_typeset(self) -> Option<ValueTypeSet> {
self.typeset_offset().map(|offset| TYPE_SETS[offset])
}
}
/// A value type set describes the permitted set of types for a type variable.
#[derive(Clone, Copy)]
pub struct ValueTypeSet {
allow_scalars: bool,
allow_simd: bool,
base: Type,
all_ints: bool,
all_floats: bool,
all_bools: bool,
}
impl ValueTypeSet {
/// Is `scalar` part of the base type set?
///
/// Note that the base type set does not have to be included in the type set proper.
fn is_base_type(&self, scalar: Type) -> bool {
scalar == self.base || (self.all_ints && scalar.is_int()) ||
(self.all_floats && scalar.is_float()) || (self.all_bools && scalar.is_bool())
}
/// Does `typ` belong to this set?
pub fn contains(&self, typ: Type) -> bool {
let allowed = if typ.is_scalar() {
self.allow_scalars
} else {
self.allow_simd
};
allowed && self.is_base_type(typ.lane_type())
}
}
/// Operand constraints. This describes the value type constraints on a single `Value` operand.
enum OperandConstraint {
/// This operand has a concrete value type.
Concrete(Type),
/// This operand can vary freely within the given type set.
/// The type set is identified by its index into the TYPE_SETS constant table.
Free(u8),
/// This operand is the same type as the controlling type variable.
Same,
/// This operand is `ctrlType.lane_type()`.
Lane,
/// This operand is `ctrlType.as_bool()`.
AsBool,
}
impl OperandConstraint {
/// Resolve this operand constraint into a concrete value type, given the value of the
/// controlling type variable.
/// Returns `None` if this is a free operand which is independent of the controlling type
/// variable.
pub fn resolve(&self, ctrl_type: Type) -> Option<Type> {
use self::OperandConstraint::*;
match *self {
Concrete(t) => Some(t),
Free(_) => None,
Same => Some(ctrl_type),
Lane => Some(ctrl_type.lane_type()),
AsBool => Some(ctrl_type.as_bool()),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@@ -83,6 +83,20 @@ impl Type {
} }
} }
/// Get a type with the same number of lanes as this type, but with the lanes replaces by
/// booleans of the same size.
pub fn as_bool(self) -> Type {
// Replace the low 4 bits with the boolean version, preserve the high 4 bits.
let lane = match self.lane_type() {
B8 | I8 => B8,
B16 | I16 => B16,
B32 | I32 | F32 => B32,
B64 | I64 | F64 => B64,
_ => B1,
};
Type(lane.0 | (self.0 & 0xf0))
}
/// Is this the VOID type? /// Is this the VOID type?
pub fn is_void(self) -> bool { pub fn is_void(self) -> bool {
self == VOID self == VOID