1475 lines
50 KiB
Rust
1475 lines
50 KiB
Rust
//! Verification and type checking of optimizations.
|
|
//!
|
|
//! For type checking, we compile the AST's type constraints down into Z3
|
|
//! variables and assertions. If Z3 finds the assertions satisfiable, then we're
|
|
//! done! If it finds them unsatisfiable, we use the `get_unsat_core` method to
|
|
//! get the minimal subset of assertions that are in conflict, and report a
|
|
//! best-effort type error message with them. These messages aren't perfect, but
|
|
//! they're Good Enough when embedded in the source text via our tracking of
|
|
//! `wast::Span`s.
|
|
//!
|
|
//! Verifying that there aren't any counter-examples (inputs for which the LHS
|
|
//! and RHS produce different results) for a particular optimization is not
|
|
//! implemented yet.
|
|
|
|
use crate::ast::{Span as _, *};
|
|
use crate::traversals::{Dfs, TraversalEvent};
|
|
use peepmatic_runtime::r#type::{BitWidth, Kind, Type};
|
|
use peepmatic_traits::{TypingContext as TypingContextTrait, TypingRules};
|
|
use std::borrow::Cow;
|
|
use std::collections::HashMap;
|
|
use std::convert::{TryFrom, TryInto};
|
|
use std::fmt;
|
|
use std::fmt::Debug;
|
|
use std::hash::Hash;
|
|
use std::iter;
|
|
use std::mem;
|
|
use std::ops::{Deref, DerefMut};
|
|
use std::path::Path;
|
|
use wast::{Error as WastError, Id, Span};
|
|
use z3::ast::Ast;
|
|
|
|
/// A verification or type checking error.
|
|
#[derive(Debug)]
|
|
pub struct VerifyError {
|
|
errors: Vec<anyhow::Error>,
|
|
}
|
|
|
|
impl fmt::Display for VerifyError {
|
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
for e in &self.errors {
|
|
writeln!(f, "{}\n", e)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for VerifyError {}
|
|
|
|
impl From<WastError> for VerifyError {
|
|
fn from(e: WastError) -> Self {
|
|
VerifyError {
|
|
errors: vec![e.into()],
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<anyhow::Error> for VerifyError {
|
|
fn from(e: anyhow::Error) -> Self {
|
|
VerifyError { errors: vec![e] }
|
|
}
|
|
}
|
|
|
|
impl VerifyError {
|
|
/// To provide a more useful error this function can be used to extract
|
|
/// relevant textual information about this error into the error itself.
|
|
///
|
|
/// The `contents` here should be the full text of the original file being
|
|
/// parsed, and this will extract a sub-slice as necessary to render in the
|
|
/// `Display` implementation later on.
|
|
pub fn set_text(&mut self, contents: &str) {
|
|
for e in &mut self.errors {
|
|
if let Some(e) = e.downcast_mut::<WastError>() {
|
|
e.set_text(contents);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// To provide a more useful error this function can be used to set
|
|
/// the file name that this error is associated with.
|
|
///
|
|
/// The `path` here will be stored in this error and later rendered in the
|
|
/// `Display` implementation.
|
|
pub fn set_path(&mut self, path: &Path) {
|
|
for e in &mut self.errors {
|
|
if let Some(e) = e.downcast_mut::<WastError>() {
|
|
e.set_path(path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Either `Ok(T)` or `Err(VerifyError)`.
|
|
pub type VerifyResult<T> = Result<T, VerifyError>;
|
|
|
|
/// Verify and type check a set of optimizations.
|
|
pub fn verify<TOperator>(opts: &Optimizations<TOperator>) -> VerifyResult<()>
|
|
where
|
|
TOperator: Copy + Debug + Eq + Hash + TypingRules,
|
|
{
|
|
if opts.optimizations.is_empty() {
|
|
return Err(anyhow::anyhow!("no optimizations").into());
|
|
}
|
|
|
|
verify_unique_left_hand_sides(opts)?;
|
|
|
|
let z3 = &z3::Context::new(&z3::Config::new());
|
|
for opt in &opts.optimizations {
|
|
verify_optimization(z3, opt)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Check that every LHS in the given optimizations is unique.
|
|
///
|
|
/// If there were duplicates, then it would be nondeterministic which one we
|
|
/// applied and would make automata construction more difficult. It is better to
|
|
/// check for duplicates and reject them if found.
|
|
fn verify_unique_left_hand_sides<TOperator>(opts: &Optimizations<TOperator>) -> VerifyResult<()>
|
|
where
|
|
TOperator: Copy + Eq + Debug + Hash,
|
|
{
|
|
let mut lefts = HashMap::new();
|
|
for opt in &opts.optimizations {
|
|
let canon_lhs = canonicalized_lhs_key(&opt.lhs);
|
|
let existing = lefts.insert(canon_lhs, opt.lhs.span());
|
|
if let Some(span) = existing {
|
|
return Err(VerifyError {
|
|
errors: vec![
|
|
anyhow::anyhow!("error: two optimizations cannot have the same left-hand side"),
|
|
WastError::new(span, "note: first use of this left-hand side".into()).into(),
|
|
WastError::new(
|
|
opt.lhs.span(),
|
|
"note: second use of this left-hand side".into(),
|
|
)
|
|
.into(),
|
|
],
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// When checking for duplicate left-hand sides, we need to consider patterns
|
|
/// that are duplicates up to renaming identifiers. For example, these LHSes
|
|
/// should be considered duplicates of each other:
|
|
///
|
|
/// ```lisp
|
|
/// (=> (iadd $x $y) ...)
|
|
/// (=> (iadd $a $b) ...)
|
|
/// ```
|
|
///
|
|
/// This function creates an opaque, canonicalized hash key for left-hand sides
|
|
/// that sees through identifier renaming.
|
|
fn canonicalized_lhs_key<TOperator>(lhs: &Lhs<TOperator>) -> impl Hash + Eq
|
|
where
|
|
TOperator: Copy + Debug + Eq + Hash,
|
|
{
|
|
let mut var_to_canon = HashMap::new();
|
|
let mut const_to_canon = HashMap::new();
|
|
let mut canonicalized = vec![];
|
|
|
|
for (event, ast) in Dfs::new(lhs) {
|
|
if event != TraversalEvent::Enter {
|
|
continue;
|
|
}
|
|
use CanonicalBit::*;
|
|
canonicalized.push(match ast {
|
|
DynAstRef::Lhs(_) => Other("Lhs"),
|
|
DynAstRef::Pattern(_) => Other("Pattern"),
|
|
DynAstRef::ValueLiteral(_) => Other("ValueLiteral"),
|
|
DynAstRef::Integer(i) => Integer(i.value),
|
|
DynAstRef::Boolean(b) => Boolean(b.value),
|
|
DynAstRef::ConditionCode(cc) => ConditionCode(cc.cc),
|
|
DynAstRef::PatternOperation(o) => Operation(o.operator, o.r#type.get()),
|
|
DynAstRef::Precondition(p) => Precondition(p.constraint),
|
|
DynAstRef::ConstraintOperand(_) => Other("ConstraintOperand"),
|
|
DynAstRef::Variable(Variable { id, .. }) => {
|
|
let new_id = var_to_canon.len() as u32;
|
|
let canon_id = var_to_canon.entry(id).or_insert(new_id);
|
|
Var(*canon_id)
|
|
}
|
|
DynAstRef::Constant(Constant { id, .. }) => {
|
|
let new_id = const_to_canon.len() as u32;
|
|
let canon_id = const_to_canon.entry(id).or_insert(new_id);
|
|
Const(*canon_id)
|
|
}
|
|
other => unreachable!("unreachable ast node: {:?}", other),
|
|
});
|
|
}
|
|
|
|
return canonicalized;
|
|
|
|
#[derive(Hash, PartialEq, Eq)]
|
|
enum CanonicalBit<TOperator> {
|
|
Var(u32),
|
|
Const(u32),
|
|
Integer(i64),
|
|
Boolean(bool),
|
|
ConditionCode(peepmatic_runtime::cc::ConditionCode),
|
|
Operation(TOperator, Option<Type>),
|
|
Precondition(Constraint),
|
|
Other(&'static str),
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct TypingContext<'a, TOperator> {
|
|
z3: &'a z3::Context,
|
|
type_kind_sort: z3::DatatypeSort<'a>,
|
|
solver: z3::Solver<'a>,
|
|
|
|
// The type of the root of the optimization. Initialized when collecting
|
|
// type constraints.
|
|
root_ty: Option<TypeVar<'a>>,
|
|
|
|
// See the comments above `enter_operation_scope`.
|
|
operation_scope: HashMap<&'static str, TypeVar<'a>>,
|
|
|
|
// A map from identifiers to the type variable describing its type.
|
|
id_to_type_var: HashMap<Id<'a>, TypeVar<'a>>,
|
|
|
|
// A list of type constraints, the span of the AST node where the constraint
|
|
// originates from, and an optional message to be displayed if the
|
|
// constraint is not satisfied.
|
|
constraints: Vec<(z3::ast::Bool<'a>, Span, Option<Cow<'static, str>>)>,
|
|
|
|
// Keep track of AST nodes that need to have their types assigned to
|
|
// them. For these AST nodes, we know what bit width to use when
|
|
// interpreting peephole optimization actions.
|
|
boolean_literals: Vec<(&'a Boolean<'a, TOperator>, TypeVar<'a>)>,
|
|
integer_literals: Vec<(&'a Integer<'a, TOperator>, TypeVar<'a>)>,
|
|
rhs_operations: Vec<(
|
|
&'a Operation<'a, TOperator, Rhs<'a, TOperator>>,
|
|
TypeVar<'a>,
|
|
)>,
|
|
}
|
|
|
|
impl<'a, TOperator> TypingContext<'a, TOperator> {
|
|
fn new(z3: &'a z3::Context) -> Self {
|
|
let type_kind_sort = z3::DatatypeBuilder::new(z3, "TypeKind")
|
|
.variant("int", vec![])
|
|
.variant("bool", vec![])
|
|
.variant("cpu_flags", vec![])
|
|
.variant("cc", vec![])
|
|
.variant("void", vec![])
|
|
.finish();
|
|
TypingContext {
|
|
z3,
|
|
solver: z3::Solver::new(z3),
|
|
root_ty: None,
|
|
operation_scope: Default::default(),
|
|
id_to_type_var: Default::default(),
|
|
type_kind_sort,
|
|
constraints: vec![],
|
|
boolean_literals: Default::default(),
|
|
integer_literals: Default::default(),
|
|
rhs_operations: Default::default(),
|
|
}
|
|
}
|
|
|
|
fn init_root_type(&mut self, span: Span, root_ty: TypeVar<'a>) {
|
|
assert!(self.root_ty.is_none());
|
|
|
|
// Make sure the root is a valid kind, i.e. not a condition code.
|
|
let is_int = self.is_int(&root_ty);
|
|
let is_bool = self.is_bool(&root_ty);
|
|
let is_void = self.is_void(&root_ty);
|
|
let is_cpu_flags = self.is_cpu_flags(&root_ty);
|
|
self.constraints.push((
|
|
z3::ast::Bool::or(&self.z3, &[&is_int, &is_bool, &is_void, &is_cpu_flags]),
|
|
span,
|
|
Some(
|
|
"the root of an optimization must be an integer, a boolean, void, or CPU flags"
|
|
.into(),
|
|
),
|
|
));
|
|
|
|
self.root_ty = Some(root_ty);
|
|
}
|
|
|
|
fn new_type_var(&self) -> TypeVar<'a> {
|
|
let kind =
|
|
z3::ast::Datatype::fresh_const(self.z3, "type-var-kind", &self.type_kind_sort.sort);
|
|
let width = z3::ast::BV::fresh_const(self.z3, "type-var-width", 8);
|
|
TypeVar { kind, width }
|
|
}
|
|
|
|
fn get_or_create_type_var_for_id(&mut self, id: Id<'a>) -> TypeVar<'a> {
|
|
if let Some(ty) = self.id_to_type_var.get(&id) {
|
|
ty.clone()
|
|
} else {
|
|
// Note: can't use the entry API because we reborrow `self` here.
|
|
let ty = self.new_type_var();
|
|
self.id_to_type_var.insert(id, ty.clone());
|
|
ty
|
|
}
|
|
}
|
|
|
|
fn get_type_var_for_id(&mut self, id: Id<'a>) -> VerifyResult<TypeVar<'a>> {
|
|
if let Some(ty) = self.id_to_type_var.get(&id) {
|
|
Ok(ty.clone())
|
|
} else {
|
|
Err(WastError::new(id.span(), format!("unknown identifier: ${}", id.name())).into())
|
|
}
|
|
}
|
|
|
|
// The `#[peepmatic]` macro for operations allows defining operations' types
|
|
// like `(iNN, iNN) -> iNN` where `iNN` all refer to the same integer type
|
|
// variable that must have the same bit width. But other operations might
|
|
// *also* have that type signature but be instantiated at a different bit
|
|
// width. We don't want to mix up which `iNN` variables are and aren't the
|
|
// same. We use this method to track scopes within which all uses of `iNN`
|
|
// and similar refer to the same type variables.
|
|
fn enter_operation_scope<'b>(
|
|
&'b mut self,
|
|
) -> impl DerefMut<Target = TypingContext<'a, TOperator>> + Drop + 'b {
|
|
assert!(self.operation_scope.is_empty());
|
|
return Scope(self);
|
|
|
|
struct Scope<'a, 'b, TOperator>(&'b mut TypingContext<'a, TOperator>)
|
|
where
|
|
'a: 'b;
|
|
|
|
impl<'a, 'b, TOperator> Deref for Scope<'a, 'b, TOperator>
|
|
where
|
|
'a: 'b,
|
|
{
|
|
type Target = TypingContext<'a, TOperator>;
|
|
fn deref(&self) -> &TypingContext<'a, TOperator> {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl<'a, 'b, TOperator> DerefMut for Scope<'a, 'b, TOperator>
|
|
where
|
|
'a: 'b,
|
|
{
|
|
fn deref_mut(&mut self) -> &mut TypingContext<'a, TOperator> {
|
|
self.0
|
|
}
|
|
}
|
|
|
|
impl<TOperator> Drop for Scope<'_, '_, TOperator> {
|
|
fn drop(&mut self) {
|
|
self.0.operation_scope.clear();
|
|
}
|
|
}
|
|
}
|
|
|
|
fn remember_boolean_literal(&mut self, b: &'a Boolean<'a, TOperator>, ty: TypeVar<'a>) {
|
|
self.assert_is_bool(b.span, &ty);
|
|
self.boolean_literals.push((b, ty));
|
|
}
|
|
|
|
fn remember_integer_literal(&mut self, i: &'a Integer<'a, TOperator>, ty: TypeVar<'a>) {
|
|
self.assert_is_integer(i.span, &ty);
|
|
self.integer_literals.push((i, ty));
|
|
}
|
|
|
|
fn remember_rhs_operation(
|
|
&mut self,
|
|
op: &'a Operation<'a, TOperator, Rhs<'a, TOperator>>,
|
|
ty: TypeVar<'a>,
|
|
) {
|
|
self.rhs_operations.push((op, ty));
|
|
}
|
|
|
|
fn is_int(&self, ty: &TypeVar<'a>) -> z3::ast::Bool<'a> {
|
|
self.type_kind_sort.variants[0]
|
|
.tester
|
|
.apply(&[&ty.kind.clone().into()])
|
|
.as_bool()
|
|
.unwrap()
|
|
}
|
|
|
|
fn is_bool(&self, ty: &TypeVar<'a>) -> z3::ast::Bool<'a> {
|
|
self.type_kind_sort.variants[1]
|
|
.tester
|
|
.apply(&[&ty.kind.clone().into()])
|
|
.as_bool()
|
|
.unwrap()
|
|
}
|
|
|
|
fn is_cpu_flags(&self, ty: &TypeVar<'a>) -> z3::ast::Bool<'a> {
|
|
self.type_kind_sort.variants[2]
|
|
.tester
|
|
.apply(&[&ty.kind.clone().into()])
|
|
.as_bool()
|
|
.unwrap()
|
|
}
|
|
|
|
fn is_condition_code(&self, ty: &TypeVar<'a>) -> z3::ast::Bool<'a> {
|
|
self.type_kind_sort.variants[3]
|
|
.tester
|
|
.apply(&[&ty.kind.clone().into()])
|
|
.as_bool()
|
|
.unwrap()
|
|
}
|
|
|
|
fn is_void(&self, ty: &TypeVar<'a>) -> z3::ast::Bool<'a> {
|
|
self.type_kind_sort.variants[4]
|
|
.tester
|
|
.apply(&[&ty.kind.clone().into()])
|
|
.as_bool()
|
|
.unwrap()
|
|
}
|
|
|
|
fn assert_is_integer(&mut self, span: Span, ty: &TypeVar<'a>) {
|
|
self.constraints.push((
|
|
self.is_int(ty),
|
|
span,
|
|
Some("type error: expected integer".into()),
|
|
));
|
|
}
|
|
|
|
fn assert_is_bool(&mut self, span: Span, ty: &TypeVar<'a>) {
|
|
self.constraints.push((
|
|
self.is_bool(ty),
|
|
span,
|
|
Some("type error: expected bool".into()),
|
|
));
|
|
}
|
|
|
|
fn assert_is_cpu_flags(&mut self, span: Span, ty: &TypeVar<'a>) {
|
|
self.constraints.push((
|
|
self.is_cpu_flags(ty),
|
|
span,
|
|
Some("type error: expected CPU flags".into()),
|
|
));
|
|
}
|
|
|
|
fn assert_is_cc(&mut self, span: Span, ty: &TypeVar<'a>) {
|
|
self.constraints.push((
|
|
self.is_condition_code(ty),
|
|
span,
|
|
Some("type error: expected condition code".into()),
|
|
));
|
|
}
|
|
|
|
fn assert_is_void(&mut self, span: Span, ty: &TypeVar<'a>) {
|
|
self.constraints.push((
|
|
self.is_void(ty),
|
|
span,
|
|
Some("type error: expected void".into()),
|
|
));
|
|
}
|
|
|
|
fn assert_bit_width(&mut self, span: Span, ty: &TypeVar<'a>, width: u8) {
|
|
debug_assert!(width == 0 || width.is_power_of_two());
|
|
let width_var = z3::ast::BV::from_i64(self.z3, width as i64, 8);
|
|
let is_width = width_var._eq(&ty.width);
|
|
self.constraints.push((
|
|
is_width,
|
|
span,
|
|
Some(format!("type error: expected bit width = {}", width).into()),
|
|
));
|
|
}
|
|
|
|
fn assert_bit_width_lt(&mut self, span: Span, a: &TypeVar<'a>, b: &TypeVar<'a>) {
|
|
self.constraints.push((
|
|
a.width.bvult(&b.width),
|
|
span,
|
|
Some("type error: expected narrower bit width".into()),
|
|
));
|
|
}
|
|
|
|
fn assert_bit_width_gt(&mut self, span: Span, a: &TypeVar<'a>, b: &TypeVar<'a>) {
|
|
self.constraints.push((
|
|
a.width.bvugt(&b.width),
|
|
span,
|
|
Some("type error: expected wider bit width".into()),
|
|
));
|
|
}
|
|
|
|
fn assert_type_eq(
|
|
&mut self,
|
|
span: Span,
|
|
lhs: &TypeVar<'a>,
|
|
rhs: &TypeVar<'a>,
|
|
msg: Option<Cow<'static, str>>,
|
|
) {
|
|
self.constraints
|
|
.push((lhs.kind._eq(&rhs.kind), span, msg.clone()));
|
|
self.constraints
|
|
.push((lhs.width._eq(&rhs.width), span, msg));
|
|
}
|
|
|
|
fn type_check(&self, span: Span) -> VerifyResult<()> {
|
|
let trackers = iter::repeat_with(|| z3::ast::Bool::fresh_const(self.z3, "type-constraint"))
|
|
.take(self.constraints.len())
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut tracker_to_diagnostics = HashMap::with_capacity(self.constraints.len());
|
|
|
|
for (constraint_data, tracker) in self.constraints.iter().zip(trackers) {
|
|
let (constraint, span, msg) = constraint_data;
|
|
self.solver.assert_and_track(constraint, &tracker);
|
|
tracker_to_diagnostics.insert(tracker, (*span, msg.clone()));
|
|
}
|
|
|
|
match self.solver.check() {
|
|
z3::SatResult::Sat => Ok(()),
|
|
z3::SatResult::Unsat => {
|
|
let core = self.solver.get_unsat_core();
|
|
if core.is_empty() {
|
|
return Err(WastError::new(
|
|
span,
|
|
"z3 determined the type constraints for this optimization are \
|
|
unsatisfiable, meaning there is a type error, but z3 did not give us any \
|
|
additional information"
|
|
.into(),
|
|
)
|
|
.into());
|
|
}
|
|
|
|
let mut errors = core
|
|
.iter()
|
|
.map(|tracker| {
|
|
let (span, msg) = &tracker_to_diagnostics[tracker];
|
|
(
|
|
*span,
|
|
WastError::new(
|
|
*span,
|
|
msg.clone().unwrap_or("type error".into()).into(),
|
|
)
|
|
.into(),
|
|
)
|
|
})
|
|
.collect::<Vec<_>>();
|
|
errors.sort_by_key(|(span, _)| *span);
|
|
let errors = errors.into_iter().map(|(_, e)| e).collect();
|
|
|
|
Err(VerifyError { errors })
|
|
}
|
|
z3::SatResult::Unknown => Err(anyhow::anyhow!(
|
|
"z3 returned 'unknown' when evaluating type constraints: {}",
|
|
self.solver
|
|
.get_reason_unknown()
|
|
.unwrap_or_else(|| "<no reason given>".into())
|
|
)
|
|
.into()),
|
|
}
|
|
}
|
|
|
|
fn assign_types(&mut self) -> VerifyResult<()> {
|
|
for (int, ty) in mem::replace(&mut self.integer_literals, vec![]) {
|
|
let width = self.ty_var_to_width(&ty)?;
|
|
int.bit_width.set(Some(width));
|
|
}
|
|
|
|
for (b, ty) in mem::replace(&mut self.boolean_literals, vec![]) {
|
|
let width = self.ty_var_to_width(&ty)?;
|
|
b.bit_width.set(Some(width));
|
|
}
|
|
|
|
for (op, ty) in mem::replace(&mut self.rhs_operations, vec![]) {
|
|
let kind = self.op_ty_var_to_kind(&ty);
|
|
let bit_width = match kind {
|
|
Kind::CpuFlags | Kind::Void => BitWidth::One,
|
|
Kind::Int | Kind::Bool => self.ty_var_to_width(&ty)?,
|
|
};
|
|
debug_assert!(op.r#type.get().is_none());
|
|
op.r#type.set(Some(Type { kind, bit_width }));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn ty_var_to_width(&self, ty_var: &TypeVar<'a>) -> VerifyResult<BitWidth> {
|
|
// Doing solver push/pops apparently clears out the model, so we have to
|
|
// re-check each time to ensure that it exists, and Z3 doesn't helpfully
|
|
// abort the process for us. This should be fast, since the solver
|
|
// remembers inferences from earlier checks.
|
|
assert_eq!(self.solver.check(), z3::SatResult::Sat);
|
|
|
|
// Check if there is more than one satisfying assignment to
|
|
// `ty_var`'s width variable. If so, then it must be polymorphic. If
|
|
// not, then it must have a fixed value.
|
|
let model = self.solver.get_model().unwrap();
|
|
let width_var = model.eval(&ty_var.width).unwrap();
|
|
let bit_width: u8 = width_var.as_u64().unwrap().try_into().unwrap();
|
|
|
|
self.solver.push();
|
|
self.solver.assert(&ty_var.width._eq(&width_var).not());
|
|
let is_polymorphic = match self.solver.check() {
|
|
z3::SatResult::Sat => true,
|
|
z3::SatResult::Unsat => false,
|
|
z3::SatResult::Unknown => panic!("Z3 cannot determine bit width of type"),
|
|
};
|
|
self.solver.pop(1);
|
|
|
|
if is_polymorphic {
|
|
// If something is polymorphic over bit widths, it must be
|
|
// polymorphic over the same bit width as the whole
|
|
// optimization.
|
|
//
|
|
// TODO: We should have a better model for bit-width
|
|
// polymorphism. The current setup works for all the use cases we
|
|
// currently care about, and is relatively easy to implement when
|
|
// matching and constructing the RHS, but is a bit ad-hoc. Maybe
|
|
// allow each LHS variable a polymorphic bit width, augment the AST
|
|
// with that info, and later emit match ops as necessary to express
|
|
// their relative constraints? *hand waves*
|
|
self.solver.push();
|
|
self.solver
|
|
.assert(&ty_var.width._eq(&self.root_ty.as_ref().unwrap().width));
|
|
match self.solver.check() {
|
|
z3::SatResult::Sat => {}
|
|
z3::SatResult::Unsat => {
|
|
return Err(anyhow::anyhow!(
|
|
"AST node is bit width polymorphic, but not over the optimization's root \
|
|
width"
|
|
)
|
|
.into())
|
|
}
|
|
z3::SatResult::Unknown => panic!("Z3 cannot determine bit width of type"),
|
|
};
|
|
self.solver.pop(1);
|
|
|
|
Ok(BitWidth::Polymorphic)
|
|
} else {
|
|
Ok(BitWidth::try_from(bit_width).unwrap())
|
|
}
|
|
}
|
|
|
|
fn op_ty_var_to_kind(&self, ty_var: &TypeVar<'a>) -> Kind {
|
|
for (predicate, kind) in [
|
|
(Self::is_int as fn(_, _) -> _, Kind::Int),
|
|
(Self::is_bool, Kind::Bool),
|
|
(Self::is_cpu_flags, Kind::CpuFlags),
|
|
(Self::is_void, Kind::Void),
|
|
]
|
|
.iter()
|
|
{
|
|
self.solver.push();
|
|
self.solver.assert(&predicate(self, ty_var));
|
|
match self.solver.check() {
|
|
z3::SatResult::Sat => {
|
|
self.solver.pop(1);
|
|
return *kind;
|
|
}
|
|
z3::SatResult::Unsat => {
|
|
self.solver.pop(1);
|
|
continue;
|
|
}
|
|
z3::SatResult::Unknown => panic!("Z3 cannot determine the type's kind"),
|
|
}
|
|
}
|
|
|
|
// This would only happen if given a `TypeVar` whose kind was a
|
|
// condition code, but we only use this function for RHS operations,
|
|
// which cannot be condition codes.
|
|
panic!("cannot convert type variable's kind to `peepmatic_runtime::type::Kind`")
|
|
}
|
|
}
|
|
|
|
impl<'a, TOperator> TypingContextTrait<'a> for TypingContext<'a, TOperator> {
|
|
type Span = Span;
|
|
type TypeVariable = TypeVar<'a>;
|
|
|
|
fn cc(&mut self, span: Span) -> TypeVar<'a> {
|
|
let ty = self.new_type_var();
|
|
self.assert_is_cc(span, &ty);
|
|
ty
|
|
}
|
|
|
|
fn bNN(&mut self, span: Span) -> TypeVar<'a> {
|
|
if let Some(ty) = self.operation_scope.get("bNN") {
|
|
return ty.clone();
|
|
}
|
|
|
|
let ty = self.new_type_var();
|
|
self.assert_is_bool(span, &ty);
|
|
self.operation_scope.insert("bNN", ty.clone());
|
|
ty
|
|
}
|
|
|
|
fn iNN(&mut self, span: Span) -> TypeVar<'a> {
|
|
if let Some(ty) = self.operation_scope.get("iNN") {
|
|
return ty.clone();
|
|
}
|
|
|
|
let ty = self.new_type_var();
|
|
self.assert_is_integer(span, &ty);
|
|
self.operation_scope.insert("iNN", ty.clone());
|
|
ty
|
|
}
|
|
|
|
fn iMM(&mut self, span: Span) -> TypeVar<'a> {
|
|
if let Some(ty) = self.operation_scope.get("iMM") {
|
|
return ty.clone();
|
|
}
|
|
|
|
let ty = self.new_type_var();
|
|
self.assert_is_integer(span, &ty);
|
|
self.operation_scope.insert("iMM", ty.clone());
|
|
ty
|
|
}
|
|
|
|
fn cpu_flags(&mut self, span: Span) -> TypeVar<'a> {
|
|
if let Some(ty) = self.operation_scope.get("cpu_flags") {
|
|
return ty.clone();
|
|
}
|
|
|
|
let ty = self.new_type_var();
|
|
self.assert_is_cpu_flags(span, &ty);
|
|
self.assert_bit_width(span, &ty, 1);
|
|
self.operation_scope.insert("cpu_flags", ty.clone());
|
|
ty
|
|
}
|
|
|
|
fn b1(&mut self, span: Span) -> TypeVar<'a> {
|
|
let b1 = self.new_type_var();
|
|
self.assert_is_bool(span, &b1);
|
|
self.assert_bit_width(span, &b1, 1);
|
|
b1
|
|
}
|
|
|
|
fn void(&mut self, span: Span) -> TypeVar<'a> {
|
|
let void = self.new_type_var();
|
|
self.assert_is_void(span, &void);
|
|
self.assert_bit_width(span, &void, 0);
|
|
void
|
|
}
|
|
|
|
fn bool_or_int(&mut self, span: Span) -> TypeVar<'a> {
|
|
let ty = self.new_type_var();
|
|
let is_int = self.type_kind_sort.variants[0]
|
|
.tester
|
|
.apply(&[&ty.kind.clone().into()])
|
|
.as_bool()
|
|
.unwrap();
|
|
let is_bool = self.type_kind_sort.variants[1]
|
|
.tester
|
|
.apply(&[&ty.kind.clone().into()])
|
|
.as_bool()
|
|
.unwrap();
|
|
self.constraints.push((
|
|
z3::ast::Bool::or(&self.z3, &[&is_int, &is_bool]),
|
|
span,
|
|
Some("type error: must be either an int or a bool type".into()),
|
|
));
|
|
ty
|
|
}
|
|
|
|
fn any_t(&mut self, _span: Span) -> TypeVar<'a> {
|
|
if let Some(ty) = self.operation_scope.get("any_t") {
|
|
return ty.clone();
|
|
}
|
|
|
|
let ty = self.new_type_var();
|
|
self.operation_scope.insert("any_t", ty.clone());
|
|
ty
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
struct TypeVar<'a> {
|
|
kind: z3::ast::Datatype<'a>,
|
|
width: z3::ast::BV<'a>,
|
|
}
|
|
|
|
fn verify_optimization<TOperator>(
|
|
z3: &z3::Context,
|
|
opt: &Optimization<TOperator>,
|
|
) -> VerifyResult<()>
|
|
where
|
|
TOperator: Copy + Debug + Eq + Hash + TypingRules,
|
|
{
|
|
let mut context = TypingContext::new(z3);
|
|
collect_type_constraints(&mut context, opt)?;
|
|
context.type_check(opt.span)?;
|
|
context.assign_types()?;
|
|
|
|
// TODO: add another pass here to check for counter-examples to this
|
|
// optimization, i.e. inputs where the LHS and RHS are not equivalent.
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn collect_type_constraints<'a, TOperator>(
|
|
context: &mut TypingContext<'a, TOperator>,
|
|
opt: &'a Optimization<'a, TOperator>,
|
|
) -> VerifyResult<()>
|
|
where
|
|
TOperator: Copy + Debug + Eq + Hash + TypingRules,
|
|
{
|
|
use crate::traversals::TraversalEvent as TE;
|
|
|
|
let lhs_ty = context.new_type_var();
|
|
context.init_root_type(opt.lhs.span, lhs_ty.clone());
|
|
|
|
let rhs_ty = context.new_type_var();
|
|
context.assert_type_eq(
|
|
opt.span,
|
|
&lhs_ty,
|
|
&rhs_ty,
|
|
Some("type error: the left-hand side and right-hand side must have the same type".into()),
|
|
);
|
|
|
|
// A stack of type variables that we are constraining as we traverse the
|
|
// AST. Operations push new type variables for their operands' expected
|
|
// types, and exiting a `Pattern` in the traversal pops them off.
|
|
let mut expected_types = vec![lhs_ty];
|
|
|
|
// Build up the type constraints for the left-hand side.
|
|
for (event, node) in Dfs::new(&opt.lhs) {
|
|
match (event, node) {
|
|
(
|
|
TE::Enter,
|
|
DynAstRef::Pattern(Pattern::Constant(Constant {
|
|
id,
|
|
span,
|
|
marker: _,
|
|
})),
|
|
)
|
|
| (
|
|
TE::Enter,
|
|
DynAstRef::Pattern(Pattern::Variable(Variable {
|
|
id,
|
|
span,
|
|
marker: _,
|
|
})),
|
|
) => {
|
|
let id = context.get_or_create_type_var_for_id(*id);
|
|
context.assert_type_eq(*span, expected_types.last().unwrap(), &id, None);
|
|
}
|
|
(TE::Enter, DynAstRef::Pattern(Pattern::ValueLiteral(ValueLiteral::Integer(i)))) => {
|
|
let ty = expected_types.last().unwrap();
|
|
context.remember_integer_literal(i, ty.clone());
|
|
}
|
|
(TE::Enter, DynAstRef::Pattern(Pattern::ValueLiteral(ValueLiteral::Boolean(b)))) => {
|
|
let ty = expected_types.last().unwrap();
|
|
context.remember_boolean_literal(b, ty.clone());
|
|
}
|
|
(
|
|
TE::Enter,
|
|
DynAstRef::Pattern(Pattern::ValueLiteral(ValueLiteral::ConditionCode(cc))),
|
|
) => {
|
|
let ty = expected_types.last().unwrap();
|
|
context.assert_is_cc(cc.span, ty);
|
|
}
|
|
(TE::Enter, DynAstRef::PatternOperation(op)) => {
|
|
let result_ty;
|
|
let mut operand_types = vec![];
|
|
{
|
|
let mut scope = context.enter_operation_scope();
|
|
result_ty = op.operator.result_type(op.span, &mut *scope);
|
|
op.operator
|
|
.immediate_types(op.span, &mut *scope, &mut operand_types);
|
|
op.operator
|
|
.parameter_types(op.span, &mut *scope, &mut operand_types);
|
|
}
|
|
|
|
if op.operands.len() != operand_types.len() {
|
|
return Err(WastError::new(
|
|
op.span,
|
|
format!(
|
|
"Expected {} operands but found {}",
|
|
operand_types.len(),
|
|
op.operands.len()
|
|
),
|
|
)
|
|
.into());
|
|
}
|
|
|
|
for imm in op
|
|
.operands
|
|
.iter()
|
|
.take(op.operator.immediates_arity() as usize)
|
|
{
|
|
match imm {
|
|
Pattern::ValueLiteral(_) |
|
|
Pattern::Constant(_) |
|
|
Pattern::Variable(_) => continue,
|
|
Pattern::Operation(op) => return Err(WastError::new(
|
|
op.span,
|
|
"operations are invalid immediates; must be a value literal, constant, \
|
|
or variable".into()
|
|
).into()),
|
|
}
|
|
}
|
|
|
|
if (op.operator.is_reduce() || op.operator.is_extend()) && op.r#type.get().is_none()
|
|
{
|
|
return Err(WastError::new(
|
|
op.span,
|
|
"`ireduce`, `sextend`, and `uextend` require an ascribed type, \
|
|
like `(sextend{i64} ...)`"
|
|
.into(),
|
|
)
|
|
.into());
|
|
}
|
|
|
|
if op.operator.is_extend() {
|
|
context.assert_bit_width_gt(op.span, &result_ty, &operand_types[0]);
|
|
}
|
|
if op.operator.is_reduce() {
|
|
context.assert_bit_width_lt(op.span, &result_ty, &operand_types[0]);
|
|
}
|
|
|
|
if let Some(ty) = op.r#type.get() {
|
|
match ty.kind {
|
|
Kind::Bool => context.assert_is_bool(op.span, &result_ty),
|
|
Kind::Int => context.assert_is_integer(op.span, &result_ty),
|
|
Kind::Void => context.assert_is_void(op.span, &result_ty),
|
|
Kind::CpuFlags => {
|
|
unreachable!("no syntax for ascribing CPU flags types right now")
|
|
}
|
|
}
|
|
if let Some(w) = ty.bit_width.fixed_width() {
|
|
context.assert_bit_width(op.span, &result_ty, w);
|
|
}
|
|
}
|
|
|
|
context.assert_type_eq(op.span, expected_types.last().unwrap(), &result_ty, None);
|
|
|
|
operand_types.reverse();
|
|
expected_types.extend(operand_types);
|
|
}
|
|
(TE::Exit, DynAstRef::Pattern(..)) => {
|
|
expected_types.pop().unwrap();
|
|
}
|
|
(TE::Enter, DynAstRef::Precondition(pre)) => {
|
|
type_constrain_precondition(context, pre)?;
|
|
}
|
|
_ => continue,
|
|
}
|
|
}
|
|
|
|
// We should have exited exactly as many patterns as we entered: one for the
|
|
// root pattern and the initial `lhs_ty`, and then the rest for the operands
|
|
// of pattern operations.
|
|
assert!(expected_types.is_empty());
|
|
|
|
// Collect the type constraints for the right-hand side.
|
|
expected_types.push(rhs_ty);
|
|
for (event, node) in Dfs::new(&opt.rhs) {
|
|
match (event, node) {
|
|
(TE::Enter, DynAstRef::Rhs(Rhs::ValueLiteral(ValueLiteral::Integer(i)))) => {
|
|
let ty = expected_types.last().unwrap();
|
|
context.remember_integer_literal(i, ty.clone());
|
|
}
|
|
(TE::Enter, DynAstRef::Rhs(Rhs::ValueLiteral(ValueLiteral::Boolean(b)))) => {
|
|
let ty = expected_types.last().unwrap();
|
|
context.remember_boolean_literal(b, ty.clone());
|
|
}
|
|
(TE::Enter, DynAstRef::Rhs(Rhs::ValueLiteral(ValueLiteral::ConditionCode(cc)))) => {
|
|
let ty = expected_types.last().unwrap();
|
|
context.assert_is_cc(cc.span, ty);
|
|
}
|
|
(
|
|
TE::Enter,
|
|
DynAstRef::Rhs(Rhs::Constant(Constant {
|
|
span,
|
|
id,
|
|
marker: _,
|
|
})),
|
|
)
|
|
| (
|
|
TE::Enter,
|
|
DynAstRef::Rhs(Rhs::Variable(Variable {
|
|
span,
|
|
id,
|
|
marker: _,
|
|
})),
|
|
) => {
|
|
let id_ty = context.get_type_var_for_id(*id)?;
|
|
context.assert_type_eq(*span, expected_types.last().unwrap(), &id_ty, None);
|
|
}
|
|
(TE::Enter, DynAstRef::RhsOperation(op)) => {
|
|
let result_ty;
|
|
let mut operand_types = vec![];
|
|
{
|
|
let mut scope = context.enter_operation_scope();
|
|
result_ty = op.operator.result_type(op.span, &mut *scope);
|
|
op.operator
|
|
.immediate_types(op.span, &mut *scope, &mut operand_types);
|
|
op.operator
|
|
.parameter_types(op.span, &mut *scope, &mut operand_types);
|
|
}
|
|
|
|
if op.operands.len() != operand_types.len() {
|
|
return Err(WastError::new(
|
|
op.span,
|
|
format!(
|
|
"Expected {} operands but found {}",
|
|
operand_types.len(),
|
|
op.operands.len()
|
|
),
|
|
)
|
|
.into());
|
|
}
|
|
|
|
for imm in op
|
|
.operands
|
|
.iter()
|
|
.take(op.operator.immediates_arity() as usize)
|
|
{
|
|
match imm {
|
|
Rhs::ValueLiteral(_)
|
|
| Rhs::Constant(_)
|
|
| Rhs::Variable(_)
|
|
| Rhs::Unquote(_) => continue,
|
|
Rhs::Operation(op) => return Err(WastError::new(
|
|
op.span,
|
|
"operations are invalid immediates; must be a value literal, unquote, \
|
|
constant, or variable"
|
|
.into(),
|
|
)
|
|
.into()),
|
|
}
|
|
}
|
|
|
|
if (op.operator.is_reduce() || op.operator.is_extend()) && op.r#type.get().is_none()
|
|
{
|
|
return Err(WastError::new(
|
|
op.span,
|
|
"`ireduce`, `sextend`, and `uextend` require an ascribed type, \
|
|
like `(sextend{i64} ...)`"
|
|
.into(),
|
|
)
|
|
.into());
|
|
}
|
|
|
|
if op.operator.is_extend() {
|
|
context.assert_bit_width_gt(op.span, &result_ty, &operand_types[0]);
|
|
}
|
|
if op.operator.is_reduce() {
|
|
context.assert_bit_width_lt(op.span, &result_ty, &operand_types[0]);
|
|
}
|
|
|
|
if let Some(ty) = op.r#type.get() {
|
|
match ty.kind {
|
|
Kind::Bool => context.assert_is_bool(op.span, &result_ty),
|
|
Kind::Int => context.assert_is_integer(op.span, &result_ty),
|
|
Kind::Void => context.assert_is_void(op.span, &result_ty),
|
|
Kind::CpuFlags => {
|
|
unreachable!("no syntax for ascribing CPU flags types right now")
|
|
}
|
|
}
|
|
if let Some(w) = ty.bit_width.fixed_width() {
|
|
context.assert_bit_width(op.span, &result_ty, w);
|
|
}
|
|
}
|
|
|
|
context.assert_type_eq(op.span, expected_types.last().unwrap(), &result_ty, None);
|
|
if op.r#type.get().is_none() {
|
|
context.remember_rhs_operation(op, result_ty);
|
|
}
|
|
|
|
operand_types.reverse();
|
|
expected_types.extend(operand_types);
|
|
}
|
|
(TE::Enter, DynAstRef::Unquote(unq)) => {
|
|
let result_ty;
|
|
let mut operand_types = vec![];
|
|
{
|
|
let mut scope = context.enter_operation_scope();
|
|
result_ty = unq.operator.result_type(unq.span, &mut *scope);
|
|
unq.operator
|
|
.immediate_types(unq.span, &mut *scope, &mut operand_types);
|
|
unq.operator
|
|
.parameter_types(unq.span, &mut *scope, &mut operand_types);
|
|
}
|
|
|
|
if unq.operands.len() != operand_types.len() {
|
|
return Err(WastError::new(
|
|
unq.span,
|
|
format!(
|
|
"Expected {} unquote operands but found {}",
|
|
operand_types.len(),
|
|
unq.operands.len()
|
|
),
|
|
)
|
|
.into());
|
|
}
|
|
|
|
for operand in &unq.operands {
|
|
match operand {
|
|
Rhs::ValueLiteral(_) | Rhs::Constant(_) => continue,
|
|
Rhs::Variable(_) | Rhs::Unquote(_) | Rhs::Operation(_) => {
|
|
return Err(WastError::new(
|
|
operand.span(),
|
|
"unquote operands must be value literals or constants".into(),
|
|
)
|
|
.into());
|
|
}
|
|
}
|
|
}
|
|
|
|
context.assert_type_eq(unq.span, expected_types.last().unwrap(), &result_ty, None);
|
|
|
|
operand_types.reverse();
|
|
expected_types.extend(operand_types);
|
|
}
|
|
(TE::Exit, DynAstRef::Rhs(..)) => {
|
|
expected_types.pop().unwrap();
|
|
}
|
|
_ => continue,
|
|
}
|
|
}
|
|
|
|
// Again, we should have popped off all the expected types when exiting
|
|
// `Rhs` nodes in the traversal.
|
|
assert!(expected_types.is_empty());
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn type_constrain_precondition<'a, TOperator>(
|
|
context: &mut TypingContext<'a, TOperator>,
|
|
pre: &Precondition<'a, TOperator>,
|
|
) -> VerifyResult<()> {
|
|
match pre.constraint {
|
|
Constraint::BitWidth => {
|
|
if pre.operands.len() != 2 {
|
|
return Err(WastError::new(
|
|
pre.span,
|
|
format!(
|
|
"the `bit-width` precondition requires exactly 2 operands, found \
|
|
{} operands",
|
|
pre.operands.len(),
|
|
),
|
|
)
|
|
.into());
|
|
}
|
|
|
|
let id = match pre.operands[0] {
|
|
ConstraintOperand::ValueLiteral(_) => {
|
|
return Err(anyhow::anyhow!(
|
|
"the `bit-width` precondition requires a constant or variable as \
|
|
its first operand"
|
|
)
|
|
.into())
|
|
}
|
|
ConstraintOperand::Constant(Constant { id, .. })
|
|
| ConstraintOperand::Variable(Variable { id, .. }) => id,
|
|
};
|
|
|
|
let width = match pre.operands[1] {
|
|
ConstraintOperand::ValueLiteral(ValueLiteral::Integer(Integer {
|
|
value, ..
|
|
})) if value == 1
|
|
|| value == 8
|
|
|| value == 16
|
|
|| value == 32
|
|
|| value == 64
|
|
|| value == 128 =>
|
|
{
|
|
value as u8
|
|
}
|
|
ref op => return Err(WastError::new(
|
|
op.span(),
|
|
"the `bit-width` precondition requires a bit width of 1, 8, 16, 32, 64, or \
|
|
128"
|
|
.into(),
|
|
)
|
|
.into()),
|
|
};
|
|
|
|
let ty = context.get_type_var_for_id(id)?;
|
|
context.assert_bit_width(pre.span, &ty, width);
|
|
Ok(())
|
|
}
|
|
Constraint::IsPowerOfTwo => {
|
|
if pre.operands.len() != 1 {
|
|
return Err(WastError::new(
|
|
pre.span,
|
|
format!(
|
|
"the `is-power-of-two` precondition requires exactly 1 operand, found \
|
|
{} operands",
|
|
pre.operands.len(),
|
|
),
|
|
)
|
|
.into());
|
|
}
|
|
match &pre.operands[0] {
|
|
ConstraintOperand::Constant(Constant { id, .. }) => {
|
|
let ty = context.get_type_var_for_id(*id)?;
|
|
context.assert_is_integer(pre.span(), &ty);
|
|
Ok(())
|
|
}
|
|
op => Err(WastError::new(
|
|
op.span(),
|
|
"`is-power-of-two` operands must be constant bindings".into(),
|
|
)
|
|
.into()),
|
|
}
|
|
}
|
|
Constraint::FitsInNativeWord => {
|
|
if pre.operands.len() != 1 {
|
|
return Err(WastError::new(
|
|
pre.span,
|
|
format!(
|
|
"the `fits-in-native-word` precondition requires exactly 1 operand, found \
|
|
{} operands",
|
|
pre.operands.len(),
|
|
),
|
|
)
|
|
.into());
|
|
}
|
|
|
|
match pre.operands[0] {
|
|
ConstraintOperand::ValueLiteral(_) => {
|
|
return Err(anyhow::anyhow!(
|
|
"the `fits-in-native-word` precondition requires a constant or variable as \
|
|
its first operand"
|
|
)
|
|
.into())
|
|
}
|
|
ConstraintOperand::Constant(Constant { id, .. })
|
|
| ConstraintOperand::Variable(Variable { id, .. }) => {
|
|
context.get_type_var_for_id(id)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use peepmatic_test_operator::TestOperator;
|
|
|
|
macro_rules! verify_ok {
|
|
($name:ident, $src:expr) => {
|
|
#[test]
|
|
fn $name() {
|
|
let buf = wast::parser::ParseBuffer::new($src).expect("should lex OK");
|
|
let opts = match wast::parser::parse::<Optimizations<TestOperator>>(&buf) {
|
|
Ok(opts) => opts,
|
|
Err(mut e) => {
|
|
e.set_path(Path::new(stringify!($name)));
|
|
e.set_text($src);
|
|
eprintln!("{}", e);
|
|
panic!("should parse OK")
|
|
}
|
|
};
|
|
match verify(&opts) {
|
|
Ok(_) => return,
|
|
Err(mut e) => {
|
|
e.set_path(Path::new(stringify!($name)));
|
|
e.set_text($src);
|
|
eprintln!("{}", e);
|
|
panic!("should verify OK")
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
macro_rules! verify_err {
|
|
($name:ident, $src:expr) => {
|
|
#[test]
|
|
fn $name() {
|
|
let buf = wast::parser::ParseBuffer::new($src).expect("should lex OK");
|
|
let opts = match wast::parser::parse::<Optimizations<TestOperator>>(&buf) {
|
|
Ok(opts) => opts,
|
|
Err(mut e) => {
|
|
e.set_path(Path::new(stringify!($name)));
|
|
e.set_text($src);
|
|
eprintln!("{}", e);
|
|
panic!("should parse OK")
|
|
}
|
|
};
|
|
match verify(&opts) {
|
|
Ok(_) => panic!("expected a verification error, but it verified OK"),
|
|
Err(mut e) => {
|
|
e.set_path(Path::new(stringify!($name)));
|
|
e.set_text($src);
|
|
eprintln!("{}", e);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
verify_ok!(bool_0, "(=> true true)");
|
|
verify_ok!(bool_1, "(=> false false)");
|
|
verify_ok!(bool_2, "(=> true false)");
|
|
verify_ok!(bool_3, "(=> false true)");
|
|
|
|
verify_err!(bool_is_not_int_0, "(=> true 42)");
|
|
verify_err!(bool_is_not_int_1, "(=> 42 true)");
|
|
|
|
verify_ok!(
|
|
bit_width_0,
|
|
"
|
|
(=> (when (iadd $x $y)
|
|
(bit-width $x 32)
|
|
(bit-width $y 32))
|
|
(iadd $x $y))
|
|
"
|
|
);
|
|
verify_err!(
|
|
bit_width_1,
|
|
"
|
|
(=> (when (iadd $x $y)
|
|
(bit-width $x 32)
|
|
(bit-width $y 64))
|
|
(iadd $x $y))
|
|
"
|
|
);
|
|
verify_err!(
|
|
bit_width_2,
|
|
"
|
|
(=> (when (iconst $C)
|
|
(bit-width $C))
|
|
5)
|
|
"
|
|
);
|
|
verify_err!(
|
|
bit_width_3,
|
|
"
|
|
(=> (when (iconst $C)
|
|
(bit-width 32 32))
|
|
5)
|
|
"
|
|
);
|
|
verify_err!(
|
|
bit_width_4,
|
|
"
|
|
(=> (when (iconst $C)
|
|
(bit-width $C $C))
|
|
5)
|
|
"
|
|
);
|
|
verify_err!(
|
|
bit_width_5,
|
|
"
|
|
(=> (when (iconst $C)
|
|
(bit-width $C2 32))
|
|
5)
|
|
"
|
|
);
|
|
verify_err!(
|
|
bit_width_6,
|
|
"
|
|
(=> (when (iconst $C)
|
|
(bit-width $C2 33))
|
|
5)
|
|
"
|
|
);
|
|
|
|
verify_ok!(
|
|
is_power_of_two_0,
|
|
"
|
|
(=> (when (imul $x $C)
|
|
(is-power-of-two $C))
|
|
(ishl $x $(log2 $C)))
|
|
"
|
|
);
|
|
verify_err!(
|
|
is_power_of_two_1,
|
|
"
|
|
(=> (when (imul $x $C)
|
|
(is-power-of-two))
|
|
5)
|
|
"
|
|
);
|
|
verify_err!(
|
|
is_power_of_two_2,
|
|
"
|
|
(=> (when (imul $x $C)
|
|
(is-power-of-two $C $C))
|
|
5)
|
|
"
|
|
);
|
|
|
|
verify_ok!(pattern_ops_0, "(=> (iadd $x $C) 5)");
|
|
verify_err!(pattern_ops_1, "(=> (iadd $x) 5)");
|
|
verify_err!(pattern_ops_2, "(=> (iadd $x $y $z) 5)");
|
|
|
|
verify_ok!(unquote_0, "(=> $C $(log2 $C))");
|
|
verify_err!(unquote_1, "(=> (iadd $C $D) $(log2 $C $D))");
|
|
verify_err!(unquote_2, "(=> $x $(log2))");
|
|
verify_ok!(unquote_3, "(=> $C $(neg $C))");
|
|
verify_err!(unquote_4, "(=> $x $(neg))");
|
|
verify_err!(unquote_5, "(=> (iadd $x $y) $(neg $x $y))");
|
|
verify_err!(unquote_6, "(=> $x $(neg $x))");
|
|
|
|
verify_ok!(rhs_0, "(=> $x (iadd $x (iconst 0)))");
|
|
verify_err!(rhs_1, "(=> $x (iadd $x))");
|
|
verify_err!(rhs_2, "(=> $x (iadd $x 0 0))");
|
|
|
|
verify_err!(no_optimizations, "");
|
|
|
|
verify_err!(
|
|
duplicate_left_hand_sides,
|
|
"
|
|
(=> (iadd $x $y) 0)
|
|
(=> (iadd $x $y) 1)
|
|
"
|
|
);
|
|
verify_err!(
|
|
canonically_duplicate_left_hand_sides_0,
|
|
"
|
|
(=> (iadd $x $y) 0)
|
|
(=> (iadd $y $x) 1)
|
|
"
|
|
);
|
|
verify_err!(
|
|
canonically_duplicate_left_hand_sides_1,
|
|
"
|
|
(=> (iadd $X $Y) 0)
|
|
(=> (iadd $Y $X) 1)
|
|
"
|
|
);
|
|
verify_err!(
|
|
canonically_duplicate_left_hand_sides_2,
|
|
"
|
|
(=> (iadd $x $x) 0)
|
|
(=> (iadd $y $y) 1)
|
|
"
|
|
);
|
|
|
|
verify_ok!(
|
|
canonically_different_left_hand_sides_0,
|
|
"
|
|
(=> (iadd $x $C) 0)
|
|
(=> (iadd $C $x) 1)
|
|
"
|
|
);
|
|
verify_ok!(
|
|
canonically_different_left_hand_sides_1,
|
|
"
|
|
(=> (iadd $x $x) 0)
|
|
(=> (iadd $x $y) 1)
|
|
"
|
|
);
|
|
|
|
verify_ok!(
|
|
fits_in_native_word_0,
|
|
"(=> (when (iadd $x $y) (fits-in-native-word $x)) 0)"
|
|
);
|
|
verify_err!(
|
|
fits_in_native_word_1,
|
|
"(=> (when (iadd $x $y) (fits-in-native-word)) 0)"
|
|
);
|
|
verify_err!(
|
|
fits_in_native_word_2,
|
|
"(=> (when (iadd $x $y) (fits-in-native-word $x $y)) 0)"
|
|
);
|
|
verify_err!(
|
|
fits_in_native_word_3,
|
|
"(=> (when (iadd $x $y) (fits-in-native-word true)) 0)"
|
|
);
|
|
|
|
verify_err!(reduce_extend_0, "(=> (sextend (ireduce -1)) 0)");
|
|
verify_err!(reduce_extend_1, "(=> (uextend (ireduce -1)) 0)");
|
|
verify_ok!(reduce_extend_2, "(=> (sextend{i64} (ireduce{i32} -1)) 0)");
|
|
verify_ok!(reduce_extend_3, "(=> (uextend{i64} (ireduce{i32} -1)) 0)");
|
|
verify_err!(reduce_extend_4, "(=> (sextend{i64} (ireduce{i64} -1)) 0)");
|
|
verify_err!(reduce_extend_5, "(=> (uextend{i64} (ireduce{i64} -1)) 0)");
|
|
verify_err!(reduce_extend_6, "(=> (sextend{i32} (ireduce{i64} -1)) 0)");
|
|
verify_err!(reduce_extend_7, "(=> (uextend{i32} (ireduce{i64} -1)) 0)");
|
|
|
|
verify_err!(
|
|
using_an_operation_as_an_immediate_in_lhs,
|
|
"(=> (iadd_imm (imul $x $y) $z) 0)"
|
|
);
|
|
verify_err!(
|
|
using_an_operation_as_an_immediate_in_rhs,
|
|
"(=> (iadd (imul $x $y) $z) (iadd_imm (imul $x $y) $z))"
|
|
);
|
|
|
|
verify_err!(
|
|
using_a_condition_code_as_the_root_of_an_optimization,
|
|
"(=> eq eq)"
|
|
);
|
|
}
|