Skip to main content

sim_lib_forge/
contract_query.rs

1//! Shape-scoped retrieval over cached FORGE contract decks.
2
3use std::collections::BTreeMap;
4
5use sim_kernel::{
6    Cx, Error, Expr, Result, ShapeRef, Symbol, Value,
7    library::{ExportRecord, LoadedLib},
8};
9use sim_shape::{Shape, ShapeRelationKind, relate_shapes};
10
11use crate::contracts::{assemble_contract_deck, export_value};
12use crate::{ContractCard, ContractDeck};
13
14/// Shape filters for contract-card retrieval.
15#[derive(Clone)]
16pub struct ShapeQuery {
17    /// Wanted callable argument Shape. Candidate arguments must subsume it.
18    pub args: Option<ShapeRef>,
19    /// Wanted callable result Shape. Candidate results must be subshapes of it.
20    pub result: Option<ShapeRef>,
21    /// Maximum number of ranked cards returned. `0` means no cards are returned.
22    pub limit: usize,
23}
24
25/// One contract card paired with its ranking score and explanation.
26#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct RankedContractCard {
28    /// The matched source-free contract card.
29    pub card: ContractCard,
30    /// Higher scores sort before lower scores.
31    pub score: i32,
32    /// Human-readable facts that contributed to the score.
33    pub reasons: Vec<String>,
34}
35
36/// Counters and query facts from the most recent contract-deck query.
37#[derive(Clone, Debug, Default, PartialEq, Eq)]
38pub struct ContractQueryReport {
39    /// Whether the deck came from an existing cache entry.
40    pub cache_hit: bool,
41    /// Cards excluded because a requested shape field was unavailable.
42    pub skipped_missing_shapes: usize,
43    /// Number of matching cards omitted because of the query limit.
44    pub capped_results: usize,
45    /// Number of cards that matched before applying the query limit.
46    pub matched_before_limit: usize,
47}
48
49/// Cached runtime contract deck keyed by a cheap registry generation marker.
50#[derive(Clone, Debug, Default, PartialEq, Eq)]
51pub struct ContractDeckCache {
52    generation: Option<RegistryGeneration>,
53    deck: ContractDeck,
54    shape_index: BTreeMap<ContractCardKey, CardShapes>,
55    hits: usize,
56    misses: usize,
57    last_report: ContractQueryReport,
58}
59
60impl ContractDeckCache {
61    /// Creates an empty contract-deck cache.
62    pub fn new() -> Self {
63        Self::default()
64    }
65
66    /// Number of queries served from an unchanged registry generation.
67    pub fn hits(&self) -> usize {
68        self.hits
69    }
70
71    /// Number of times the deck was rebuilt for a new registry generation.
72    pub fn misses(&self) -> usize {
73        self.misses
74    }
75
76    /// Number of cards currently held by the cached deck.
77    pub fn cached_card_count(&self) -> usize {
78        self.deck.cards.len()
79    }
80
81    /// Facts recorded for the most recent query.
82    pub fn last_report(&self) -> &ContractQueryReport {
83        &self.last_report
84    }
85}
86
87/// Query a cached FORGE contract deck by argument and result Shape.
88///
89/// Argument matching is contravariant: a candidate callable must accept at
90/// least the requested arguments. Result matching is covariant: a candidate
91/// callable must return a shape contained by the requested result shape.
92pub fn query_contract_deck(
93    cx: &mut Cx,
94    cache: &mut ContractDeckCache,
95    query: &ShapeQuery,
96) -> Result<Vec<RankedContractCard>> {
97    let cache_hit = ensure_cached_deck(cx, cache)?;
98    let mut skipped_missing_shapes = 0;
99    let mut ranked = Vec::new();
100
101    for card in &cache.deck.cards {
102        let key = ContractCardKey::from_card(card);
103        let shapes = cache.shape_index.get(&key);
104        let mut score = 0;
105        let mut reasons = Vec::new();
106
107        if let Some(wanted) = &query.args {
108            let Some(candidate) = shapes.and_then(|shapes| shapes.args.as_ref()) else {
109                skipped_missing_shapes += 1;
110                continue;
111            };
112            let exact = shape_field_exact(cx, card.args_shape.as_ref(), wanted)?;
113            let Some(points) = shape_relation_score(
114                cx,
115                candidate,
116                wanted,
117                QueryRelation::Subsumes,
118                exact,
119                "args",
120            )?
121            else {
122                continue;
123            };
124            score += points.score;
125            reasons.extend(points.reasons);
126        }
127
128        if let Some(wanted) = &query.result {
129            let Some(candidate) = shapes.and_then(|shapes| shapes.result.as_ref()) else {
130                skipped_missing_shapes += 1;
131                continue;
132            };
133            let exact = shape_field_exact(cx, card.result_shape.as_ref(), wanted)?;
134            let Some(points) = shape_relation_score(
135                cx,
136                candidate,
137                wanted,
138                QueryRelation::SubshapeOf,
139                exact,
140                "result",
141            )?
142            else {
143                continue;
144            };
145            score += points.score;
146            reasons.extend(points.reasons);
147        }
148
149        if query.args.is_none() && query.result.is_none() {
150            reasons.push("unfiltered".to_owned());
151        }
152
153        ranked.push(RankedContractCard {
154            card: card.clone(),
155            score,
156            reasons,
157        });
158    }
159
160    ranked.sort_by(|left, right| {
161        right
162            .score
163            .cmp(&left.score)
164            .then_with(|| left.card.lib.cmp(&right.card.lib))
165            .then_with(|| left.card.symbol.cmp(&right.card.symbol))
166            .then_with(|| left.card.export_kind.cmp(&right.card.export_kind))
167    });
168
169    let matched_before_limit = ranked.len();
170    let capped_results = matched_before_limit.saturating_sub(query.limit);
171    ranked.truncate(query.limit);
172    cache.last_report = ContractQueryReport {
173        cache_hit,
174        skipped_missing_shapes,
175        capped_results,
176        matched_before_limit,
177    };
178
179    Ok(ranked)
180}
181
182fn ensure_cached_deck(cx: &mut Cx, cache: &mut ContractDeckCache) -> Result<bool> {
183    let generation = registry_generation(cx);
184    if cache.generation == Some(generation) {
185        cache.hits += 1;
186        return Ok(true);
187    }
188
189    cache.deck = assemble_contract_deck(cx)?;
190    cache.shape_index = collect_contract_shapes(cx)?;
191    cache.generation = Some(generation);
192    cache.misses += 1;
193    Ok(false)
194}
195
196#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
197struct RegistryGeneration {
198    lib_count: usize,
199    export_count: usize,
200    fingerprint: u64,
201}
202
203fn registry_generation(cx: &Cx) -> RegistryGeneration {
204    let mut marker = RegistryGeneration {
205        lib_count: 0,
206        export_count: 0,
207        fingerprint: FNV_OFFSET,
208    };
209    for loaded in cx.registry().libs() {
210        marker.lib_count += 1;
211        marker.export_count += loaded.exports.len();
212        mix_loaded_lib(&mut marker.fingerprint, loaded);
213    }
214    marker
215}
216
217const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
218const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
219
220fn mix_loaded_lib(hash: &mut u64, loaded: &LoadedLib) {
221    mix_u64(hash, loaded.id.0 as u64);
222    mix_symbol(hash, &loaded.manifest.id);
223    mix_bytes(hash, loaded.manifest.version.0.as_bytes());
224    mix_u64(hash, loaded.trusted as u64);
225    for export in &loaded.exports {
226        mix_export(hash, export);
227    }
228}
229
230fn mix_export(hash: &mut u64, export: &ExportRecord) {
231    mix_symbol(hash, export.kind.symbol());
232    mix_symbol(hash, &export.symbol);
233    mix_bytes(hash, format!("{:?}", export.state).as_bytes());
234}
235
236fn mix_symbol(hash: &mut u64, symbol: &Symbol) {
237    mix_bytes(hash, symbol.as_qualified_str().as_bytes());
238}
239
240fn mix_u64(hash: &mut u64, value: u64) {
241    mix_bytes(hash, &value.to_le_bytes());
242}
243
244fn mix_bytes(hash: &mut u64, bytes: &[u8]) {
245    for byte in bytes {
246        *hash ^= u64::from(*byte);
247        *hash = hash.wrapping_mul(FNV_PRIME);
248    }
249}
250
251#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
252struct ContractCardKey {
253    lib: Symbol,
254    export_kind: Symbol,
255    symbol: Symbol,
256}
257
258impl ContractCardKey {
259    fn from_card(card: &ContractCard) -> Self {
260        Self {
261            lib: card.lib.clone(),
262            export_kind: card.export_kind.clone(),
263            symbol: card.symbol.clone(),
264        }
265    }
266
267    fn from_export(loaded: &LoadedLib, export: &ExportRecord) -> Self {
268        Self {
269            lib: loaded.manifest.id.clone(),
270            export_kind: export.kind.symbol().clone(),
271            symbol: export.symbol.clone(),
272        }
273    }
274}
275
276#[derive(Clone, Debug, Default, PartialEq, Eq)]
277struct CardShapes {
278    args: Option<ShapeRef>,
279    result: Option<ShapeRef>,
280}
281
282fn collect_contract_shapes(cx: &mut Cx) -> Result<BTreeMap<ContractCardKey, CardShapes>> {
283    let loaded_libs = cx.registry().libs().to_vec();
284    let mut index = BTreeMap::new();
285
286    for loaded in loaded_libs {
287        for export in &loaded.exports {
288            let Some(value) = export_value(cx, export) else {
289                continue;
290            };
291            let Some(callable) = value.object().as_callable() else {
292                continue;
293            };
294            let args = callable.browse_args_shape(cx)?.filter(value_is_shape);
295            let result = callable.browse_result_shape(cx)?.filter(value_is_shape);
296            index.insert(
297                ContractCardKey::from_export(&loaded, export),
298                CardShapes { args, result },
299            );
300        }
301    }
302
303    Ok(index)
304}
305
306fn value_is_shape(value: &Value) -> bool {
307    value.object().as_shape().is_some()
308}
309
310#[derive(Clone, Copy, Debug, PartialEq, Eq)]
311enum QueryRelation {
312    Subsumes,
313    SubshapeOf,
314}
315
316fn query_relation_matches(
317    cx: &mut Cx,
318    candidate: &dyn Shape,
319    wanted: &dyn Shape,
320    relation: QueryRelation,
321) -> Result<bool> {
322    let relation_kind = relate_shapes(cx, candidate, wanted, &[])?.kind;
323    Ok(match relation {
324        QueryRelation::Subsumes => matches!(
325            relation_kind,
326            ShapeRelationKind::Equal | ShapeRelationKind::RightSubshape
327        ),
328        QueryRelation::SubshapeOf => matches!(
329            relation_kind,
330            ShapeRelationKind::Equal | ShapeRelationKind::LeftSubshape
331        ),
332    })
333}
334
335#[derive(Clone, Debug, PartialEq, Eq)]
336struct ShapeScore {
337    score: i32,
338    reasons: Vec<String>,
339}
340
341fn shape_relation_score(
342    cx: &mut Cx,
343    candidate: &ShapeRef,
344    wanted: &ShapeRef,
345    relation: QueryRelation,
346    exact: bool,
347    label: &str,
348) -> Result<Option<ShapeScore>> {
349    let candidate = shape_ref_as_shape("candidate", candidate)?;
350    let wanted = shape_ref_as_shape("wanted", wanted)?;
351    if !query_relation_matches(cx, candidate, wanted, relation)? {
352        return Ok(None);
353    }
354
355    let relation_kind = relate_shapes(cx, candidate, wanted, &[])?.kind;
356    let (mut score, relation_reason) = match (relation, relation_kind) {
357        (_, ShapeRelationKind::Equal) => (120, format!("{label} exact")),
358        (QueryRelation::Subsumes, ShapeRelationKind::RightSubshape) => {
359            (80, format!("{label} subsumes query"))
360        }
361        (QueryRelation::SubshapeOf, ShapeRelationKind::LeftSubshape) => {
362            (90, format!("{label} narrows query"))
363        }
364        _ => return Ok(None),
365    };
366    let mut reasons = vec![relation_reason];
367    if exact {
368        score += 10;
369        reasons.push(format!("{label} field exact"));
370    }
371    Ok(Some(ShapeScore { score, reasons }))
372}
373
374fn shape_ref_as_shape<'a>(label: &str, value: &'a ShapeRef) -> Result<&'a dyn Shape> {
375    value
376        .object()
377        .as_shape()
378        .ok_or_else(|| Error::Eval(format!("{label} ShapeQuery value is not a Shape")))
379}
380
381fn shape_field_exact(cx: &mut Cx, candidate: Option<&Expr>, wanted: &ShapeRef) -> Result<bool> {
382    let Some(candidate) = candidate else {
383        return Ok(false);
384    };
385    Ok(candidate == &wanted.object().as_expr(cx)?)
386}