1use std::sync::Arc;
6
7use sim_citizen_derive::non_citizen;
8use sim_kernel::{
9 ClassRef, Cx, DefaultFactory, Error, Expr, Factory, NumberLiteral, Object, ObjectEncode,
10 ObjectEncoding, Result, Symbol, Value,
11};
12
13use crate::{MatchScore, ShapeMatch};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum MatchHookKind {
18 Mark,
20 Accept,
22 Discard,
24 Annotate,
26}
27
28impl MatchHookKind {
29 pub fn preserves_acceptance(self) -> bool {
31 matches!(self, Self::Mark | Self::Annotate)
32 }
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum MatchHookTargetKind {
38 Value,
40 Expr,
42}
43
44#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum MatchHookPhase {
47 BeforeInner,
49 AfterInner,
51}
52
53#[derive(Clone, Debug, PartialEq, Eq)]
55pub struct MatchHookContext {
56 pub hook_index: usize,
58 pub phase: MatchHookPhase,
60 pub target_kind: MatchHookTargetKind,
62 pub shape_label: String,
64}
65
66#[derive(Clone, Debug, PartialEq, Eq)]
68pub enum MatchHookDecision {
69 Pass,
71 Mark {
73 message: String,
75 },
76 Accept {
78 reason: String,
80 score: MatchScore,
82 },
83 Discard {
85 reason: String,
87 },
88 Annotate {
90 message: String,
92 score_delta: i32,
94 },
95}
96
97pub trait MatchHook: Send + Sync {
99 fn symbol(&self) -> Symbol;
101 fn kind(&self) -> MatchHookKind;
103 fn object_encoding(&self) -> Option<ObjectEncoding> {
105 None
106 }
107 fn apply(
109 &self,
110 cx: &mut Cx,
111 ctx: &MatchHookContext,
112 current: Option<&ShapeMatch>,
113 ) -> Result<MatchHookDecision>;
114}
115
116#[non_citizen(
118 reason = "may wrap custom live hook code; built-in pure hook descriptors use shape/*Hook citizens",
119 kind = "function",
120 descriptor = "shape/live-hook"
121)]
122#[derive(Clone)]
123pub struct MatchHookObject {
124 hook: Arc<dyn MatchHook>,
125}
126
127impl MatchHookObject {
128 pub fn new(hook: Arc<dyn MatchHook>) -> Self {
130 Self { hook }
131 }
132
133 pub fn hook(&self) -> Arc<dyn MatchHook> {
135 self.hook.clone()
136 }
137}
138
139impl Object for MatchHookObject {
140 fn display(&self, _cx: &mut Cx) -> Result<String> {
141 Ok(format!(
142 "#<shape-hook {} {}>",
143 self.hook.symbol(),
144 hook_kind_name(self.hook.kind())
145 ))
146 }
147
148 fn as_any(&self) -> &dyn std::any::Any {
149 self
150 }
151}
152
153impl sim_kernel::ObjectCompat for MatchHookObject {
154 fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
155 if let Some(ObjectEncoding::Constructor { class, .. }) = self.hook.object_encoding()
156 && let Some(value) = cx.registry().class_by_symbol(&class)
157 {
158 return Ok(value.clone());
159 }
160 cx.factory().nil()
161 }
162
163 fn as_expr(&self, _cx: &mut Cx) -> Result<Expr> {
164 match self.object_encoding(_cx)? {
165 ObjectEncoding::Constructor { class, args } => Ok(Expr::Call {
166 operator: Box::new(Expr::Symbol(class)),
167 args,
168 }),
169 _ => Err(Error::Eval(format!(
170 "shape hook {} produced a non-constructor object encoding; only \
171 constructor encodings can render as an expression",
172 self.hook.symbol()
173 ))),
174 }
175 }
176
177 fn as_object_encoder(&self) -> Option<&dyn ObjectEncode> {
178 self.hook.object_encoding().is_some().then_some(self)
179 }
180}
181
182impl ObjectEncode for MatchHookObject {
183 fn object_encoding(&self, _cx: &mut Cx) -> Result<ObjectEncoding> {
184 self.hook.object_encoding().ok_or_else(|| {
185 Error::Eval(format!(
186 "shape hook {} is not a pure descriptor citizen",
187 self.hook.symbol()
188 ))
189 })
190 }
191}
192
193pub fn hook_value(hook: Arc<dyn MatchHook>) -> Value {
195 DefaultFactory
196 .opaque(Arc::new(MatchHookObject::new(hook)))
197 .expect("hook object should always be boxable")
198}
199
200pub fn hook_ref_arc(value: &Value) -> Result<Arc<dyn MatchHook>> {
202 value
203 .object()
204 .downcast_ref::<MatchHookObject>()
205 .map(MatchHookObject::hook)
206 .ok_or(Error::TypeMismatch {
207 expected: "shape-hook",
208 found: "non-shape-hook",
209 })
210}
211
212#[derive(Clone, Default)]
214pub struct TraceMarkHook;
215
216impl MatchHook for TraceMarkHook {
217 fn symbol(&self) -> Symbol {
218 Symbol::qualified("shape", "trace-mark")
219 }
220
221 fn kind(&self) -> MatchHookKind {
222 MatchHookKind::Mark
223 }
224
225 fn object_encoding(&self) -> Option<ObjectEncoding> {
226 Some(hook_encoding(trace_mark_hook_class_symbol(), Vec::new()))
227 }
228
229 fn apply(
230 &self,
231 _cx: &mut Cx,
232 ctx: &MatchHookContext,
233 _current: Option<&ShapeMatch>,
234 ) -> Result<MatchHookDecision> {
235 Ok(MatchHookDecision::Mark {
236 message: ctx.shape_label.clone(),
237 })
238 }
239}
240
241#[derive(Clone)]
243pub struct ScoreFloorHook {
244 floor: i32,
245}
246
247impl ScoreFloorHook {
248 pub fn new(floor: i32) -> Self {
250 Self { floor }
251 }
252
253 pub fn floor(&self) -> i32 {
255 self.floor
256 }
257}
258
259impl MatchHook for ScoreFloorHook {
260 fn symbol(&self) -> Symbol {
261 Symbol::qualified("shape", "score-floor")
262 }
263
264 fn kind(&self) -> MatchHookKind {
265 MatchHookKind::Annotate
266 }
267
268 fn object_encoding(&self) -> Option<ObjectEncoding> {
269 Some(hook_encoding(
270 score_floor_hook_class_symbol(),
271 vec![int_expr(self.floor)],
272 ))
273 }
274
275 fn apply(
276 &self,
277 _cx: &mut Cx,
278 _ctx: &MatchHookContext,
279 current: Option<&ShapeMatch>,
280 ) -> Result<MatchHookDecision> {
281 let Some(current) = current else {
282 return Ok(MatchHookDecision::Pass);
283 };
284 if current.accepted && current.score.value() < self.floor {
285 return Ok(MatchHookDecision::Annotate {
286 message: format!("score floor {}", self.floor),
287 score_delta: self.floor - current.score.value(),
288 });
289 }
290 Ok(MatchHookDecision::Pass)
291 }
292}
293
294#[derive(Clone, Default)]
296pub struct AcceptOnNoDiagnosticsHook;
297
298impl MatchHook for AcceptOnNoDiagnosticsHook {
299 fn symbol(&self) -> Symbol {
300 Symbol::qualified("shape", "accept-on-no-diagnostics")
301 }
302
303 fn kind(&self) -> MatchHookKind {
304 MatchHookKind::Accept
305 }
306
307 fn object_encoding(&self) -> Option<ObjectEncoding> {
308 Some(hook_encoding(
309 accept_on_no_diagnostics_hook_class_symbol(),
310 Vec::new(),
311 ))
312 }
313
314 fn apply(
315 &self,
316 _cx: &mut Cx,
317 _ctx: &MatchHookContext,
318 current: Option<&ShapeMatch>,
319 ) -> Result<MatchHookDecision> {
320 let Some(current) = current else {
321 return Ok(MatchHookDecision::Pass);
322 };
323 if !current.accepted && current.diagnostics.is_empty() {
324 return Ok(MatchHookDecision::Accept {
325 reason: "no diagnostics".to_owned(),
326 score: MatchScore::exact(1),
327 });
328 }
329 Ok(MatchHookDecision::Pass)
330 }
331}
332
333#[derive(Clone)]
335pub struct DiscardOnDiagnosticPrefixHook {
336 prefix: String,
337}
338
339impl DiscardOnDiagnosticPrefixHook {
340 pub fn new(prefix: impl Into<String>) -> Self {
342 Self {
343 prefix: prefix.into(),
344 }
345 }
346
347 pub fn prefix(&self) -> &str {
349 &self.prefix
350 }
351}
352
353impl MatchHook for DiscardOnDiagnosticPrefixHook {
354 fn symbol(&self) -> Symbol {
355 Symbol::qualified("shape", "discard-on-diagnostic-prefix")
356 }
357
358 fn kind(&self) -> MatchHookKind {
359 MatchHookKind::Discard
360 }
361
362 fn object_encoding(&self) -> Option<ObjectEncoding> {
363 Some(hook_encoding(
364 discard_on_diagnostic_prefix_hook_class_symbol(),
365 vec![Expr::String(self.prefix.clone())],
366 ))
367 }
368
369 fn apply(
370 &self,
371 _cx: &mut Cx,
372 _ctx: &MatchHookContext,
373 current: Option<&ShapeMatch>,
374 ) -> Result<MatchHookDecision> {
375 let Some(current) = current else {
376 return Ok(MatchHookDecision::Pass);
377 };
378 if current.accepted
379 && current
380 .diagnostics
381 .iter()
382 .any(|diagnostic| diagnostic.message.starts_with(&self.prefix))
383 {
384 return Ok(MatchHookDecision::Discard {
385 reason: self.prefix.clone(),
386 });
387 }
388 Ok(MatchHookDecision::Pass)
389 }
390}
391
392pub(crate) fn hook_kind_name(kind: MatchHookKind) -> &'static str {
393 match kind {
394 MatchHookKind::Mark => "mark",
395 MatchHookKind::Accept => "accept",
396 MatchHookKind::Discard => "discard",
397 MatchHookKind::Annotate => "annotate",
398 }
399}
400
401pub fn trace_mark_hook_class_symbol() -> Symbol {
403 Symbol::qualified("shape", "TraceMarkHook")
404}
405
406pub fn score_floor_hook_class_symbol() -> Symbol {
408 Symbol::qualified("shape", "ScoreFloorHook")
409}
410
411pub fn accept_on_no_diagnostics_hook_class_symbol() -> Symbol {
414 Symbol::qualified("shape", "AcceptOnNoDiagnosticsHook")
415}
416
417pub fn discard_on_diagnostic_prefix_hook_class_symbol() -> Symbol {
420 Symbol::qualified("shape", "DiscardOnDiagnosticPrefixHook")
421}
422
423fn hook_encoding(class: Symbol, fields: Vec<Expr>) -> ObjectEncoding {
424 let mut args = Vec::with_capacity(fields.len() + 1);
425 args.push(Expr::Symbol(Symbol::new("v1")));
426 args.extend(fields);
427 ObjectEncoding::Constructor { class, args }
428}
429
430fn int_expr(value: i32) -> Expr {
431 Expr::Number(NumberLiteral {
432 domain: Symbol::qualified("citizen", "int"),
433 canonical: value.to_string(),
434 })
435}