Skip to main content

sim_kernel/
rank.rs

1//! Rank metadata: the contract for ordering and navigating ranked spaces.
2//!
3//! The kernel defines the rank operation keys and the space/coordinate
4//! predicate vocabulary; libraries supply the concrete ranking behavior.
5
6use crate::{
7    card::{card_help_predicate, card_kind_predicate, card_ops_predicate},
8    claim::{Claim, ClaimPattern},
9    datum::Datum,
10    env::Cx,
11    error::Result,
12    id::Symbol,
13    ref_id::{ContentId, Coordinate, Ref},
14    term::OpKey,
15};
16
17/// Operation key for mapping a value to its rank in a space.
18pub fn rank_rank_op_key() -> OpKey {
19    rank_op_key("rank")
20}
21
22/// Operation key for mapping a rank back to its value.
23pub fn rank_unrank_op_key() -> OpKey {
24    rank_op_key("unrank")
25}
26
27/// Operation key for listing a coordinate's neighbors.
28pub fn rank_neighbors_op_key() -> OpKey {
29    rank_op_key("neighbors")
30}
31
32/// Operation key for advancing to the next coordinate in order.
33pub fn rank_order_next_op_key() -> OpKey {
34    rank_op_key("order-next")
35}
36
37/// Card kind symbol identifying a rank space.
38pub fn rank_space_kind() -> Symbol {
39    rank_symbol("space")
40}
41
42/// Card kind symbol identifying a rank coordinate.
43pub fn rank_coordinate_kind() -> Symbol {
44    rank_symbol("coordinate")
45}
46
47/// Claim predicate naming a coordinate's space.
48pub fn rank_space_predicate() -> Symbol {
49    rank_symbol("space")
50}
51
52/// Claim predicate naming a coordinate's ordinal.
53pub fn rank_ordinal_predicate() -> Symbol {
54    rank_symbol("ordinal")
55}
56
57/// Build a coordinate reference for `ordinal` within `space`.
58///
59/// # Examples
60///
61/// ```
62/// # use std::sync::Arc;
63/// # use sim_kernel::{DefaultFactory, NoopEvalPolicy};
64/// # use sim_kernel::env::Cx;
65/// # use sim_kernel::datum::Datum;
66/// # use sim_kernel::datum_store::DatumStore;
67/// # use sim_kernel::id::Symbol;
68/// # use sim_kernel::rank::rank_coordinate;
69/// # use sim_kernel::ref_id::Ref;
70/// let mut cx = Cx::new(
71///     Arc::new(NoopEvalPolicy),
72///     Arc::new(DefaultFactory),
73///     sim_kernel::HandleSeed::new(7),
74/// );
75/// let ordinal = cx
76///     .datum_store_mut()
77///     .intern(Datum::String("first".to_owned()))
78///     .unwrap();
79/// let coord = rank_coordinate(Symbol::qualified("rank", "expr-small"), ordinal);
80/// assert!(matches!(coord, Ref::Coord(_)));
81/// ```
82pub fn rank_coordinate(space: Symbol, ordinal: ContentId) -> Ref {
83    Ref::Coord(Coordinate { space, ordinal })
84}
85
86/// Publish the kind, operation, and optional help claims describing a rank
87/// space, inserting each only if not already present.
88pub fn publish_rank_space_claims(cx: &mut Cx, space: Symbol, help: Option<&str>) -> Result<()> {
89    let subject = Ref::Symbol(space);
90    insert_once(
91        cx,
92        subject.clone(),
93        card_kind_predicate(),
94        Ref::Symbol(rank_space_kind()),
95    )?;
96    for op in [
97        rank_rank_op_key(),
98        rank_unrank_op_key(),
99        rank_neighbors_op_key(),
100        rank_order_next_op_key(),
101    ] {
102        insert_once(
103            cx,
104            subject.clone(),
105            card_ops_predicate(),
106            Ref::Symbol(op_symbol(&op)),
107        )?;
108    }
109    if let Some(help) = help {
110        let help_ref = Claim::intern_object(cx.datum_store_mut(), Datum::String(help.to_owned()))?;
111        insert_once(cx, subject, card_help_predicate(), help_ref)?;
112    }
113    Ok(())
114}
115
116/// Publish the kind, space, and ordinal claims describing a rank coordinate,
117/// inserting each only if not already present.
118pub fn publish_coordinate_claims(cx: &mut Cx, coordinate: Coordinate) -> Result<()> {
119    let subject = Ref::Coord(coordinate.clone());
120    insert_once(
121        cx,
122        subject.clone(),
123        card_kind_predicate(),
124        Ref::Symbol(rank_coordinate_kind()),
125    )?;
126    insert_once(
127        cx,
128        subject.clone(),
129        rank_space_predicate(),
130        Ref::Symbol(coordinate.space),
131    )?;
132    insert_once(
133        cx,
134        subject,
135        rank_ordinal_predicate(),
136        Ref::Content(coordinate.ordinal),
137    )
138}
139
140fn insert_once(cx: &mut Cx, subject: Ref, predicate: Symbol, object: Ref) -> Result<()> {
141    let exists = !cx
142        .query_facts(ClaimPattern::exact(
143            subject.clone(),
144            predicate.clone(),
145            object.clone(),
146        ))?
147        .is_empty();
148    if !exists {
149        cx.insert_fact(Claim::public(subject, predicate, object))?;
150    }
151    Ok(())
152}
153
154fn rank_op_key(name: &str) -> OpKey {
155    OpKey::new(Symbol::new("rank"), Symbol::new(name), 1)
156}
157
158fn op_symbol(op: &OpKey) -> Symbol {
159    Symbol::qualified(
160        op.namespace.to_string(),
161        format!("{}.v{}", op.name, op.version),
162    )
163}
164
165fn rank_symbol(name: &str) -> Symbol {
166    Symbol::qualified("rank", name)
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::{
173        DefaultFactory, Expr, NoopEvalPolicy, card::card_for_ref, datum_store::DatumStore,
174    };
175    use std::sync::Arc;
176
177    #[test]
178    fn rank_space_and_coordinate_claims_are_publishable_without_accessor() {
179        let mut cx = Cx::new(
180            Arc::new(NoopEvalPolicy),
181            Arc::new(DefaultFactory),
182            crate::HandleSeed::new(7),
183        );
184        let space = Symbol::qualified("rank", "expr-small");
185        publish_rank_space_claims(&mut cx, space.clone(), Some("small expression rank")).unwrap();
186
187        let ordinal = cx
188            .datum_store_mut()
189            .intern(Datum::String("first".to_owned()))
190            .unwrap();
191        let coordinate = Coordinate {
192            space: space.clone(),
193            ordinal: ordinal.clone(),
194        };
195        publish_coordinate_claims(&mut cx, coordinate.clone()).unwrap();
196
197        assert_has_claim(
198            &cx,
199            Ref::Symbol(space),
200            card_kind_predicate(),
201            Ref::Symbol(rank_space_kind()),
202        );
203        assert_has_claim(
204            &cx,
205            Ref::Coord(coordinate),
206            rank_ordinal_predicate(),
207            Ref::Content(ordinal),
208        );
209    }
210
211    #[test]
212    fn rank_space_and_coordinate_claims_project_to_cards() {
213        let mut cx = Cx::new(
214            Arc::new(NoopEvalPolicy),
215            Arc::new(DefaultFactory),
216            crate::HandleSeed::new(7),
217        );
218        let space = Symbol::qualified("rank", "expr-small");
219        publish_rank_space_claims(&mut cx, space.clone(), Some("small expression rank")).unwrap();
220
221        let ordinal = cx
222            .datum_store_mut()
223            .intern(Datum::String("first".to_owned()))
224            .unwrap();
225        let coordinate = Coordinate {
226            space: space.clone(),
227            ordinal,
228        };
229        publish_coordinate_claims(&mut cx, coordinate.clone()).unwrap();
230
231        let space_card = card_expr(&mut cx, Ref::Symbol(space));
232        assert_eq!(
233            table_value(&space_card, "kind"),
234            Some(&Expr::Symbol(rank_space_kind()))
235        );
236        assert_eq!(
237            table_value(&space_card, "help"),
238            Some(&Expr::String("small expression rank".to_owned()))
239        );
240        assert_list_contains_symbol(
241            table_value(&space_card, "ops").expect("rank ops"),
242            Symbol::qualified("rank", "rank.v1"),
243        );
244        assert_list_contains_symbol(
245            table_value(&space_card, "ops").expect("rank ops"),
246            Symbol::qualified("rank", "unrank.v1"),
247        );
248
249        let coordinate_card = card_expr(&mut cx, Ref::Coord(coordinate));
250        assert_eq!(
251            table_value(&coordinate_card, "kind"),
252            Some(&Expr::Symbol(rank_coordinate_kind()))
253        );
254    }
255
256    fn assert_has_claim(cx: &Cx, subject: Ref, predicate: Symbol, object: Ref) {
257        let claims = cx
258            .query_facts(ClaimPattern::exact(subject, predicate, object))
259            .unwrap();
260        assert_eq!(claims.len(), 1);
261    }
262
263    fn card_expr(cx: &mut Cx, subject: Ref) -> Expr {
264        card_for_ref(cx, subject)
265            .unwrap()
266            .object()
267            .as_expr(cx)
268            .unwrap()
269    }
270
271    fn table_value<'a>(expr: &'a Expr, key: &str) -> Option<&'a Expr> {
272        let Expr::Map(entries) = expr else {
273            return None;
274        };
275        entries.iter().find_map(|(entry_key, entry_value)| {
276            let Expr::Symbol(entry_key) = entry_key else {
277                return None;
278            };
279            (entry_key == &Symbol::new(key)).then_some(entry_value)
280        })
281    }
282
283    fn assert_list_contains_symbol(expr: &Expr, expected: Symbol) {
284        assert!(matches!(expr, Expr::List(_)), "expected list");
285        let Expr::List(items) = expr else {
286            return;
287        };
288        assert!(
289            items
290                .iter()
291                .any(|item| item == &Expr::Symbol(expected.clone())),
292            "expected list to contain {expected}"
293        );
294    }
295}