backhopper_core/compat/arg_shape.rs
1// Copyright (C) 2026 Michael S. Klishin and Contributors
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// See LICENSE-APACHE and LICENSE-MIT for details.
4
5//! Coarse classification of argument shapes at call sites and clause heads.
6//!
7//! A clause-head pattern is encoded as `Vec<ArgShape>`: one entry per
8//! formal parameter. A call site's arguments are encoded the same way:
9//! one entry per actual. Matching is per-position: each call argument
10//! must `satisfies` the clause's pattern at the same position.
11//!
12//! `Unknown` is the safe escape hatch: when the extractor cannot
13//! classify an actual or pattern with confidence, it emits `Unknown`,
14//! and matching always accepts `Unknown` on either side so no false
15//! positives result.
16
17use serde::{Deserialize, Serialize};
18
19use crate::model::names::RecordName;
20
21#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
23#[serde(tag = "kind", rename_all = "snake_case")]
24pub enum ArgShape {
25 /// Variable identifier (starts with `_` or capital letter).
26 Variable,
27 /// Atom literal: `foo`, `'quoted atom'`.
28 Atom { name: String },
29 /// Integer literal.
30 Integer,
31 /// Float literal.
32 Float,
33 /// Binary literal: `<<...>>`.
34 Binary,
35 /// List literal: `[...]`.
36 List,
37 /// Tuple literal: `{...}` (size is the number of elements).
38 Tuple { size: usize },
39 /// Record reference: `#name{...}`.
40 Record { name: RecordName },
41 /// String literal: `"..."`.
42 String,
43 /// Anonymous fun: `fun ... end` or `fun mod:f/A`.
44 Fun,
45 /// Anything the extractor cannot classify. Always matches.
46 Unknown,
47}
48
49impl ArgShape {
50 /// Returns true when `self` (a call-site argument) is acceptable
51 /// against `pattern` (a clause-head argument at the pin).
52 ///
53 /// Conservative: we only return `false` when we can *prove* a
54 /// mismatch from static information. Anything we can't classify
55 /// confidently is treated as a potential match.
56 ///
57 /// `Unknown` on either side accepts anything. `Variable` on either
58 /// side accepts anything too: a call-site variable can hold any
59 /// runtime value, and a clause-head variable binds to any input.
60 pub fn satisfies(&self, pattern: &ArgShape) -> bool {
61 if matches!(self, ArgShape::Unknown) || matches!(pattern, ArgShape::Unknown) {
62 return true;
63 }
64 if matches!(self, ArgShape::Variable) || matches!(pattern, ArgShape::Variable) {
65 return true;
66 }
67 match (self, pattern) {
68 (ArgShape::Atom { name: a }, ArgShape::Atom { name: b }) => a == b,
69 (ArgShape::Tuple { size: a }, ArgShape::Tuple { size: b }) => a == b,
70 (ArgShape::Record { name: a }, ArgShape::Record { name: b }) => a == b,
71 (ArgShape::Integer, ArgShape::Integer)
72 | (ArgShape::Float, ArgShape::Float)
73 | (ArgShape::Binary, ArgShape::Binary)
74 | (ArgShape::List, ArgShape::List)
75 | (ArgShape::String, ArgShape::String)
76 | (ArgShape::Fun, ArgShape::Fun) => true,
77 _ => false,
78 }
79 }
80}
81
82/// True when `args` (the call site's actuals) satisfies *any* of the
83/// supplied clause-head patterns. Empty `clauses` returns true: with
84/// no pattern data recorded, we can't penalize the call.
85pub fn satisfies_any(args: &[ArgShape], clauses: &[Vec<ArgShape>]) -> bool {
86 if clauses.is_empty() {
87 return true;
88 }
89 clauses.iter().any(|clause| {
90 clause.len() == args.len()
91 && args
92 .iter()
93 .zip(clause.iter())
94 .all(|(actual, pattern)| actual.satisfies(pattern))
95 })
96}