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, cc::ConditionCode,
instruction_set::InstructionSet, instruction_set::InstructionSet,
part::{Constant, Part}, part::{Constant, Part},
paths::Path,
r#type::{BitWidth, Kind, Type}, r#type::{BitWidth, Kind, Type},
PeepholeOptimizations, PeepholeOptimizer, PeepholeOptimizations, PeepholeOptimizer,
}; };
use peepmatic_traits::TypingRules;
use std::borrow::Cow; use std::borrow::Cow;
use std::boxed::Box; use std::boxed::Box;
use std::convert::{TryFrom, TryInto}; use std::convert::{TryFrom, TryInto};
use std::iter;
use std::ptr; use std::ptr;
use std::sync::atomic::{AtomicPtr, Ordering}; 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 { fn peepmatic_ty_to_ir_ty(ty: Type, dfg: &DataFlowGraph, root: Inst) -> types::Type {
match (ty.kind, bit_width(dfg, ty.bit_width, root)) { match (ty.kind, bit_width(dfg, ty.bit_width, root)) {
(Kind::Int, 8) => types::I8, (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, &self,
pos: &mut FuncCursor<'b>, pos: &mut FuncCursor<'b>,
root: ValueOrInst, value_or_inst: ValueOrInst,
path: Path, operands: &mut E,
) -> Option<Part<ValueOrInst>> { ) -> Option<Opcode>
// The root is path [0]. where
debug_assert!(!path.0.is_empty()); E: Extend<Part<Self::Instruction>>,
debug_assert_eq!(path.0[0], 0); {
let inst = value_or_inst.resolve_inst(&pos.func.dfg)?;
let mut part = Part::Instruction(root); Some(match pos.func.dfg[inst] {
for p in path.0[1..].iter().copied() { InstructionData::Binary {
let inst = part.as_instruction()?.resolve_inst(&pos.func.dfg)?; opcode: opcode @ Opcode::Band,
let operator = pos.func.dfg[inst].opcode(); args,
}
if p < operator.immediates_arity() { | InstructionData::Binary {
part = get_immediate(&pos.func.dfg, inst, p as usize); opcode: opcode @ Opcode::Bor,
continue; 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
} }
let arg = p - operator.immediates_arity(); InstructionData::BinaryImm64 {
let arg = arg as usize; opcode: opcode @ Opcode::BandImm,
let value = get_argument(&pos.func.dfg, inst, arg)?; imm,
part = Part::Instruction(value.into()); 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
}
log::trace!("get_part_at_path({:?}) = {:?}", path, part); InstructionData::Branch {
Some(part) 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
}
fn operator(&self, pos: &mut FuncCursor<'b>, value_or_inst: ValueOrInst) -> Option<Opcode> { InstructionData::CondTrap {
let inst = value_or_inst.resolve_inst(&pos.func.dfg)?; opcode: opcode @ Opcode::Trapnz,
Some(pos.func.dfg[inst].opcode()) 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( 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][] that ensures that the DSL's optimizations are sound, similar to what [Alive][]
does for LLVM optimizations. 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 [Cranelift]: https://github.com/bytecodealliance/wasmtime/tree/main/cranelift#readme
[Souper]: https://github.com/google/souper [Souper]: https://github.com/google/souper
[Alive]: https://github.com/AliveToolkit/alive2 [Alive]: https://github.com/AliveToolkit/alive2

View File

@@ -1,7 +1,6 @@
//! Interfacing with actual instructions. //! Interfacing with actual instructions.
use crate::part::{Constant, Part}; use crate::part::{Constant, Part};
use crate::paths::Path;
use crate::r#type::Type; use crate::r#type::Type;
use std::fmt::Debug; use std::fmt::Debug;
use std::hash::Hash; use std::hash::Hash;
@@ -54,26 +53,22 @@ pub unsafe trait InstructionSet<'a> {
new: Part<Self::Instruction>, new: Part<Self::Instruction>,
) -> 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. /// Get the given instruction's operator.
/// ///
/// If the instruction isn't supported, then `None` should be returned. /// 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, &self,
context: &mut Self::Context, context: &mut Self::Context,
instr: Self::Instruction, instr: Self::Instruction,
) -> Option<Self::Operator>; operands: &mut E,
) -> Option<Self::Operator>
where
E: Extend<Part<Self::Instruction>>;
/// Make a unary instruction. /// Make a unary instruction.
/// ///

View File

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

View File

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

View File

@@ -5,7 +5,6 @@ use crate::instruction_set::InstructionSet;
use crate::integer_interner::IntegerInterner; use crate::integer_interner::IntegerInterner;
use crate::linear::{Action, MatchOp, MatchResult}; use crate::linear::{Action, MatchOp, MatchResult};
use crate::optimizer::PeepholeOptimizer; use crate::optimizer::PeepholeOptimizer;
use crate::paths::PathInterner;
use peepmatic_automata::Automaton; use peepmatic_automata::Automaton;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt::Debug; use std::fmt::Debug;
@@ -25,9 +24,6 @@ pub struct PeepholeOptimizations<TOperator>
where where
TOperator: 'static + Copy + Debug + Eq + Hash, 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 /// 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. /// result of match operations. So we intern them and refer to them by id.
pub integers: IntegerInterner, pub integers: IntegerInterner,
@@ -88,6 +84,7 @@ where
PeepholeOptimizer { PeepholeOptimizer {
peep_opt: self, peep_opt: self,
instr_set, instr_set,
left_hand_sides: vec![],
right_hand_sides: vec![], right_hand_sides: vec![],
actions: vec![], actions: vec![],
backtracking_states: vec![], backtracking_states: vec![],

View File

@@ -27,9 +27,10 @@ where
{ {
pub(crate) peep_opt: &'peep PeepholeOptimizations<TInstructionSet::Operator>, pub(crate) peep_opt: &'peep PeepholeOptimizations<TInstructionSet::Operator>,
pub(crate) instr_set: TInstructionSet, 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) right_hand_sides: Vec<Part<TInstructionSet::Instruction>>,
pub(crate) actions: Vec<Action<TInstructionSet::Operator>>, 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> impl<'peep, 'ctx, TInstructionSet> Debug for PeepholeOptimizer<'peep, 'ctx, TInstructionSet>
@@ -40,6 +41,7 @@ where
let PeepholeOptimizer { let PeepholeOptimizer {
peep_opt, peep_opt,
instr_set: _, instr_set: _,
left_hand_sides,
right_hand_sides, right_hand_sides,
actions, actions,
backtracking_states, backtracking_states,
@@ -47,6 +49,7 @@ where
f.debug_struct("PeepholeOptimizer") f.debug_struct("PeepholeOptimizer")
.field("peep_opt", peep_opt) .field("peep_opt", peep_opt)
.field("instr_set", &"_") .field("instr_set", &"_")
.field("left_hand_sides", left_hand_sides)
.field("right_hand_sides", right_hand_sides) .field("right_hand_sides", right_hand_sides)
.field("actions", actions) .field("actions", actions)
.field("backtracking_states", backtracking_states) .field("backtracking_states", backtracking_states)
@@ -117,12 +120,8 @@ where
for action in actions.drain(..) { for action in actions.drain(..) {
log::trace!("Evaluating action: {:?}", action); log::trace!("Evaluating action: {:?}", action);
match action { match action {
Action::GetLhs { path } => { Action::GetLhs { lhs } => {
let path = self.peep_opt.paths.lookup(path); let lhs = self.left_hand_sides[lhs.0 as usize];
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");
self.right_hand_sides.push(lhs); self.right_hand_sides.push(lhs);
} }
Action::UnaryUnquote { operator, operand } => { Action::UnaryUnquote { operator, operand } => {
@@ -284,22 +283,17 @@ where
log::trace!("Evaluating match operation: {:?}", match_op); log::trace!("Evaluating match operation: {:?}", match_op);
let result: MatchResult = (|| match match_op { let result: MatchResult = (|| match match_op {
Opcode { path } => { Opcode(id) => {
let path = self.peep_opt.paths.lookup(path); let part = self.left_hand_sides[id.0 as usize];
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
let inst = part.as_instruction().ok_or(Else)?; 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()) Ok(op.into())
} }
IsConst { path } => { IsConst(id) => {
let path = self.peep_opt.paths.lookup(path); let part = self.left_hand_sides[id.0 as usize];
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
let is_const = match part { let is_const = match part {
Part::Instruction(i) => { Part::Instruction(i) => {
self.instr_set.instruction_to_constant(context, i).is_some() self.instr_set.instruction_to_constant(context, i).is_some()
@@ -308,12 +302,8 @@ where
}; };
bool_to_match_result(is_const) bool_to_match_result(is_const)
} }
IsPowerOfTwo { path } => { IsPowerOfTwo(id) => {
let path = self.peep_opt.paths.lookup(path); let part = self.left_hand_sides[id.0 as usize];
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
match part { match part {
Part::Constant(c) => { Part::Constant(c) => {
let is_pow2 = c.as_int().unwrap().is_power_of_two(); 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(); let is_pow2 = c.as_int().unwrap().is_power_of_two();
bool_to_match_result(is_pow2) bool_to_match_result(is_pow2)
} }
Part::ConditionCode(_) => unreachable!( Part::ConditionCode(_) => unreachable!("IsPowerOfTwo on a condition code"),
"IsPowerOfTwo on a condition
code"
),
} }
} }
BitWidth { path } => { BitWidth(id) => {
let path = self.peep_opt.paths.lookup(path); let part = self.left_hand_sides[id.0 as usize];
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
let bit_width = match part { let bit_width = match part {
Part::Instruction(i) => self.instr_set.instruction_result_bit_width(context, i), Part::Instruction(i) => self.instr_set.instruction_result_bit_width(context, i),
Part::Constant(Constant::Int(_, w)) | Part::Constant(Constant::Bool(_, w)) => { Part::Constant(Constant::Int(_, w)) | Part::Constant(Constant::Bool(_, w)) => {
@@ -355,15 +338,11 @@ where
); );
Ok(unsafe { NonZeroU32::new_unchecked(bit_width as u32) }) 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); let native_word_size = self.instr_set.native_word_size_in_bits(context);
debug_assert!(native_word_size.is_power_of_two()); debug_assert!(native_word_size.is_power_of_two());
let path = self.peep_opt.paths.lookup(path); let part = self.left_hand_sides[id.0 as usize];
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
let fits = match part { let fits = match part {
Part::Instruction(i) => { Part::Instruction(i) => {
let size = self.instr_set.instruction_result_bit_width(context, i); let size = self.instr_set.instruction_result_bit_width(context, i);
@@ -378,17 +357,9 @@ where
}; };
bool_to_match_result(fits) bool_to_match_result(fits)
} }
Eq { path_a, path_b } => { Eq(a, b) => {
let path_a = self.peep_opt.paths.lookup(path_a); let part_a = self.left_hand_sides[a.0 as usize];
let part_a = self let part_b = self.left_hand_sides[b.0 as usize];
.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)?;
let eq = match (part_a, part_b) { let eq = match (part_a, part_b) {
(Part::Instruction(inst), Part::Constant(c1)) (Part::Instruction(inst), Part::Constant(c1))
| (Part::Constant(c1), Part::Instruction(inst)) => { | (Part::Constant(c1), Part::Instruction(inst)) => {
@@ -401,12 +372,8 @@ where
}; };
bool_to_match_result(eq) bool_to_match_result(eq)
} }
IntegerValue { path } => { IntegerValue(id) => {
let path = self.peep_opt.paths.lookup(path); let part = self.left_hand_sides[id.0 as usize];
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
match part { match part {
Part::Constant(c) => { Part::Constant(c) => {
let x = c.as_int().ok_or(Else)?; let x = c.as_int().ok_or(Else)?;
@@ -425,12 +392,8 @@ where
Part::ConditionCode(_) => unreachable!("IntegerValue on condition code"), Part::ConditionCode(_) => unreachable!("IntegerValue on condition code"),
} }
} }
BooleanValue { path } => { BooleanValue(id) => {
let path = self.peep_opt.paths.lookup(path); let part = self.left_hand_sides[id.0 as usize];
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
match part { match part {
Part::Constant(c) => { Part::Constant(c) => {
let b = c.as_bool().ok_or(Else)?; let b = c.as_bool().ok_or(Else)?;
@@ -447,12 +410,8 @@ where
Part::ConditionCode(_) => unreachable!("IntegerValue on condition code"), Part::ConditionCode(_) => unreachable!("IntegerValue on condition code"),
} }
} }
ConditionCode { path } => { ConditionCode(id) => {
let path = self.peep_opt.paths.lookup(path); let part = self.left_hand_sides[id.0 as usize];
let part = self
.instr_set
.get_part_at_path(context, root, path)
.ok_or(Else)?;
let cc = part.as_condition_code().ok_or(Else)?; let cc = part.as_condition_code().ok_or(Else)?;
let cc = cc as u32; let cc = cc as u32;
debug_assert!(cc != 0); debug_assert!(cc != 0);
@@ -483,12 +442,20 @@ where
self.backtracking_states.clear(); self.backtracking_states.clear();
self.actions.clear(); self.actions.clear();
self.right_hand_sides.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 r#final = None;
let mut query = self.peep_opt.automata.query(); let mut query = self.peep_opt.automata.query();
loop { loop {
log::trace!("Current state: {:?}", query.current_state()); 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 query.is_in_final_state() {
// If we're in a final state (which means an optimization is // 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 // optimization, we want to be able to backtrack to this state and
// then try taking the `Else` transition. // then try taking the `Else` transition.
if query.has_transition_on(&Err(Else)) { if query.has_transition_on(&Err(Else)) {
self.backtracking_states self.backtracking_states.push((
.push((query.current_state(), self.actions.len())); query.current_state(),
self.actions.len(),
self.left_hand_sides.len(),
));
} }
let match_op = match query.current_state_data() { let match_op = match query.current_state_data() {
@@ -522,9 +492,10 @@ where
actions actions
} else if r#final.is_some() { } else if r#final.is_some() {
break; 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); query.go_to_state(state);
self.actions.truncate(actions_len); self.actions.truncate(actions_len);
self.left_hand_sides.truncate(lhs_len);
query query
.next(&Err(Else)) .next(&Err(Else))
.expect("backtracking states always have `Else` transitions") .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, cc::ConditionCode,
instruction_set::InstructionSet, instruction_set::InstructionSet,
part::{Constant, Part}, part::{Constant, Part},
paths::Path,
r#type::{BitWidth, Kind, Type}, r#type::{BitWidth, Kind, Type},
}; };
use peepmatic_test_operator::TestOperator; use peepmatic_test_operator::TestOperator;
@@ -329,43 +328,23 @@ unsafe impl<'a> InstructionSet<'a> for TestIsa {
new new
} }
fn get_part_at_path( fn operator<E>(
&self, &self,
program: &mut Program, program: &mut Program,
root: Instruction, instr: Self::Instruction,
path: Path, operands: &mut E,
) -> Option<Part<Instruction>> { ) -> Option<Self::Operator>
log::debug!("get_part_at_path({:?})", path); where
E: Extend<Part<Instruction>>,
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> {
log::debug!("operator({:?})", instr); log::debug!("operator({:?})", instr);
let data = program.data(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) 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 { for opt in &opts.optimizations {
let mut insertion = builder.insert(); let mut insertion = builder.insert();
for inc in &opt.increments { let mut is_first = true;
// Ensure that this state's associated data is this increment's for m in &opt.matches {
// match operation. // Ensure that this state's associated data is this match's
// operation.
if let Some(op) = insertion.get_state_data() { if let Some(op) = insertion.get_state_data() {
assert_eq!(*op, inc.operation); assert_eq!(*op, m.operation);
} else { } 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(); insertion.finish();
} }

View File

@@ -7,7 +7,6 @@ use peepmatic_runtime::{
cc::ConditionCode, cc::ConditionCode,
integer_interner::{IntegerId, IntegerInterner}, integer_interner::{IntegerId, IntegerInterner},
linear, linear,
paths::{PathId, PathInterner},
}; };
use std::convert::{TryFrom, TryInto}; use std::convert::{TryFrom, TryInto};
use std::fmt::Debug; use std::fmt::Debug;
@@ -15,7 +14,7 @@ use std::io::{self, Write};
use std::num::{NonZeroU16, NonZeroU32}; use std::num::{NonZeroU16, NonZeroU32};
#[derive(Debug)] #[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>]>> impl<TOperator> DotFmt<linear::MatchResult, linear::MatchOp, Box<[linear::Action<TOperator>]>>
for PeepholeDotFmt<'_> for PeepholeDotFmt<'_>
@@ -44,7 +43,7 @@ where
write!(w, "{}", cc) write!(w, "{}", cc)
} }
linear::MatchOp::IntegerValue { .. } => { linear::MatchOp::IntegerValue { .. } => {
let x = self.1.lookup(IntegerId( let x = self.0.lookup(IntegerId(
NonZeroU16::new(x.get().try_into().unwrap()).unwrap(), NonZeroU16::new(x.get().try_into().unwrap()).unwrap(),
)); ));
write!(w, "{}", x) write!(w, "{}", x)
@@ -61,17 +60,16 @@ where
write!(w, r#"<font face="monospace">"#)?; write!(w, r#"<font face="monospace">"#)?;
let p = p(self.0);
match op { match op {
Opcode { path } => write!(w, "opcode @ {}", p(path))?, Opcode(id) => write!(w, "opcode $lhs{}", id.0)?,
IsConst { path } => write!(w, "is-const? @ {}", p(path))?, IsConst(id) => write!(w, "is-const? $lhs{}", id.0)?,
IsPowerOfTwo { path } => write!(w, "is-power-of-two? @ {}", p(path))?, IsPowerOfTwo(id) => write!(w, "is-power-of-two? $lhs{}", id.0)?,
BitWidth { path } => write!(w, "bit-width @ {}", p(path))?, BitWidth(id) => write!(w, "bit-width $lhs{}", id.0)?,
FitsInNativeWord { path } => write!(w, "fits-in-native-word @ {}", p(path))?, FitsInNativeWord(id) => write!(w, "fits-in-native-word $lhs{}", id.0)?,
Eq { path_a, path_b } => write!(w, "{} == {}", p(path_a), p(path_b))?, Eq(a, b) => write!(w, "$lhs{} == $lhs{}", a.0, b.0)?,
IntegerValue { path } => write!(w, "integer-value @ {}", p(path))?, IntegerValue(id) => write!(w, "integer-value $lhs{}", id.0)?,
BooleanValue { path } => write!(w, "boolean-value @ {}", p(path))?, BooleanValue(id) => write!(w, "boolean-value $lhs{}", id.0)?,
ConditionCode { path } => write!(w, "condition-code @ {}", p(path))?, ConditionCode(id) => write!(w, "condition-code $lhs{}", id.0)?,
Nop => write!(w, "nop")?, Nop => write!(w, "nop")?,
} }
@@ -91,11 +89,9 @@ where
write!(w, r#"<font face="monospace">"#)?; write!(w, r#"<font face="monospace">"#)?;
let p = p(self.0);
for a in actions.iter() { for a in actions.iter() {
match a { match a {
GetLhs { path } => write!(w, "get-lhs @ {}<br/>", p(path))?, GetLhs { lhs } => write!(w, "get-lhs $lhs{}<br/>", lhs.0)?,
UnaryUnquote { operator, operand } => { UnaryUnquote { operator, operand } => {
write!(w, "eval {:?} $rhs{}<br/>", operator, operand.0)? write!(w, "eval {:?} $rhs{}<br/>", operator, operand.0)?
} }
@@ -107,7 +103,7 @@ where
MakeIntegerConst { MakeIntegerConst {
value, value,
bit_width: _, bit_width: _,
} => write!(w, "make {}<br/>", self.1.lookup(*value))?, } => write!(w, "make {}<br/>", self.0.lookup(*value))?,
MakeBooleanConst { MakeBooleanConst {
value, value,
bit_width: _, bit_width: _,
@@ -142,13 +138,3 @@ where
writeln!(w, "</font>") 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); sort_lexicographically(&mut opts);
let automata = automatize(&opts); let automata = automatize(&opts);
let paths = opts.paths;
let integers = opts.integers; let integers = opts.integers;
if let Ok(path) = std::env::var("PEEPMATIC_DOT") { 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) { if let Err(e) = automata.write_dot_file(&f, &path) {
panic!( panic!(
"failed to write GraphViz Dot file to PEEPMATIC_DOT={}; error: {}", "failed to write GraphViz Dot file to PEEPMATIC_DOT={}; error: {}",
@@ -155,11 +154,7 @@ where
} }
} }
Ok(PeepholeOptimizations { Ok(PeepholeOptimizations { integers, automata })
paths,
integers,
automata,
})
} }
#[cfg(test)] #[cfg(test)]

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
//! Traversals over the AST. //! Traversals over the AST.
use crate::ast::*; use crate::ast::*;
use std::collections::VecDeque;
use std::fmt::Debug; use std::fmt::Debug;
use std::hash::Hash; 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;