1use 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#[derive(Clone)]
16pub struct ShapeQuery {
17 pub args: Option<ShapeRef>,
19 pub result: Option<ShapeRef>,
21 pub limit: usize,
23}
24
25#[derive(Clone, Debug, PartialEq, Eq)]
27pub struct RankedContractCard {
28 pub card: ContractCard,
30 pub score: i32,
32 pub reasons: Vec<String>,
34}
35
36#[derive(Clone, Debug, Default, PartialEq, Eq)]
38pub struct ContractQueryReport {
39 pub cache_hit: bool,
41 pub skipped_missing_shapes: usize,
43 pub capped_results: usize,
45 pub matched_before_limit: usize,
47}
48
49#[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 pub fn new() -> Self {
63 Self::default()
64 }
65
66 pub fn hits(&self) -> usize {
68 self.hits
69 }
70
71 pub fn misses(&self) -> usize {
73 self.misses
74 }
75
76 pub fn cached_card_count(&self) -> usize {
78 self.deck.cards.len()
79 }
80
81 pub fn last_report(&self) -> &ContractQueryReport {
83 &self.last_report
84 }
85}
86
87pub 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}