use std::collections::BTreeMap;
use sim_kernel::{
Cx, Error, Expr, Result, ShapeRef, Symbol, Value,
library::{ExportRecord, LoadedLib},
};
use sim_shape::{Shape, ShapeRelationKind, relate_shapes};
use crate::contracts::{assemble_contract_deck, export_value};
use crate::{ContractCard, ContractDeck};
#[derive(Clone)]
pub struct ShapeQuery {
pub args: Option<ShapeRef>,
pub result: Option<ShapeRef>,
pub limit: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RankedContractCard {
pub card: ContractCard,
pub score: i32,
pub reasons: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ContractQueryReport {
pub cache_hit: bool,
pub skipped_missing_shapes: usize,
pub capped_results: usize,
pub matched_before_limit: usize,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ContractDeckCache {
generation: Option<RegistryGeneration>,
deck: ContractDeck,
shape_index: BTreeMap<ContractCardKey, CardShapes>,
hits: usize,
misses: usize,
last_report: ContractQueryReport,
}
impl ContractDeckCache {
pub fn new() -> Self {
Self::default()
}
pub fn hits(&self) -> usize {
self.hits
}
pub fn misses(&self) -> usize {
self.misses
}
pub fn cached_card_count(&self) -> usize {
self.deck.cards.len()
}
pub fn last_report(&self) -> &ContractQueryReport {
&self.last_report
}
}
pub fn query_contract_deck(
cx: &mut Cx,
cache: &mut ContractDeckCache,
query: &ShapeQuery,
) -> Result<Vec<RankedContractCard>> {
let cache_hit = ensure_cached_deck(cx, cache)?;
let mut skipped_missing_shapes = 0;
let mut ranked = Vec::new();
for card in &cache.deck.cards {
let key = ContractCardKey::from_card(card);
let shapes = cache.shape_index.get(&key);
let mut score = 0;
let mut reasons = Vec::new();
if let Some(wanted) = &query.args {
let Some(candidate) = shapes.and_then(|shapes| shapes.args.as_ref()) else {
skipped_missing_shapes += 1;
continue;
};
let exact = shape_field_exact(cx, card.args_shape.as_ref(), wanted)?;
let Some(points) = shape_relation_score(
cx,
candidate,
wanted,
QueryRelation::Subsumes,
exact,
"args",
)?
else {
continue;
};
score += points.score;
reasons.extend(points.reasons);
}
if let Some(wanted) = &query.result {
let Some(candidate) = shapes.and_then(|shapes| shapes.result.as_ref()) else {
skipped_missing_shapes += 1;
continue;
};
let exact = shape_field_exact(cx, card.result_shape.as_ref(), wanted)?;
let Some(points) = shape_relation_score(
cx,
candidate,
wanted,
QueryRelation::SubshapeOf,
exact,
"result",
)?
else {
continue;
};
score += points.score;
reasons.extend(points.reasons);
}
if query.args.is_none() && query.result.is_none() {
reasons.push("unfiltered".to_owned());
}
ranked.push(RankedContractCard {
card: card.clone(),
score,
reasons,
});
}
ranked.sort_by(|left, right| {
right
.score
.cmp(&left.score)
.then_with(|| left.card.lib.cmp(&right.card.lib))
.then_with(|| left.card.symbol.cmp(&right.card.symbol))
.then_with(|| left.card.export_kind.cmp(&right.card.export_kind))
});
let matched_before_limit = ranked.len();
let capped_results = matched_before_limit.saturating_sub(query.limit);
ranked.truncate(query.limit);
cache.last_report = ContractQueryReport {
cache_hit,
skipped_missing_shapes,
capped_results,
matched_before_limit,
};
Ok(ranked)
}
fn ensure_cached_deck(cx: &mut Cx, cache: &mut ContractDeckCache) -> Result<bool> {
let generation = registry_generation(cx);
if cache.generation == Some(generation) {
cache.hits += 1;
return Ok(true);
}
cache.deck = assemble_contract_deck(cx)?;
cache.shape_index = collect_contract_shapes(cx)?;
cache.generation = Some(generation);
cache.misses += 1;
Ok(false)
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
struct RegistryGeneration {
lib_count: usize,
export_count: usize,
fingerprint: u64,
}
fn registry_generation(cx: &Cx) -> RegistryGeneration {
let mut marker = RegistryGeneration {
lib_count: 0,
export_count: 0,
fingerprint: FNV_OFFSET,
};
for loaded in cx.registry().libs() {
marker.lib_count += 1;
marker.export_count += loaded.exports.len();
mix_loaded_lib(&mut marker.fingerprint, loaded);
}
marker
}
const FNV_OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
fn mix_loaded_lib(hash: &mut u64, loaded: &LoadedLib) {
mix_u64(hash, loaded.id.0 as u64);
mix_symbol(hash, &loaded.manifest.id);
mix_bytes(hash, loaded.manifest.version.0.as_bytes());
mix_u64(hash, loaded.trusted as u64);
for export in &loaded.exports {
mix_export(hash, export);
}
}
fn mix_export(hash: &mut u64, export: &ExportRecord) {
mix_symbol(hash, export.kind.symbol());
mix_symbol(hash, &export.symbol);
mix_bytes(hash, format!("{:?}", export.state).as_bytes());
}
fn mix_symbol(hash: &mut u64, symbol: &Symbol) {
mix_bytes(hash, symbol.as_qualified_str().as_bytes());
}
fn mix_u64(hash: &mut u64, value: u64) {
mix_bytes(hash, &value.to_le_bytes());
}
fn mix_bytes(hash: &mut u64, bytes: &[u8]) {
for byte in bytes {
*hash ^= u64::from(*byte);
*hash = hash.wrapping_mul(FNV_PRIME);
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
struct ContractCardKey {
lib: Symbol,
export_kind: Symbol,
symbol: Symbol,
}
impl ContractCardKey {
fn from_card(card: &ContractCard) -> Self {
Self {
lib: card.lib.clone(),
export_kind: card.export_kind.clone(),
symbol: card.symbol.clone(),
}
}
fn from_export(loaded: &LoadedLib, export: &ExportRecord) -> Self {
Self {
lib: loaded.manifest.id.clone(),
export_kind: export.kind.symbol().clone(),
symbol: export.symbol.clone(),
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct CardShapes {
args: Option<ShapeRef>,
result: Option<ShapeRef>,
}
fn collect_contract_shapes(cx: &mut Cx) -> Result<BTreeMap<ContractCardKey, CardShapes>> {
let loaded_libs = cx.registry().libs().to_vec();
let mut index = BTreeMap::new();
for loaded in loaded_libs {
for export in &loaded.exports {
let Some(value) = export_value(cx, export) else {
continue;
};
let Some(callable) = value.object().as_callable() else {
continue;
};
let args = callable.browse_args_shape(cx)?.filter(value_is_shape);
let result = callable.browse_result_shape(cx)?.filter(value_is_shape);
index.insert(
ContractCardKey::from_export(&loaded, export),
CardShapes { args, result },
);
}
}
Ok(index)
}
fn value_is_shape(value: &Value) -> bool {
value.object().as_shape().is_some()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum QueryRelation {
Subsumes,
SubshapeOf,
}
fn query_relation_matches(
cx: &mut Cx,
candidate: &dyn Shape,
wanted: &dyn Shape,
relation: QueryRelation,
) -> Result<bool> {
let relation_kind = relate_shapes(cx, candidate, wanted, &[])?.kind;
Ok(match relation {
QueryRelation::Subsumes => matches!(
relation_kind,
ShapeRelationKind::Equal | ShapeRelationKind::RightSubshape
),
QueryRelation::SubshapeOf => matches!(
relation_kind,
ShapeRelationKind::Equal | ShapeRelationKind::LeftSubshape
),
})
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct ShapeScore {
score: i32,
reasons: Vec<String>,
}
fn shape_relation_score(
cx: &mut Cx,
candidate: &ShapeRef,
wanted: &ShapeRef,
relation: QueryRelation,
exact: bool,
label: &str,
) -> Result<Option<ShapeScore>> {
let candidate = shape_ref_as_shape("candidate", candidate)?;
let wanted = shape_ref_as_shape("wanted", wanted)?;
if !query_relation_matches(cx, candidate, wanted, relation)? {
return Ok(None);
}
let relation_kind = relate_shapes(cx, candidate, wanted, &[])?.kind;
let (mut score, relation_reason) = match (relation, relation_kind) {
(_, ShapeRelationKind::Equal) => (120, format!("{label} exact")),
(QueryRelation::Subsumes, ShapeRelationKind::RightSubshape) => {
(80, format!("{label} subsumes query"))
}
(QueryRelation::SubshapeOf, ShapeRelationKind::LeftSubshape) => {
(90, format!("{label} narrows query"))
}
_ => return Ok(None),
};
let mut reasons = vec![relation_reason];
if exact {
score += 10;
reasons.push(format!("{label} field exact"));
}
Ok(Some(ShapeScore { score, reasons }))
}
fn shape_ref_as_shape<'a>(label: &str, value: &'a ShapeRef) -> Result<&'a dyn Shape> {
value
.object()
.as_shape()
.ok_or_else(|| Error::Eval(format!("{label} ShapeQuery value is not a Shape")))
}
fn shape_field_exact(cx: &mut Cx, candidate: Option<&Expr>, wanted: &ShapeRef) -> Result<bool> {
let Some(candidate) = candidate else {
return Ok(false);
};
Ok(candidate == &wanted.object().as_expr(cx)?)
}