From 692a75978494f977e975d300d31623f9ed80442a Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 31 Mar 2025 20:06:04 +0100 Subject: [PATCH 01/19] No need for ConstFoldContext to impl Deref --- hugr-passes/src/const_fold.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/hugr-passes/src/const_fold.rs b/hugr-passes/src/const_fold.rs index 7552ed36f0..1dc4554cfa 100644 --- a/hugr-passes/src/const_fold.rs +++ b/hugr-passes/src/const_fold.rs @@ -207,13 +207,6 @@ pub fn constant_fold_pass(h: &mut H) { struct ConstFoldContext<'a, H>(&'a H); -impl std::ops::Deref for ConstFoldContext<'_, H> { - type Target = H; - fn deref(&self) -> &H { - self.0 - } -} - impl> ConstLoader> for ConstFoldContext<'_, H> { type Node = H::Node; @@ -244,7 +237,7 @@ impl> ConstLoader> for ConstFoldCo }; // Returning the function body as a value, here, would be sufficient for inlining IndirectCall // but not for transforming to a direct Call. - let func = DescendantsGraph::>::try_new(&**self, node).ok()?; + let func = DescendantsGraph::>::try_new(self.0, node).ok()?; Some(ValueHandle::new_const_hugr( ConstLocation::Node(node), Box::new(func.extract_hugr()), From 48143f558ba5c43f23eb9fe9531804ffe3f403fe Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 4 Apr 2025 15:24:17 +0100 Subject: [PATCH 02/19] Add PartialValue::LoadedFunction --- hugr-passes/src/const_fold.rs | 2 +- hugr-passes/src/dataflow.rs | 12 +- hugr-passes/src/dataflow/datalog.rs | 22 +-- hugr-passes/src/dataflow/partial_value.rs | 214 ++++++++++++++-------- hugr-passes/src/dataflow/results.rs | 18 +- hugr-passes/src/dataflow/test.rs | 7 +- hugr-passes/src/dataflow/value_row.rs | 38 ++-- 7 files changed, 190 insertions(+), 123 deletions(-) diff --git a/hugr-passes/src/const_fold.rs b/hugr-passes/src/const_fold.rs index 1dc4554cfa..7368bf7103 100644 --- a/hugr-passes/src/const_fold.rs +++ b/hugr-passes/src/const_fold.rs @@ -131,7 +131,7 @@ impl ConstantFoldPass { n, ip, results - .try_read_wire_concrete::(Wire::new(src, outp)) + .try_read_wire_concrete::(Wire::new(src, outp)) .ok()?, )) }) diff --git a/hugr-passes/src/dataflow.rs b/hugr-passes/src/dataflow.rs index 43caa9c946..bec178e295 100644 --- a/hugr-passes/src/dataflow.rs +++ b/hugr-passes/src/dataflow.rs @@ -9,7 +9,7 @@ mod results; pub use results::{AnalysisResults, TailLoopTermination}; mod partial_value; -pub use partial_value::{AbstractValue, PartialSum, PartialValue, Sum}; +pub use partial_value::{AbstractValue, LoadedFunction, PartialSum, PartialValue, Sum}; use hugr_core::ops::constant::OpaqueValue; use hugr_core::ops::{ExtensionOp, Value}; @@ -31,8 +31,8 @@ pub trait DFContext: ConstLoader { &mut self, _node: Self::Node, _e: &ExtensionOp, - _ins: &[PartialValue], - _outs: &mut [PartialValue], + _ins: &[PartialValue], + _outs: &mut [PartialValue], ) { } } @@ -94,7 +94,7 @@ pub fn partial_from_const<'a, V, CL: ConstLoader>( cl: &CL, loc: impl Into>, cst: &Value, -) -> PartialValue +) -> PartialValue where CL::Node: 'a, { @@ -120,8 +120,8 @@ where /// A row of inputs to a node contains bottom (can't happen, the node /// can't execute) if any element [contains_bottom](PartialValue::contains_bottom). -pub fn row_contains_bottom<'a, V: AbstractValue + 'a>( - elements: impl IntoIterator>, +pub fn row_contains_bottom<'a, V: 'a, N: 'a>( + elements: impl IntoIterator>, ) -> bool { elements.into_iter().any(PartialValue::contains_bottom) } diff --git a/hugr-passes/src/dataflow/datalog.rs b/hugr-passes/src/dataflow/datalog.rs index 13e510daf7..0e195240ae 100644 --- a/hugr-passes/src/dataflow/datalog.rs +++ b/hugr-passes/src/dataflow/datalog.rs @@ -15,7 +15,7 @@ use super::{ PartialValue, }; -type PV = PartialValue; +type PV = PartialValue; /// Basic structure for performing an analysis. Usage: /// 1. Make a new instance via [Self::new()] @@ -27,7 +27,7 @@ type PV = PartialValue; /// 3. Call [Self::run] to produce [AnalysisResults] pub struct Machine( H, - HashMap)>>, + HashMap)>>, ); impl Machine { @@ -40,7 +40,7 @@ impl Machine { impl Machine { /// Provide initial values for a wire - these will be `join`d with any computed /// or any value previously prepopulated for the same Wire. - pub fn prepopulate_wire(&mut self, w: Wire, v: PartialValue) { + pub fn prepopulate_wire(&mut self, w: Wire, v: PartialValue) { for (n, inp) in self.0.linked_inputs(w.node(), w.source()) { self.1.entry(n).or_default().push((inp, v.clone())); } @@ -54,7 +54,7 @@ impl Machine { pub fn prepopulate_inputs( &mut self, parent: H::Node, - in_values: impl IntoIterator)>, + in_values: impl IntoIterator)>, ) -> Result<(), OpType> { match self.0.get_optype(parent) { OpType::DataflowBlock(_) | OpType::Case(_) | OpType::FuncDefn(_) => { @@ -102,7 +102,7 @@ impl Machine { pub fn run( mut self, context: impl DFContext, - in_values: impl IntoIterator)>, + in_values: impl IntoIterator)>, ) -> AnalysisResults { let root = self.0.root(); if self.0.get_optype(root).is_module() { @@ -138,7 +138,7 @@ impl Machine { pub(super) fn run_datalog( mut ctx: impl DFContext, hugr: H, - in_wire_value_proto: Vec<(H::Node, IncomingPort, PV)>, + in_wire_value_proto: Vec<(H::Node, IncomingPort, PV)>, ) -> AnalysisResults { // ascent-(macro-)generated code generates a bunch of warnings, // keep code in here to a minimum. @@ -155,9 +155,9 @@ pub(super) fn run_datalog( relation parent_of_node(H::Node, H::Node); // is parent of relation input_child(H::Node, H::Node); // has 1st child that is its `Input` relation output_child(H::Node, H::Node); // has 2nd child that is its `Output` - lattice out_wire_value(H::Node, OutgoingPort, PV); // produces, on , the value - lattice in_wire_value(H::Node, IncomingPort, PV); // receives, on , the value - lattice node_in_value_row(H::Node, ValueRow); // 's inputs are + lattice out_wire_value(H::Node, OutgoingPort, PV); // produces, on , the value + lattice in_wire_value(H::Node, IncomingPort, PV); // receives, on , the value + lattice node_in_value_row(H::Node, ValueRow); // 's inputs are node(n) <-- for n in hugr.nodes(); @@ -341,9 +341,9 @@ fn propagate_leaf_op( ctx: &mut impl DFContext, hugr: &H, n: H::Node, - ins: &[PV], + ins: &[PV], num_outs: usize, -) -> Option> { +) -> Option> { match hugr.get_optype(n) { // Handle basics here. We could instead leave these to DFContext, // but at least we'd want these impls to be easily reusable. diff --git a/hugr-passes/src/dataflow/partial_value.rs b/hugr-passes/src/dataflow/partial_value.rs index f2a4978067..d2781d534c 100644 --- a/hugr-passes/src/dataflow/partial_value.rs +++ b/hugr-passes/src/dataflow/partial_value.rs @@ -1,7 +1,8 @@ use ascent::lattice::BoundedLattice; use ascent::Lattice; use hugr_core::ops::Value; -use hugr_core::types::{ConstTypeError, SumType, Type, TypeEnum, TypeRow}; +use hugr_core::types::{ConstTypeError, SumType, Type, TypeArg, TypeEnum, TypeRow}; +use hugr_core::Node; use itertools::{zip_eq, Itertools}; use std::cmp::Ordering; use std::collections::HashMap; @@ -51,15 +52,23 @@ pub struct Sum { pub st: SumType, } +/// The output of an [LoadFunction](hugr_core::ops::LoadFunction) - a "pointer" +/// to a function at a specific node, instantiated with the provided type-args. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct LoadedFunction { + pub func_node: N, + pub args: Vec, +} + /// A representation of a value of [SumType], that may have one or more possible tags, /// with a [PartialValue] representation of each element-value of each possible tag. #[derive(PartialEq, Clone, Eq)] -pub struct PartialSum(pub HashMap>>); +pub struct PartialSum(pub HashMap>>); -impl PartialSum { +impl PartialSum { /// New instance for a single known tag. /// (Multi-tag instances can be created via [Self::try_join_mut].) - pub fn new_variant(tag: usize, values: impl IntoIterator>) -> Self { + pub fn new_variant(tag: usize, values: impl IntoIterator>) -> Self { Self(HashMap::from([(tag, Vec::from_iter(values))])) } @@ -75,9 +84,21 @@ impl PartialSum { pv.assert_invariants(); } } + + /// Whether this sum might have the specified tag + pub fn supports_tag(&self, tag: usize) -> bool { + self.0.contains_key(&tag) + } + + /// Can this ever occur at runtime? See [PartialValue::contains_bottom] + pub fn contains_bottom(&self) -> bool { + self.0 + .iter() + .all(|(_tag, elements)| row_contains_bottom(elements)) + } } -impl PartialSum { +impl PartialSum { /// Joins (towards `Top`) self with another [PartialSum]. If successful, returns /// whether `self` has changed. /// @@ -141,12 +162,9 @@ impl PartialSum { } Ok(changed) } +} - /// Whether this sum might have the specified tag - pub fn supports_tag(&self, tag: usize) -> bool { - self.0.contains_key(&tag) - } - +impl PartialSum { /// Turns this instance into a [Sum] of some "concrete" value type `C`, /// *if* this PartialSum has exactly one possible tag. /// @@ -155,10 +173,14 @@ impl PartialSum { /// If this PartialSum had multiple possible tags; or if `typ` was not a [TypeEnum::Sum] /// supporting the single possible tag with the correct number of elements and no row variables; /// or if converting a child element failed via [PartialValue::try_into_concrete]. - pub fn try_into_sum(self, typ: &Type) -> Result, ExtractValueError> + pub fn try_into_sum( + self, + typ: &Type, + ) -> Result, ExtractValueError> where V: TryInto, Sum: TryInto, + LoadedFunction: TryInto, { if self.0.len() != 1 { return Err(ExtractValueError::MultipleVariants(self)); @@ -185,22 +207,15 @@ impl PartialSum { num_elements: v.len(), }) } - - /// Can this ever occur at runtime? See [PartialValue::contains_bottom] - pub fn contains_bottom(&self) -> bool { - self.0 - .iter() - .all(|(_tag, elements)| row_contains_bottom(elements)) - } } /// An error converting a [PartialValue] or [PartialSum] into a concrete value type /// via [PartialValue::try_into_concrete] or [PartialSum::try_into_sum] #[derive(Clone, Debug, PartialEq, Eq, Error)] #[allow(missing_docs)] -pub enum ExtractValueError { +pub enum ExtractValueError { #[error("PartialSum value had multiple possible tags: {0}")] - MultipleVariants(PartialSum), + MultipleVariants(PartialSum), #[error("Value contained `Bottom`")] ValueIsBottom, #[error("Value contained `Top`")] @@ -209,6 +224,8 @@ pub enum ExtractValueError { CouldNotConvert(V, #[source] VE), #[error("Could not build Sum from concrete element values")] CouldNotBuildSum(#[source] SE), + #[error("Could not turn LoadedFunction into concrete")] + CouldNotLoadFunction(#[source] LE), #[error("Expected a SumType with tag {tag} having {num_elements} elements, found {typ}")] BadSumType { typ: Type, @@ -217,14 +234,14 @@ pub enum ExtractValueError { }, } -impl PartialSum { +impl PartialSum { /// If this Sum might have the specified `tag`, get the elements inside that tag. - pub fn variant_values(&self, variant: usize) -> Option>> { + pub fn variant_values(&self, variant: usize) -> Option>> { self.0.get(&variant).cloned() } } -impl PartialOrd for PartialSum { +impl PartialOrd for PartialSum { fn partial_cmp(&self, other: &Self) -> Option { let max_key = self.0.keys().chain(other.0.keys()).copied().max().unwrap(); let (mut keys1, mut keys2) = (vec![0; max_key + 1], vec![0; max_key + 1]); @@ -254,13 +271,13 @@ impl PartialOrd for PartialSum { } } -impl std::fmt::Debug for PartialSum { +impl std::fmt::Debug for PartialSum { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } -impl Hash for PartialSum { +impl Hash for PartialSum { fn hash(&self, state: &mut H) { for (k, v) in &self.0 { k.hash(state); @@ -273,30 +290,32 @@ impl Hash for PartialSum { /// for use in dataflow analysis, including that an instance may be a [PartialSum] /// of values of the underlying representation #[derive(PartialEq, Clone, Eq, Hash, Debug)] -pub enum PartialValue { +pub enum PartialValue { /// No possibilities known (so far) Bottom, + /// The output of an [LoadFunction](hugr_core::ops::LoadFunction) + LoadedFunction(LoadedFunction), /// A single value (of the underlying representation) Value(V), /// Sum (with at least one, perhaps several, possible tags) of underlying values - PartialSum(PartialSum), + PartialSum(PartialSum), /// Might be more than one distinct value of the underlying type `V` Top, } -impl From for PartialValue { +impl From for PartialValue { fn from(v: V) -> Self { Self::Value(v) } } -impl From> for PartialValue { - fn from(v: PartialSum) -> Self { +impl From> for PartialValue { + fn from(v: PartialSum) -> Self { Self::PartialSum(v) } } -impl PartialValue { +impl PartialValue { fn assert_invariants(&self) { if let Self::PartialSum(ps) = self { ps.assert_invariants(); @@ -312,33 +331,59 @@ impl PartialValue { pub fn new_unit() -> Self { Self::new_variant(0, []) } + + /// New instance of self for a [LoadFunction](hugr_core::ops::LoadFunction) + pub fn new_load(func_node: N, args: impl Into>) -> Self { + Self::LoadedFunction(LoadedFunction { + func_node, + args: args.into(), + }) + } + + /// Tells us whether this value might be a Sum with the specified `tag` + pub fn supports_tag(&self, tag: usize) -> bool { + match self { + PartialValue::Bottom | PartialValue::Value(_) | PartialValue::LoadedFunction(_) => { + false + } + PartialValue::PartialSum(ps) => ps.supports_tag(tag), + PartialValue::Top => true, + } + } + + /// A value contains bottom means that it cannot occur during execution: + /// it may be an artefact during bootstrapping of the analysis, or else + /// the value depends upon a `panic` or a loop that + /// [never terminates](super::TailLoopTermination::NeverBreaks). + pub fn contains_bottom(&self) -> bool { + match self { + PartialValue::Bottom => true, + PartialValue::Top | PartialValue::Value(_) | PartialValue::LoadedFunction(_) => false, + PartialValue::PartialSum(ps) => ps.contains_bottom(), + } + } } -impl PartialValue { +impl PartialValue { /// If this value might be a Sum with the specified `tag`, get the elements inside that tag. /// /// # Panics /// /// if the value is believed, for that tag, to have a number of values other than `len` - pub fn variant_values(&self, tag: usize, len: usize) -> Option>> { + pub fn variant_values(&self, tag: usize, len: usize) -> Option>> { let vals = match self { - PartialValue::Bottom | PartialValue::Value(_) => return None, + PartialValue::Bottom | PartialValue::Value(_) | PartialValue::LoadedFunction(_) => { + return None + } PartialValue::PartialSum(ps) => ps.variant_values(tag)?, PartialValue::Top => vec![PartialValue::Top; len], }; assert_eq!(vals.len(), len); Some(vals) } +} - /// Tells us whether this value might be a Sum with the specified `tag` - pub fn supports_tag(&self, tag: usize) -> bool { - match self { - PartialValue::Bottom | PartialValue::Value(_) => false, - PartialValue::PartialSum(ps) => ps.supports_tag(tag), - PartialValue::Top => true, - } - } - +impl PartialValue { /// Turns this instance into some "concrete" value type `C`, *if* it is a single value, /// or a [Sum](PartialValue::PartialSum) (of a single tag) convertible by /// [PartialSum::try_into_sum]. @@ -348,16 +393,23 @@ impl PartialValue { /// If this PartialValue was `Top` or `Bottom`, or was a [PartialSum](PartialValue::PartialSum) /// that could not be converted into a [Sum] by [PartialSum::try_into_sum] (e.g. if `typ` is /// incorrect), or if that [Sum] could not be converted into a `V2`. - pub fn try_into_concrete(self, typ: &Type) -> Result> + pub fn try_into_concrete( + self, + typ: &Type, + ) -> Result> where V: TryInto, Sum: TryInto, + LoadedFunction: TryInto, { match self { Self::Value(v) => v .clone() .try_into() - .map_err(|e| ExtractValueError::CouldNotConvert(v.clone(), e)), + .map_err(|e| ExtractValueError::CouldNotConvert(v, e)), + Self::LoadedFunction(lf) => lf + .try_into() + .map_err(ExtractValueError::CouldNotLoadFunction), Self::PartialSum(ps) => ps .try_into_sum(typ)? .try_into() @@ -366,18 +418,6 @@ impl PartialValue { Self::Bottom => Err(ExtractValueError::ValueIsBottom), } } - - /// A value contains bottom means that it cannot occur during execution: - /// it may be an artefact during bootstrapping of the analysis, or else - /// the value depends upon a `panic` or a loop that - /// [never terminates](super::TailLoopTermination::NeverBreaks). - pub fn contains_bottom(&self) -> bool { - match self { - PartialValue::Bottom => true, - PartialValue::Top | PartialValue::Value(_) => false, - PartialValue::PartialSum(ps) => ps.contains_bottom(), - } - } } impl TryFrom> for Value { @@ -388,7 +428,15 @@ impl TryFrom> for Value { } } -impl Lattice for PartialValue { +impl TryFrom> for Value { + type Error = LoadedFunction; + + fn try_from(value: LoadedFunction) -> Result { + Err(value) + } +} + +impl Lattice for PartialValue { fn join_mut(&mut self, other: Self) -> bool { self.assert_invariants(); let mut old_self = Self::Top; @@ -400,13 +448,17 @@ impl Lattice for PartialValue { Some((h3, b)) => (Self::Value(h3), b), None => (Self::Top, true), }, + (Self::LoadedFunction(lf1), Self::LoadedFunction(lf2)) + if lf1.func_node == lf2.func_node => + { + // TODO we should also require TypeArgs to be equal by at the moment these are ignored + (Self::LoadedFunction(lf1), false) + } (Self::PartialSum(mut ps1), Self::PartialSum(ps2)) => match ps1.try_join_mut(ps2) { Ok(ch) => (Self::PartialSum(ps1), ch), Err(_) => (Self::Top, true), }, - (Self::Value(_), Self::PartialSum(_)) | (Self::PartialSum(_), Self::Value(_)) => { - (Self::Top, true) - } + _ => (Self::Top, true), }; *self = res; ch @@ -423,20 +475,24 @@ impl Lattice for PartialValue { Some((h3, ch)) => (Self::Value(h3), ch), None => (Self::Bottom, true), }, + (Self::LoadedFunction(lf1), Self::LoadedFunction(lf2)) + if lf1.func_node == lf2.func_node => + { + // TODO we should also require TypeArgs to be equal by at the moment these are ignored + (Self::LoadedFunction(lf1), false) + } (Self::PartialSum(mut ps1), Self::PartialSum(ps2)) => match ps1.try_meet_mut(ps2) { Ok(ch) => (Self::PartialSum(ps1), ch), Err(_) => (Self::Bottom, true), }, - (Self::Value(_), Self::PartialSum(_)) | (Self::PartialSum(_), Self::Value(_)) => { - (Self::Bottom, true) - } + _ => (Self::Bottom, true), }; *self = res; ch } } -impl BoundedLattice for PartialValue { +impl BoundedLattice for PartialValue { fn top() -> Self { Self::Top } @@ -446,7 +502,7 @@ impl BoundedLattice for PartialValue { } } -impl PartialOrd for PartialValue { +impl PartialOrd for PartialValue { fn partial_cmp(&self, other: &Self) -> Option { use std::cmp::Ordering; match (self, other) { @@ -457,6 +513,9 @@ impl PartialOrd for PartialValue { (Self::Top, _) => Some(Ordering::Greater), (_, Self::Top) => Some(Ordering::Less), (Self::Value(v1), Self::Value(v2)) => (v1 == v2).then_some(Ordering::Equal), + (Self::LoadedFunction(lf1), Self::LoadedFunction(lf2)) => { + (lf1 == lf2).then_some(Ordering::Equal) + } (Self::PartialSum(ps1), Self::PartialSum(ps2)) => ps1.partial_cmp(ps2), _ => None, } @@ -468,6 +527,7 @@ mod test { use std::sync::Arc; use ascent::{lattice::BoundedLattice, Lattice}; + use hugr_core::Node; use itertools::{zip_eq, Itertools as _}; use prop::sample::subsequence; use proptest::prelude::*; @@ -506,7 +566,7 @@ mod test { } impl TestSumType { - fn check_value(&self, pv: &PartialValue) -> bool { + fn check_value(&self, pv: &PartialValue) -> bool { match (self, pv) { (_, PartialValue::Bottom) | (_, PartialValue::Top) => true, (Self::Leaf(None), _) => pv == &PartialValue::new_unit(), @@ -567,7 +627,7 @@ mod test { fn single_sum_strat( tag: usize, elems: Vec>, - ) -> impl Strategy> { + ) -> impl Strategy> { elems .iter() .map(Arc::as_ref) @@ -578,11 +638,11 @@ mod test { fn partial_sum_strat( variants: &[Vec>], - ) -> impl Strategy> { + ) -> impl Strategy> { // We have to clone the `variants` here but only as far as the Vec>> let tagged_variants = variants.iter().cloned().enumerate().collect::>(); // The type annotation here (and the .boxed() enabling it) are just for documentation - let sum_variants_strat: BoxedStrategy>> = + let sum_variants_strat: BoxedStrategy>> = subsequence(tagged_variants, 1..=variants.len()) .prop_flat_map(|selected_variants| { selected_variants @@ -591,7 +651,7 @@ mod test { .collect::>() }) .boxed(); - sum_variants_strat.prop_map(|psums: Vec>| { + sum_variants_strat.prop_map(|psums: Vec>| { let mut psums = psums.into_iter(); let first = psums.next().unwrap(); psums.fold(first, |mut a, b| { @@ -603,7 +663,7 @@ mod test { fn any_partial_value_of_type( ust: &TestSumType, - ) -> impl Strategy> { + ) -> impl Strategy> { match ust { TestSumType::Leaf(None) => Just(PartialValue::new_unit()).boxed(), TestSumType::Leaf(Some(i)) => (0..*i) @@ -616,15 +676,16 @@ mod test { fn any_partial_value_with( params: ::Parameters, - ) -> impl Strategy> { + ) -> impl Strategy> { any_with::(params).prop_flat_map(|t| any_partial_value_of_type(&t)) } - fn any_partial_value() -> impl Strategy> { + fn any_partial_value() -> impl Strategy> { any_partial_value_with(Default::default()) } - fn any_partial_values() -> impl Strategy; N]> { + fn any_partial_values( + ) -> impl Strategy; N]> { any::().prop_flat_map(|ust| { TryInto::<[_; N]>::try_into( (0..N) @@ -635,7 +696,8 @@ mod test { }) } - fn any_typed_partial_value() -> impl Strategy)> { + fn any_typed_partial_value( + ) -> impl Strategy)> { any::() .prop_flat_map(|t| any_partial_value_of_type(&t).prop_map(move |v| (t.clone(), v))) } diff --git a/hugr-passes/src/dataflow/results.rs b/hugr-passes/src/dataflow/results.rs index c40f1d87f2..c3d1db6960 100644 --- a/hugr-passes/src/dataflow/results.rs +++ b/hugr-passes/src/dataflow/results.rs @@ -2,16 +2,16 @@ use std::collections::HashMap; use hugr_core::{HugrView, IncomingPort, PortIndex, Wire}; -use super::{partial_value::ExtractValueError, AbstractValue, PartialValue, Sum}; +use super::{partial_value::ExtractValueError, AbstractValue, LoadedFunction, PartialValue, Sum}; /// Results of a dataflow analysis, packaged with the Hugr for easy inspection. /// Methods allow inspection, specifically [read_out_wire](Self::read_out_wire). pub struct AnalysisResults { pub(super) hugr: H, - pub(super) in_wire_value: Vec<(H::Node, IncomingPort, PartialValue)>, + pub(super) in_wire_value: Vec<(H::Node, IncomingPort, PartialValue)>, pub(super) case_reachable: Vec<(H::Node, H::Node)>, pub(super) bb_reachable: Vec<(H::Node, H::Node)>, - pub(super) out_wire_values: HashMap, PartialValue>, + pub(super) out_wire_values: HashMap, PartialValue>, } impl AnalysisResults { @@ -21,7 +21,7 @@ impl AnalysisResults { } /// Gets the lattice value computed for the given wire - pub fn read_out_wire(&self, w: Wire) -> Option> { + pub fn read_out_wire(&self, w: Wire) -> Option> { self.out_wire_values.get(&w).cloned() } @@ -84,12 +84,14 @@ impl AnalysisResults { /// `None` if the analysis did not produce a result for that wire, or if /// the Hugr did not have a [Type](hugr_core::types::Type) for the specified wire /// `Some(e)` if [conversion to a concrete value](PartialValue::try_into_concrete) failed with error `e` - pub fn try_read_wire_concrete( + pub fn try_read_wire_concrete( &self, w: Wire, - ) -> Result>> + ) -> Result>> where - V2: TryFrom + TryFrom, Error = SE>, + V2: TryFrom + + TryFrom, Error = SE> + + TryFrom, Error = LE>, { let v = self.read_out_wire(w).ok_or(None)?; let (_, typ) = self @@ -116,7 +118,7 @@ pub enum TailLoopTermination { } impl TailLoopTermination { - fn from_control_value(v: &PartialValue) -> Self { + fn from_control_value(v: &PartialValue) -> Self { let (may_continue, may_break) = (v.supports_tag(0), v.supports_tag(1)); if may_break { if may_continue { diff --git a/hugr-passes/src/dataflow/test.rs b/hugr-passes/src/dataflow/test.rs index 3af0097f77..94443cc908 100644 --- a/hugr-passes/src/dataflow/test.rs +++ b/hugr-passes/src/dataflow/test.rs @@ -19,7 +19,10 @@ use hugr_core::{ use hugr_core::{Hugr, Node, Wire}; use rstest::{fixture, rstest}; -use super::{AbstractValue, ConstLoader, DFContext, Machine, PartialValue, TailLoopTermination}; +use super::{ + AbstractValue, ConstLoader, DFContext, Machine, PartialValue, + TailLoopTermination, +}; // ------- Minimal implementation of DFContext and AbstractValue ------- #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -296,7 +299,7 @@ fn test_conditional() { let cond_r1: Value = results.try_read_wire_concrete(cond_o1).unwrap(); assert_eq!(cond_r1, Value::false_val()); assert!(results - .try_read_wire_concrete::(cond_o2) + .try_read_wire_concrete::(cond_o2) .is_err()); assert_eq!(results.case_reachable(case1.node()), Some(false)); // arg_pv is variant 1 or 2 only diff --git a/hugr-passes/src/dataflow/value_row.rs b/hugr-passes/src/dataflow/value_row.rs index 50cf103183..43c842d917 100644 --- a/hugr-passes/src/dataflow/value_row.rs +++ b/hugr-passes/src/dataflow/value_row.rs @@ -5,25 +5,25 @@ use std::{ ops::{Index, IndexMut}, }; -use ascent::{lattice::BoundedLattice, Lattice}; +use ascent::Lattice; use itertools::zip_eq; use super::{AbstractValue, PartialValue}; #[derive(PartialEq, Clone, Debug, Eq, Hash)] -pub(super) struct ValueRow(Vec>); +pub(super) struct ValueRow(Vec>); -impl ValueRow { +impl ValueRow { pub fn new(len: usize) -> Self { - Self(vec![PartialValue::bottom(); len]) + Self(vec![PartialValue::Bottom; len]) } - pub fn set(mut self, idx: usize, v: PartialValue) -> Self { + pub fn set(mut self, idx: usize, v: PartialValue) -> Self { *self.0.get_mut(idx).unwrap() = v; self } - pub fn singleton(v: PartialValue) -> Self { + pub fn singleton(v: PartialValue) -> Self { Self(vec![v]) } @@ -34,25 +34,25 @@ impl ValueRow { &self, variant: usize, len: usize, - ) -> Option>> { + ) -> Option>> { let vals = self[0].variant_values(variant, len)?; Some(vals.into_iter().chain(self.0[1..].to_owned())) } } -impl FromIterator> for ValueRow { - fn from_iter>>(iter: T) -> Self { +impl FromIterator> for ValueRow { + fn from_iter>>(iter: T) -> Self { Self(iter.into_iter().collect()) } } -impl PartialOrd for ValueRow { +impl PartialOrd for ValueRow { fn partial_cmp(&self, other: &Self) -> Option { self.0.partial_cmp(&other.0) } } -impl Lattice for ValueRow { +impl Lattice for ValueRow { fn join_mut(&mut self, other: Self) -> bool { assert_eq!(self.0.len(), other.0.len()); let mut changed = false; @@ -72,30 +72,30 @@ impl Lattice for ValueRow { } } -impl IntoIterator for ValueRow { - type Item = PartialValue; +impl IntoIterator for ValueRow { + type Item = PartialValue; - type IntoIter = > as IntoIterator>::IntoIter; + type IntoIter = > as IntoIterator>::IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } -impl Index for ValueRow +impl Index for ValueRow where - Vec>: Index, + Vec>: Index, { - type Output = > as Index>::Output; + type Output = > as Index>::Output; fn index(&self, index: Idx) -> &Self::Output { self.0.index(index) } } -impl IndexMut for ValueRow +impl IndexMut for ValueRow where - Vec>: IndexMut, + Vec>: IndexMut, { fn index_mut(&mut self, index: Idx) -> &mut Self::Output { self.0.index_mut(index) From 5809594880e4c1af4f679a17b503953c9b92dcdc Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 4 Apr 2025 15:40:07 +0100 Subject: [PATCH 03/19] Docs, clippy::type_complexity --- hugr-passes/src/dataflow/datalog.rs | 11 ++++++----- hugr-passes/src/dataflow/partial_value.rs | 2 ++ hugr-passes/src/dataflow/results.rs | 10 +++++++--- hugr-passes/src/dataflow/test.rs | 5 +---- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/hugr-passes/src/dataflow/datalog.rs b/hugr-passes/src/dataflow/datalog.rs index 0e195240ae..934cecdff8 100644 --- a/hugr-passes/src/dataflow/datalog.rs +++ b/hugr-passes/src/dataflow/datalog.rs @@ -17,6 +17,8 @@ use super::{ type PV = PartialValue; +type NodeInputs = Vec<(IncomingPort, PV)>; + /// Basic structure for performing an analysis. Usage: /// 1. Make a new instance via [Self::new()] /// 2. (Optionally) zero or more calls to [Self::prepopulate_wire] and/or @@ -25,10 +27,7 @@ type PV = PartialValue; /// [Self::prepopulate_inputs] can be used on each externally-callable /// [FuncDefn](OpType::FuncDefn) to set all inputs to [PartialValue::Top]. /// 3. Call [Self::run] to produce [AnalysisResults] -pub struct Machine( - H, - HashMap)>>, -); +pub struct Machine(H, HashMap>); impl Machine { /// Create a new Machine to analyse the given Hugr(View) @@ -135,10 +134,12 @@ impl Machine { } } +pub(super) type InWire = (N, IncomingPort, PartialValue); + pub(super) fn run_datalog( mut ctx: impl DFContext, hugr: H, - in_wire_value_proto: Vec<(H::Node, IncomingPort, PV)>, + in_wire_value_proto: Vec>, ) -> AnalysisResults { // ascent-(macro-)generated code generates a bunch of warnings, // keep code in here to a minimum. diff --git a/hugr-passes/src/dataflow/partial_value.rs b/hugr-passes/src/dataflow/partial_value.rs index d2781d534c..46e981244e 100644 --- a/hugr-passes/src/dataflow/partial_value.rs +++ b/hugr-passes/src/dataflow/partial_value.rs @@ -56,7 +56,9 @@ pub struct Sum { /// to a function at a specific node, instantiated with the provided type-args. #[derive(Clone, Debug, Hash, PartialEq, Eq)] pub struct LoadedFunction { + /// The [FuncDefn](hugr_core::ops::FuncDefn) or `FuncDecl`` that was loaded pub func_node: N, + /// The type arguments provided when loading pub args: Vec, } diff --git a/hugr-passes/src/dataflow/results.rs b/hugr-passes/src/dataflow/results.rs index c3d1db6960..a6b864f5ed 100644 --- a/hugr-passes/src/dataflow/results.rs +++ b/hugr-passes/src/dataflow/results.rs @@ -1,14 +1,17 @@ use std::collections::HashMap; -use hugr_core::{HugrView, IncomingPort, PortIndex, Wire}; +use hugr_core::{HugrView, PortIndex, Wire}; -use super::{partial_value::ExtractValueError, AbstractValue, LoadedFunction, PartialValue, Sum}; +use super::{ + datalog::InWire, partial_value::ExtractValueError, AbstractValue, LoadedFunction, PartialValue, + Sum, +}; /// Results of a dataflow analysis, packaged with the Hugr for easy inspection. /// Methods allow inspection, specifically [read_out_wire](Self::read_out_wire). pub struct AnalysisResults { pub(super) hugr: H, - pub(super) in_wire_value: Vec<(H::Node, IncomingPort, PartialValue)>, + pub(super) in_wire_value: Vec>, pub(super) case_reachable: Vec<(H::Node, H::Node)>, pub(super) bb_reachable: Vec<(H::Node, H::Node)>, pub(super) out_wire_values: HashMap, PartialValue>, @@ -84,6 +87,7 @@ impl AnalysisResults { /// `None` if the analysis did not produce a result for that wire, or if /// the Hugr did not have a [Type](hugr_core::types::Type) for the specified wire /// `Some(e)` if [conversion to a concrete value](PartialValue::try_into_concrete) failed with error `e` + #[allow(clippy::type_complexity)] pub fn try_read_wire_concrete( &self, w: Wire, diff --git a/hugr-passes/src/dataflow/test.rs b/hugr-passes/src/dataflow/test.rs index 94443cc908..ca0cfdb443 100644 --- a/hugr-passes/src/dataflow/test.rs +++ b/hugr-passes/src/dataflow/test.rs @@ -19,10 +19,7 @@ use hugr_core::{ use hugr_core::{Hugr, Node, Wire}; use rstest::{fixture, rstest}; -use super::{ - AbstractValue, ConstLoader, DFContext, Machine, PartialValue, - TailLoopTermination, -}; +use super::{AbstractValue, ConstLoader, DFContext, Machine, PartialValue, TailLoopTermination}; // ------- Minimal implementation of DFContext and AbstractValue ------- #[derive(Debug, Clone, PartialEq, Eq, Hash)] From cccefe4c49df0de66bb50319248db9dcc4341c73 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 4 Apr 2025 15:25:49 +0100 Subject: [PATCH 04/19] datalog handles LoadFunction, drop value_from_function (TODO ConstFoldCtx ignores self.0) --- hugr-passes/src/const_fold.rs | 31 +++++------------------------ hugr-passes/src/dataflow.rs | 5 +++-- hugr-passes/src/dataflow/datalog.rs | 12 ++++++----- 3 files changed, 15 insertions(+), 33 deletions(-) diff --git a/hugr-passes/src/const_fold.rs b/hugr-passes/src/const_fold.rs index 7368bf7103..ef5fc355ca 100644 --- a/hugr-passes/src/const_fold.rs +++ b/hugr-passes/src/const_fold.rs @@ -7,15 +7,11 @@ use std::{collections::HashMap, sync::Arc}; use thiserror::Error; use hugr_core::{ - hugr::{ - hugrmut::HugrMut, - views::{DescendantsGraph, ExtractHugr, HierarchyView}, - }, + hugr::hugrmut::HugrMut, ops::{ - constant::OpaqueValue, handle::FuncID, Const, DataflowOpTrait, ExtensionOp, LoadConstant, - OpType, Value, + constant::OpaqueValue, Const, DataflowOpTrait, ExtensionOp, LoadConstant, OpType, Value, }, - types::{EdgeKind, TypeArg}, + types::EdgeKind, HugrView, IncomingPort, Node, NodeIndex, OutgoingPort, PortIndex, Wire, }; use value_handle::ValueHandle; @@ -205,7 +201,8 @@ pub fn constant_fold_pass(h: &mut H) { c.run(h).unwrap() } -struct ConstFoldContext<'a, H>(&'a H); +// Probably intend to remove this in a future PR, but not certain, so leaving in for now +struct ConstFoldContext<'a, H>(#[allow(unused)] &'a H); impl> ConstLoader> for ConstFoldContext<'_, H> { type Node = H::Node; @@ -225,24 +222,6 @@ impl> ConstLoader> for ConstFoldCo ) -> Option> { Some(ValueHandle::new_const_hugr(loc, Box::new(h.clone()))) } - - fn value_from_function( - &self, - node: H::Node, - type_args: &[TypeArg], - ) -> Option> { - if !type_args.is_empty() { - // TODO: substitution across Hugr (https://github.com/CQCL/hugr/issues/709) - return None; - }; - // Returning the function body as a value, here, would be sufficient for inlining IndirectCall - // but not for transforming to a direct Call. - let func = DescendantsGraph::>::try_new(self.0, node).ok()?; - Some(ValueHandle::new_const_hugr( - ConstLocation::Node(node), - Box::new(func.extract_hugr()), - )) - } } impl> DFContext> for ConstFoldContext<'_, H> { diff --git a/hugr-passes/src/dataflow.rs b/hugr-passes/src/dataflow.rs index bec178e295..7029ec0b2a 100644 --- a/hugr-passes/src/dataflow.rs +++ b/hugr-passes/src/dataflow.rs @@ -55,8 +55,8 @@ impl From for ConstLocation<'_, N> { } /// Trait for loading [PartialValue]s from constant [Value]s in a Hugr. -/// Implementors will likely want to override some/all of [Self::value_from_opaque], -/// [Self::value_from_const_hugr], and [Self::value_from_function]: the defaults +/// Implementors will likely want to override either/both of [Self::value_from_opaque] +/// and [Self::value_from_const_hugr]: the defaults /// are "correct" but maximally conservative (minimally informative). pub trait ConstLoader { /// The type of nodes in the Hugr. @@ -81,6 +81,7 @@ pub trait ConstLoader { /// [FuncDefn]: hugr_core::ops::FuncDefn /// [FuncDecl]: hugr_core::ops::FuncDecl /// [LoadFunction]: hugr_core::ops::LoadFunction + #[deprecated(note = "Automatically handled by Datalog, implementation will be ignored")] fn value_from_function(&self, _node: Self::Node, _type_args: &[TypeArg]) -> Option { None } diff --git a/hugr-passes/src/dataflow/datalog.rs b/hugr-passes/src/dataflow/datalog.rs index 934cecdff8..cce8cf4020 100644 --- a/hugr-passes/src/dataflow/datalog.rs +++ b/hugr-passes/src/dataflow/datalog.rs @@ -12,7 +12,7 @@ use hugr_core::{HugrView, IncomingPort, OutgoingPort, PortIndex as _, Wire}; use super::value_row::ValueRow; use super::{ partial_from_const, row_contains_bottom, AbstractValue, AnalysisResults, DFContext, - PartialValue, + LoadedFunction, PartialValue, }; type PV = PartialValue; @@ -381,10 +381,12 @@ fn propagate_leaf_op( .unwrap() .0; // Node could be a FuncDefn or a FuncDecl, so do not pass the node itself - Some(ValueRow::singleton( - ctx.value_from_function(func_node, &load_op.type_args) - .map_or(PV::Top, PV::Value), - )) + Some(ValueRow::singleton(PartialValue::LoadedFunction( + LoadedFunction { + func_node, + args: load_op.type_args.clone(), + }, + ))) } OpType::ExtensionOp(e) => { Some(ValueRow::from_iter(if row_contains_bottom(ins) { From ca442c4b7facdc96e0de676a3e48a2a3a481d37f Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 4 Apr 2025 15:17:36 +0100 Subject: [PATCH 05/19] And handle CallIndirect --- hugr-passes/src/dataflow/datalog.rs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/hugr-passes/src/dataflow/datalog.rs b/hugr-passes/src/dataflow/datalog.rs index cce8cf4020..d1f47be23b 100644 --- a/hugr-passes/src/dataflow/datalog.rs +++ b/hugr-passes/src/dataflow/datalog.rs @@ -323,6 +323,24 @@ pub(super) fn run_datalog( func_call(call, func), output_child(func, outp), in_wire_value(outp, p, v); + + // CallIndirect -------------------- + relation indirect_call(H::Node, H::Node); // is an `IndirectCall` to `FuncDefn` + indirect_call(call, func_node) <-- + node(call), + if let OpType::CallIndirect(_) = hugr.get_optype(*call), + in_wire_value(call, IncomingPort::from(0), v), + if let PartialValue::LoadedFunction(LoadedFunction {func_node, ..}) = v; + + out_wire_value(inp, OutgoingPort::from(p.index()-1), v) <-- + indirect_call(call, func), + input_child(func, inp), + in_wire_value(call, p, v); + + out_wire_value(call, OutgoingPort::from(p.index()), v) <-- + indirect_call(call, func), + output_child(func, outp), + in_wire_value(outp, p, v); }; let out_wire_values = all_results .out_wire_value @@ -363,8 +381,7 @@ fn propagate_leaf_op( ins.iter().cloned(), )])), OpType::Input(_) | OpType::Output(_) | OpType::ExitBlock(_) => None, // handled by parent - OpType::Call(_) => None, // handled via Input/Output of FuncDefn - OpType::Const(_) => None, // handled by LoadConstant: + OpType::Call(_) | OpType::CallIndirect(_) => None, // handled via Input/Output of FuncDefn OpType::LoadConstant(load_op) => { assert!(ins.is_empty()); // static edge, so need to find constant let const_node = hugr @@ -404,6 +421,7 @@ fn propagate_leaf_op( outs })) } - o => todo!("Unhandled: {:?}", o), // At least CallIndirect, and OpType is "non-exhaustive" + // We only call propagate_leaf_op for dataflow op non-containers, + o => todo!("Unhandled: {:?}", o), // and OpType is non-exhaustive } } From a90dc97a07e1203d0d70230c7fff892790779cb5 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 4 Apr 2025 20:41:05 +0100 Subject: [PATCH 06/19] First bit of test - default to Top if called-func unknown --- hugr-passes/src/dataflow/datalog.rs | 11 ++++- hugr-passes/src/dataflow/test.rs | 65 ++++++++++++++++++++++++++++- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/hugr-passes/src/dataflow/datalog.rs b/hugr-passes/src/dataflow/datalog.rs index d1f47be23b..008d9a91db 100644 --- a/hugr-passes/src/dataflow/datalog.rs +++ b/hugr-passes/src/dataflow/datalog.rs @@ -6,7 +6,7 @@ use ascent::lattice::BoundedLattice; use itertools::Itertools; use hugr_core::extension::prelude::{MakeTuple, UnpackTuple}; -use hugr_core::ops::{OpTrait, OpType, TailLoop}; +use hugr_core::ops::{DataflowOpTrait, OpTrait, OpType, TailLoop}; use hugr_core::{HugrView, IncomingPort, OutgoingPort, PortIndex as _, Wire}; use super::value_row::ValueRow; @@ -341,6 +341,15 @@ pub(super) fn run_datalog( indirect_call(call, func), output_child(func, outp), in_wire_value(outp, p, v); + + // Default out-value is Bottom, but if we can't determine the called function, + // assign everything to Top + out_wire_value(call, p, PV::Top) <-- + node(call), + if let OpType::CallIndirect(ci) = hugr.get_optype(*call), + in_wire_value(call, IncomingPort::from(0), v), + if !matches!(v, PartialValue::LoadedFunction(_)), + for p in ci.signature().output_ports(); }; let out_wire_values = all_results .out_wire_value diff --git a/hugr-passes/src/dataflow/test.rs b/hugr-passes/src/dataflow/test.rs index ca0cfdb443..f51ad2600e 100644 --- a/hugr-passes/src/dataflow/test.rs +++ b/hugr-passes/src/dataflow/test.rs @@ -1,9 +1,9 @@ use ascent::{lattice::BoundedLattice, Lattice}; -use hugr_core::builder::{CFGBuilder, Container, DataflowHugr, ModuleBuilder}; +use hugr_core::builder::{inout_sig, CFGBuilder, Container, DataflowHugr, ModuleBuilder}; use hugr_core::hugr::views::{DescendantsGraph, HierarchyView}; use hugr_core::ops::handle::DfgID; -use hugr_core::ops::TailLoop; +use hugr_core::ops::{CallIndirect, TailLoop}; use hugr_core::types::TypeRow; use hugr_core::{ builder::{endo_sig, DFGBuilder, Dataflow, DataflowSubContainer, HugrBuilder, SubContainer}, @@ -547,3 +547,64 @@ fn test_module() { ); } } + +#[test] +fn call_indirect() { + let b2b = || Signature::new_endo(bool_t()); + let mut dfb = DFGBuilder::new(inout_sig(vec![bool_t(); 3], vec![bool_t(); 2])).unwrap(); + + let [id1, id2] = ["id1", "[id2]"].map(|name| { + let fb = dfb.define_function(name, b2b()).unwrap(); + let [inp] = fb.input_wires_arr(); + fb.finish_with_outputs([inp]).unwrap() + }); + + let [inp_direct, which, inp_indirect] = dfb.input_wires_arr(); + let [res1] = dfb + .call(id1.handle(), &[], [inp_direct]) + .unwrap() + .outputs_arr(); + + // We'll unconditionally load both functions, to demonstrate that it's + // the CallIndirect that matters, not just which functions are loaded. + let lf1 = dfb.load_func(id1.handle(), &[]).unwrap(); + let lf2 = dfb.load_func(id2.handle(), &[]).unwrap(); + let bool_func = || Type::new_function(b2b()); + let mut cond = dfb + .conditional_builder( + (vec![type_row![]; 2], which), + [(bool_func(), lf1), (bool_func(), lf2)], + bool_func().into(), + ) + .unwrap(); + let case_false = cond.case_builder(0).unwrap(); + let [f0, _f1] = case_false.input_wires_arr(); + case_false.finish_with_outputs([f0]).unwrap(); + let case_true = cond.case_builder(1).unwrap(); + let [_f0, f1] = case_true.input_wires_arr(); + case_true.finish_with_outputs([f1]).unwrap(); + let [tgt] = cond.finish_sub_container().unwrap().outputs_arr(); + let [res2] = dfb + .add_dataflow_op(CallIndirect { signature: b2b() }, [tgt, inp_indirect]) + .unwrap() + .outputs_arr(); + let h = dfb.finish_hugr_with_outputs([res1, res2]).unwrap(); + + // 1. Test with `which` unknown -> second output unknown + let (w1, w2) = (Wire::new(h.root(), 0), Wire::new(h.root(), 1)); + for inp1 in [pv_false(), pv_true()] { + for inp2 in [pv_false(), pv_true()] { + let results = Machine::new(&h).run( + TestContext, + [ + (0.into(), inp1.clone()), + (1.into(), PartialValue::Top), + (2.into(), inp2), + ], + ); + assert_eq!(results.read_out_wire(w1), Some(inp1.clone())); + assert_eq!(results.read_out_wire(w2), Some(PartialValue::Top)); + ); + } + } +} From 6e2ad7f1f50a7160869b5a4911fe7283a2834323 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 4 Apr 2025 20:49:21 +0100 Subject: [PATCH 07/19] Test2, fix port numbering and narrow unknown-called-func case --- hugr-passes/src/dataflow/datalog.rs | 6 ++++-- hugr-passes/src/dataflow/test.rs | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/hugr-passes/src/dataflow/datalog.rs b/hugr-passes/src/dataflow/datalog.rs index 008d9a91db..127d0c79d1 100644 --- a/hugr-passes/src/dataflow/datalog.rs +++ b/hugr-passes/src/dataflow/datalog.rs @@ -335,7 +335,8 @@ pub(super) fn run_datalog( out_wire_value(inp, OutgoingPort::from(p.index()-1), v) <-- indirect_call(call, func), input_child(func, inp), - in_wire_value(call, p, v); + in_wire_value(call, p, v) + if p.index() > 0; out_wire_value(call, OutgoingPort::from(p.index()), v) <-- indirect_call(call, func), @@ -348,7 +349,8 @@ pub(super) fn run_datalog( node(call), if let OpType::CallIndirect(ci) = hugr.get_optype(*call), in_wire_value(call, IncomingPort::from(0), v), - if !matches!(v, PartialValue::LoadedFunction(_)), + // Second alternative below addresses function::Value's: + if matches!(v, PartialValue::Top | PartialValue::Value(_)), for p in ci.signature().output_ports(); }; let out_wire_values = all_results diff --git a/hugr-passes/src/dataflow/test.rs b/hugr-passes/src/dataflow/test.rs index f51ad2600e..66ffb891cf 100644 --- a/hugr-passes/src/dataflow/test.rs +++ b/hugr-passes/src/dataflow/test.rs @@ -604,7 +604,22 @@ fn call_indirect() { ); assert_eq!(results.read_out_wire(w1), Some(inp1.clone())); assert_eq!(results.read_out_wire(w2), Some(PartialValue::Top)); + } + } + + // 2. Test with `which` selecting second function -> both passthrough + for inp1 in [pv_false(), pv_true()] { + for inp2 in [pv_false(), pv_true()] { + let results = Machine::new(&h).run( + TestContext, + [ + (0.into(), inp1.clone()), + (1.into(), pv_true()), + (2.into(), inp2.clone()), + ], ); + assert_eq!(results.read_out_wire(w1), Some(inp1.clone())); + assert_eq!(results.read_out_wire(w2), Some(inp2.clone())); } } } From 2cd85479a77896c026bad76c1e10d95f4070622b Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 4 Apr 2025 20:58:39 +0100 Subject: [PATCH 08/19] Test3, plus refactor test --- hugr-passes/src/dataflow/test.rs | 36 ++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/hugr-passes/src/dataflow/test.rs b/hugr-passes/src/dataflow/test.rs index 66ffb891cf..3e8a454d86 100644 --- a/hugr-passes/src/dataflow/test.rs +++ b/hugr-passes/src/dataflow/test.rs @@ -590,18 +590,17 @@ fn call_indirect() { .outputs_arr(); let h = dfb.finish_hugr_with_outputs([res1, res2]).unwrap(); + let run = |v0, v1, v2| { + Machine::new(&h).run( + TestContext, + [(0.into(), v0), (1.into(), v1), (2.into(), v2)], + ) + }; // 1. Test with `which` unknown -> second output unknown let (w1, w2) = (Wire::new(h.root(), 0), Wire::new(h.root(), 1)); for inp1 in [pv_false(), pv_true()] { for inp2 in [pv_false(), pv_true()] { - let results = Machine::new(&h).run( - TestContext, - [ - (0.into(), inp1.clone()), - (1.into(), PartialValue::Top), - (2.into(), inp2), - ], - ); + let results = run(inp1.clone(), PartialValue::Top, inp2); assert_eq!(results.read_out_wire(w1), Some(inp1.clone())); assert_eq!(results.read_out_wire(w2), Some(PartialValue::Top)); } @@ -610,16 +609,21 @@ fn call_indirect() { // 2. Test with `which` selecting second function -> both passthrough for inp1 in [pv_false(), pv_true()] { for inp2 in [pv_false(), pv_true()] { - let results = Machine::new(&h).run( - TestContext, - [ - (0.into(), inp1.clone()), - (1.into(), pv_true()), - (2.into(), inp2.clone()), - ], - ); + let results = run(inp1.clone(), pv_true(), inp2.clone()); assert_eq!(results.read_out_wire(w1), Some(inp1.clone())); assert_eq!(results.read_out_wire(w2), Some(inp2.clone())); } } + + //3. Test with `which` selecting first function -> alias + for (inp1, inp2) in [(pv_false(), pv_true()), (pv_true(), pv_false())] { + // A. same input bool to both calls + let results1 = run(inp1.clone(), pv_false(), inp1.clone()); + assert_eq!(results1.read_out_wire(w1), Some(inp1.clone())); + assert_eq!(results1.read_out_wire(w2), Some(inp1.clone())); + // B. different inputs to both calls. Both alias - even the Call. + let results2 = run(inp1, pv_false(), inp2); + assert_eq!(results2.read_out_wire(w1), Some(pv_true_or_false())); + assert_eq!(results2.read_out_wire(w2), Some(pv_true_or_false())); + } } From ba33062d3f5562017b52e9dab8e1a2c8c429753a Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 4 Apr 2025 21:23:25 +0100 Subject: [PATCH 09/19] refactor into cases of rstest --- hugr-passes/src/dataflow/test.rs | 53 +++++++++++++++----------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/hugr-passes/src/dataflow/test.rs b/hugr-passes/src/dataflow/test.rs index 3e8a454d86..3737d65cd3 100644 --- a/hugr-passes/src/dataflow/test.rs +++ b/hugr-passes/src/dataflow/test.rs @@ -548,8 +548,12 @@ fn test_module() { } } -#[test] -fn call_indirect() { +#[rstest] +#[case(pv_false(), pv_false())] +#[case(pv_false(), pv_true())] +#[case(pv_true(), pv_false())] +#[case(pv_true(), pv_true())] +fn call_indirect(#[case] inp1: PartialValue, #[case] inp2: PartialValue) { let b2b = || Signature::new_endo(bool_t()); let mut dfb = DFGBuilder::new(inout_sig(vec![bool_t(); 3], vec![bool_t(); 2])).unwrap(); @@ -590,40 +594,31 @@ fn call_indirect() { .outputs_arr(); let h = dfb.finish_hugr_with_outputs([res1, res2]).unwrap(); - let run = |v0, v1, v2| { + let run = |which| { Machine::new(&h).run( TestContext, - [(0.into(), v0), (1.into(), v1), (2.into(), v2)], + [ + (0.into(), inp1.clone()), + (1.into(), which), + (2.into(), inp2.clone()), + ], ) }; - // 1. Test with `which` unknown -> second output unknown let (w1, w2) = (Wire::new(h.root(), 0), Wire::new(h.root(), 1)); - for inp1 in [pv_false(), pv_true()] { - for inp2 in [pv_false(), pv_true()] { - let results = run(inp1.clone(), PartialValue::Top, inp2); - assert_eq!(results.read_out_wire(w1), Some(inp1.clone())); - assert_eq!(results.read_out_wire(w2), Some(PartialValue::Top)); - } - } + + // 1. Test with `which` unknown -> second output unknown + let results = run(PartialValue::Top); + assert_eq!(results.read_out_wire(w1), Some(inp1.clone())); + assert_eq!(results.read_out_wire(w2), Some(PartialValue::Top)); // 2. Test with `which` selecting second function -> both passthrough - for inp1 in [pv_false(), pv_true()] { - for inp2 in [pv_false(), pv_true()] { - let results = run(inp1.clone(), pv_true(), inp2.clone()); - assert_eq!(results.read_out_wire(w1), Some(inp1.clone())); - assert_eq!(results.read_out_wire(w2), Some(inp2.clone())); - } - } + let results = run(pv_true()); + assert_eq!(results.read_out_wire(w1), Some(inp1.clone())); + assert_eq!(results.read_out_wire(w2), Some(inp2.clone())); //3. Test with `which` selecting first function -> alias - for (inp1, inp2) in [(pv_false(), pv_true()), (pv_true(), pv_false())] { - // A. same input bool to both calls - let results1 = run(inp1.clone(), pv_false(), inp1.clone()); - assert_eq!(results1.read_out_wire(w1), Some(inp1.clone())); - assert_eq!(results1.read_out_wire(w2), Some(inp1.clone())); - // B. different inputs to both calls. Both alias - even the Call. - let results2 = run(inp1, pv_false(), inp2); - assert_eq!(results2.read_out_wire(w1), Some(pv_true_or_false())); - assert_eq!(results2.read_out_wire(w2), Some(pv_true_or_false())); - } + let results = run(pv_false()); + let out = Some(inp1.join(inp2)); + assert_eq!(results.read_out_wire(w1), out); + assert_eq!(results.read_out_wire(w2), out); } From ee128ce8b1fe7603fcdf80ec323d831cfccad207 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Fri, 4 Apr 2025 22:32:11 +0100 Subject: [PATCH 10/19] PartialSum default N=Node --- hugr-passes/src/dataflow/partial_value.rs | 25 ++++++++++------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/hugr-passes/src/dataflow/partial_value.rs b/hugr-passes/src/dataflow/partial_value.rs index 46e981244e..9472c62f95 100644 --- a/hugr-passes/src/dataflow/partial_value.rs +++ b/hugr-passes/src/dataflow/partial_value.rs @@ -65,7 +65,7 @@ pub struct LoadedFunction { /// A representation of a value of [SumType], that may have one or more possible tags, /// with a [PartialValue] representation of each element-value of each possible tag. #[derive(PartialEq, Clone, Eq)] -pub struct PartialSum(pub HashMap>>); +pub struct PartialSum(pub HashMap>>); impl PartialSum { /// New instance for a single known tag. @@ -529,7 +529,6 @@ mod test { use std::sync::Arc; use ascent::{lattice::BoundedLattice, Lattice}; - use hugr_core::Node; use itertools::{zip_eq, Itertools as _}; use prop::sample::subsequence; use proptest::prelude::*; @@ -568,7 +567,7 @@ mod test { } impl TestSumType { - fn check_value(&self, pv: &PartialValue) -> bool { + fn check_value(&self, pv: &PartialValue) -> bool { match (self, pv) { (_, PartialValue::Bottom) | (_, PartialValue::Top) => true, (Self::Leaf(None), _) => pv == &PartialValue::new_unit(), @@ -629,7 +628,7 @@ mod test { fn single_sum_strat( tag: usize, elems: Vec>, - ) -> impl Strategy> { + ) -> impl Strategy> { elems .iter() .map(Arc::as_ref) @@ -640,11 +639,11 @@ mod test { fn partial_sum_strat( variants: &[Vec>], - ) -> impl Strategy> { + ) -> impl Strategy> { // We have to clone the `variants` here but only as far as the Vec>> let tagged_variants = variants.iter().cloned().enumerate().collect::>(); // The type annotation here (and the .boxed() enabling it) are just for documentation - let sum_variants_strat: BoxedStrategy>> = + let sum_variants_strat: BoxedStrategy>> = subsequence(tagged_variants, 1..=variants.len()) .prop_flat_map(|selected_variants| { selected_variants @@ -653,7 +652,7 @@ mod test { .collect::>() }) .boxed(); - sum_variants_strat.prop_map(|psums: Vec>| { + sum_variants_strat.prop_map(|psums: Vec>| { let mut psums = psums.into_iter(); let first = psums.next().unwrap(); psums.fold(first, |mut a, b| { @@ -665,7 +664,7 @@ mod test { fn any_partial_value_of_type( ust: &TestSumType, - ) -> impl Strategy> { + ) -> impl Strategy> { match ust { TestSumType::Leaf(None) => Just(PartialValue::new_unit()).boxed(), TestSumType::Leaf(Some(i)) => (0..*i) @@ -678,16 +677,15 @@ mod test { fn any_partial_value_with( params: ::Parameters, - ) -> impl Strategy> { + ) -> impl Strategy> { any_with::(params).prop_flat_map(|t| any_partial_value_of_type(&t)) } - fn any_partial_value() -> impl Strategy> { + fn any_partial_value() -> impl Strategy> { any_partial_value_with(Default::default()) } - fn any_partial_values( - ) -> impl Strategy; N]> { + fn any_partial_values() -> impl Strategy; N]> { any::().prop_flat_map(|ust| { TryInto::<[_; N]>::try_into( (0..N) @@ -698,8 +696,7 @@ mod test { }) } - fn any_typed_partial_value( - ) -> impl Strategy)> { + fn any_typed_partial_value() -> impl Strategy)> { any::() .prop_flat_map(|t| any_partial_value_of_type(&t).prop_map(move |v| (t.clone(), v))) } From 2757a3312e71b71211b05eb0722eea4c97e0635e Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Sat, 5 Apr 2025 08:59:12 +0100 Subject: [PATCH 11/19] require TryFrom,Error=LoadedFunction>, format w/ Debug --- hugr-passes/src/const_fold.rs | 2 +- hugr-passes/src/dataflow/partial_value.rs | 18 +++++++++--------- hugr-passes/src/dataflow/results.rs | 6 +++--- hugr-passes/src/dataflow/test.rs | 2 +- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/hugr-passes/src/const_fold.rs b/hugr-passes/src/const_fold.rs index ef5fc355ca..0599d26af2 100644 --- a/hugr-passes/src/const_fold.rs +++ b/hugr-passes/src/const_fold.rs @@ -127,7 +127,7 @@ impl ConstantFoldPass { n, ip, results - .try_read_wire_concrete::(Wire::new(src, outp)) + .try_read_wire_concrete::(Wire::new(src, outp)) .ok()?, )) }) diff --git a/hugr-passes/src/dataflow/partial_value.rs b/hugr-passes/src/dataflow/partial_value.rs index 9472c62f95..41691fb061 100644 --- a/hugr-passes/src/dataflow/partial_value.rs +++ b/hugr-passes/src/dataflow/partial_value.rs @@ -175,14 +175,14 @@ impl PartialSum { /// If this PartialSum had multiple possible tags; or if `typ` was not a [TypeEnum::Sum] /// supporting the single possible tag with the correct number of elements and no row variables; /// or if converting a child element failed via [PartialValue::try_into_concrete]. - pub fn try_into_sum( + pub fn try_into_sum( self, typ: &Type, - ) -> Result, ExtractValueError> + ) -> Result, ExtractValueError> where V: TryInto, Sum: TryInto, - LoadedFunction: TryInto, + LoadedFunction: TryInto>, { if self.0.len() != 1 { return Err(ExtractValueError::MultipleVariants(self)); @@ -215,7 +215,7 @@ impl PartialSum { /// via [PartialValue::try_into_concrete] or [PartialSum::try_into_sum] #[derive(Clone, Debug, PartialEq, Eq, Error)] #[allow(missing_docs)] -pub enum ExtractValueError { +pub enum ExtractValueError { #[error("PartialSum value had multiple possible tags: {0}")] MultipleVariants(PartialSum), #[error("Value contained `Bottom`")] @@ -226,8 +226,8 @@ pub enum ExtractValueError { CouldNotConvert(V, #[source] VE), #[error("Could not build Sum from concrete element values")] CouldNotBuildSum(#[source] SE), - #[error("Could not turn LoadedFunction into concrete")] - CouldNotLoadFunction(#[source] LE), + #[error("Could not convert into concrete function pointer {0}")] + CouldNotLoadFunction(LoadedFunction), #[error("Expected a SumType with tag {tag} having {num_elements} elements, found {typ}")] BadSumType { typ: Type, @@ -395,14 +395,14 @@ impl PartialValue { /// If this PartialValue was `Top` or `Bottom`, or was a [PartialSum](PartialValue::PartialSum) /// that could not be converted into a [Sum] by [PartialSum::try_into_sum] (e.g. if `typ` is /// incorrect), or if that [Sum] could not be converted into a `V2`. - pub fn try_into_concrete( + pub fn try_into_concrete( self, typ: &Type, - ) -> Result> + ) -> Result> where V: TryInto, Sum: TryInto, - LoadedFunction: TryInto, + LoadedFunction: TryInto>, { match self { Self::Value(v) => v diff --git a/hugr-passes/src/dataflow/results.rs b/hugr-passes/src/dataflow/results.rs index a6b864f5ed..d5d6bb2fab 100644 --- a/hugr-passes/src/dataflow/results.rs +++ b/hugr-passes/src/dataflow/results.rs @@ -88,14 +88,14 @@ impl AnalysisResults { /// the Hugr did not have a [Type](hugr_core::types::Type) for the specified wire /// `Some(e)` if [conversion to a concrete value](PartialValue::try_into_concrete) failed with error `e` #[allow(clippy::type_complexity)] - pub fn try_read_wire_concrete( + pub fn try_read_wire_concrete( &self, w: Wire, - ) -> Result>> + ) -> Result>> where V2: TryFrom + TryFrom, Error = SE> - + TryFrom, Error = LE>, + + TryFrom, Error = LoadedFunction>, { let v = self.read_out_wire(w).ok_or(None)?; let (_, typ) = self diff --git a/hugr-passes/src/dataflow/test.rs b/hugr-passes/src/dataflow/test.rs index 3737d65cd3..f57f3d7aab 100644 --- a/hugr-passes/src/dataflow/test.rs +++ b/hugr-passes/src/dataflow/test.rs @@ -296,7 +296,7 @@ fn test_conditional() { let cond_r1: Value = results.try_read_wire_concrete(cond_o1).unwrap(); assert_eq!(cond_r1, Value::false_val()); assert!(results - .try_read_wire_concrete::(cond_o2) + .try_read_wire_concrete::(cond_o2) .is_err()); assert_eq!(results.case_reachable(case1.node()), Some(false)); // arg_pv is variant 1 or 2 only From 3c79739926e4eef3368878be3e8e5baf1d927f33 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 7 Apr 2025 10:18:28 +0100 Subject: [PATCH 12/19] Use PartialValue::new_load --- hugr-passes/src/dataflow/datalog.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/hugr-passes/src/dataflow/datalog.rs b/hugr-passes/src/dataflow/datalog.rs index 127d0c79d1..ee0a1a1448 100644 --- a/hugr-passes/src/dataflow/datalog.rs +++ b/hugr-passes/src/dataflow/datalog.rs @@ -409,11 +409,9 @@ fn propagate_leaf_op( .unwrap() .0; // Node could be a FuncDefn or a FuncDecl, so do not pass the node itself - Some(ValueRow::singleton(PartialValue::LoadedFunction( - LoadedFunction { - func_node, - args: load_op.type_args.clone(), - }, + Some(ValueRow::singleton(PartialValue::new_load( + func_node, + load_op.type_args.clone(), ))) } OpType::ExtensionOp(e) => { From 1191150e862035ee7e3dc539a980ea36c2e559f0 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Mon, 7 Apr 2025 16:13:11 +0100 Subject: [PATCH 13/19] Make call_indirect a lattice, add load_func --- hugr-passes/src/dataflow/datalog.rs | 59 ++++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/hugr-passes/src/dataflow/datalog.rs b/hugr-passes/src/dataflow/datalog.rs index ee0a1a1448..986378dc51 100644 --- a/hugr-passes/src/dataflow/datalog.rs +++ b/hugr-passes/src/dataflow/datalog.rs @@ -3,6 +3,8 @@ use std::collections::HashMap; use ascent::lattice::BoundedLattice; +use ascent::Lattice; +use hugr_core::core::HugrNode; use itertools::Itertools; use hugr_core::extension::prelude::{MakeTuple, UnpackTuple}; @@ -325,21 +327,23 @@ pub(super) fn run_datalog( in_wire_value(outp, p, v); // CallIndirect -------------------- - relation indirect_call(H::Node, H::Node); // is an `IndirectCall` to `FuncDefn` - indirect_call(call, func_node) <-- + lattice indirect_call(H::Node, LatticeWrapper); // is an `IndirectCall` to `FuncDefn` + indirect_call(call, tgt) <-- node(call), if let OpType::CallIndirect(_) = hugr.get_optype(*call), in_wire_value(call, IncomingPort::from(0), v), - if let PartialValue::LoadedFunction(LoadedFunction {func_node, ..}) = v; + let tgt = load_func(v); out_wire_value(inp, OutgoingPort::from(p.index()-1), v) <-- - indirect_call(call, func), + indirect_call(call, lv), + if let LatticeWrapper::Value(func) = lv, input_child(func, inp), in_wire_value(call, p, v) if p.index() > 0; out_wire_value(call, OutgoingPort::from(p.index()), v) <-- - indirect_call(call, func), + indirect_call(call, lv), + if let LatticeWrapper::Value(func) = lv, output_child(func, outp), in_wire_value(outp, p, v); @@ -367,6 +371,51 @@ pub(super) fn run_datalog( } } +#[derive(PartialEq, Eq, Hash, Clone, PartialOrd)] +enum LatticeWrapper { + Bottom, + Value(T), + Top, +} + +impl Lattice for LatticeWrapper { + fn meet_mut(&mut self, other: Self) -> bool { + if *self == other || *self == LatticeWrapper::Bottom || other == LatticeWrapper::Top { + return false; + }; + if *self == LatticeWrapper::Top || other == LatticeWrapper::Bottom { + *self = other; + return true; + }; + // Both are `Value`s and not equal + *self = LatticeWrapper::Bottom; + true + } + + fn join_mut(&mut self, other: Self) -> bool { + if *self == other || *self == LatticeWrapper::Top || other == LatticeWrapper::Bottom { + return false; + }; + if *self == LatticeWrapper::Bottom || other == LatticeWrapper::Top { + *self = other; + return true; + }; + // Both are `Value`s and are not equal + *self = LatticeWrapper::Top; + true + } +} + +fn load_func(v: &PV) -> LatticeWrapper { + match v { + PartialValue::Bottom | PartialValue::PartialSum(_) => LatticeWrapper::Bottom, + PartialValue::LoadedFunction(LoadedFunction { func_node, .. }) => { + LatticeWrapper::Value(*func_node) + } + PartialValue::Value(_) | PartialValue::Top => LatticeWrapper::Top, + } +} + fn propagate_leaf_op( ctx: &mut impl DFContext, hugr: &H, From 4ba5603bf0c2c0a6ebda744ee27799892e2c66a5 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 8 Apr 2025 13:07:17 +0100 Subject: [PATCH 14/19] Drop Hugr from inside ConstFoldContext --- hugr-passes/src/const_fold.rs | 27 +++++++++++++-------------- hugr-passes/src/const_fold/test.rs | 6 ++---- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/hugr-passes/src/const_fold.rs b/hugr-passes/src/const_fold.rs index 0599d26af2..31a1ddfdb8 100644 --- a/hugr-passes/src/const_fold.rs +++ b/hugr-passes/src/const_fold.rs @@ -98,7 +98,7 @@ impl ConstantFoldPass { n, in_vals.iter().map(|(p, v)| { let const_with_dummy_loc = partial_from_const( - &ConstFoldContext(hugr), + &ConstFoldContext, ConstLocation::Field(p.index(), &fresh_node.into()), v, ); @@ -108,7 +108,7 @@ impl ConstantFoldPass { .map_err(|opty| ConstFoldError::InvalidEntryPoint(n, opty))?; } - let results = m.run(ConstFoldContext(hugr), []); + let results = m.run(ConstFoldContext, []); let mb_root_inp = hugr.get_io(hugr.root()).map(|[i, _]| i); let wires_to_break = hugr @@ -201,36 +201,35 @@ pub fn constant_fold_pass(h: &mut H) { c.run(h).unwrap() } -// Probably intend to remove this in a future PR, but not certain, so leaving in for now -struct ConstFoldContext<'a, H>(#[allow(unused)] &'a H); +struct ConstFoldContext; -impl> ConstLoader> for ConstFoldContext<'_, H> { - type Node = H::Node; +impl ConstLoader> for ConstFoldContext { + type Node = Node; fn value_from_opaque( &self, - loc: ConstLocation, + loc: ConstLocation, val: &OpaqueValue, - ) -> Option> { + ) -> Option> { Some(ValueHandle::new_opaque(loc, val.clone())) } fn value_from_const_hugr( &self, - loc: ConstLocation, + loc: ConstLocation, h: &hugr_core::Hugr, - ) -> Option> { + ) -> Option> { Some(ValueHandle::new_const_hugr(loc, Box::new(h.clone()))) } } -impl> DFContext> for ConstFoldContext<'_, H> { +impl DFContext> for ConstFoldContext { fn interpret_leaf_op( &mut self, - node: H::Node, + node: Node, op: &ExtensionOp, - ins: &[PartialValue>], - outs: &mut [PartialValue>], + ins: &[PartialValue>], + outs: &mut [PartialValue>], ) { let sig = op.signature(); let known_ins = sig diff --git a/hugr-passes/src/const_fold/test.rs b/hugr-passes/src/const_fold/test.rs index b84d65d7d7..58e69c568c 100644 --- a/hugr-passes/src/const_fold/test.rs +++ b/hugr-passes/src/const_fold/test.rs @@ -42,8 +42,7 @@ fn value_handling(#[case] k: impl CustomConst + Clone, #[case] eq: bool) { let n = Node::from(portgraph::NodeIndex::new(7)); let st = SumType::new([vec![k.get_type()], vec![]]); let subject_val = Value::sum(0, [k.clone().into()], st).unwrap(); - let temp = Hugr::default(); - let ctx: ConstFoldContext = ConstFoldContext(&temp); + let ctx = ConstFoldContext; let v1 = partial_from_const(&ctx, n, &subject_val); let v1_subfield = { @@ -114,8 +113,7 @@ fn test_add(#[case] a: f64, #[case] b: f64, #[case] c: f64) { v.get_custom_value::().unwrap().value() } let [n, n_a, n_b] = [0, 1, 2].map(portgraph::NodeIndex::new).map(Node::from); - let temp = Hugr::default(); - let mut ctx = ConstFoldContext(&temp); + let mut ctx = ConstFoldContext; let v_a = partial_from_const(&ctx, n_a, &f2c(a)); let v_b = partial_from_const(&ctx, n_b, &f2c(b)); assert_eq!(unwrap_float(v_a.clone()), a); From a8e45536ab85106d457c08e2b39fa5f81d912ad6 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 8 Apr 2025 13:29:22 +0100 Subject: [PATCH 15/19] trait AsConcrete --- hugr-passes/src/const_fold.rs | 2 +- hugr-passes/src/const_fold/value_handle.rs | 23 ++++-- hugr-passes/src/dataflow.rs | 2 +- hugr-passes/src/dataflow/partial_value.rs | 82 ++++++++++------------ hugr-passes/src/dataflow/results.rs | 12 +--- hugr-passes/src/dataflow/test.rs | 29 ++++++-- 6 files changed, 84 insertions(+), 66 deletions(-) diff --git a/hugr-passes/src/const_fold.rs b/hugr-passes/src/const_fold.rs index 31a1ddfdb8..e73e3cd0eb 100644 --- a/hugr-passes/src/const_fold.rs +++ b/hugr-passes/src/const_fold.rs @@ -127,7 +127,7 @@ impl ConstantFoldPass { n, ip, results - .try_read_wire_concrete::(Wire::new(src, outp)) + .try_read_wire_concrete::(Wire::new(src, outp)) .ok()?, )) }) diff --git a/hugr-passes/src/const_fold/value_handle.rs b/hugr-passes/src/const_fold/value_handle.rs index bda7bffd23..e5c99a8e73 100644 --- a/hugr-passes/src/const_fold/value_handle.rs +++ b/hugr-passes/src/const_fold/value_handle.rs @@ -1,16 +1,18 @@ //! Total equality (and hence [AbstractValue] support for [Value]s //! (by adding a source-Node and part unhashable constants) use std::collections::hash_map::DefaultHasher; // Moves into std::hash in Rust 1.76. +use std::convert::Infallible; use std::hash::{Hash, Hasher}; use std::sync::Arc; use hugr_core::core::HugrNode; use hugr_core::ops::constant::OpaqueValue; use hugr_core::ops::Value; +use hugr_core::types::ConstTypeError; use hugr_core::{Hugr, Node}; use itertools::Either; -use crate::dataflow::{AbstractValue, ConstLocation}; +use crate::dataflow::{AbstractValue, AsConcrete, ConstLocation, LoadedFunction, Sum}; /// A custom constant that has been successfully hashed via [TryHash](hugr_core::ops::constant::TryHash) #[derive(Clone, Debug)] @@ -153,9 +155,12 @@ impl Hash for ValueHandle { // Unfortunately we need From for Value to be able to pass // Value's into interpret_leaf_op. So that probably doesn't make sense... -impl From> for Value { - fn from(value: ValueHandle) -> Self { - match value { +impl AsConcrete, N> for Value { + type ValErr = Infallible; + type SumErr = ConstTypeError; + + fn from_value(value: ValueHandle) -> Result { + Ok(match value { ValueHandle::Hashable(HashedConst { val, .. }) | ValueHandle::Unhashable { leaf: Either::Left(val), @@ -169,7 +174,15 @@ impl From> for Value { } => Value::function(Arc::try_unwrap(hugr).unwrap_or_else(|a| a.as_ref().clone())) .map_err(|e| e.to_string()) .unwrap(), - } + }) + } + + fn from_sum(value: Sum) -> Result { + Self::sum(value.tag, value.values, value.st) + } + + fn from_func(func: LoadedFunction) -> Result> { + Err(func) } } diff --git a/hugr-passes/src/dataflow.rs b/hugr-passes/src/dataflow.rs index 7029ec0b2a..1f7c1ae5af 100644 --- a/hugr-passes/src/dataflow.rs +++ b/hugr-passes/src/dataflow.rs @@ -9,7 +9,7 @@ mod results; pub use results::{AnalysisResults, TailLoopTermination}; mod partial_value; -pub use partial_value::{AbstractValue, LoadedFunction, PartialSum, PartialValue, Sum}; +pub use partial_value::{AbstractValue, AsConcrete, LoadedFunction, PartialSum, PartialValue, Sum}; use hugr_core::ops::constant::OpaqueValue; use hugr_core::ops::{ExtensionOp, Value}; diff --git a/hugr-passes/src/dataflow/partial_value.rs b/hugr-passes/src/dataflow/partial_value.rs index 41691fb061..a79ad8b28e 100644 --- a/hugr-passes/src/dataflow/partial_value.rs +++ b/hugr-passes/src/dataflow/partial_value.rs @@ -1,7 +1,6 @@ use ascent::lattice::BoundedLattice; use ascent::Lattice; -use hugr_core::ops::Value; -use hugr_core::types::{ConstTypeError, SumType, Type, TypeArg, TypeEnum, TypeRow}; +use hugr_core::types::{SumType, Type, TypeArg, TypeEnum, TypeRow}; use hugr_core::Node; use itertools::{zip_eq, Itertools}; use std::cmp::Ordering; @@ -166,6 +165,30 @@ impl PartialSum { } } +/// Trait implemented by value types into which [PartialValue]s can be converted, +/// so long as the PV has no [Top](PartialValue::Top), [Bottom](PartialValue::Bottom) +/// or [PartialSum]s with more than one possible tag. See [PartialSum::try_into_sum] +/// and [PartialValue::try_into_concrete]. +/// +/// `V` is the type of [AbstractValue] from which `Self` can (fallibly) be constructed, +/// `N` is the type of [HugrNode](hugr_core::core::HugrNode) for function pointers +pub trait AsConcrete: Sized { + /// Kind of error raised when creating `Self` from a value `V`, see [Self::from_value] + type ValErr: std::error::Error; + /// Kind of error that may be raised when creating `Self` from a [Sum] of `Self`s, + /// see [Self::from_sum] + type SumErr: std::error::Error; + + /// Convert an abstract value into concrete + fn from_value(val: V) -> Result; + + /// Convert a sum (of concrete values, already recursively converted) into concrete + fn from_sum(sum: Sum) -> Result; + + /// Convert a function pointer into a concrete value + fn from_func(func: LoadedFunction) -> Result>; +} + impl PartialSum { /// Turns this instance into a [Sum] of some "concrete" value type `C`, /// *if* this PartialSum has exactly one possible tag. @@ -175,15 +198,11 @@ impl PartialSum { /// If this PartialSum had multiple possible tags; or if `typ` was not a [TypeEnum::Sum] /// supporting the single possible tag with the correct number of elements and no row variables; /// or if converting a child element failed via [PartialValue::try_into_concrete]. - pub fn try_into_sum( + #[allow(clippy::type_complexity)] // Since C is a parameter, can't declare type aliases + pub fn try_into_sum>( self, typ: &Type, - ) -> Result, ExtractValueError> - where - V: TryInto, - Sum: TryInto, - LoadedFunction: TryInto>, - { + ) -> Result, ExtractValueError> { if self.0.len() != 1 { return Err(ExtractValueError::MultipleVariants(self)); } @@ -395,49 +414,26 @@ impl PartialValue { /// If this PartialValue was `Top` or `Bottom`, or was a [PartialSum](PartialValue::PartialSum) /// that could not be converted into a [Sum] by [PartialSum::try_into_sum] (e.g. if `typ` is /// incorrect), or if that [Sum] could not be converted into a `V2`. - pub fn try_into_concrete( + pub fn try_into_concrete>( self, typ: &Type, - ) -> Result> - where - V: TryInto, - Sum: TryInto, - LoadedFunction: TryInto>, - { + ) -> Result> { match self { - Self::Value(v) => v - .clone() - .try_into() - .map_err(|e| ExtractValueError::CouldNotConvert(v, e)), - Self::LoadedFunction(lf) => lf - .try_into() - .map_err(ExtractValueError::CouldNotLoadFunction), - Self::PartialSum(ps) => ps - .try_into_sum(typ)? - .try_into() - .map_err(ExtractValueError::CouldNotBuildSum), + Self::Value(v) => { + C::from_value(v.clone()).map_err(|e| ExtractValueError::CouldNotConvert(v, e)) + } + Self::LoadedFunction(lf) => { + C::from_func(lf).map_err(ExtractValueError::CouldNotLoadFunction) + } + Self::PartialSum(ps) => { + C::from_sum(ps.try_into_sum(typ)?).map_err(ExtractValueError::CouldNotBuildSum) + } Self::Top => Err(ExtractValueError::ValueIsTop), Self::Bottom => Err(ExtractValueError::ValueIsBottom), } } } -impl TryFrom> for Value { - type Error = ConstTypeError; - - fn try_from(value: Sum) -> Result { - Self::sum(value.tag, value.values, value.st) - } -} - -impl TryFrom> for Value { - type Error = LoadedFunction; - - fn try_from(value: LoadedFunction) -> Result { - Err(value) - } -} - impl Lattice for PartialValue { fn join_mut(&mut self, other: Self) -> bool { self.assert_invariants(); diff --git a/hugr-passes/src/dataflow/results.rs b/hugr-passes/src/dataflow/results.rs index d5d6bb2fab..c4a94a9e75 100644 --- a/hugr-passes/src/dataflow/results.rs +++ b/hugr-passes/src/dataflow/results.rs @@ -3,8 +3,7 @@ use std::collections::HashMap; use hugr_core::{HugrView, PortIndex, Wire}; use super::{ - datalog::InWire, partial_value::ExtractValueError, AbstractValue, LoadedFunction, PartialValue, - Sum, + datalog::InWire, partial_value::ExtractValueError, AbstractValue, AsConcrete, PartialValue, }; /// Results of a dataflow analysis, packaged with the Hugr for easy inspection. @@ -88,15 +87,10 @@ impl AnalysisResults { /// the Hugr did not have a [Type](hugr_core::types::Type) for the specified wire /// `Some(e)` if [conversion to a concrete value](PartialValue::try_into_concrete) failed with error `e` #[allow(clippy::type_complexity)] - pub fn try_read_wire_concrete( + pub fn try_read_wire_concrete>( &self, w: Wire, - ) -> Result>> - where - V2: TryFrom - + TryFrom, Error = SE> - + TryFrom, Error = LoadedFunction>, - { + ) -> Result>> { let v = self.read_out_wire(w).ok_or(None)?; let (_, typ) = self .hugr diff --git a/hugr-passes/src/dataflow/test.rs b/hugr-passes/src/dataflow/test.rs index f57f3d7aab..1c4b4e4396 100644 --- a/hugr-passes/src/dataflow/test.rs +++ b/hugr-passes/src/dataflow/test.rs @@ -1,10 +1,12 @@ +use std::convert::Infallible; + use ascent::{lattice::BoundedLattice, Lattice}; use hugr_core::builder::{inout_sig, CFGBuilder, Container, DataflowHugr, ModuleBuilder}; use hugr_core::hugr::views::{DescendantsGraph, HierarchyView}; use hugr_core::ops::handle::DfgID; use hugr_core::ops::{CallIndirect, TailLoop}; -use hugr_core::types::TypeRow; +use hugr_core::types::{ConstTypeError, TypeRow}; use hugr_core::{ builder::{endo_sig, DFGBuilder, Dataflow, DataflowSubContainer, HugrBuilder, SubContainer}, extension::{ @@ -19,7 +21,10 @@ use hugr_core::{ use hugr_core::{Hugr, Node, Wire}; use rstest::{fixture, rstest}; -use super::{AbstractValue, ConstLoader, DFContext, Machine, PartialValue, TailLoopTermination}; +use super::{ + AbstractValue, AsConcrete, ConstLoader, DFContext, LoadedFunction, Machine, PartialValue, Sum, + TailLoopTermination, +}; // ------- Minimal implementation of DFContext and AbstractValue ------- #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -35,10 +40,22 @@ impl ConstLoader for TestContext { impl DFContext for TestContext {} // This allows testing creation of tuple/sum Values (only) -impl From for Value { - fn from(v: Void) -> Self { +impl AsConcrete for Value { + type ValErr = Infallible; + + type SumErr = ConstTypeError; + + fn from_value(v: Void) -> Result { match v {} } + + fn from_sum(value: Sum) -> Result { + Self::sum(value.tag, value.values, value.st) + } + + fn from_func(func: LoadedFunction) -> Result> { + Err(func) + } } fn pv_false() -> PartialValue { @@ -295,9 +312,7 @@ fn test_conditional() { let cond_r1: Value = results.try_read_wire_concrete(cond_o1).unwrap(); assert_eq!(cond_r1, Value::false_val()); - assert!(results - .try_read_wire_concrete::(cond_o2) - .is_err()); + assert!(results.try_read_wire_concrete::(cond_o2).is_err()); assert_eq!(results.case_reachable(case1.node()), Some(false)); // arg_pv is variant 1 or 2 only assert_eq!(results.case_reachable(case2.node()), Some(true)); From f032848947029e6051517aa26a1c4a7fd5313ad5 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 15 Apr 2025 16:39:45 +0100 Subject: [PATCH 16/19] LatticeWrapper requires only PartialEq+PartialOrd --- hugr-passes/src/dataflow/datalog.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/hugr-passes/src/dataflow/datalog.rs b/hugr-passes/src/dataflow/datalog.rs index 986378dc51..a5911c665f 100644 --- a/hugr-passes/src/dataflow/datalog.rs +++ b/hugr-passes/src/dataflow/datalog.rs @@ -4,7 +4,6 @@ use std::collections::HashMap; use ascent::lattice::BoundedLattice; use ascent::Lattice; -use hugr_core::core::HugrNode; use itertools::Itertools; use hugr_core::extension::prelude::{MakeTuple, UnpackTuple}; @@ -378,7 +377,7 @@ enum LatticeWrapper { Top, } -impl Lattice for LatticeWrapper { +impl Lattice for LatticeWrapper { fn meet_mut(&mut self, other: Self) -> bool { if *self == other || *self == LatticeWrapper::Bottom || other == LatticeWrapper::Top { return false; From 3066b654e10ddb8bdc0ce2305d2795d1733dbdbf Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 15 Apr 2025 17:22:35 +0100 Subject: [PATCH 17/19] PartialValue: Arbitrary+TestSumType etc. generates LoadedFunction --- hugr-passes/src/dataflow/partial_value.rs | 36 ++++++++++++++++------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/hugr-passes/src/dataflow/partial_value.rs b/hugr-passes/src/dataflow/partial_value.rs index a79ad8b28e..7ccb761b22 100644 --- a/hugr-passes/src/dataflow/partial_value.rs +++ b/hugr-passes/src/dataflow/partial_value.rs @@ -449,7 +449,7 @@ impl Lattice for PartialValue (Self::LoadedFunction(lf1), Self::LoadedFunction(lf2)) if lf1.func_node == lf2.func_node => { - // TODO we should also require TypeArgs to be equal by at the moment these are ignored + // TODO we should also join the TypeArgs but at the moment these are ignored (Self::LoadedFunction(lf1), false) } (Self::PartialSum(mut ps1), Self::PartialSum(ps2)) => match ps1.try_join_mut(ps2) { @@ -476,7 +476,7 @@ impl Lattice for PartialValue (Self::LoadedFunction(lf1), Self::LoadedFunction(lf2)) if lf1.func_node == lf2.func_node => { - // TODO we should also require TypeArgs to be equal by at the moment these are ignored + // TODO we should also meet the TypeArgs but at the moment these are ignored (Self::LoadedFunction(lf1), false) } (Self::PartialSum(mut ps1), Self::PartialSum(ps2)) => match ps1.try_meet_mut(ps2) { @@ -525,19 +525,20 @@ mod test { use std::sync::Arc; use ascent::{lattice::BoundedLattice, Lattice}; + use hugr_core::NodeIndex; use itertools::{zip_eq, Itertools as _}; use prop::sample::subsequence; use proptest::prelude::*; use proptest_recurse::{StrategyExt, StrategySet}; - use super::{AbstractValue, PartialSum, PartialValue}; + use super::{AbstractValue, LoadedFunction, PartialSum, PartialValue}; #[derive(Debug, PartialEq, Eq, Clone)] enum TestSumType { Branch(Vec>>), - /// None => unit, Some => TestValue <= this *usize* - Leaf(Option), + LeafVal(usize), // contains a TestValue <= this usize + LeafPtr(usize), // contains a LoadedFunction with node <= this *usize* } #[derive(Clone, Debug, PartialEq, Eq, Hash)] @@ -566,8 +567,11 @@ mod test { fn check_value(&self, pv: &PartialValue) -> bool { match (self, pv) { (_, PartialValue::Bottom) | (_, PartialValue::Top) => true, - (Self::Leaf(None), _) => pv == &PartialValue::new_unit(), - (Self::Leaf(Some(max)), PartialValue::Value(TestValue(val))) => val <= max, + (Self::LeafVal(max), PartialValue::Value(TestValue(val))) => val <= max, + ( + Self::LeafPtr(max), + PartialValue::LoadedFunction(LoadedFunction { func_node, args }), + ) => args.len() == 0 && func_node.index() <= *max, (Self::Branch(sop), PartialValue::PartialSum(ps)) => { for (k, v) in &ps.0 { if *k >= sop.len() { @@ -594,8 +598,11 @@ mod test { fn arbitrary_with(params: Self::Parameters) -> Self::Strategy { fn arb(params: SumTypeParams, set: &mut StrategySet) -> SBoxedStrategy { use proptest::collection::vec; - let int_strat = (0..usize::MAX).prop_map(|i| TestSumType::Leaf(Some(i))); - let leaf_strat = prop_oneof![Just(TestSumType::Leaf(None)), int_strat]; + let leaf_strat = prop_oneof![ + (0..usize::MAX).prop_map(TestSumType::LeafVal), + // This is the maximum value accepted by portgraph::NodeIndex::new + (0..(2usize ^ 31 - 2)).prop_map(TestSumType::LeafPtr) + ]; leaf_strat.prop_mutually_recursive( params.depth as u32, params.desired_size as u32, @@ -662,11 +669,18 @@ mod test { ust: &TestSumType, ) -> impl Strategy> { match ust { - TestSumType::Leaf(None) => Just(PartialValue::new_unit()).boxed(), - TestSumType::Leaf(Some(i)) => (0..*i) + TestSumType::LeafVal(i) => (0..=*i) .prop_map(TestValue) .prop_map(PartialValue::from) .boxed(), + TestSumType::LeafPtr(i) => (0..=*i) + .prop_map(|i| { + PartialValue::LoadedFunction(LoadedFunction { + func_node: portgraph::NodeIndex::new(i).into(), + args: vec![], + }) + }) + .boxed(), TestSumType::Branch(sop) => partial_sum_strat(sop).prop_map(PartialValue::from).boxed(), } } From 3eb0c7f97cadc6c881d4bdc249926ea97fe2704f Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 15 Apr 2025 17:28:32 +0100 Subject: [PATCH 18/19] clippy, fix predecence --- hugr-passes/src/dataflow/partial_value.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/hugr-passes/src/dataflow/partial_value.rs b/hugr-passes/src/dataflow/partial_value.rs index 7ccb761b22..240f4f2d63 100644 --- a/hugr-passes/src/dataflow/partial_value.rs +++ b/hugr-passes/src/dataflow/partial_value.rs @@ -571,7 +571,7 @@ mod test { ( Self::LeafPtr(max), PartialValue::LoadedFunction(LoadedFunction { func_node, args }), - ) => args.len() == 0 && func_node.index() <= *max, + ) => args.is_empty() && func_node.index() <= *max, (Self::Branch(sop), PartialValue::PartialSum(ps)) => { for (k, v) in &ps.0 { if *k >= sop.len() { @@ -601,7 +601,7 @@ mod test { let leaf_strat = prop_oneof![ (0..usize::MAX).prop_map(TestSumType::LeafVal), // This is the maximum value accepted by portgraph::NodeIndex::new - (0..(2usize ^ 31 - 2)).prop_map(TestSumType::LeafPtr) + (0..((2usize ^ 31) - 2)).prop_map(TestSumType::LeafPtr) ]; leaf_strat.prop_mutually_recursive( params.depth as u32, From 887a3861253c25c39d665c0c2dc593a8f99ba371 Mon Sep 17 00:00:00 2001 From: Alan Lawrence Date: Tue, 15 Apr 2025 18:13:14 +0100 Subject: [PATCH 19/19] test LatticeWrapper --- hugr-passes/src/dataflow/datalog.rs | 49 ++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/hugr-passes/src/dataflow/datalog.rs b/hugr-passes/src/dataflow/datalog.rs index a5911c665f..ad1a993456 100644 --- a/hugr-passes/src/dataflow/datalog.rs +++ b/hugr-passes/src/dataflow/datalog.rs @@ -370,7 +370,7 @@ pub(super) fn run_datalog( } } -#[derive(PartialEq, Eq, Hash, Clone, PartialOrd)] +#[derive(Debug, PartialEq, Eq, Hash, Clone, PartialOrd)] enum LatticeWrapper { Bottom, Value(T), @@ -482,3 +482,50 @@ fn propagate_leaf_op( o => todo!("Unhandled: {:?}", o), // and OpType is non-exhaustive } } + +#[cfg(test)] +mod test { + use ascent::Lattice; + + use super::LatticeWrapper; + + #[test] + fn latwrap_join() { + for lv in [ + LatticeWrapper::Value(3), + LatticeWrapper::Value(5), + LatticeWrapper::Top, + ] { + let mut subject = LatticeWrapper::Bottom; + assert!(subject.join_mut(lv.clone())); + assert_eq!(subject, lv); + assert!(!subject.join_mut(lv.clone())); + assert_eq!(subject, lv); + assert_eq!( + subject.join_mut(LatticeWrapper::Value(11)), + lv != LatticeWrapper::Top + ); + assert_eq!(subject, LatticeWrapper::Top); + } + } + + #[test] + fn latwrap_meet() { + for lv in [ + LatticeWrapper::Bottom, + LatticeWrapper::Value(3), + LatticeWrapper::Value(5), + ] { + let mut subject = LatticeWrapper::Top; + assert!(subject.meet_mut(lv.clone())); + assert_eq!(subject, lv); + assert!(!subject.meet_mut(lv.clone())); + assert_eq!(subject, lv); + assert_eq!( + subject.meet_mut(LatticeWrapper::Value(11)), + lv != LatticeWrapper::Bottom + ); + assert_eq!(subject, LatticeWrapper::Bottom); + } + } +}