1use 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
16pub struct HookedShape {
37 inner: Arc<dyn Shape>,
38 hooks: Vec<Arc<dyn MatchHook>>,
39}
40
41impl HookedShape {
42 pub fn new(inner: Arc<dyn Shape>, hooks: Vec<Arc<dyn MatchHook>>) -> Self {
44 Self { inner, hooks }
45 }
46
47 pub fn inner(&self) -> &Arc<dyn Shape> {
49 &self.inner
50 }
51
52 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 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}