Skip to main content

sim_lib_view/
dispatch.rs

1//! Shape-based lens dispatch.
2//!
3//! Choosing a lens for a value is overload selection, which is exactly what the
4//! kernel `Shape` matcher already does. The dispatcher reuses that matcher and
5//! the documented resolution order; it is implemented once here and never
6//! reimplemented per domain. Resolution order:
7//!
8//! 1. explicit operator choice;
9//! 2. saved workspace preference;
10//! 3. best Shape match (most specific wins; ties by quality then cost);
11//! 4. class match fallback;
12//! 5. the universal default (always matches, lowest quality).
13//!
14//! Every candidate must pass capability filtering; a denied lens is skipped and
15//! resolution falls through, ending at the read-only universal default.
16
17use std::collections::BTreeMap;
18use std::sync::Arc;
19
20use sim_kernel::{CapabilityName, Cx, Error, Expr, Result, Symbol};
21
22use crate::codec::SurfaceCodec;
23use crate::contract::{Lens, LensKind};
24
25/// The context a dispatch runs in: operator choice, saved preference, active
26/// mode, the value's class, and the capability predicate.
27pub struct DispatchContext<'a> {
28    /// An explicit lens choice (from `intent/set-lens`).
29    pub explicit: Option<Symbol>,
30    /// A saved workspace preference for this resource.
31    pub preference: Option<Symbol>,
32    /// The active experience mode, if any.
33    pub active_mode: Option<Symbol>,
34    /// The value's class symbol, for the class-match fallback.
35    pub value_class: Option<Symbol>,
36    /// Returns whether a capability is granted to the operator.
37    pub granted: &'a dyn Fn(&CapabilityName) -> bool,
38}
39
40impl<'a> DispatchContext<'a> {
41    /// A context that grants every capability and has no preferences.
42    pub fn permissive(grant_all: &'a dyn Fn(&CapabilityName) -> bool) -> Self {
43        Self {
44            explicit: None,
45            preference: None,
46            active_mode: None,
47            value_class: None,
48            granted: grant_all,
49        }
50    }
51}
52
53/// Why the dispatcher chose a lens.
54#[derive(Clone, Debug, PartialEq, Eq)]
55pub enum DispatchReason {
56    /// Selected by explicit operator choice.
57    Explicit,
58    /// Selected by saved workspace preference.
59    Preference,
60    /// Selected as the best Shape match, with the winning match score.
61    ShapeMatch(i32),
62    /// Selected by class-match fallback.
63    ClassMatch,
64    /// Selected as the universal default.
65    UniversalDefault,
66}
67
68/// The outcome of a successful dispatch.
69#[derive(Clone, Debug, PartialEq, Eq)]
70pub struct DispatchOutcome {
71    /// The chosen lens id.
72    pub lens_id: Symbol,
73    /// Why it was chosen.
74    pub reason: DispatchReason,
75}
76
77/// A registry of lenses with a single shared dispatcher.
78#[derive(Default)]
79pub struct LensRegistry {
80    lenses: Vec<Lens>,
81    surface_codecs: BTreeMap<Symbol, Arc<dyn SurfaceCodec>>,
82}
83
84impl LensRegistry {
85    /// An empty registry.
86    pub fn new() -> Self {
87        Self::default()
88    }
89
90    /// Register a lens (last registration of an id wins on exact ties).
91    pub fn register(&mut self, lens: Lens) {
92        self.lenses.push(lens);
93    }
94
95    /// Register a reversible surface codec.
96    pub fn register_surface_codec(&mut self, id: Symbol, codec: Arc<dyn SurfaceCodec>) {
97        self.surface_codecs.insert(id, codec);
98    }
99
100    /// Look up a reversible surface codec by id.
101    pub fn surface_codec(&self, id: &Symbol) -> Option<Arc<dyn SurfaceCodec>> {
102        self.surface_codecs.get(id).cloned()
103    }
104
105    /// Look up a lens by id.
106    pub fn get(&self, id: &Symbol) -> Option<&Lens> {
107        self.lenses.iter().find(|lens| &lens.meta.id == id)
108    }
109
110    /// All registered lenses.
111    pub fn lenses(&self) -> &[Lens] {
112        &self.lenses
113    }
114
115    /// Dispatch a `View` lens for `target`.
116    pub fn dispatch_view(
117        &self,
118        cx: &mut Cx,
119        target: &Expr,
120        ctx: &DispatchContext,
121    ) -> Result<DispatchOutcome> {
122        self.dispatch(cx, LensKind::View, target, ctx)
123    }
124
125    /// Dispatch an `Editor` lens for `target`.
126    pub fn dispatch_editor(
127        &self,
128        cx: &mut Cx,
129        target: &Expr,
130        ctx: &DispatchContext,
131    ) -> Result<DispatchOutcome> {
132        self.dispatch(cx, LensKind::Editor, target, ctx)
133    }
134
135    /// Resolve a lens of `kind` for `target` per the documented order.
136    pub fn dispatch(
137        &self,
138        cx: &mut Cx,
139        kind: LensKind,
140        target: &Expr,
141        ctx: &DispatchContext,
142    ) -> Result<DispatchOutcome> {
143        // 1. explicit operator choice.
144        if let Some(outcome) = self.pick_named(&ctx.explicit, kind, ctx, DispatchReason::Explicit) {
145            return Ok(outcome);
146        }
147        // 2. saved workspace preference.
148        if let Some(outcome) =
149            self.pick_named(&ctx.preference, kind, ctx, DispatchReason::Preference)
150        {
151            return Ok(outcome);
152        }
153        // 3. best Shape match (most specific wins; ties by quality then cost).
154        if let Some(outcome) = self.pick_shape_match(cx, kind, target, ctx)? {
155            return Ok(outcome);
156        }
157        // 4. class match fallback.
158        if let Some(outcome) = self.pick_class_match(kind, ctx) {
159            return Ok(outcome);
160        }
161        // 5. universal default.
162        if let Some(lens) = self.lenses.iter().find(|lens| {
163            lens.meta.kind == kind && lens.meta.universal_default && self.allowed(lens, ctx)
164        }) {
165            return Ok(DispatchOutcome {
166                lens_id: lens.meta.id.clone(),
167                reason: DispatchReason::UniversalDefault,
168            });
169        }
170        Err(Error::HostError(format!(
171            "no {kind:?} lens available for the value (not even a universal default)"
172        )))
173    }
174
175    pub(crate) fn allowed(&self, lens: &Lens, ctx: &DispatchContext) -> bool {
176        lens.meta
177            .required_capabilities
178            .iter()
179            .all(|capability| (ctx.granted)(capability))
180    }
181
182    fn pick_named(
183        &self,
184        id: &Option<Symbol>,
185        kind: LensKind,
186        ctx: &DispatchContext,
187        reason: DispatchReason,
188    ) -> Option<DispatchOutcome> {
189        let id = id.as_ref()?;
190        let lens = self.get(id)?;
191        if lens.meta.kind == kind && self.allowed(lens, ctx) {
192            Some(DispatchOutcome {
193                lens_id: id.clone(),
194                reason,
195            })
196        } else {
197            None
198        }
199    }
200
201    fn pick_shape_match(
202        &self,
203        cx: &mut Cx,
204        kind: LensKind,
205        target: &Expr,
206        ctx: &DispatchContext,
207    ) -> Result<Option<DispatchOutcome>> {
208        let mut best: Option<(i32, i32, i32, Symbol)> = None;
209        for lens in &self.lenses {
210            if lens.meta.kind != kind || lens.meta.universal_default || !self.allowed(lens, ctx) {
211                continue;
212            }
213            let Some(score) = best_shape_score(cx, lens, target)? else {
214                continue;
215            };
216            // Rank by (score, quality, -cost); first registered wins exact ties.
217            let candidate = (score, lens.meta.quality, -lens.meta.cost);
218            let better = match &best {
219                Some((bs, bq, bc, _)) => candidate > (*bs, *bq, *bc),
220                None => true,
221            };
222            if better {
223                best = Some((candidate.0, candidate.1, candidate.2, lens.meta.id.clone()));
224            }
225        }
226        Ok(best.map(|(score, _, _, lens_id)| DispatchOutcome {
227            lens_id,
228            reason: DispatchReason::ShapeMatch(score),
229        }))
230    }
231
232    fn pick_class_match(&self, kind: LensKind, ctx: &DispatchContext) -> Option<DispatchOutcome> {
233        let class = ctx.value_class.as_ref()?;
234        let mut best: Option<(i32, i32, Symbol)> = None;
235        for lens in &self.lenses {
236            if lens.meta.kind != kind
237                || lens.meta.universal_default
238                || !self.allowed(lens, ctx)
239                || !lens.meta.claimed_classes.contains(class)
240            {
241                continue;
242            }
243            let candidate = (lens.meta.quality, -lens.meta.cost);
244            let better = match &best {
245                Some((bq, bc, _)) => candidate > (*bq, *bc),
246                None => true,
247            };
248            if better {
249                best = Some((candidate.0, candidate.1, lens.meta.id.clone()));
250            }
251        }
252        best.map(|(_, _, lens_id)| DispatchOutcome {
253            lens_id,
254            reason: DispatchReason::ClassMatch,
255        })
256    }
257}
258
259/// The best accepted Shape match score among a lens's claimed Shapes, if any.
260pub(crate) fn best_shape_score(cx: &mut Cx, lens: &Lens, target: &Expr) -> Result<Option<i32>> {
261    let mut best: Option<i32> = None;
262    for shape_value in &lens.meta.claimed_shapes {
263        let Some(shape) = shape_value.object().as_shape() else {
264            continue;
265        };
266        let matched = shape.check_expr(cx, target)?;
267        if matched.accepted {
268            let score = matched.score.value();
269            best = Some(best.map_or(score, |current| current.max(score)));
270        }
271    }
272    Ok(best)
273}