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