Skip to main content

sim_shape/functions/
browse_signature.rs

1//! Non-enforcing callable signature metadata.
2//!
3//! This adapter is deliberately observational: it delegates both evaluated
4//! and raw-expression calls to the wrapped callable and supplies only the two
5//! browse slots defined by the kernel's [`Callable`] protocol.
6
7use std::sync::Arc;
8
9use sim_kernel::{
10    Args, Callable, ClaimPattern, ClaimSink, ClassRef, Cx, Datum, Error, Object, ObjectCompat,
11    ObjectHeader, OpKey, RawArgs, Result, ShapeRef, Value,
12};
13
14/// A callable decorated with argument and result Shapes used only by browsers.
15///
16/// Construction fails closed when `callable` is not callable or either
17/// metadata value does not expose the Shape protocol. Invocation never reads
18/// the metadata and therefore cannot check, coerce, rank, or select calls.
19#[derive(Clone)]
20pub struct BrowseSignature {
21    callable: Value,
22    args: Option<ShapeRef>,
23    result: Option<ShapeRef>,
24}
25
26impl BrowseSignature {
27    /// Validate and build a browse-only signature around `callable`.
28    pub fn new(callable: Value, args: Option<ShapeRef>, result: Option<ShapeRef>) -> Result<Self> {
29        if callable.object().as_callable().is_none() {
30            return Err(Error::HostError(
31                "browse signature requires a callable value".to_owned(),
32            ));
33        }
34        for (slot, shape) in [("arguments", args.as_ref()), ("result", result.as_ref())] {
35            if shape.is_some_and(|value| value.object().as_shape().is_none()) {
36                return Err(Error::HostError(format!(
37                    "browse signature {slot} metadata must be a Shape value"
38                )));
39            }
40        }
41        Ok(Self {
42            callable,
43            args,
44            result,
45        })
46    }
47
48    fn inner(&self) -> &dyn Callable {
49        self.callable
50            .object()
51            .as_callable()
52            .expect("BrowseSignature construction validated the callable")
53    }
54}
55
56/// Wrap a callable as a runtime value with non-enforcing browse metadata.
57pub fn browse_signature(
58    cx: &mut Cx,
59    callable: Value,
60    args: Option<ShapeRef>,
61    result: Option<ShapeRef>,
62) -> Result<Value> {
63    cx.factory()
64        .opaque(Arc::new(BrowseSignature::new(callable, args, result)?))
65}
66
67impl Object for BrowseSignature {
68    fn header(&self) -> &ObjectHeader {
69        self.callable.object().header()
70    }
71
72    fn op(&self, key: &OpKey) -> Option<&dyn sim_kernel::Op> {
73        self.callable.object().op(key)
74    }
75
76    fn claims(&self, cx: &mut Cx, pattern: &ClaimPattern, sink: &mut dyn ClaimSink) -> Result<()> {
77        self.callable.object().claims(cx, pattern, sink)
78    }
79
80    fn snapshot(&self, cx: &mut Cx) -> Result<Option<Datum>> {
81        self.callable.object().snapshot(cx)
82    }
83
84    fn display(&self, cx: &mut Cx) -> Result<String> {
85        self.callable.object().display(cx)
86    }
87
88    fn as_any(&self) -> &dyn std::any::Any {
89        self
90    }
91}
92
93impl ObjectCompat for BrowseSignature {
94    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
95        self.callable.object().class(cx)
96    }
97
98    fn as_callable(&self) -> Option<&dyn Callable> {
99        Some(self)
100    }
101
102    fn as_expr(&self, cx: &mut Cx) -> Result<sim_kernel::Expr> {
103        self.callable.object().as_expr(cx)
104    }
105
106    fn truth(&self, cx: &mut Cx) -> Result<bool> {
107        self.callable.object().truth(cx)
108    }
109
110    fn as_table(&self, cx: &mut Cx) -> Result<Value> {
111        self.callable.object().as_table(cx)
112    }
113}
114
115impl Callable for BrowseSignature {
116    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
117        self.inner().call(cx, args)
118    }
119
120    fn browse_args_shape(&self, _cx: &mut Cx) -> Result<Option<ShapeRef>> {
121        Ok(self.args.clone())
122    }
123
124    fn browse_result_shape(&self, _cx: &mut Cx) -> Result<Option<ShapeRef>> {
125        Ok(self.result.clone())
126    }
127
128    fn call_exprs(&self, cx: &mut Cx, args: RawArgs) -> Result<Value> {
129        self.inner().call_exprs(cx, args)
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use std::sync::{
136        Arc,
137        atomic::{AtomicUsize, Ordering},
138    };
139
140    use sim_kernel::{Expr, Object, ObjectCompat, RawArgs, Symbol, testing::bare_cx as cx};
141
142    use super::*;
143    use crate::{
144        AnyShape, ExactExprShape, ExprKindShape, ListShape, OneOfShape, Shape, shape_value,
145    };
146
147    struct Specimen {
148        evaluated_calls: Arc<AtomicUsize>,
149        raw_calls: Arc<AtomicUsize>,
150    }
151
152    impl Object for Specimen {
153        fn display(&self, _cx: &mut Cx) -> Result<String> {
154            Ok("#<non-typescript-specimen>".to_owned())
155        }
156
157        fn as_any(&self) -> &dyn std::any::Any {
158            self
159        }
160    }
161
162    impl ObjectCompat for Specimen {
163        fn as_callable(&self) -> Option<&dyn Callable> {
164            Some(self)
165        }
166    }
167
168    impl Callable for Specimen {
169        fn call(&self, cx: &mut Cx, _args: Args) -> Result<Value> {
170            self.evaluated_calls.fetch_add(1, Ordering::SeqCst);
171            cx.factory().string("unchecked-result".to_owned())
172        }
173
174        fn call_exprs(&self, cx: &mut Cx, args: RawArgs) -> Result<Value> {
175            self.raw_calls.fetch_add(1, Ordering::SeqCst);
176            cx.factory().expr(Expr::List(args.into_exprs()))
177        }
178    }
179
180    fn specimen(cx: &mut Cx) -> (Value, Arc<AtomicUsize>, Arc<AtomicUsize>) {
181        let evaluated_calls = Arc::new(AtomicUsize::new(0));
182        let raw_calls = Arc::new(AtomicUsize::new(0));
183        let value = cx
184            .factory()
185            .opaque(Arc::new(Specimen {
186                evaluated_calls: evaluated_calls.clone(),
187                raw_calls: raw_calls.clone(),
188            }))
189            .unwrap();
190        (value, evaluated_calls, raw_calls)
191    }
192
193    #[test]
194    fn metadata_is_observational_for_evaluated_and_raw_calls() {
195        let mut cx = cx();
196        let (inner, evaluated_calls, raw_calls) = specimen(&mut cx);
197        let impossible_args = shape_value(
198            Symbol::new("literal-args"),
199            Arc::new(ExactExprShape::new(Expr::List(vec![Expr::String(
200                "never supplied".to_owned(),
201            )]))),
202        );
203        let impossible_result = shape_value(
204            Symbol::new("number-result"),
205            Arc::new(ExprKindShape::new(sim_kernel::ExprKind::Number)),
206        );
207        let wrapped = browse_signature(
208            &mut cx,
209            inner,
210            Some(impossible_args.clone()),
211            Some(impossible_result.clone()),
212        )
213        .unwrap();
214        let callable = wrapped.object().as_callable().unwrap();
215
216        let result = callable.call(&mut cx, Args::new(Vec::new())).unwrap();
217        assert_eq!(
218            result.object().display(&mut cx).unwrap(),
219            "unchecked-result"
220        );
221        assert_eq!(evaluated_calls.load(Ordering::SeqCst), 1);
222
223        let raw = vec![Expr::Symbol(Symbol::new("unevaluated"))];
224        let result = callable
225            .call_exprs(&mut cx, RawArgs::new(raw.clone()))
226            .unwrap();
227        assert_eq!(result.object().as_expr(&mut cx).unwrap(), Expr::List(raw));
228        assert_eq!(raw_calls.load(Ordering::SeqCst), 1);
229        assert_eq!(evaluated_calls.load(Ordering::SeqCst), 1);
230        assert_eq!(
231            callable.browse_args_shape(&mut cx).unwrap(),
232            Some(impossible_args)
233        );
234        assert_eq!(
235            callable.browse_result_shape(&mut cx).unwrap(),
236            Some(impossible_result)
237        );
238    }
239
240    #[test]
241    fn faithful_shape_categories_are_retained_and_non_shapes_fail_closed() {
242        let mut cx = cx();
243        let categories: Vec<(&str, Arc<dyn Shape>)> = vec![
244            (
245                "primitive",
246                Arc::new(ExprKindShape::new(sim_kernel::ExprKind::Bool)),
247            ),
248            ("literal", Arc::new(ExactExprShape::new(Expr::Bool(true)))),
249            (
250                "union",
251                Arc::new(OneOfShape::new(vec![
252                    Arc::new(ExprKindShape::new(sim_kernel::ExprKind::Bool)),
253                    Arc::new(ExprKindShape::new(sim_kernel::ExprKind::String)),
254                ])),
255            ),
256            (
257                "tuple-or-array",
258                Arc::new(ListShape::new(vec![Arc::new(AnyShape)])),
259            ),
260            ("bounded-named", Arc::new(AnyShape)),
261        ];
262
263        for (category, shape) in categories {
264            let (inner, _, _) = specimen(&mut cx);
265            let metadata = shape_value(Symbol::qualified("law", category), shape);
266            let wrapped = browse_signature(&mut cx, inner, Some(metadata.clone()), None).unwrap();
267            assert_eq!(
268                wrapped
269                    .object()
270                    .as_callable()
271                    .unwrap()
272                    .browse_args_shape(&mut cx)
273                    .unwrap(),
274                Some(metadata),
275                "{category} metadata must be retained without projection"
276            );
277        }
278
279        let (inner, _, _) = specimen(&mut cx);
280        let not_a_shape = cx
281            .factory()
282            .string("conditional type widened to any".to_owned())
283            .unwrap();
284        let error = BrowseSignature::new(inner, Some(not_a_shape), None)
285            .err()
286            .expect("non-equivalent metadata must be rejected");
287        assert!(error.to_string().contains("must be a Shape value"));
288    }
289}