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(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
71/// let ordinal = cx
72///     .datum_store_mut()
73///     .intern(Datum::String("first".to_owned()))
74///     .unwrap();
75/// let coord = rank_coordinate(Symbol::qualified("rank", "expr-small"), ordinal);
76/// assert!(matches!(coord, Ref::Coord(_)));
77/// ```
78pub fn rank_coordinate(space: Symbol, ordinal: ContentId) -> Ref {
79    Ref::Coord(Coordinate { space, ordinal })
80}
81
82/// Publish the kind, operation, and optional help claims describing a rank
83/// space, inserting each only if not already present.
84pub fn publish_rank_space_claims(cx: &mut Cx, space: Symbol, help: Option<&str>) -> Result<()> {
85    let subject = Ref::Symbol(space);
86    insert_once(
87        cx,
88        subject.clone(),
89        card_kind_predicate(),
90        Ref::Symbol(rank_space_kind()),
91    )?;
92    for op in [
93        rank_rank_op_key(),
94        rank_unrank_op_key(),
95        rank_neighbors_op_key(),
96        rank_order_next_op_key(),
97    ] {
98        insert_once(
99            cx,
100            subject.clone(),
101            card_ops_predicate(),
102            Ref::Symbol(op_symbol(&op)),
103        )?;
104    }
105    if let Some(help) = help {
106        let help_ref = Claim::intern_object(cx.datum_store_mut(), Datum::String(help.to_owned()))?;
107        insert_once(cx, subject, card_help_predicate(), help_ref)?;
108    }
109    Ok(())
110}
111
112/// Publish the kind, space, and ordinal claims describing a rank coordinate,
113/// inserting each only if not already present.
114pub fn publish_coordinate_claims(cx: &mut Cx, coordinate: Coordinate) -> Result<()> {
115    let subject = Ref::Coord(coordinate.clone());
116    insert_once(
117        cx,
118        subject.clone(),
119        card_kind_predicate(),
120        Ref::Symbol(rank_coordinate_kind()),
121    )?;
122    insert_once(
123        cx,
124        subject.clone(),
125        rank_space_predicate(),
126        Ref::Symbol(coordinate.space),
127    )?;
128    insert_once(
129        cx,
130        subject,
131        rank_ordinal_predicate(),
132        Ref::Content(coordinate.ordinal),
133    )
134}
135
136fn insert_once(cx: &mut Cx, subject: Ref, predicate: Symbol, object: Ref) -> Result<()> {
137    let exists = !cx
138        .query_facts(ClaimPattern::exact(
139            subject.clone(),
140            predicate.clone(),
141            object.clone(),
142        ))?
143        .is_empty();
144    if !exists {
145        cx.insert_fact(Claim::public(subject, predicate, object))?;
146    }
147    Ok(())
148}
149
150fn rank_op_key(name: &str) -> OpKey {
151    OpKey::new(Symbol::new("rank"), Symbol::new(name), 1)
152}
153
154fn op_symbol(op: &OpKey) -> Symbol {
155    Symbol::qualified(
156        op.namespace.to_string(),
157        format!("{}.v{}", op.name, op.version),
158    )
159}
160
161fn rank_symbol(name: &str) -> Symbol {
162    Symbol::qualified("rank", name)
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168    use crate::{
169        DefaultFactory, Expr, NoopEvalPolicy, card::card_for_ref, datum_store::DatumStore,
170    };
171    use std::sync::Arc;
172
173    #[test]
174    fn rank_space_and_coordinate_claims_are_publishable_without_accessor() {
175        let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
176        let space = Symbol::qualified("rank", "expr-small");
177        publish_rank_space_claims(&mut cx, space.clone(), Some("small expression rank")).unwrap();
178
179        let ordinal = cx
180            .datum_store_mut()
181            .intern(Datum::String("first".to_owned()))
182            .unwrap();
183        let coordinate = Coordinate {
184            space: space.clone(),
185            ordinal: ordinal.clone(),
186        };
187        publish_coordinate_claims(&mut cx, coordinate.clone()).unwrap();
188
189        assert_has_claim(
190            &cx,
191            Ref::Symbol(space),
192            card_kind_predicate(),
193            Ref::Symbol(rank_space_kind()),
194        );
195        assert_has_claim(
196            &cx,
197            Ref::Coord(coordinate),
198            rank_ordinal_predicate(),
199            Ref::Content(ordinal),
200        );
201    }
202
203    #[test]
204    fn rank_space_and_coordinate_claims_project_to_cards() {
205        let mut cx = Cx::new(Arc::new(NoopEvalPolicy), Arc::new(DefaultFactory));
206        let space = Symbol::qualified("rank", "expr-small");
207        publish_rank_space_claims(&mut cx, space.clone(), Some("small expression rank")).unwrap();
208
209        let ordinal = cx
210            .datum_store_mut()
211            .intern(Datum::String("first".to_owned()))
212            .unwrap();
213        let coordinate = Coordinate {
214            space: space.clone(),
215            ordinal,
216        };
217        publish_coordinate_claims(&mut cx, coordinate.clone()).unwrap();
218
219        let space_card = card_expr(&mut cx, Ref::Symbol(space));
220        assert_eq!(
221            table_value(&space_card, "kind"),
222            Some(&Expr::Symbol(rank_space_kind()))
223        );
224        assert_eq!(
225            table_value(&space_card, "help"),
226            Some(&Expr::String("small expression rank".to_owned()))
227        );
228        assert_list_contains_symbol(
229            table_value(&space_card, "ops").expect("rank ops"),
230            Symbol::qualified("rank", "rank.v1"),
231        );
232        assert_list_contains_symbol(
233            table_value(&space_card, "ops").expect("rank ops"),
234            Symbol::qualified("rank", "unrank.v1"),
235        );
236
237        let coordinate_card = card_expr(&mut cx, Ref::Coord(coordinate));
238        assert_eq!(
239            table_value(&coordinate_card, "kind"),
240            Some(&Expr::Symbol(rank_coordinate_kind()))
241        );
242    }
243
244    fn assert_has_claim(cx: &Cx, subject: Ref, predicate: Symbol, object: Ref) {
245        let claims = cx
246            .query_facts(ClaimPattern::exact(subject, predicate, object))
247            .unwrap();
248        assert_eq!(claims.len(), 1);
249    }
250
251    fn card_expr(cx: &mut Cx, subject: Ref) -> Expr {
252        card_for_ref(cx, subject)
253            .unwrap()
254            .object()
255            .as_expr(cx)
256            .unwrap()
257    }
258
259    fn table_value<'a>(expr: &'a Expr, key: &str) -> Option<&'a Expr> {
260        let Expr::Map(entries) = expr else {
261            return None;
262        };
263        entries.iter().find_map(|(entry_key, entry_value)| {
264            let Expr::Symbol(entry_key) = entry_key else {
265                return None;
266            };
267            (entry_key == &Symbol::new(key)).then_some(entry_value)
268        })
269    }
270
271    fn assert_list_contains_symbol(expr: &Expr, expected: Symbol) {
272        assert!(matches!(expr, Expr::List(_)), "expected list");
273        let Expr::List(items) = expr else {
274            return;
275        };
276        assert!(
277            items
278                .iter()
279                .any(|item| item == &Expr::Symbol(expected.clone())),
280            "expected list to contain {expected}"
281        );
282    }
283}