Skip to main content

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 fallback: 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}