Skip to main content

sim_shape/hooks/
hooked.rs

1//! `HookedShape`: a shape that wraps an inner shape and runs an ordered list of
2//! match hooks around each check to adjust acceptance, score, and diagnostics.
3
4use std::sync::Arc;
5
6use sim_kernel::{Cx, Diagnostic, Expr, Result, ShapeRef, Value, shape_is_subshape_of};
7
8use crate::{
9    MatchScore, Shape, ShapeDoc, ShapeMatch,
10    hooks::types::{
11        MatchHook, MatchHookContext, MatchHookDecision, MatchHookKind, MatchHookPhase,
12        MatchHookTargetKind,
13    },
14};
15
16/// Shape wrapper that runs neutral match hooks around an inner shape.
17///
18/// `HookedShape` keeps the kernel `Shape` trait unchanged. Mark hooks observe,
19/// accept hooks can repair rejections, discard hooks can veto acceptances, and
20/// annotate hooks can adjust score and diagnostics without changing acceptance.
21///
22/// ```rust
23/// # use std::sync::Arc;
24/// # use sim_kernel::{Cx, DefaultFactory, Expr, NoopEvalPolicy};
25/// # use sim_shape::{AnyShape, HookedShape, Shape, TraceMarkHook};
26/// # let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
27/// let shape = HookedShape::new(Arc::new(AnyShape), vec![Arc::new(TraceMarkHook)]);
28/// let matched = shape.check_expr(&mut cx, &Expr::Bool(true)).unwrap();
29///
30/// assert!(matched.accepted);
31/// assert!(matched
32///     .diagnostics
33///     .iter()
34///     .any(|diagnostic| diagnostic.message.starts_with("shape-hook:mark")));
35/// ```
36pub struct HookedShape {
37    inner: Arc<dyn Shape>,
38    hooks: Vec<Arc<dyn MatchHook>>,
39}
40
41impl HookedShape {
42    /// Wrap an inner shape with an ordered list of match hooks.
43    pub fn new(inner: Arc<dyn Shape>, hooks: Vec<Arc<dyn MatchHook>>) -> Self {
44        Self { inner, hooks }
45    }
46
47    /// Borrow the wrapped inner shape.
48    pub fn inner(&self) -> &Arc<dyn Shape> {
49        &self.inner
50    }
51
52    /// Borrow the hooks run around the inner shape, in registration order.
53    pub fn hooks(&self) -> &[Arc<dyn MatchHook>] {
54        &self.hooks
55    }
56
57    fn acceptance_transparent(&self) -> bool {
58        self.hooks
59            .iter()
60            .all(|hook| hook.kind().preserves_acceptance())
61    }
62
63    fn matches_transparent_hook_stack(&self, other: &Self) -> bool {
64        self.acceptance_transparent()
65            && other.acceptance_transparent()
66            && self.hooks.len() == other.hooks.len()
67            && self
68                .hooks
69                .iter()
70                .zip(other.hooks.iter())
71                .all(|(left, right)| {
72                    left.kind() == right.kind()
73                        && left.kind().preserves_acceptance()
74                        && left.symbol() == right.symbol()
75                        && left.object_encoding() == right.object_encoding()
76                })
77    }
78}
79
80impl Shape for HookedShape {
81    fn parents(&self, _cx: &mut Cx) -> Result<Vec<ShapeRef>> {
82        // Hook wrappers must not leak the inner shape's parent lattice into
83        // the generic walk; conservative subshape proofs go through the
84        // wrapper's own override instead.
85        Ok(Vec::new())
86    }
87
88    fn is_effectful(&self) -> bool {
89        self.inner.is_effectful()
90    }
91
92    fn is_total(&self) -> bool {
93        self.acceptance_transparent() && self.inner.is_total()
94    }
95
96    fn is_subshape_of(&self, cx: &mut Cx, parent: &dyn Shape) -> Result<Option<bool>> {
97        let Some(parent) = parent.as_any().downcast_ref::<Self>() else {
98            return Ok(None);
99        };
100        if !self.matches_transparent_hook_stack(parent) {
101            return Ok(None);
102        }
103        shape_is_subshape_of(cx, self.inner.as_ref(), parent.inner.as_ref()).map(Some)
104    }
105
106    fn check_value(&self, cx: &mut Cx, value: Value) -> Result<ShapeMatch> {
107        let label = self.inner.describe(cx)?.name;
108        let before = self.run_marks(
109            cx,
110            MatchHookTargetKind::Value,
111            MatchHookPhase::BeforeInner,
112            &label,
113            None,
114        )?;
115        let matched = self.inner.check_value(cx, value)?;
116        self.finish_match(cx, MatchHookTargetKind::Value, label, matched, before)
117    }
118
119    fn check_expr(&self, cx: &mut Cx, expr: &Expr) -> Result<ShapeMatch> {
120        let label = self.inner.describe(cx)?.name;
121        let before = self.run_marks(
122            cx,
123            MatchHookTargetKind::Expr,
124            MatchHookPhase::BeforeInner,
125            &label,
126            None,
127        )?;
128        let matched = self.inner.check_expr(cx, expr)?;
129        self.finish_match(cx, MatchHookTargetKind::Expr, label, matched, before)
130    }
131
132    fn describe(&self, cx: &mut Cx) -> Result<ShapeDoc> {
133        let mut doc = ShapeDoc::new("hooked shape").with_detail(self.inner.describe(cx)?.name);
134        for hook in &self.hooks {
135            doc = doc.with_detail(hook.symbol().to_string());
136        }
137        Ok(doc)
138    }
139}
140
141impl HookedShape {
142    fn finish_match(
143        &self,
144        cx: &mut Cx,
145        target_kind: MatchHookTargetKind,
146        label: String,
147        mut matched: ShapeMatch,
148        before_marks: Vec<Diagnostic>,
149    ) -> Result<ShapeMatch> {
150        matched.diagnostics.extend(before_marks);
151        let after_marks = self.run_marks(
152            cx,
153            target_kind,
154            MatchHookPhase::AfterInner,
155            &label,
156            Some(&matched),
157        )?;
158        matched.diagnostics.extend(after_marks);
159
160        if !matched.accepted {
161            matched = self.run_accept_hooks(cx, target_kind, &label, matched)?;
162        }
163        if matched.accepted {
164            matched = self.run_discard_hooks(cx, target_kind, &label, matched)?;
165        }
166        self.run_annotate_hooks(cx, target_kind, &label, matched)
167    }
168
169    fn run_marks(
170        &self,
171        cx: &mut Cx,
172        target_kind: MatchHookTargetKind,
173        phase: MatchHookPhase,
174        shape_label: &str,
175        current: Option<&ShapeMatch>,
176    ) -> Result<Vec<Diagnostic>> {
177        let mut diagnostics = Vec::new();
178        for (hook_index, hook) in self.hooks.iter().enumerate() {
179            if hook.kind() != MatchHookKind::Mark {
180                continue;
181            }
182            let ctx = MatchHookContext {
183                hook_index,
184                phase,
185                target_kind,
186                shape_label: shape_label.to_owned(),
187            };
188            if let MatchHookDecision::Mark { message } = hook.apply(cx, &ctx, current)? {
189                diagnostics.push(Diagnostic::info(format!("shape-hook:mark {message}")));
190            }
191        }
192        Ok(diagnostics)
193    }
194
195    fn run_accept_hooks(
196        &self,
197        cx: &mut Cx,
198        target_kind: MatchHookTargetKind,
199        shape_label: &str,
200        mut matched: ShapeMatch,
201    ) -> Result<ShapeMatch> {
202        for (hook_index, hook) in self.hooks.iter().enumerate() {
203            if hook.kind() != MatchHookKind::Accept {
204                continue;
205            }
206            let ctx = MatchHookContext {
207                hook_index,
208                phase: MatchHookPhase::AfterInner,
209                target_kind,
210                shape_label: shape_label.to_owned(),
211            };
212            if let MatchHookDecision::Accept { reason, score } =
213                hook.apply(cx, &ctx, Some(&matched))?
214            {
215                matched.accepted = true;
216                if matched.score == MatchScore::reject() {
217                    matched.score = score;
218                }
219                matched
220                    .diagnostics
221                    .push(Diagnostic::info(format!("shape-hook:accept {reason}")));
222            }
223        }
224        Ok(matched)
225    }
226
227    fn run_discard_hooks(
228        &self,
229        cx: &mut Cx,
230        target_kind: MatchHookTargetKind,
231        shape_label: &str,
232        mut matched: ShapeMatch,
233    ) -> Result<ShapeMatch> {
234        for (hook_index, hook) in self.hooks.iter().enumerate() {
235            if hook.kind() != MatchHookKind::Discard {
236                continue;
237            }
238            let ctx = MatchHookContext {
239                hook_index,
240                phase: MatchHookPhase::AfterInner,
241                target_kind,
242                shape_label: shape_label.to_owned(),
243            };
244            if let MatchHookDecision::Discard { reason } = hook.apply(cx, &ctx, Some(&matched))? {
245                matched.accepted = false;
246                matched.score = MatchScore::reject();
247                matched
248                    .diagnostics
249                    .push(Diagnostic::error(format!("shape-hook:discard {reason}")));
250                break;
251            }
252        }
253        Ok(matched)
254    }
255
256    fn run_annotate_hooks(
257        &self,
258        cx: &mut Cx,
259        target_kind: MatchHookTargetKind,
260        shape_label: &str,
261        mut matched: ShapeMatch,
262    ) -> Result<ShapeMatch> {
263        for (hook_index, hook) in self.hooks.iter().enumerate() {
264            if hook.kind() != MatchHookKind::Annotate {
265                continue;
266            }
267            let ctx = MatchHookContext {
268                hook_index,
269                phase: MatchHookPhase::AfterInner,
270                target_kind,
271                shape_label: shape_label.to_owned(),
272            };
273            if let MatchHookDecision::Annotate {
274                message,
275                score_delta,
276            } = hook.apply(cx, &ctx, Some(&matched))?
277            {
278                matched.score += MatchScore::exact(score_delta);
279                matched
280                    .diagnostics
281                    .push(Diagnostic::info(format!("shape-hook:annotate {message}")));
282            }
283        }
284        Ok(matched)
285    }
286}