Skip to main content

sim_lib_standard_core/
diff.rs

1//! Profile diffing: structural comparison of two language profiles.
2
3use sim_kernel::{Cx, Expr, OpKey, Result, Symbol};
4
5use crate::{LanguageProfile, standard_diff_capability};
6
7/// Whether two profiles compared equal or differed.
8#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub enum ProfileDiffStatus {
10    /// The two profiles are structurally identical.
11    Same,
12    /// The two profiles differ in at least one field.
13    Different,
14}
15
16/// A single differing field between two profiles, with both sides as
17/// expressions.
18#[derive(Clone, Debug, PartialEq, Eq)]
19pub struct ProfileDifference {
20    /// Qualified name of the differing field.
21    pub field: Symbol,
22    /// Left-side value.
23    pub left: Expr,
24    /// Right-side value.
25    pub right: Expr,
26}
27
28/// Structural comparison of two language profiles, produced by
29/// [`standard_diff_stub`].
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct ProfileDiff {
32    /// Symbol of the left profile.
33    pub left: Symbol,
34    /// Symbol of the right profile.
35    pub right: Symbol,
36    /// Overall same/different status.
37    pub status: ProfileDiffStatus,
38    /// Organs used by both profiles.
39    pub shared_organs: Vec<Symbol>,
40    /// Organs used only by the left profile.
41    pub left_only_organs: Vec<Symbol>,
42    /// Organs used only by the right profile.
43    pub right_only_organs: Vec<Symbol>,
44    /// Per-field differences.
45    pub differences: Vec<ProfileDifference>,
46}
47
48impl ProfileDiff {
49    /// Whether the two profiles compared as identical.
50    pub fn is_same(&self) -> bool {
51        self.status == ProfileDiffStatus::Same
52    }
53}
54
55/// Operation key for the standard diff operation.
56pub fn standard_diff_op_key() -> OpKey {
57    OpKey::new(Symbol::new("standard"), Symbol::new("diff"), 1)
58}
59
60/// Symbol naming the profile-diff operation on the codec surface.
61pub fn profile_diff_symbol() -> Symbol {
62    Symbol::qualified("profile", "diff")
63}
64
65/// Diff `left` against `right`, gated on [`standard_diff_capability`].
66///
67/// Compares reader, lowering, eval-policy, organs, numeric tower, capabilities,
68/// unsupported forms, and conformance tests, returning a [`ProfileDiff`].
69///
70/// [`standard_diff_capability`]: crate::standard_diff_capability
71///
72/// # Examples
73///
74/// ```
75/// use std::sync::Arc;
76///
77/// use sim_kernel::{Cx, DefaultFactory, NoopEvalPolicy, Symbol};
78/// use sim_lib_standard_core::{LanguageProfile, standard_diff_capability, standard_diff_stub};
79///
80/// let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
81/// cx.grant(standard_diff_capability());
82///
83/// let left = LanguageProfile::new(Symbol::qualified("lang", "a/v1"))
84///     .with_reader(Symbol::qualified("codec", "lisp"));
85/// let right = LanguageProfile::new(Symbol::qualified("lang", "b/v1"))
86///     .with_reader(Symbol::qualified("codec", "json"));
87///
88/// assert!(standard_diff_stub(&cx, &left, &left).unwrap().is_same());
89/// assert!(!standard_diff_stub(&cx, &left, &right).unwrap().is_same());
90/// ```
91pub fn standard_diff_stub(
92    cx: &Cx,
93    left: &LanguageProfile,
94    right: &LanguageProfile,
95) -> Result<ProfileDiff> {
96    cx.require(&standard_diff_capability())?;
97    let left_organs = left
98        .organs
99        .iter()
100        .map(|organ| organ.organ.clone())
101        .collect::<Vec<_>>();
102    let right_organs = right
103        .organs
104        .iter()
105        .map(|organ| organ.organ.clone())
106        .collect::<Vec<_>>();
107    let mut differences = Vec::new();
108    push_difference(
109        &mut differences,
110        "reader",
111        Expr::Symbol(left.reader.clone()),
112        Expr::Symbol(right.reader.clone()),
113    );
114    push_difference(
115        &mut differences,
116        "lowering",
117        Expr::Symbol(left.lowering.clone()),
118        Expr::Symbol(right.lowering.clone()),
119    );
120    push_difference(
121        &mut differences,
122        "eval-policy",
123        Expr::Symbol(left.eval_policy.clone()),
124        Expr::Symbol(right.eval_policy.clone()),
125    );
126    push_difference(
127        &mut differences,
128        "organs",
129        symbols_expr(&left_organs),
130        symbols_expr(&right_organs),
131    );
132    push_difference(
133        &mut differences,
134        "numeric",
135        optional_symbol_expr(left.numeric_tower.as_ref()),
136        optional_symbol_expr(right.numeric_tower.as_ref()),
137    );
138    push_difference(
139        &mut differences,
140        "capabilities",
141        capability_expr(left),
142        capability_expr(right),
143    );
144    push_difference(
145        &mut differences,
146        "unsupported",
147        symbols_expr(&left.unsupported_forms),
148        symbols_expr(&right.unsupported_forms),
149    );
150    push_difference(
151        &mut differences,
152        "conformance-tests",
153        symbols_expr(&left.conformance_tests),
154        symbols_expr(&right.conformance_tests),
155    );
156
157    Ok(ProfileDiff {
158        left: left.symbol.clone(),
159        right: right.symbol.clone(),
160        status: if differences.is_empty() {
161            ProfileDiffStatus::Same
162        } else {
163            ProfileDiffStatus::Different
164        },
165        shared_organs: left_organs
166            .iter()
167            .filter(|organ| right_organs.contains(organ))
168            .cloned()
169            .collect(),
170        left_only_organs: left_organs
171            .iter()
172            .filter(|organ| !right_organs.contains(organ))
173            .cloned()
174            .collect(),
175        right_only_organs: right_organs
176            .iter()
177            .filter(|organ| !left_organs.contains(organ))
178            .cloned()
179            .collect(),
180        differences,
181    })
182}
183
184fn push_difference(differences: &mut Vec<ProfileDifference>, field: &str, left: Expr, right: Expr) {
185    if left != right {
186        differences.push(ProfileDifference {
187            field: Symbol::qualified("profile/diff", field.to_owned()),
188            left,
189            right,
190        });
191    }
192}
193
194fn symbols_expr(symbols: &[Symbol]) -> Expr {
195    Expr::List(symbols.iter().cloned().map(Expr::Symbol).collect())
196}
197
198fn optional_symbol_expr(symbol: Option<&Symbol>) -> Expr {
199    symbol.cloned().map(Expr::Symbol).unwrap_or(Expr::Nil)
200}
201
202fn capability_expr(profile: &LanguageProfile) -> Expr {
203    Expr::List(
204        profile
205            .capabilities
206            .iter()
207            .map(|capability| Expr::Symbol(capability.as_symbol()))
208            .collect(),
209    )
210}