Merge pull request #2257 from fitzgen/peepmatic-no-paths-in-linear-ir

Peepmatic: Do not use paths in linear IR
This commit is contained in:
Nick Fitzgerald
2020-10-13 12:18:26 -07:00
committed by GitHub
17 changed files with 846 additions and 1064 deletions

View File

@@ -14,14 +14,13 @@ use peepmatic_runtime::{
cc::ConditionCode,
instruction_set::InstructionSet,
part::{Constant, Part},
paths::Path,
r#type::{BitWidth, Kind, Type},
PeepholeOptimizations, PeepholeOptimizer,
};
use peepmatic_traits::TypingRules;
use std::borrow::Cow;
use std::boxed::Box;
use std::convert::{TryFrom, TryInto};
use std::iter;
use std::ptr;
use std::sync::atomic::{AtomicPtr, Ordering};
@@ -573,35 +572,6 @@ fn intcc_to_peepmatic(cc: IntCC) -> ConditionCode {
}
}
fn get_immediate(dfg: &DataFlowGraph, inst: Inst, i: usize) -> Part<ValueOrInst> {
return match dfg[inst] {
InstructionData::BinaryImm64 { imm, .. } if i == 0 => imm.into(),
InstructionData::BranchIcmp { cond, .. } if i == 0 => intcc_to_peepmatic(cond).into(),
InstructionData::BranchInt { cond, .. } if i == 0 => intcc_to_peepmatic(cond).into(),
InstructionData::IntCompare { cond, .. } if i == 0 => intcc_to_peepmatic(cond).into(),
InstructionData::IntCompareImm { cond, .. } if i == 0 => intcc_to_peepmatic(cond).into(),
InstructionData::IntCompareImm { imm, .. } if i == 1 => imm.into(),
InstructionData::IntCond { cond, .. } if i == 0 => intcc_to_peepmatic(cond).into(),
InstructionData::IntCondTrap { cond, .. } if i == 0 => intcc_to_peepmatic(cond).into(),
InstructionData::IntSelect { cond, .. } if i == 0 => intcc_to_peepmatic(cond).into(),
InstructionData::UnaryBool { imm, .. } if i == 0 => {
Constant::Bool(imm, BitWidth::Polymorphic).into()
}
InstructionData::UnaryImm { imm, .. } if i == 0 => imm.into(),
ref otherwise => unsupported(otherwise),
};
#[inline(never)]
#[cold]
fn unsupported(data: &InstructionData) -> ! {
panic!("unsupported instruction data: {:?}", data)
}
}
fn get_argument(dfg: &DataFlowGraph, inst: Inst, i: usize) -> Option<Value> {
dfg.inst_args(inst).get(i).copied()
}
fn peepmatic_ty_to_ir_ty(ty: Type, dfg: &DataFlowGraph, root: Inst) -> types::Type {
match (ty.kind, bit_width(dfg, ty.bit_width, root)) {
(Kind::Int, 8) => types::I8,
@@ -681,39 +651,290 @@ unsafe impl<'a, 'b> InstructionSet<'b> for &'a dyn TargetIsa {
}
}
fn get_part_at_path(
fn operator<E>(
&self,
pos: &mut FuncCursor<'b>,
root: ValueOrInst,
path: Path,
) -> Option<Part<ValueOrInst>> {
// The root is path [0].
debug_assert!(!path.0.is_empty());
debug_assert_eq!(path.0[0], 0);
let mut part = Part::Instruction(root);
for p in path.0[1..].iter().copied() {
let inst = part.as_instruction()?.resolve_inst(&pos.func.dfg)?;
let operator = pos.func.dfg[inst].opcode();
if p < operator.immediates_arity() {
part = get_immediate(&pos.func.dfg, inst, p as usize);
continue;
}
let arg = p - operator.immediates_arity();
let arg = arg as usize;
let value = get_argument(&pos.func.dfg, inst, arg)?;
part = Part::Instruction(value.into());
}
log::trace!("get_part_at_path({:?}) = {:?}", path, part);
Some(part)
}
fn operator(&self, pos: &mut FuncCursor<'b>, value_or_inst: ValueOrInst) -> Option<Opcode> {
value_or_inst: ValueOrInst,
operands: &mut E,
) -> Option<Opcode>
where
E: Extend<Part<Self::Instruction>>,
{
let inst = value_or_inst.resolve_inst(&pos.func.dfg)?;
Some(pos.func.dfg[inst].opcode())
Some(match pos.func.dfg[inst] {
InstructionData::Binary {
opcode: opcode @ Opcode::Band,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Bor,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Bxor,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Iadd,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Ifcmp,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Imul,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Ishl,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Isub,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Rotl,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Rotr,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Sdiv,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Srem,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Sshr,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Udiv,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Urem,
args,
}
| InstructionData::Binary {
opcode: opcode @ Opcode::Ushr,
args,
} => {
operands.extend(args.iter().map(|v| Part::Instruction((*v).into())));
opcode
}
InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::BandImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::BorImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::BxorImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::IaddImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::IfcmpImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::ImulImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::IrsubImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::IshlImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::RotlImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::RotrImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::SdivImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::SremImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::SshrImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::UdivImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::UremImm,
imm,
arg,
}
| InstructionData::BinaryImm64 {
opcode: opcode @ Opcode::UshrImm,
imm,
arg,
} => {
operands.extend(
iter::once(imm.into()).chain(iter::once(Part::Instruction(arg.into()))),
);
opcode
}
InstructionData::Branch {
opcode: opcode @ Opcode::Brnz,
ref args,
destination: _,
}
| InstructionData::Branch {
opcode: opcode @ Opcode::Brz,
ref args,
destination: _,
} => {
operands.extend(
args.as_slice(&pos.func.dfg.value_lists)
.iter()
.map(|v| Part::Instruction((*v).into()))
// NB: Peepmatic only knows about the condition, not any
// of the arguments to the block, which are special
// cased elsewhere, if/when we actually replace the
// instruction.
.take(1),
);
opcode
}
InstructionData::CondTrap {
opcode: opcode @ Opcode::Trapnz,
arg,
code: _,
}
| InstructionData::CondTrap {
opcode: opcode @ Opcode::Trapz,
arg,
code: _,
} => {
operands.extend(iter::once(Part::Instruction(arg.into())));
opcode
}
InstructionData::IntCompare {
opcode: opcode @ Opcode::Icmp,
cond,
args,
} => {
operands.extend(
iter::once(intcc_to_peepmatic(cond).into())
.chain(args.iter().map(|v| Part::Instruction((*v).into()))),
);
opcode
}
InstructionData::IntCompareImm {
opcode: opcode @ Opcode::IcmpImm,
cond,
imm,
arg,
} => {
operands.extend(
iter::once(intcc_to_peepmatic(cond).into())
.chain(iter::once(Part::Constant(imm.into())))
.chain(iter::once(Part::Instruction(arg.into()))),
);
opcode
}
InstructionData::Ternary {
opcode: opcode @ Opcode::Select,
ref args,
} => {
operands.extend(args.iter().map(|v| Part::Instruction((*v).into())));
opcode
}
InstructionData::Unary {
opcode: opcode @ Opcode::AdjustSpDown,
arg,
}
| InstructionData::Unary {
opcode: opcode @ Opcode::Bint,
arg,
}
| InstructionData::Unary {
opcode: opcode @ Opcode::Ireduce,
arg,
}
| InstructionData::Unary {
opcode: opcode @ Opcode::Sextend,
arg,
}
| InstructionData::Unary {
opcode: opcode @ Opcode::Uextend,
arg,
} => {
operands.extend(iter::once(Part::Instruction(arg.into())));
opcode
}
InstructionData::UnaryBool { opcode, imm } => {
operands.extend(iter::once(Part::Constant(Constant::Bool(
imm,
BitWidth::Polymorphic,
))));
opcode
}
InstructionData::UnaryImm {
opcode: opcode @ Opcode::AdjustSpDownImm,
imm,
}
| InstructionData::UnaryImm {
opcode: opcode @ Opcode::Iconst,
imm,
} => {
operands.extend(iter::once(imm.into()));
opcode
}
ref otherwise => {
log::trace!("Not supported by Peepmatic: {:?}", otherwise);
return None;
}
})
}
fn make_inst_1(

View File

@@ -54,11 +54,6 @@ superoptimizer like [Souper][]. Eventually, `peepmatic` should have a verifier
that ensures that the DSL's optimizations are sound, similar to what [Alive][]
does for LLVM optimizations.
Currently, `peepmatic` is targeting peephole optimizers that operate on
Cranelift's clif intermediate representation. The intended next target is
Cranelift's new backend's "vcode" intermediate representation. Supporting
non-Cranelift targets is not a goal.
[Cranelift]: https://github.com/bytecodealliance/wasmtime/tree/main/cranelift#readme
[Souper]: https://github.com/google/souper
[Alive]: https://github.com/AliveToolkit/alive2

View File

@@ -1,7 +1,6 @@
//! Interfacing with actual instructions.
use crate::part::{Constant, Part};
use crate::paths::Path;
use crate::r#type::Type;
use std::fmt::Debug;
use std::hash::Hash;
@@ -54,26 +53,22 @@ pub unsafe trait InstructionSet<'a> {
new: Part<Self::Instruction>,
) -> Self::Instruction;
/// Get the instruction, constant, or condition code at the given path.
///
/// If there is no such entity at the given path (e.g. we run into a
/// function parameter and can't traverse the path any further) then `None`
/// should be returned.
fn get_part_at_path(
&self,
context: &mut Self::Context,
root: Self::Instruction,
path: Path,
) -> Option<Part<Self::Instruction>>;
/// Get the given instruction's operator.
///
/// If the instruction isn't supported, then `None` should be returned.
fn operator(
///
/// Additionally, if `Some` is returned, then the instruction's operands
/// must be pushed in order into `operands`. E.g. calling this method on
/// `(iadd $x $y)` would return `Some(iadd)` and extend `operands` with
/// `[$x, $y]`.
fn operator<E>(
&self,
context: &mut Self::Context,
instr: Self::Instruction,
) -> Option<Self::Operator>;
operands: &mut E,
) -> Option<Self::Operator>
where
E: Extend<Part<Self::Instruction>>;
/// Make a unary instruction.
///

View File

@@ -25,7 +25,6 @@ pub mod linear;
pub mod optimizations;
pub mod optimizer;
pub mod part;
pub mod paths;
pub mod r#type;
pub mod unquote;

View File

@@ -7,7 +7,6 @@
use crate::cc::ConditionCode;
use crate::integer_interner::{IntegerId, IntegerInterner};
use crate::paths::{PathId, PathInterner};
use crate::r#type::{BitWidth, Type};
use crate::unquote::UnquoteOperator;
use serde::{Deserialize, Serialize};
@@ -24,9 +23,6 @@ where
/// The linear optimizations.
pub optimizations: Vec<Optimization<TOperator>>,
/// The de-duplicated paths referenced by these optimizations.
pub paths: PathInterner,
/// The integer literals referenced by these optimizations.
pub integers: IntegerInterner,
}
@@ -37,8 +33,13 @@ pub struct Optimization<TOperator>
where
TOperator: 'static + Copy + Debug + Eq + Hash,
{
/// The chain of increments for this optimization.
pub increments: Vec<Increment<TOperator>>,
/// The chain of match operations and expected results for this
/// optimization.
pub matches: Vec<Match>,
/// Actions to perform, given that the operation resulted in the expected
/// value.
pub actions: Vec<Action<TOperator>>,
}
/// Match any value.
@@ -61,31 +62,20 @@ pub fn bool_to_match_result(b: bool) -> MatchResult {
unsafe { Ok(NonZeroU32::new_unchecked(b + 1)) }
}
/// A partial match of an optimization's LHS and partial construction of its
/// RHS.
/// A partial match of an optimization's LHS.
///
/// An increment is a matching operation, the expected result from that
/// operation to continue to the next increment, and the actions to take to
/// build up the LHS scope and RHS instructions given that we got the expected
/// result from this increment's matching operation. Each increment will
/// basically become a state and a transition edge out of that state in the
/// final automata.
/// An match is composed of a matching operation and the expected result of that
/// operation. Each match will basically become a state and a transition edge
/// out of that state in the final automata.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Increment<TOperator>
where
TOperator: 'static + Copy + Debug + Eq + Hash,
{
pub struct Match {
/// The matching operation to perform.
pub operation: MatchOp,
/// The expected result of our matching operation, that enables us to
/// continue to the next increment, or `Else` for "don't care"
/// wildcard-style matching.
/// continue to the next match, or `Else` for "don't care" wildcard-style
/// matching.
pub expected: MatchResult,
/// Actions to perform, given that the operation resulted in the expected
/// value.
pub actions: Vec<Action<TOperator>>,
}
/// A matching operation to be performed on some Cranelift instruction as part
@@ -93,79 +83,54 @@ where
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub enum MatchOp {
/// Switch on the opcode of an instruction.
Opcode {
/// The path to the instruction whose opcode we're switching on.
path: PathId,
},
///
/// Upon successfully matching an instruction's opcode, bind each of its
/// operands to a LHS temporary.
Opcode(LhsId),
/// Does an instruction have a constant value?
IsConst {
/// The path to the instruction (or immediate) that we're checking
/// whether it is constant or not.
path: PathId,
},
IsConst(LhsId),
/// Is the constant value a power of two?
IsPowerOfTwo {
/// The path to the instruction (or immediate) that we are checking
/// whether it is a constant power of two or not.
path: PathId,
},
IsPowerOfTwo(LhsId),
/// Switch on the bit width of a value.
BitWidth {
/// The path to the instruction (or immediate) whose result's bit width
/// we are checking.
path: PathId,
},
BitWidth(LhsId),
/// Does the value fit in our target architecture's native word size?
FitsInNativeWord {
/// The path to the instruction (or immediate) whose result we are
/// checking whether it fits in a native word or not.
path: PathId,
},
FitsInNativeWord(LhsId),
/// Are the instructions (or immediates) at the given paths the same?
Eq {
/// The path to the first instruction (or immediate).
path_a: PathId,
/// The path to the second instruction (or immediate).
path_b: PathId,
},
/// Are the instructions (or immediates) the same?
Eq(LhsId, LhsId),
/// Switch on the constant integer value of an instruction.
IntegerValue {
/// The path to the instruction.
path: PathId,
},
IntegerValue(LhsId),
/// Switch on the constant boolean value of an instruction.
BooleanValue {
/// The path to the instruction.
path: PathId,
},
BooleanValue(LhsId),
/// Switch on a condition code.
ConditionCode {
/// The path to the condition code.
path: PathId,
},
ConditionCode(LhsId),
/// No operation. Always evaluates to `None`.
/// No operation. Always evaluates to `Else`.
///
/// Exceedingly rare in real optimizations; nonetheless required to support
/// Never appears in real optimizations; nonetheless required to support
/// corner cases of the DSL, such as a LHS pattern that is nothing but a
/// variable pattern.
/// variable.
Nop,
}
/// A canonicalized identifier for a left-hand side value that was bound in a
/// pattern.
///
/// These are defined in a pre-order traversal of the LHS pattern by successful
/// `MatchOp::Opcode` matches.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct LhsId(pub u16);
/// A canonicalized identifier for a right-hand side value.
///
/// These are defined by RHS actions.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct RhsId(pub u16);
@@ -177,8 +142,8 @@ pub struct RhsId(pub u16);
pub enum Action<TOperator> {
/// Reuse something from the left-hand side.
GetLhs {
/// The path to the instruction or value.
path: PathId,
/// The left-hand side instruction or value.
lhs: LhsId,
},
/// Perform compile-time evaluation.

View File

@@ -5,7 +5,6 @@ use crate::instruction_set::InstructionSet;
use crate::integer_interner::IntegerInterner;
use crate::linear::{Action, MatchOp, MatchResult};
use crate::optimizer::PeepholeOptimizer;
use crate::paths::PathInterner;
use peepmatic_automata::Automaton;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
@@ -25,9 +24,6 @@ pub struct PeepholeOptimizations<TOperator>
where
TOperator: 'static + Copy + Debug + Eq + Hash,
{
/// The instruction paths referenced by the peephole optimizations.
pub paths: PathInterner,
/// Not all integers we're matching on fit in the `u32` that we use as the
/// result of match operations. So we intern them and refer to them by id.
pub integers: IntegerInterner,
@@ -88,6 +84,7 @@ where
PeepholeOptimizer {
peep_opt: self,
instr_set,
left_hand_sides: vec![],
right_hand_sides: vec![],
actions: vec![],
backtracking_states: vec![],

View File

@@ -27,9 +27,10 @@ where
{
pub(crate) peep_opt: &'peep PeepholeOptimizations<TInstructionSet::Operator>,
pub(crate) instr_set: TInstructionSet,
pub(crate) left_hand_sides: Vec<Part<TInstructionSet::Instruction>>,
pub(crate) right_hand_sides: Vec<Part<TInstructionSet::Instruction>>,
pub(crate) actions: Vec<Action<TInstructionSet::Operator>>,
pub(crate) backtracking_states: Vec<(State, usize)>,
pub(crate) backtracking_states: Vec<(State, usize, usize)>,
}
impl<'peep, 'ctx, TInstructionSet> Debug for PeepholeOptimizer<'peep, 'ctx, TInstructionSet>
@@ -40,6 +41,7 @@ where
let PeepholeOptimizer {
peep_opt,
instr_set: _,
left_hand_sides,
right_hand_sides,
actions,
backtracking_states,
@@ -47,6 +49,7 @@ where
f.debug_struct("PeepholeOptimizer")
.field("peep_opt", peep_opt)
.field("instr_set", &"_")
.field("left_hand_sides", left_hand_sides)
.field("right_hand_sides", right_hand_sides)
.field("actions", actions)
.field("backtracking_states", backtracking_states)
@@ -117,12 +120,8 @@ where
for action in actions.drain(..) {
log::trace!("Evaluating action: {:?}", action);
match action {
Action::GetLhs { path } => {
let path = self.peep_opt.paths.lookup(path);
let lhs = self
.instr_set
.get_part_at_path(context, root, path)
.expect("should always get part at path OK by the time it is bound");
Action::GetLhs { lhs } => {
let lhs = self.left_hand_sides[lhs.0 as usize];
self.right_hand_sides.push(lhs);
}
Action::UnaryUnquote { operator, operand } => {
@@ -284,22 +283,17 @@ where
log::trace!("Evaluating match operation: {:?}", match_op);
let result: MatchResult = (|| match match_op {
Opcode { path } => {
let path = self.peep_opt.paths.lookup(path);
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
Opcode(id) => {
let part = self.left_hand_sides[id.0 as usize];
let inst = part.as_instruction().ok_or(Else)?;
let op = self.instr_set.operator(context, inst).ok_or(Else)?;
let op = self
.instr_set
.operator(context, inst, &mut self.left_hand_sides)
.ok_or(Else)?;
Ok(op.into())
}
IsConst { path } => {
let path = self.peep_opt.paths.lookup(path);
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
IsConst(id) => {
let part = self.left_hand_sides[id.0 as usize];
let is_const = match part {
Part::Instruction(i) => {
self.instr_set.instruction_to_constant(context, i).is_some()
@@ -308,12 +302,8 @@ where
};
bool_to_match_result(is_const)
}
IsPowerOfTwo { path } => {
let path = self.peep_opt.paths.lookup(path);
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
IsPowerOfTwo(id) => {
let part = self.left_hand_sides[id.0 as usize];
match part {
Part::Constant(c) => {
let is_pow2 = c.as_int().unwrap().is_power_of_two();
@@ -327,18 +317,11 @@ where
let is_pow2 = c.as_int().unwrap().is_power_of_two();
bool_to_match_result(is_pow2)
}
Part::ConditionCode(_) => unreachable!(
"IsPowerOfTwo on a condition
code"
),
Part::ConditionCode(_) => unreachable!("IsPowerOfTwo on a condition code"),
}
}
BitWidth { path } => {
let path = self.peep_opt.paths.lookup(path);
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
BitWidth(id) => {
let part = self.left_hand_sides[id.0 as usize];
let bit_width = match part {
Part::Instruction(i) => self.instr_set.instruction_result_bit_width(context, i),
Part::Constant(Constant::Int(_, w)) | Part::Constant(Constant::Bool(_, w)) => {
@@ -355,15 +338,11 @@ where
);
Ok(unsafe { NonZeroU32::new_unchecked(bit_width as u32) })
}
FitsInNativeWord { path } => {
FitsInNativeWord(id) => {
let native_word_size = self.instr_set.native_word_size_in_bits(context);
debug_assert!(native_word_size.is_power_of_two());
let path = self.peep_opt.paths.lookup(path);
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
let part = self.left_hand_sides[id.0 as usize];
let fits = match part {
Part::Instruction(i) => {
let size = self.instr_set.instruction_result_bit_width(context, i);
@@ -378,17 +357,9 @@ where
};
bool_to_match_result(fits)
}
Eq { path_a, path_b } => {
let path_a = self.peep_opt.paths.lookup(path_a);
let part_a = self
.instr_set
.get_part_at_path(context, root, path_a)
.ok_or(Else)?;
let path_b = self.peep_opt.paths.lookup(path_b);
let part_b = self
.instr_set
.get_part_at_path(context, root, path_b)
.ok_or(Else)?;
Eq(a, b) => {
let part_a = self.left_hand_sides[a.0 as usize];
let part_b = self.left_hand_sides[b.0 as usize];
let eq = match (part_a, part_b) {
(Part::Instruction(inst), Part::Constant(c1))
| (Part::Constant(c1), Part::Instruction(inst)) => {
@@ -401,12 +372,8 @@ where
};
bool_to_match_result(eq)
}
IntegerValue { path } => {
let path = self.peep_opt.paths.lookup(path);
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
IntegerValue(id) => {
let part = self.left_hand_sides[id.0 as usize];
match part {
Part::Constant(c) => {
let x = c.as_int().ok_or(Else)?;
@@ -425,12 +392,8 @@ where
Part::ConditionCode(_) => unreachable!("IntegerValue on condition code"),
}
}
BooleanValue { path } => {
let path = self.peep_opt.paths.lookup(path);
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
BooleanValue(id) => {
let part = self.left_hand_sides[id.0 as usize];
match part {
Part::Constant(c) => {
let b = c.as_bool().ok_or(Else)?;
@@ -447,12 +410,8 @@ where
Part::ConditionCode(_) => unreachable!("IntegerValue on condition code"),
}
}
ConditionCode { path } => {
let path = self.peep_opt.paths.lookup(path);
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
ConditionCode(id) => {
let part = self.left_hand_sides[id.0 as usize];
let cc = part.as_condition_code().ok_or(Else)?;
let cc = cc as u32;
debug_assert!(cc != 0);
@@ -483,12 +442,20 @@ where
self.backtracking_states.clear();
self.actions.clear();
self.right_hand_sides.clear();
self.left_hand_sides.clear();
// `LhsId(0)` is always the root.
self.left_hand_sides.push(Part::Instruction(root));
let mut r#final = None;
let mut query = self.peep_opt.automata.query();
loop {
log::trace!("Current state: {:?}", query.current_state());
log::trace!(
"self.left_hand_sides = {:#?}",
self.left_hand_sides.iter().enumerate().collect::<Vec<_>>()
);
if query.is_in_final_state() {
// If we're in a final state (which means an optimization is
@@ -507,8 +474,11 @@ where
// optimization, we want to be able to backtrack to this state and
// then try taking the `Else` transition.
if query.has_transition_on(&Err(Else)) {
self.backtracking_states
.push((query.current_state(), self.actions.len()));
self.backtracking_states.push((
query.current_state(),
self.actions.len(),
self.left_hand_sides.len(),
));
}
let match_op = match query.current_state_data() {
@@ -522,9 +492,10 @@ where
actions
} else if r#final.is_some() {
break;
} else if let Some((state, actions_len)) = self.backtracking_states.pop() {
} else if let Some((state, actions_len, lhs_len)) = self.backtracking_states.pop() {
query.go_to_state(state);
self.actions.truncate(actions_len);
self.left_hand_sides.truncate(lhs_len);
query
.next(&Err(Else))
.expect("backtracking states always have `Else` transitions")

View File

@@ -1,242 +0,0 @@
//! Representing paths through the dataflow graph.
//!
//! Paths are relative from a *root* instruction, which is the instruction we
//! are determining which, if any, optimizations apply.
//!
//! Paths are series of indices through each instruction's children as we
//! traverse down the graph from the root. Children are immediates followed by
//! arguments: `[imm0, imm1, ..., immN, arg0, arg1, ..., argN]`.
//!
//! ## Examples
//!
//! * `[0]` is the path to the root.
//! * `[0, 0]` is the path to the root's first child.
//! * `[0, 1]` is the path to the root's second child.
//! * `[0, 1, 0]` is the path to the root's second child's first child.
//!
//! ## Interning
//!
//! To avoid extra allocations, de-duplicate paths, and reference them via a
//! fixed-length value, we intern paths inside a `PathInterner` and then
//! reference them via `PathId`.
// TODO: Make `[]` the path to the root, and get rid of this redundant leading
// zero that is currently in every single path.
use serde::de::{Deserializer, SeqAccess, Visitor};
use serde::ser::{SerializeSeq, Serializer};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::convert::TryInto;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
/// A path through the data-flow graph from the root instruction.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Path<'a>(pub &'a [u8]);
impl Path<'_> {
/// Construct a new path through the data-flow graph from the root
/// instruction.
pub fn new(path: &impl AsRef<[u8]>) -> Path {
Path(path.as_ref())
}
}
/// An identifier for an interned path.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct PathId(u16);
/// An interner and de-duplicator for `Path`s.
///
/// Can be serialized and deserialized while maintaining the same id to interned
/// path mapping.
#[derive(Debug, Default)]
pub struct PathInterner {
/// A map from a path (whose owned data is inside `arena`) to the canonical
/// `PathId` we assigned it when interning it.
map: HashMap<UnsafePath, PathId>,
/// A map from a `PathId` index to an unsafe, self-borrowed path pointing
/// into `arena`. It is safe to given these out as safe `Path`s, as long as
/// the lifetime is not longer than this `PathInterner`'s lifetime.
paths: Vec<UnsafePath>,
/// Bump allocation arena for path data. The bump arena ensures that these
/// allocations never move, and are therefore safe for self-references.
arena: bumpalo::Bump,
}
impl PathInterner {
/// Construct a new, empty `PathInterner`.
#[inline]
pub fn new() -> Self {
Self::default()
}
/// Intern a path into this `PathInterner`, returning its canonical
/// `PathId`.
///
/// If we've already interned this path before, then the existing id we
/// already assigned to it is returned. If we've never seen this path
/// before, then it is copied into this `PathInterner` and a new id is
/// assigned to it.
#[inline]
pub fn intern<'a>(&mut self, path: Path<'a>) -> PathId {
let unsafe_path = unsafe { UnsafePath::from_path(&path) };
if let Some(id) = self.map.get(&unsafe_path) {
return *id;
}
self.intern_new(path)
}
#[inline(never)]
fn intern_new<'a>(&mut self, path: Path<'a>) -> PathId {
let id: u16 = self
.paths
.len()
.try_into()
.expect("too many paths interned");
let id = PathId(id);
let our_path = self.arena.alloc_slice_copy(&path.0);
let unsafe_path = unsafe { UnsafePath::from_slice(&our_path) };
self.paths.push(unsafe_path.clone());
let old = self.map.insert(unsafe_path, id);
debug_assert!(old.is_none());
debug_assert_eq!(self.lookup(id), path);
debug_assert_eq!(self.intern(path), id);
id
}
/// Lookup a previously interned path by id.
#[inline]
pub fn lookup<'a>(&'a self, id: PathId) -> Path<'a> {
let unsafe_path = self
.paths
.get(id.0 as usize)
.unwrap_or_else(|| Self::lookup_failure());
unsafe { unsafe_path.as_path() }
}
#[inline(never)]
fn lookup_failure() -> ! {
panic!(
"no path for the given id; this can only happen when mixing `PathId`s with different \
`PathInterner`s"
)
}
}
impl Serialize for PathInterner {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut seq = serializer.serialize_seq(Some(self.paths.len()))?;
for p in &self.paths {
let p = unsafe { p.as_path() };
seq.serialize_element(&p)?;
}
seq.end()
}
}
impl<'de> Deserialize<'de> for PathInterner {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_seq(PathInternerVisitor {
marker: PhantomData,
})
}
}
struct PathInternerVisitor {
marker: PhantomData<fn() -> PathInterner>,
}
impl<'de> Visitor<'de> for PathInternerVisitor {
type Value = PathInterner;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "a `peepmatic_runtime::paths::PathInterner`")
}
fn visit_seq<M>(self, mut access: M) -> Result<Self::Value, M::Error>
where
M: SeqAccess<'de>,
{
const DEFAULT_CAPACITY: usize = 16;
let capacity = access.size_hint().unwrap_or(DEFAULT_CAPACITY);
let mut interner = PathInterner {
map: HashMap::with_capacity(capacity),
paths: Vec::with_capacity(capacity),
arena: bumpalo::Bump::new(),
};
while let Some(path) = access.next_element::<Path>()? {
interner.intern(path);
}
Ok(interner)
}
}
/// An unsafe, unchecked borrow of a path. Not for use outside of
/// `PathInterner`!
#[derive(Clone, Debug)]
struct UnsafePath {
ptr: *const u8,
len: usize,
}
impl PartialEq for UnsafePath {
fn eq(&self, rhs: &UnsafePath) -> bool {
unsafe { self.as_slice() == rhs.as_slice() }
}
}
impl Eq for UnsafePath {}
impl Hash for UnsafePath {
fn hash<H>(&self, hasher: &mut H)
where
H: Hasher,
{
unsafe { self.as_slice().hash(hasher) }
}
}
/// Safety: callers must ensure that the constructed values won't have unsafe
/// usages of `PartialEq`, `Eq`, or `Hash`.
impl UnsafePath {
unsafe fn from_path(p: &Path) -> Self {
Self::from_slice(&p.0)
}
unsafe fn from_slice(s: &[u8]) -> Self {
UnsafePath {
ptr: s.as_ptr(),
len: s.len(),
}
}
}
/// Safety: callers must ensure that `'a` does not outlive the lifetime of the
/// underlying data.
impl UnsafePath {
unsafe fn as_slice<'a>(&self) -> &'a [u8] {
std::slice::from_raw_parts(self.ptr, self.len)
}
unsafe fn as_path<'a>(&self) -> Path<'a> {
Path(self.as_slice())
}
}

View File

@@ -6,7 +6,6 @@ use peepmatic_runtime::{
cc::ConditionCode,
instruction_set::InstructionSet,
part::{Constant, Part},
paths::Path,
r#type::{BitWidth, Kind, Type},
};
use peepmatic_test_operator::TestOperator;
@@ -329,43 +328,23 @@ unsafe impl<'a> InstructionSet<'a> for TestIsa {
new
}
fn get_part_at_path(
fn operator<E>(
&self,
program: &mut Program,
root: Instruction,
path: Path,
) -> Option<Part<Instruction>> {
log::debug!("get_part_at_path({:?})", path);
assert!(!path.0.is_empty());
assert_eq!(path.0[0], 0);
let mut part = Part::Instruction(root);
for p in &path.0[1..] {
if let Part::Instruction(inst) = part {
let data = program.data(inst);
let p = *p as usize;
if p < data.immediates.len() {
part = data.immediates[p].into();
continue;
}
if let Some(inst) = data.arguments.get(p - data.immediates.len()).copied() {
part = Part::Instruction(inst);
continue;
}
}
return None;
}
Some(part)
}
fn operator(&self, program: &mut Program, instr: Instruction) -> Option<TestOperator> {
instr: Self::Instruction,
operands: &mut E,
) -> Option<Self::Operator>
where
E: Extend<Part<Instruction>>,
{
log::debug!("operator({:?})", instr);
let data = program.data(instr);
operands.extend(
data.immediates
.iter()
.map(|imm| Part::from(*imm))
.chain(data.arguments.iter().map(|inst| Part::Instruction(*inst))),
);
Some(data.operator)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 252 KiB

View File

@@ -19,16 +19,23 @@ where
for opt in &opts.optimizations {
let mut insertion = builder.insert();
for inc in &opt.increments {
// Ensure that this state's associated data is this increment's
// match operation.
let mut is_first = true;
for m in &opt.matches {
// Ensure that this state's associated data is this match's
// operation.
if let Some(op) = insertion.get_state_data() {
assert_eq!(*op, inc.operation);
assert_eq!(*op, m.operation);
} else {
insertion.set_state_data(inc.operation);
insertion.set_state_data(m.operation);
}
insertion.next(inc.expected, inc.actions.clone().into_boxed_slice());
let actions = if is_first {
is_first = false;
opt.actions.clone().into_boxed_slice()
} else {
vec![].into_boxed_slice()
};
insertion.next(m.expected, actions);
}
insertion.finish();
}

View File

@@ -7,7 +7,6 @@ use peepmatic_runtime::{
cc::ConditionCode,
integer_interner::{IntegerId, IntegerInterner},
linear,
paths::{PathId, PathInterner},
};
use std::convert::{TryFrom, TryInto};
use std::fmt::Debug;
@@ -15,7 +14,7 @@ use std::io::{self, Write};
use std::num::{NonZeroU16, NonZeroU32};
#[derive(Debug)]
pub(crate) struct PeepholeDotFmt<'a>(pub(crate) &'a PathInterner, pub(crate) &'a IntegerInterner);
pub(crate) struct PeepholeDotFmt<'a>(pub(crate) &'a IntegerInterner);
impl<TOperator> DotFmt<linear::MatchResult, linear::MatchOp, Box<[linear::Action<TOperator>]>>
for PeepholeDotFmt<'_>
@@ -44,7 +43,7 @@ where
write!(w, "{}", cc)
}
linear::MatchOp::IntegerValue { .. } => {
let x = self.1.lookup(IntegerId(
let x = self.0.lookup(IntegerId(
NonZeroU16::new(x.get().try_into().unwrap()).unwrap(),
));
write!(w, "{}", x)
@@ -61,17 +60,16 @@ where
write!(w, r#"<font face="monospace">"#)?;
let p = p(self.0);
match op {
Opcode { path } => write!(w, "opcode @ {}", p(path))?,
IsConst { path } => write!(w, "is-const? @ {}", p(path))?,
IsPowerOfTwo { path } => write!(w, "is-power-of-two? @ {}", p(path))?,
BitWidth { path } => write!(w, "bit-width @ {}", p(path))?,
FitsInNativeWord { path } => write!(w, "fits-in-native-word @ {}", p(path))?,
Eq { path_a, path_b } => write!(w, "{} == {}", p(path_a), p(path_b))?,
IntegerValue { path } => write!(w, "integer-value @ {}", p(path))?,
BooleanValue { path } => write!(w, "boolean-value @ {}", p(path))?,
ConditionCode { path } => write!(w, "condition-code @ {}", p(path))?,
Opcode(id) => write!(w, "opcode $lhs{}", id.0)?,
IsConst(id) => write!(w, "is-const? $lhs{}", id.0)?,
IsPowerOfTwo(id) => write!(w, "is-power-of-two? $lhs{}", id.0)?,
BitWidth(id) => write!(w, "bit-width $lhs{}", id.0)?,
FitsInNativeWord(id) => write!(w, "fits-in-native-word $lhs{}", id.0)?,
Eq(a, b) => write!(w, "$lhs{} == $lhs{}", a.0, b.0)?,
IntegerValue(id) => write!(w, "integer-value $lhs{}", id.0)?,
BooleanValue(id) => write!(w, "boolean-value $lhs{}", id.0)?,
ConditionCode(id) => write!(w, "condition-code $lhs{}", id.0)?,
Nop => write!(w, "nop")?,
}
@@ -91,11 +89,9 @@ where
write!(w, r#"<font face="monospace">"#)?;
let p = p(self.0);
for a in actions.iter() {
match a {
GetLhs { path } => write!(w, "get-lhs @ {}<br/>", p(path))?,
GetLhs { lhs } => write!(w, "get-lhs $lhs{}<br/>", lhs.0)?,
UnaryUnquote { operator, operand } => {
write!(w, "eval {:?} $rhs{}<br/>", operator, operand.0)?
}
@@ -107,7 +103,7 @@ where
MakeIntegerConst {
value,
bit_width: _,
} => write!(w, "make {}<br/>", self.1.lookup(*value))?,
} => write!(w, "make {}<br/>", self.0.lookup(*value))?,
MakeBooleanConst {
value,
bit_width: _,
@@ -142,13 +138,3 @@ where
writeln!(w, "</font>")
}
}
fn p<'a>(paths: &'a PathInterner) -> impl Fn(&PathId) -> String + 'a {
move |path: &PathId| {
let mut s = vec![];
for b in paths.lookup(*path).0 {
s.push(b.to_string());
}
s.join(".")
}
}

View File

@@ -142,11 +142,10 @@ where
sort_lexicographically(&mut opts);
let automata = automatize(&opts);
let paths = opts.paths;
let integers = opts.integers;
if let Ok(path) = std::env::var("PEEPMATIC_DOT") {
let f = dot_fmt::PeepholeDotFmt(&paths, &integers);
let f = dot_fmt::PeepholeDotFmt(&integers);
if let Err(e) = automata.write_dot_file(&f, &path) {
panic!(
"failed to write GraphViz Dot file to PEEPMATIC_DOT={}; error: {}",
@@ -155,11 +154,7 @@ where
}
}
Ok(PeepholeOptimizations {
paths,
integers,
automata,
})
Ok(PeepholeOptimizations { integers, automata })
}
#[cfg(test)]

View File

@@ -1,9 +1,6 @@
//! Passes over the linear IR.
use peepmatic_runtime::{
linear,
paths::{PathId, PathInterner},
};
use peepmatic_runtime::linear;
use std::cmp::Ordering;
use std::fmt::Debug;
use std::hash::Hash;
@@ -33,13 +30,12 @@ where
{
let linear::Optimizations {
ref mut optimizations,
ref paths,
..
} = opts;
// NB: we *cannot* use an unstable sort here, because we want deterministic
// compilation of optimizations to automata.
optimizations.sort_by(|a, b| compare_optimization_generality(paths, a, b));
optimizations.sort_by(compare_optimization_generality);
debug_assert!(is_sorted_by_generality(opts));
}
@@ -52,17 +48,14 @@ where
{
let linear::Optimizations {
ref mut optimizations,
ref paths,
..
} = opts;
// NB: we *cannot* use an unstable sort here, same as above.
optimizations
.sort_by(|a, b| compare_optimizations(paths, a, b, |a_len, b_len| a_len.cmp(&b_len)));
optimizations.sort_by(|a, b| compare_optimizations(a, b, |a_len, b_len| a_len.cmp(&b_len)));
}
fn compare_optimizations<TOperator>(
paths: &PathInterner,
a: &linear::Optimization<TOperator>,
b: &linear::Optimization<TOperator>,
compare_lengths: impl Fn(usize, usize) -> Ordering,
@@ -70,8 +63,8 @@ fn compare_optimizations<TOperator>(
where
TOperator: Copy + Debug + Eq + Hash,
{
for (a, b) in a.increments.iter().zip(b.increments.iter()) {
let c = compare_match_op_generality(paths, a.operation, b.operation);
for (a, b) in a.matches.iter().zip(b.matches.iter()) {
let c = compare_match_op_generality(a.operation, b.operation);
if c != Ordering::Equal {
return c;
}
@@ -87,90 +80,66 @@ where
}
}
compare_lengths(a.increments.len(), b.increments.len())
compare_lengths(a.matches.len(), b.matches.len())
}
fn compare_optimization_generality<TOperator>(
paths: &PathInterner,
a: &linear::Optimization<TOperator>,
b: &linear::Optimization<TOperator>,
) -> Ordering
where
TOperator: Copy + Debug + Eq + Hash,
{
compare_optimizations(paths, a, b, |a_len, b_len| {
compare_optimizations(a, b, |a_len, b_len| {
// If they shared equivalent prefixes, then compare lengths and invert the
// result because longer patterns are less general than shorter patterns.
a_len.cmp(&b_len).reverse()
})
}
fn compare_match_op_generality(
paths: &PathInterner,
a: linear::MatchOp,
b: linear::MatchOp,
) -> Ordering {
fn compare_match_op_generality(a: linear::MatchOp, b: linear::MatchOp) -> Ordering {
use linear::MatchOp::*;
match (a, b) {
(Opcode { path: a }, Opcode { path: b }) => compare_paths(paths, a, b),
(Opcode { .. }, _) => Ordering::Less,
(_, Opcode { .. }) => Ordering::Greater,
(Opcode(a), Opcode(b)) => a.cmp(&b),
(Opcode(_), _) => Ordering::Less,
(_, Opcode(_)) => Ordering::Greater,
(IntegerValue { path: a }, IntegerValue { path: b }) => compare_paths(paths, a, b),
(IntegerValue { .. }, _) => Ordering::Less,
(_, IntegerValue { .. }) => Ordering::Greater,
(IntegerValue(a), IntegerValue(b)) => a.cmp(&b),
(IntegerValue(_), _) => Ordering::Less,
(_, IntegerValue(_)) => Ordering::Greater,
(BooleanValue { path: a }, BooleanValue { path: b }) => compare_paths(paths, a, b),
(BooleanValue { .. }, _) => Ordering::Less,
(_, BooleanValue { .. }) => Ordering::Greater,
(BooleanValue(a), BooleanValue(b)) => a.cmp(&b),
(BooleanValue(_), _) => Ordering::Less,
(_, BooleanValue(_)) => Ordering::Greater,
(ConditionCode { path: a }, ConditionCode { path: b }) => compare_paths(paths, a, b),
(ConditionCode { .. }, _) => Ordering::Less,
(_, ConditionCode { .. }) => Ordering::Greater,
(ConditionCode(a), ConditionCode(b)) => a.cmp(&b),
(ConditionCode(_), _) => Ordering::Less,
(_, ConditionCode(_)) => Ordering::Greater,
(IsConst { path: a }, IsConst { path: b }) => compare_paths(paths, a, b),
(IsConst { .. }, _) => Ordering::Less,
(_, IsConst { .. }) => Ordering::Greater,
(IsConst(a), IsConst(b)) => a.cmp(&b),
(IsConst(_), _) => Ordering::Less,
(_, IsConst(_)) => Ordering::Greater,
(
Eq {
path_a: pa1,
path_b: pb1,
},
Eq {
path_a: pa2,
path_b: pb2,
},
) => compare_paths(paths, pa1, pa2).then(compare_paths(paths, pb1, pb2)),
(Eq { .. }, _) => Ordering::Less,
(_, Eq { .. }) => Ordering::Greater,
(Eq(a1, b1), Eq(a2, b2)) => a1.cmp(&a2).then(b1.cmp(&b2)),
(Eq(..), _) => Ordering::Less,
(_, Eq(..)) => Ordering::Greater,
(IsPowerOfTwo { path: a }, IsPowerOfTwo { path: b }) => compare_paths(paths, a, b),
(IsPowerOfTwo { .. }, _) => Ordering::Less,
(_, IsPowerOfTwo { .. }) => Ordering::Greater,
(IsPowerOfTwo(a), IsPowerOfTwo(b)) => a.cmp(&b),
(IsPowerOfTwo(_), _) => Ordering::Less,
(_, IsPowerOfTwo(_)) => Ordering::Greater,
(BitWidth { path: a }, BitWidth { path: b }) => compare_paths(paths, a, b),
(BitWidth { .. }, _) => Ordering::Less,
(_, BitWidth { .. }) => Ordering::Greater,
(BitWidth(a), BitWidth(b)) => a.cmp(&b),
(BitWidth(_), _) => Ordering::Less,
(_, BitWidth(_)) => Ordering::Greater,
(FitsInNativeWord { path: a }, FitsInNativeWord { path: b }) => compare_paths(paths, a, b),
(FitsInNativeWord { .. }, _) => Ordering::Less,
(_, FitsInNativeWord { .. }) => Ordering::Greater,
(FitsInNativeWord(a), FitsInNativeWord(b)) => a.cmp(&b),
(FitsInNativeWord(_), _) => Ordering::Less,
(_, FitsInNativeWord(_)) => Ordering::Greater,
(Nop, Nop) => Ordering::Equal,
}
}
fn compare_paths(paths: &PathInterner, a: PathId, b: PathId) -> Ordering {
if a == b {
Ordering::Equal
} else {
let a = paths.lookup(a);
let b = paths.lookup(b);
a.0.cmp(&b.0)
}
}
/// Are the given optimizations sorted from least to most general?
pub(crate) fn is_sorted_by_generality<TOperator>(opts: &linear::Optimizations<TOperator>) -> bool
where
@@ -178,7 +147,7 @@ where
{
opts.optimizations
.windows(2)
.all(|w| compare_optimization_generality(&opts.paths, &w[0], &w[1]) <= Ordering::Equal)
.all(|w| compare_optimization_generality(&w[0], &w[1]) <= Ordering::Equal)
}
/// Are the given optimizations sorted lexicographically?
@@ -189,8 +158,7 @@ where
TOperator: Copy + Debug + Eq + Hash,
{
opts.optimizations.windows(2).all(|w| {
compare_optimizations(&opts.paths, &w[0], &w[1], |a_len, b_len| a_len.cmp(&b_len))
<= Ordering::Equal
compare_optimizations(&w[0], &w[1], |a_len, b_len| a_len.cmp(&b_len)) <= Ordering::Equal
})
}
@@ -238,28 +206,27 @@ where
let mut prefix = vec![];
for opt in &mut opts.optimizations {
assert!(!opt.increments.is_empty());
assert!(!opt.matches.is_empty());
let mut old_increments = opt.increments.iter().peekable();
let mut new_increments = vec![];
let mut old_matches = opt.matches.iter().peekable();
let mut new_matches = vec![];
for (last_op, last_expected) in &prefix {
match old_increments.peek() {
match old_matches.peek() {
None => {
break;
}
Some(inc) if *last_op == inc.operation => {
let inc = old_increments.next().unwrap();
new_increments.push(inc.clone());
let inc = old_matches.next().unwrap();
new_matches.push(inc.clone());
if inc.expected != *last_expected {
break;
}
}
Some(_) => {
new_increments.push(linear::Increment {
new_matches.push(linear::Match {
operation: *last_op,
expected: Err(linear::Else),
actions: vec![],
});
if last_expected.is_ok() {
break;
@@ -268,16 +235,12 @@ where
}
}
new_increments.extend(old_increments.cloned());
assert!(new_increments.len() >= opt.increments.len());
opt.increments = new_increments;
new_matches.extend(old_matches.cloned());
assert!(new_matches.len() >= opt.matches.len());
opt.matches = new_matches;
prefix.clear();
prefix.extend(
opt.increments
.iter()
.map(|inc| (inc.operation, inc.expected)),
);
prefix.extend(opt.matches.iter().map(|inc| (inc.operation, inc.expected)));
}
// Should still be sorted after this pass.
@@ -291,21 +254,20 @@ where
/// for the DSL's edge-cases than it is to try and statically eliminate their
/// existence completely. So we just emit nop match operations for all variable
/// patterns, and then in this post-processing pass, we fuse them and their
/// actions with their preceding increment.
/// actions with their preceding match.
pub fn remove_unnecessary_nops<TOperator>(opts: &mut linear::Optimizations<TOperator>)
where
TOperator: Copy + Debug + Eq + Hash,
{
for opt in &mut opts.optimizations {
if opt.increments.len() < 2 {
debug_assert!(!opt.increments.is_empty());
if opt.matches.len() < 2 {
debug_assert!(!opt.matches.is_empty());
continue;
}
for i in (1..opt.increments.len()).rev() {
if let linear::MatchOp::Nop = opt.increments[i].operation {
let nop = opt.increments.remove(i);
opt.increments[i - 1].actions.extend(nop.actions);
for i in (1..opt.matches.len()).rev() {
if let linear::MatchOp::Nop = opt.matches[i].operation {
opt.matches.remove(i);
}
}
}
@@ -315,10 +277,7 @@ where
mod tests {
use super::*;
use crate::ast::*;
use peepmatic_runtime::{
linear::{bool_to_match_result, Else, MatchOp::*, MatchResult},
paths::*,
};
use peepmatic_runtime::linear::{bool_to_match_result, Else, LhsId, MatchOp::*, MatchResult};
use peepmatic_test_operator::TestOperator;
use std::num::NonZeroU32;
@@ -357,7 +316,7 @@ mod tests {
.optimizations
.iter()
.map(|o| {
o.increments
o.matches
.iter()
.map(|i| format!("{:?} == {:?}", i.operation, i.expected))
.collect::<Vec<_>>()
@@ -371,7 +330,7 @@ mod tests {
.optimizations
.iter()
.map(|o| {
o.increments
o.matches
.iter()
.map(|i| format!("{:?} == {:?}", i.operation, i.expected))
.collect::<Vec<_>>()
@@ -380,7 +339,6 @@ mod tests {
eprintln!("after = {:#?}", before);
let linear::Optimizations {
mut paths,
mut integers,
optimizations,
} = opts;
@@ -388,16 +346,15 @@ mod tests {
let actual: Vec<Vec<_>> = optimizations
.iter()
.map(|o| {
o.increments
o.matches
.iter()
.map(|i| (i.operation, i.expected))
.collect()
})
.collect();
let mut p = |p: &[u8]| paths.intern(Path::new(&p));
let mut i = |i: u64| Ok(integers.intern(i).into());
let expected = $make_expected(&mut p, &mut i);
let expected = $make_expected(&mut i);
assert_eq!(expected, actual);
}
@@ -435,7 +392,7 @@ mod tests {
.optimizations
.iter()
.map(|o| {
o.increments
o.matches
.iter()
.map(|i| format!("{:?} == {:?}", i.operation, i.expected))
.collect::<Vec<_>>()
@@ -449,7 +406,7 @@ mod tests {
.optimizations
.iter()
.map(|o| {
o.increments
o.matches
.iter()
.map(|i| format!("{:?} == {:?}", i.operation, i.expected))
.collect::<Vec<_>>()
@@ -458,7 +415,6 @@ mod tests {
eprintln!("after = {:#?}", before);
let linear::Optimizations {
mut paths,
mut integers,
optimizations,
} = opts;
@@ -466,16 +422,15 @@ mod tests {
let actual: Vec<Vec<_>> = optimizations
.iter()
.map(|o| {
o.increments
o.matches
.iter()
.map(|i| (i.operation, i.expected))
.collect()
})
.collect();
let mut p = |p: &[u8]| paths.intern(Path::new(&p));
let mut i = |i: u64| Ok(integers.intern(i).into());
let expected = $make_expected(&mut p, &mut i);
let expected = $make_expected(&mut i);
assert_eq!(expected, actual);
}
@@ -494,55 +449,43 @@ mod tests {
(=> (iadd $x 42) 0)
(=> (iadd $x (iadd $y $z)) 0)
",
|p: &mut dyn FnMut(&[u8]) -> PathId, i: &mut dyn FnMut(u64) -> MatchResult| vec![
|i: &mut dyn FnMut(u64) -> MatchResult| vec![
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Iadd.into())),
(Opcode(LhsId(0)), Ok(TestOperator::Iadd.into())),
(Nop, Err(Else)),
(Opcode { path: p(&[0, 1]) }, Ok(TestOperator::Iadd.into())),
(Opcode(LhsId(2)), Ok(TestOperator::Iadd.into())),
(Nop, Err(Else)),
(Nop, Err(Else)),
],
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Iadd.into())),
(Opcode(LhsId(0)), Ok(TestOperator::Iadd.into())),
(Nop, Err(Else)),
(IntegerValue { path: p(&[0, 1]) }, i(42))
(IntegerValue(LhsId(2)), i(42))
],
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Iadd.into())),
(Opcode(LhsId(0)), Ok(TestOperator::Iadd.into())),
(Nop, Err(Else)),
(IsConst { path: p(&[0, 1]) }, bool_to_match_result(true)),
(
IsPowerOfTwo { path: p(&[0, 1]) },
bool_to_match_result(true)
)
(IsConst(LhsId(2)), bool_to_match_result(true)),
(IsPowerOfTwo(LhsId(2)), bool_to_match_result(true))
],
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Iadd.into())),
(Opcode(LhsId(0)), Ok(TestOperator::Iadd.into())),
(Nop, Err(Else)),
(IsConst { path: p(&[0, 1]) }, bool_to_match_result(true)),
(
BitWidth { path: p(&[0, 0]) },
Ok(NonZeroU32::new(32).unwrap())
)
(IsConst(LhsId(2)), bool_to_match_result(true)),
(BitWidth(LhsId(1)), Ok(NonZeroU32::new(32).unwrap()))
],
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Iadd.into())),
(Opcode(LhsId(0)), Ok(TestOperator::Iadd.into())),
(Nop, Err(Else)),
(IsConst { path: p(&[0, 1]) }, bool_to_match_result(true))
(IsConst(LhsId(2)), bool_to_match_result(true))
],
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Iadd.into())),
(Opcode(LhsId(0)), Ok(TestOperator::Iadd.into())),
(Nop, Err(Else)),
(
Eq {
path_a: p(&[0, 1]),
path_b: p(&[0, 0]),
},
bool_to_match_result(true)
)
(Eq(LhsId(2), LhsId(1)), bool_to_match_result(true))
],
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Iadd.into())),
(Opcode(LhsId(0)), Ok(TestOperator::Iadd.into())),
(Nop, Err(Else)),
(Nop, Err(Else)),
],
@@ -560,36 +503,36 @@ mod tests {
(=> (imul 2 $x) (ishl $x 1))
(=> (imul $x 2) (ishl $x 1))
",
|p: &mut dyn FnMut(&[u8]) -> PathId, i: &mut dyn FnMut(u64) -> MatchResult| vec![
|i: &mut dyn FnMut(u64) -> MatchResult| vec![
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Imul.into())),
(IntegerValue { path: p(&[0, 0]) }, i(2)),
(Opcode(LhsId(0)), Ok(TestOperator::Imul.into())),
(IntegerValue(LhsId(1)), i(2)),
(Nop, Err(Else))
],
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Imul.into())),
(IntegerValue { path: p(&[0, 0]) }, i(1)),
(Opcode(LhsId(0)), Ok(TestOperator::Imul.into())),
(IntegerValue(LhsId(1)), i(1)),
(Nop, Err(Else))
],
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Imul.into())),
(Opcode(LhsId(0)), Ok(TestOperator::Imul.into())),
(Nop, Err(Else)),
(IntegerValue { path: p(&[0, 1]) }, i(2))
(IntegerValue(LhsId(2)), i(2))
],
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Imul.into())),
(Opcode(LhsId(0)), Ok(TestOperator::Imul.into())),
(Nop, Err(Else)),
(IntegerValue { path: p(&[0, 1]) }, i(1))
(IntegerValue(LhsId(2)), i(1))
],
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Iadd.into())),
(IntegerValue { path: p(&[0, 0]) }, i(0)),
(Opcode(LhsId(0)), Ok(TestOperator::Iadd.into())),
(IntegerValue(LhsId(1)), i(0)),
(Nop, Err(Else))
],
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Iadd.into())),
(Opcode(LhsId(0)), Ok(TestOperator::Iadd.into())),
(Nop, Err(Else)),
(IntegerValue { path: p(&[0, 1]) }, i(0))
(IntegerValue(LhsId(2)), i(0))
]
]
);
@@ -603,32 +546,20 @@ mod tests {
(=> (bor (bor $x $y) $y)
(bor $x $y))
",
|p: &mut dyn FnMut(&[u8]) -> PathId, i: &mut dyn FnMut(u64) -> MatchResult| vec![
|i: &mut dyn FnMut(u64) -> MatchResult| vec![
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Bor.into())),
(Opcode { path: p(&[0, 0]) }, Ok(TestOperator::Bor.into())),
(Opcode(LhsId(0)), Ok(TestOperator::Bor.into())),
(Opcode(LhsId(1)), Ok(TestOperator::Bor.into())),
(Nop, Err(Else)),
(Eq(LhsId(3), LhsId(2)), bool_to_match_result(true)),
(Nop, Err(Else)),
(
Eq {
path_a: p(&[0, 1]),
path_b: p(&[0, 0, 0]),
},
bool_to_match_result(true)
),
],
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Bor.into())),
(Opcode { path: p(&[0, 0]) }, Ok(TestOperator::Bor.into())),
(Opcode(LhsId(0)), Ok(TestOperator::Bor.into())),
(Opcode(LhsId(1)), Ok(TestOperator::Bor.into())),
(Nop, Err(Else)),
(Nop, Err(Else)),
(
Eq {
path_a: p(&[0, 1]),
path_b: p(&[0, 0, 1]),
},
bool_to_match_result(true)
),
(Eq(LhsId(4), LhsId(2)), bool_to_match_result(true)),
],
]
);
@@ -642,39 +573,21 @@ mod tests {
(=> (bor (bor $x $y) $y)
(bor $x $y))
",
|p: &mut dyn FnMut(&[u8]) -> PathId, i: &mut dyn FnMut(u64) -> MatchResult| vec![
|i: &mut dyn FnMut(u64) -> MatchResult| vec![
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Bor.into())),
(Opcode { path: p(&[0, 0]) }, Ok(TestOperator::Bor.into())),
(Opcode(LhsId(0)), Ok(TestOperator::Bor.into())),
(Opcode(LhsId(1)), Ok(TestOperator::Bor.into())),
(Nop, Err(Else)),
(Eq(LhsId(3), LhsId(2)), bool_to_match_result(true)),
(Nop, Err(Else)),
(
Eq {
path_a: p(&[0, 1]),
path_b: p(&[0, 0, 0]),
},
bool_to_match_result(true)
),
],
vec![
(Opcode { path: p(&[0]) }, Ok(TestOperator::Bor.into())),
(Opcode { path: p(&[0, 0]) }, Ok(TestOperator::Bor.into())),
(Opcode(LhsId(0)), Ok(TestOperator::Bor.into())),
(Opcode(LhsId(1)), Ok(TestOperator::Bor.into())),
(Nop, Err(Else)),
(Eq(LhsId(3), LhsId(2)), Err(Else)),
(Nop, Err(Else)),
(
Eq {
path_a: p(&[0, 1]),
path_b: p(&[0, 0, 0]),
},
Err(Else),
),
(
Eq {
path_a: p(&[0, 1]),
path_b: p(&[0, 0, 1]),
},
bool_to_match_result(true)
),
(Eq(LhsId(4), LhsId(2)), bool_to_match_result(true)),
],
]
);

View File

@@ -18,46 +18,49 @@
//! (ishl $x $(log2 C)))
//! ```
//!
//! Then we should get the following linear chain of "increments":
//! Then the left-hand side becomes the following linear chain of "matches":
//!
//! ```ignore
//! [
//! // ( Match Operation, Expected Value, Actions )
//! ( Opcode@0, imul, [$x = GetLhs@0.0, $C = GetLhs@0.1, ...] ),
//! ( IsConst(C), true, [] ),
//! ( IsPowerOfTwo(C), true, [] ),
//! // ( Match Operation, Expected Value )
//! ( Opcode@0, imul ),
//! ( IsConst(C), true ),
//! ( IsPowerOfTwo(C), true ),
//! ]
//! ```
//!
//! Each increment will essentially become a state and a transition out of that
//! state in the final automata, along with the actions to perform when taking
//! that transition. The actions record the scope of matches from the left-hand
//! side and also incrementally build the right-hand side's instructions. (Note
//! that we've elided the actions that build up the optimization's right-hand
//! side in this example.)
//! And the right-hand side becomes this linear chain of "actions":
//!
//! ```ignore
//! [
//! $rhs0 = get lhs @ 0.0 // $x
//! $rhs1 = get lhs @ 0.1 // $C
//! $rhs2 = eval log2 $rhs1
//! $rhs3 = make ishl $rhs0, $rhs2
//! ]
//! ```
//!
//! Each match will essentially become a state and a transition out of that
//! state in the final automata. The actions record the scope of matches from
//! the left-hand side and also incrementally build the right-hand side's
//! instructions.
//!
//! ## General Principles
//!
//! Here are the general principles that linearization should adhere to:
//!
//! * Actions should be pushed as early in the optimization's increment chain as
//! they can be. This means the tail has fewer side effects, and is therefore
//! more likely to be share-able with other optimizations in the automata that
//! we build.
//! * Don't match on a subtree until we know it exists. That is, match on
//! parents before matching on children.
//!
//! * RHS actions cannot reference matches from the LHS until they've been
//! defined. And finally, an RHS operation's operands must be defined before
//! the RHS operation itself. In general, definitions must come before uses!
//!
//! * Shorter increment chains are better! This means fewer tests when matching
//! * Shorter match chains are better! This means fewer tests when matching
//! left-hand sides, and a more-compact, more-cache-friendly automata, and
//! ultimately, a faster automata.
//!
//! * An increment's match operation should be a switch rather than a predicate
//! that returns a boolean. For example, we switch on an instruction's opcode,
//! rather than ask whether this operation is an `imul`. This allows for more
//! prefix sharing in the automata, which (again) makes it more compact and
//! more cache friendly.
//! * An match operation should be a switch rather than a predicate that returns
//! a boolean. For example, we switch on an instruction's opcode, rather than
//! ask whether this operation is an `imul`. This allows for more prefix
//! sharing in the automata, which (again) makes it more compact and more
//! cache friendly.
//!
//! ## Implementation Overview
//!
@@ -67,13 +70,15 @@
//! precondition.
//!
//! Within matching the pattern structure, we emit matching operations in a
//! pre-order traversal of the pattern. This ensures that we've already matched
//! an operation before we consider its operands, and therefore we already know
//! the operands exist. See `PatternPreOrder` for details.
//! breadth-first traversal of the pattern. This ensures that we've already
//! matched an operation before we consider its operands, and therefore we
//! already know the operands exist. It also lets us fuse "what opcode does this
//! instruction have?" and "define temporary variables for this instruction's
//! operands" into a single operation. See `PatternBfs` for details.
//!
//! As we define the match operations for a pattern, we remember the path where
//! each LHS id first occurred. These will later be reused when building the RHS
//! actions. See `LhsIdToPath` for details.
//! actions. See `LhsCanonicalizer` for details.
//!
//! After we've generated the match operations and expected result of those
//! match operations, then we generate the right-hand side actions. The
@@ -85,12 +90,8 @@
//! linear optimization translation function.
use crate::ast::*;
use crate::traversals::Dfs;
use peepmatic_runtime::{
integer_interner::IntegerInterner,
linear,
paths::{Path, PathId, PathInterner},
};
use crate::traversals::{Bfs, Dfs};
use peepmatic_runtime::{integer_interner::IntegerInterner, linear};
use std::collections::BTreeMap;
use std::convert::TryInto;
use std::fmt::Debug;
@@ -105,79 +106,74 @@ where
TOperator: Copy + Debug + Eq + Hash + Into<NonZeroU32>,
{
let mut optimizations = vec![];
let mut paths = PathInterner::new();
let mut integers = IntegerInterner::new();
for opt in &opts.optimizations {
let lin_opt = linearize_optimization(&mut paths, &mut integers, opt);
let lin_opt = linearize_optimization(&mut integers, opt);
optimizations.push(lin_opt);
}
linear::Optimizations {
optimizations,
paths,
integers,
}
}
/// Translate an AST optimization into a linear optimization!
fn linearize_optimization<TOperator>(
paths: &mut PathInterner,
integers: &mut IntegerInterner,
opt: &Optimization<TOperator>,
) -> linear::Optimization<TOperator>
where
TOperator: Copy + Debug + Eq + Hash + Into<NonZeroU32>,
{
let mut increments: Vec<linear::Increment<_>> = vec![];
let mut matches: Vec<linear::Match> = vec![];
let mut lhs_id_to_path = LhsIdToPath::new();
let mut lhs_canonicalizer = LhsCanonicalizer::new();
// We do a pre-order traversal of the LHS because we don't know whether a
// child actually exists to match on until we've matched its parent, and we
// don't want to emit matching operations on things that might not exist!
let mut patterns = PatternPreOrder::new(&opt.lhs.pattern);
while let Some((path, pattern)) = patterns.next(paths) {
// Create the matching parts of an `Increment` for this part of the
// pattern, without any actions yet.
let (operation, expected) = pattern.to_linear_match_op(integers, &lhs_id_to_path, path);
increments.push(linear::Increment {
// We do a breadth-first traversal of the LHS because we don't know whether
// a child actually exists to match on until we've matched its parent, and
// we don't want to emit matching operations on things that might not exist!
for (id, pattern) in PatternBfs::new(&opt.lhs.pattern) {
// Create the matching parts of an `Match` for this part of the
// pattern.
let (operation, expected) = pattern.to_linear_match_op(integers, &lhs_canonicalizer, id);
matches.push(linear::Match {
operation,
expected,
actions: vec![],
});
lhs_id_to_path.remember_path_to_pattern_ids(pattern, path);
lhs_canonicalizer.remember_linear_id(pattern, id);
// Some operations require type ascriptions for us to infer the correct
// bit width of their results: `ireduce`, `sextend`, `uextend`, etc.
// When there is such a type ascription in the pattern, insert another
// increment that checks the instruction-being-matched's bit width.
// match that checks the instruction-being-matched's bit width.
if let Pattern::Operation(Operation { r#type, .. }) = pattern {
if let Some(w) = r#type.get().and_then(|ty| ty.bit_width.fixed_width()) {
debug_assert!(w != 0, "All fixed-width bit widths are non-zero");
let expected = Ok(unsafe { NonZeroU32::new_unchecked(w as u32) });
increments.push(linear::Increment {
operation: linear::MatchOp::BitWidth { path },
matches.push(linear::Match {
operation: linear::MatchOp::BitWidth(id),
expected,
actions: vec![],
});
}
}
}
// Now that we've added all the increments for the LHS pattern, add the
// increments for its preconditions.
// Now that we've added all the matches for the LHS pattern, add the
// matches for its preconditions.
for pre in &opt.lhs.preconditions {
increments.push(pre.to_linear_increment(&lhs_id_to_path));
matches.push(pre.to_linear_match(&lhs_canonicalizer));
}
assert!(!increments.is_empty());
assert!(!matches.is_empty());
// Finally, generate the RHS-building actions and attach them to the first increment.
// Finally, generate the RHS-building actions and attach them to the first match.
let mut rhs_builder = RhsBuilder::new(&opt.rhs);
rhs_builder.add_rhs_build_actions(integers, &lhs_id_to_path, &mut increments[0].actions);
let mut actions = vec![];
rhs_builder.add_rhs_build_actions(integers, &lhs_canonicalizer, &mut actions);
linear::Optimization { increments }
linear::Optimization { matches, actions }
}
/// A post-order, depth-first traversal of right-hand sides.
@@ -214,55 +210,46 @@ where
}
}
/// A pre-order, depth-first traversal of left-hand side patterns.
/// A breadth-first traversal of left-hand side patterns.
///
/// Keeps track of the path to each pattern, and yields it along side the
/// Keeps track of the `LhsId` of each pattern, and yields it along side the
/// pattern AST node.
struct PatternPreOrder<'a, TOperator> {
last_child: Option<u8>,
path: Vec<u8>,
dfs: Dfs<'a, TOperator>,
///
/// We use a breadth-first traversal because we fuse "which opcode is this?" and
/// "assign operands to temporaries" into a single linear match operation. A
/// breadth-first traversal aligns with "match this opcode, and on success bind
/// all of its operands to temporaries". Fusing these operations into one is
/// important for attaining similar performance as an open-coded Rust `match`
/// expression, which would also fuse these operations via pattern matching.
struct PatternBfs<'a, TOperator> {
next_id: u16,
bfs: Bfs<'a, TOperator>,
}
impl<'a, TOperator> PatternPreOrder<'a, TOperator>
impl<'a, TOperator> PatternBfs<'a, TOperator>
where
TOperator: Copy + Debug + Eq + Hash,
{
fn new(pattern: &'a Pattern<'a, TOperator>) -> Self {
Self {
last_child: None,
path: vec![],
dfs: Dfs::new(pattern),
next_id: 0,
bfs: Bfs::new(pattern),
}
}
}
fn next(&mut self, paths: &mut PathInterner) -> Option<(PathId, &'a Pattern<'a, TOperator>)> {
use crate::traversals::TraversalEvent as TE;
impl<'a, TOperator> Iterator for PatternBfs<'a, TOperator>
where
TOperator: 'a + Copy + Debug + Eq + Hash,
{
type Item = (linear::LhsId, &'a Pattern<'a, TOperator>);
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.dfs.next()? {
(TE::Enter, DynAstRef::Pattern(pattern)) => {
let last_child = self.last_child.take();
self.path.push(match last_child {
None => 0,
Some(c) => {
assert!(
c < std::u8::MAX,
"operators must have less than or equal u8::MAX arity"
);
c + 1
}
});
let path = paths.intern(Path(&self.path));
return Some((path, pattern));
}
(TE::Exit, DynAstRef::Pattern(_)) => {
self.last_child = Some(
self.path
.pop()
.expect("should always have a non-empty path during traversal"),
);
}
_ => {}
if let DynAstRef::Pattern(pattern) = self.bfs.next()? {
let id = linear::LhsId(self.next_id);
self.next_id = self.next_id.checked_add(1).unwrap();
return Some((id, pattern));
}
}
}
@@ -270,42 +257,37 @@ where
/// A map from left-hand side identifiers to the path in the left-hand side
/// where they first occurred.
struct LhsIdToPath<'a, TOperator> {
id_to_path: BTreeMap<&'a str, PathId>,
struct LhsCanonicalizer<'a, TOperator> {
id_to_linear: BTreeMap<&'a str, linear::LhsId>,
_marker: PhantomData<&'a TOperator>,
}
impl<'a, TOperator> LhsIdToPath<'a, TOperator> {
/// Construct a new, empty `LhsIdToPath`.
impl<'a, TOperator> LhsCanonicalizer<'a, TOperator> {
/// Construct a new, empty `LhsCanonicalizer`.
fn new() -> Self {
Self {
id_to_path: Default::default(),
id_to_linear: Default::default(),
_marker: PhantomData,
}
}
/// Have we already seen the given identifier?
fn get_first_occurrence(&self, id: &Id) -> Option<PathId> {
self.id_to_path.get(id.name()).copied()
/// Get the canonical `linear::LhsId` for the given variable, if any.
fn get(&self, id: &Id) -> Option<linear::LhsId> {
self.id_to_linear.get(id.name()).copied()
}
/// Get the path within the left-hand side pattern where we first saw the
/// given AST id.
///
/// ## Panics
///
/// Panics if the given AST id has not already been canonicalized.
fn unwrap_first_occurrence(&self, id: &Id) -> PathId {
self.id_to_path[id.name()]
}
/// Remember the path to any LHS ids used in the given pattern.
fn remember_path_to_pattern_ids(&mut self, pattern: &'a Pattern<'a, TOperator>, path: PathId) {
/// Remember the canonical `linear::LhsId` for any variables or constants
/// used in the given pattern.
fn remember_linear_id(
&mut self,
pattern: &'a Pattern<'a, TOperator>,
linear_id: linear::LhsId,
) {
match pattern {
// If this is the first time we've seen an identifier defined on the
// left-hand side, remember it.
Pattern::Variable(Variable { id, .. }) | Pattern::Constant(Constant { id, .. }) => {
self.id_to_path.entry(id.name()).or_insert(path);
self.id_to_linear.entry(id.name()).or_insert(linear_id);
}
_ => {}
}
@@ -358,11 +340,11 @@ where
fn add_rhs_build_actions(
&mut self,
integers: &mut IntegerInterner,
lhs_id_to_path: &LhsIdToPath<TOperator>,
lhs_canonicalizer: &LhsCanonicalizer<TOperator>,
actions: &mut Vec<linear::Action<TOperator>>,
) {
while let Some(rhs) = self.rhs_post_order.next() {
actions.push(self.rhs_to_linear_action(integers, lhs_id_to_path, rhs));
actions.push(self.rhs_to_linear_action(integers, lhs_canonicalizer, rhs));
let id = linear::RhsId(self.rhs_span_to_id.len().try_into().unwrap());
self.rhs_span_to_id.insert(rhs.span(), id);
}
@@ -371,7 +353,7 @@ where
fn rhs_to_linear_action(
&self,
integers: &mut IntegerInterner,
lhs_id_to_path: &LhsIdToPath<TOperator>,
lhs_canonicalizer: &LhsCanonicalizer<TOperator>,
rhs: &Rhs<TOperator>,
) -> linear::Action<TOperator> {
match rhs {
@@ -393,8 +375,8 @@ where
linear::Action::MakeConditionCode { cc: *cc }
}
Rhs::Variable(Variable { id, .. }) | Rhs::Constant(Constant { id, .. }) => {
let path = lhs_id_to_path.unwrap_first_occurrence(id);
linear::Action::GetLhs { path }
let lhs = lhs_canonicalizer.get(id).unwrap();
linear::Action::GetLhs { lhs }
}
Rhs::Unquote(unq) => match unq.operands.len() {
1 => linear::Action::UnaryUnquote {
@@ -452,22 +434,18 @@ impl<TOperator> Precondition<'_, TOperator>
where
TOperator: Copy + Debug + Eq + Hash + Into<NonZeroU32>,
{
/// Convert this precondition into a `linear::Increment`.
fn to_linear_increment(
&self,
lhs_id_to_path: &LhsIdToPath<TOperator>,
) -> linear::Increment<TOperator> {
/// Convert this precondition into a `linear::Match`.
fn to_linear_match(&self, lhs_canonicalizer: &LhsCanonicalizer<TOperator>) -> linear::Match {
match self.constraint {
Constraint::IsPowerOfTwo => {
let id = match &self.operands[0] {
ConstraintOperand::Constant(Constant { id, .. }) => id,
_ => unreachable!("checked in verification"),
};
let path = lhs_id_to_path.unwrap_first_occurrence(&id);
linear::Increment {
operation: linear::MatchOp::IsPowerOfTwo { path },
let id = lhs_canonicalizer.get(&id).unwrap();
linear::Match {
operation: linear::MatchOp::IsPowerOfTwo(id),
expected: linear::bool_to_match_result(true),
actions: vec![],
}
}
Constraint::BitWidth => {
@@ -476,7 +454,7 @@ where
| ConstraintOperand::Variable(Variable { id, .. }) => id,
_ => unreachable!("checked in verification"),
};
let path = lhs_id_to_path.unwrap_first_occurrence(&id);
let id = lhs_canonicalizer.get(&id).unwrap();
let width = match &self.operands[1] {
ConstraintOperand::ValueLiteral(ValueLiteral::Integer(Integer {
@@ -490,10 +468,9 @@ where
assert!((width as u8).is_power_of_two());
let expected = Ok(unsafe { NonZeroU32::new_unchecked(width as u32) });
linear::Increment {
operation: linear::MatchOp::BitWidth { path },
linear::Match {
operation: linear::MatchOp::BitWidth(id),
expected,
actions: vec![],
}
}
Constraint::FitsInNativeWord => {
@@ -502,11 +479,10 @@ where
| ConstraintOperand::Variable(Variable { id, .. }) => id,
_ => unreachable!("checked in verification"),
};
let path = lhs_id_to_path.unwrap_first_occurrence(&id);
linear::Increment {
operation: linear::MatchOp::FitsInNativeWord { path },
let id = lhs_canonicalizer.get(&id).unwrap();
linear::Match {
operation: linear::MatchOp::FitsInNativeWord(id),
expected: linear::bool_to_match_result(true),
actions: vec![],
}
}
}
@@ -525,52 +501,46 @@ where
fn to_linear_match_op(
&self,
integers: &mut IntegerInterner,
lhs_id_to_path: &LhsIdToPath<TOperator>,
path: PathId,
lhs_canonicalizer: &LhsCanonicalizer<TOperator>,
linear_lhs_id: linear::LhsId,
) -> (linear::MatchOp, linear::MatchResult)
where
TOperator: Into<NonZeroU32>,
{
match self {
Pattern::ValueLiteral(ValueLiteral::Integer(Integer { value, .. })) => (
linear::MatchOp::IntegerValue { path },
linear::MatchOp::IntegerValue(linear_lhs_id),
Ok(integers.intern(*value as u64).into()),
),
Pattern::ValueLiteral(ValueLiteral::Boolean(Boolean { value, .. })) => (
linear::MatchOp::BooleanValue { path },
linear::MatchOp::BooleanValue(linear_lhs_id),
linear::bool_to_match_result(*value),
),
Pattern::ValueLiteral(ValueLiteral::ConditionCode(ConditionCode { cc, .. })) => {
let cc = *cc as u32;
debug_assert!(cc != 0, "no `ConditionCode` variants are zero");
let expected = Ok(unsafe { NonZeroU32::new_unchecked(cc) });
(linear::MatchOp::ConditionCode { path }, expected)
(linear::MatchOp::ConditionCode(linear_lhs_id), expected)
}
Pattern::Constant(Constant { id, .. }) => {
if let Some(path_b) = lhs_id_to_path.get_first_occurrence(id) {
debug_assert!(path != path_b);
if let Some(linear_lhs_id2) = lhs_canonicalizer.get(id) {
debug_assert!(linear_lhs_id != linear_lhs_id2);
(
linear::MatchOp::Eq {
path_a: path,
path_b,
},
linear::MatchOp::Eq(linear_lhs_id, linear_lhs_id2),
linear::bool_to_match_result(true),
)
} else {
(
linear::MatchOp::IsConst { path },
linear::MatchOp::IsConst(linear_lhs_id),
linear::bool_to_match_result(true),
)
}
}
Pattern::Variable(Variable { id, .. }) => {
if let Some(path_b) = lhs_id_to_path.get_first_occurrence(id) {
debug_assert!(path != path_b);
if let Some(linear_lhs_id2) = lhs_canonicalizer.get(id) {
debug_assert!(linear_lhs_id != linear_lhs_id2);
(
linear::MatchOp::Eq {
path_a: path,
path_b,
},
linear::MatchOp::Eq(linear_lhs_id, linear_lhs_id2),
linear::bool_to_match_result(true),
)
} else {
@@ -579,7 +549,7 @@ where
}
Pattern::Operation(op) => {
let expected = Ok(op.operator.into());
(linear::MatchOp::Opcode { path }, expected)
(linear::MatchOp::Opcode(linear_lhs_id), expected)
}
}
}
@@ -590,8 +560,9 @@ mod tests {
use super::*;
use peepmatic_runtime::{
integer_interner::IntegerId,
linear::{bool_to_match_result, Action::*, Else, MatchOp::*},
linear::{bool_to_match_result, Action::*, Else, LhsId, MatchOp::*, RhsId},
r#type::{BitWidth, Kind, Type},
unquote::UnquoteOperator,
};
use peepmatic_test_operator::TestOperator;
@@ -625,24 +596,18 @@ mod tests {
panic!("should verify OK")
}
let mut paths = PathInterner::new();
let mut p = |p: &[u8]| paths.intern(Path::new(&p));
let mut integers = IntegerInterner::new();
let mut i = |i: u64| integers.intern(i);
#[allow(unused_variables)]
let make_expected: fn(
&mut dyn FnMut(&[u8]) -> PathId,
&mut dyn FnMut(u64) -> IntegerId,
) -> Vec<linear::Increment<TestOperator>> = $make_expected;
let expected = make_expected(&mut p, &mut i);
dbg!(&expected);
) -> (Vec<linear::Match>, Vec<linear::Action<_>>) = $make_expected;
let actual = linearize_optimization(&mut paths, &mut integers, &opts.optimizations[0]);
dbg!(&actual.increments);
assert_eq!(expected, actual.increments);
let expected = make_expected(&mut i);
let actual = linearize_optimization(&mut integers, &opts.optimizations[0]);
assert_eq!(expected.0, actual.matches);
assert_eq!(expected.1, actual.actions);
}
};
}
@@ -652,194 +617,191 @@ mod tests {
"
(=> (when (imul $x $C)
(is-power-of-two $C))
(ishl $x $C))
(ishl $x $(log2 $C)))
",
|p, i| vec![
linear::Increment {
operation: Opcode { path: p(&[0]) },
|i| (
vec![
linear::Match {
operation: Opcode(LhsId(0)),
expected: Ok(TestOperator::Imul.into()),
actions: vec![
GetLhs { path: p(&[0, 0]) },
GetLhs { path: p(&[0, 1]) },
},
linear::Match {
operation: Nop,
expected: Err(Else),
},
linear::Match {
operation: IsConst(LhsId(2)),
expected: bool_to_match_result(true),
},
linear::Match {
operation: IsPowerOfTwo(LhsId(2)),
expected: bool_to_match_result(true),
},
],
vec![
GetLhs { lhs: LhsId(1) },
GetLhs { lhs: LhsId(2) },
UnaryUnquote {
operator: UnquoteOperator::Log2,
operand: RhsId(1)
},
MakeBinaryInst {
operator: TestOperator::Ishl,
r#type: Type {
kind: Kind::Int,
bit_width: BitWidth::Polymorphic,
},
operands: [linear::RhsId(0), linear::RhsId(1)],
},
],
},
linear::Increment {
operation: Nop,
expected: Err(Else),
actions: vec![],
},
linear::Increment {
operation: IsConst { path: p(&[0, 1]) },
expected: bool_to_match_result(true),
actions: vec![],
},
linear::Increment {
operation: IsPowerOfTwo { path: p(&[0, 1]) },
expected: bool_to_match_result(true),
actions: vec![],
bit_width: BitWidth::Polymorphic
},
operands: [RhsId(0), RhsId(2)]
}
],
),
);
linearizes_to!(variable_pattern_id_optimization, "(=> $x $x)", |p, i| vec![
linear::Increment {
linearizes_to!(variable_pattern_id_optimization, "(=> $x $x)", |i| (
vec![linear::Match {
operation: Nop,
expected: Err(Else),
actions: vec![GetLhs { path: p(&[0]) }],
}
]);
}],
vec![GetLhs { lhs: LhsId(0) }],
));
linearizes_to!(constant_pattern_id_optimization, "(=> $C $C)", |p, i| vec![
linear::Increment {
operation: IsConst { path: p(&[0]) },
linearizes_to!(constant_pattern_id_optimization, "(=> $C $C)", |i| (
vec![linear::Match {
operation: IsConst(LhsId(0)),
expected: bool_to_match_result(true),
actions: vec![GetLhs { path: p(&[0]) }],
}
]);
}],
vec![GetLhs { lhs: LhsId(0) }],
));
linearizes_to!(
boolean_literal_id_optimization,
"(=> true true)",
|p, i| vec![linear::Increment {
operation: BooleanValue { path: p(&[0]) },
linearizes_to!(boolean_literal_id_optimization, "(=> true true)", |i| (
vec![linear::Match {
operation: BooleanValue(LhsId(0)),
expected: bool_to_match_result(true),
actions: vec![MakeBooleanConst {
}],
vec![MakeBooleanConst {
value: true,
bit_width: BitWidth::Polymorphic,
}],
}]
);
));
linearizes_to!(number_literal_id_optimization, "(=> 5 5)", |p, i| vec![
linear::Increment {
operation: IntegerValue { path: p(&[0]) },
linearizes_to!(number_literal_id_optimization, "(=> 5 5)", |i| (
vec![linear::Match {
operation: IntegerValue(LhsId(0)),
expected: Ok(i(5).into()),
actions: vec![MakeIntegerConst {
}],
vec![MakeIntegerConst {
value: i(5),
bit_width: BitWidth::Polymorphic,
}],
}
]);
));
linearizes_to!(
operation_id_optimization,
"(=> (iconst $C) (iconst $C))",
|p, i| vec![
linear::Increment {
operation: Opcode { path: p(&[0]) },
|i| (
vec![
linear::Match {
operation: Opcode(LhsId(0)),
expected: Ok(TestOperator::Iconst.into()),
actions: vec![
GetLhs { path: p(&[0, 0]) },
},
linear::Match {
operation: IsConst(LhsId(1)),
expected: bool_to_match_result(true),
},
],
vec![
GetLhs { lhs: LhsId(1) },
MakeUnaryInst {
operator: TestOperator::Iconst,
r#type: Type {
kind: Kind::Int,
bit_width: BitWidth::Polymorphic,
},
operand: linear::RhsId(0),
operand: RhsId(0),
},
],
},
linear::Increment {
operation: IsConst { path: p(&[0, 0]) },
expected: bool_to_match_result(true),
actions: vec![],
},
]
),
);
linearizes_to!(
redundant_bor,
"(=> (bor $x (bor $x $y)) (bor $x $y))",
|p, i| vec![
linear::Increment {
operation: Opcode { path: p(&[0]) },
|i| (
vec![
linear::Match {
operation: Opcode(LhsId(0)),
expected: Ok(TestOperator::Bor.into()),
actions: vec![
GetLhs { path: p(&[0, 0]) },
GetLhs {
path: p(&[0, 1, 1]),
},
linear::Match {
operation: Nop,
expected: Err(Else),
},
linear::Match {
operation: Opcode(LhsId(2)),
expected: Ok(TestOperator::Bor.into()),
},
linear::Match {
operation: Eq(LhsId(3), LhsId(1)),
expected: bool_to_match_result(true),
},
linear::Match {
operation: Nop,
expected: Err(Else),
},
],
vec![
GetLhs { lhs: LhsId(1) },
GetLhs { lhs: LhsId(4) },
MakeBinaryInst {
operator: TestOperator::Bor,
r#type: Type {
kind: Kind::Int,
bit_width: BitWidth::Polymorphic,
},
operands: [linear::RhsId(0), linear::RhsId(1)],
operands: [RhsId(0), RhsId(1)],
},
],
},
linear::Increment {
operation: Nop,
expected: Err(Else),
actions: vec![],
},
linear::Increment {
operation: Opcode { path: p(&[0, 1]) },
expected: Ok(TestOperator::Bor.into()),
actions: vec![],
},
linear::Increment {
operation: Eq {
path_a: p(&[0, 1, 0]),
path_b: p(&[0, 0]),
},
expected: bool_to_match_result(true),
actions: vec![],
},
linear::Increment {
operation: Nop,
expected: Err(Else),
actions: vec![],
},
]
),
);
linearizes_to!(
large_integers,
// u64::MAX
"(=> 18446744073709551615 0)",
|p, i| vec![linear::Increment {
operation: IntegerValue { path: p(&[0]) },
|i| (
vec![linear::Match {
operation: IntegerValue(LhsId(0)),
expected: Ok(i(std::u64::MAX).into()),
actions: vec![MakeIntegerConst {
}],
vec![MakeIntegerConst {
value: i(0),
bit_width: BitWidth::Polymorphic,
}],
}]
),
);
linearizes_to!(
ireduce_with_type_ascription,
"(=> (ireduce{i32} $x) 0)",
|p, i| vec![
linear::Increment {
operation: Opcode { path: p(&[0]) },
|i| (
vec![
linear::Match {
operation: Opcode(LhsId(0)),
expected: Ok(TestOperator::Ireduce.into()),
actions: vec![MakeIntegerConst {
},
linear::Match {
operation: linear::MatchOp::BitWidth(LhsId(0)),
expected: Ok(NonZeroU32::new(32).unwrap()),
},
linear::Match {
operation: Nop,
expected: Err(Else),
},
],
vec![MakeIntegerConst {
value: i(0),
bit_width: BitWidth::ThirtyTwo,
}],
},
linear::Increment {
operation: linear::MatchOp::BitWidth { path: p(&[0]) },
expected: Ok(NonZeroU32::new(32).unwrap()),
actions: vec![],
},
linear::Increment {
operation: Nop,
expected: Err(Else),
actions: vec![],
},
]
),
);
}

View File

@@ -1,6 +1,7 @@
//! Traversals over the AST.
use crate::ast::*;
use std::collections::VecDeque;
use std::fmt::Debug;
use std::hash::Hash;
@@ -103,6 +104,44 @@ where
}
}
/// A breadth-first traversal of an AST
///
/// This implementation is not recursive, and exposes an `Iterator` interface
/// that yields `DynAstRef` items.
///
/// The traversal can walk a whole set of `Optimization`s or just a subtree of
/// the AST, because the `new` constructor takes anything that can convert into
/// a `DynAstRef`.
#[derive(Clone, Debug)]
pub struct Bfs<'a, TOperator> {
queue: VecDeque<DynAstRef<'a, TOperator>>,
}
impl<'a, TOperator> Bfs<'a, TOperator>
where
TOperator: Copy + Debug + Eq + Hash,
{
/// Construct a new `Bfs` traversal starting at the given `start` AST node.
pub fn new(start: impl Into<DynAstRef<'a, TOperator>>) -> Self {
let mut queue = VecDeque::with_capacity(16);
queue.push_back(start.into());
Bfs { queue }
}
}
impl<'a, TOperator> Iterator for Bfs<'a, TOperator>
where
TOperator: Copy + Debug + Eq + Hash,
{
type Item = DynAstRef<'a, TOperator>;
fn next(&mut self) -> Option<Self::Item> {
let node = self.queue.pop_front()?;
node.child_nodes(&mut self.queue);
Some(node)
}
}
#[cfg(test)]
mod tests {
use super::*;