Skip to main content

sim_lib_control/
ops.rs

1use std::sync::Arc;
2
3use sim_kernel::{
4    Args, Callable, ClassRef, Cx, Error, Expr, NumberLiteral, Object, ObjectCompat, RawArgs, Ref,
5    Result, Symbol, Value,
6    control::{
7        ControlAbort, ControlCapture, ControlPrompt, ControlResume, abort, capture,
8        default_control_result_shape, prompt, resume,
9    },
10};
11
12use crate::model::{ContinuationValue, ControlResultValue};
13
14/// A callable runtime object exposing one control primitive.
15///
16/// The core [`ControlFunction`] variants (`prompt`, `capture`, `abort`,
17/// `resume`) are installed by the control lib as `control/*` functions, turning
18/// the kernel control-policy operations into callables the runtime can invoke.
19#[derive(Clone)]
20pub struct ControlFunction {
21    kind: ControlFunctionKind,
22}
23
24#[derive(Clone, Copy)]
25enum ControlFunctionKind {
26    Prompt,
27    Capture,
28    Abort,
29    Resume,
30    PhysicalSensingTrace,
31}
32
33impl ControlFunction {
34    /// Builds the `control/prompt` function, which establishes a prompt.
35    pub fn prompt() -> Self {
36        Self {
37            kind: ControlFunctionKind::Prompt,
38        }
39    }
40
41    /// Builds the `control/capture` function, which captures a continuation.
42    pub fn capture() -> Self {
43        Self {
44            kind: ControlFunctionKind::Capture,
45        }
46    }
47
48    /// Builds the `control/abort` function, which aborts to a prompt.
49    pub fn abort() -> Self {
50        Self {
51            kind: ControlFunctionKind::Abort,
52        }
53    }
54
55    /// Builds the `control/resume` function, which resumes a continuation.
56    pub fn resume() -> Self {
57        Self {
58            kind: ControlFunctionKind::Resume,
59        }
60    }
61
62    /// Builds the deterministic physical-sensing descriptor fixture.
63    pub fn physical_sensing_trace() -> Self {
64        Self {
65            kind: ControlFunctionKind::PhysicalSensingTrace,
66        }
67    }
68
69    /// Returns the `control/*` symbol under which this function is exported.
70    pub fn symbol(&self) -> Symbol {
71        self.kind.symbol()
72    }
73}
74
75impl Object for ControlFunction {
76    fn display(&self, _cx: &mut Cx) -> Result<String> {
77        Ok(format!("#<function {}>", self.kind.symbol()))
78    }
79
80    fn as_any(&self) -> &dyn std::any::Any {
81        self
82    }
83}
84
85impl ObjectCompat for ControlFunction {
86    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
87        cx.resolve_class(&Symbol::qualified("core", "Function"))
88    }
89
90    fn as_callable(&self) -> Option<&dyn Callable> {
91        Some(self)
92    }
93}
94
95impl Callable for ControlFunction {
96    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
97        self.kind.call(cx, args.into_vec())
98    }
99
100    fn call_exprs(&self, cx: &mut Cx, args: RawArgs) -> Result<Value> {
101        let values = args
102            .into_exprs()
103            .into_iter()
104            .map(|expr| cx.eval_expr(expr))
105            .collect::<Result<Vec<_>>>()?;
106        self.kind.call(cx, values)
107    }
108}
109
110impl ControlFunctionKind {
111    fn symbol(self) -> Symbol {
112        match self {
113            Self::Prompt => prompt_symbol(),
114            Self::Capture => capture_symbol(),
115            Self::Abort => abort_symbol(),
116            Self::Resume => resume_symbol(),
117            Self::PhysicalSensingTrace => physical_sensing_trace_symbol(),
118        }
119    }
120
121    fn call(self, cx: &mut Cx, args: Vec<Value>) -> Result<Value> {
122        match self {
123            Self::Prompt => call_prompt(cx, args),
124            Self::Capture => call_capture(cx, args),
125            Self::Abort => call_abort(cx, args),
126            Self::Resume => call_resume(cx, args),
127            Self::PhysicalSensingTrace => call_physical_sensing_trace(cx, args),
128        }
129    }
130}
131
132/// Returns the `control/prompt` symbol.
133pub fn prompt_symbol() -> Symbol {
134    Symbol::qualified("control", "prompt")
135}
136
137/// Returns the `control/capture` symbol.
138pub fn capture_symbol() -> Symbol {
139    Symbol::qualified("control", "capture")
140}
141
142/// Returns the `control/abort` symbol.
143pub fn abort_symbol() -> Symbol {
144    Symbol::qualified("control", "abort")
145}
146
147/// Returns the `control/resume` symbol.
148pub fn resume_symbol() -> Symbol {
149    Symbol::qualified("control", "resume")
150}
151
152/// Returns the `control/physical-sensing-trace` fixture symbol.
153pub fn physical_sensing_trace_symbol() -> Symbol {
154    Symbol::qualified("control", "physical-sensing-trace")
155}
156
157fn call_prompt(cx: &mut Cx, args: Vec<Value>) -> Result<Value> {
158    let refs = refs_from_args(cx, args, "control/prompt")?;
159    let [prompt_ref, value_ref] = refs.as_slice() else {
160        return Err(arity_error("control/prompt", "prompt value"));
161    };
162    let prompt_ref = prompt_ref.clone();
163    let value_ref = value_ref.clone();
164    let result = prompt(
165        cx,
166        ControlPrompt::new(
167            prompt_ref,
168            value_ref.clone(),
169            default_control_result_shape(),
170        ),
171        |_cx| Ok(value_ref),
172    )?;
173    control_result_value(cx, result)
174}
175
176fn call_capture(cx: &mut Cx, args: Vec<Value>) -> Result<Value> {
177    let multishot = optional_bool_arg(cx, args.get(3))?;
178    let refs = refs_from_args(cx, args.into_iter().take(3).collect(), "control/capture")?;
179    let [prompt_ref, continuation_ref, value_ref] = refs.as_slice() else {
180        return Err(arity_error(
181            "control/capture",
182            "prompt continuation value [multishot]",
183        ));
184    };
185    let mut request = ControlCapture::new(
186        prompt_ref.clone(),
187        continuation_ref.clone(),
188        value_ref.clone(),
189        default_control_result_shape(),
190    );
191    if multishot {
192        request = request.multishot();
193    }
194    let capture_result = capture(cx, request)?;
195    cx.factory().opaque(Arc::new(ContinuationValue::new(
196        continuation_ref.clone(),
197        capture_result,
198        multishot,
199    )))
200}
201
202fn call_abort(cx: &mut Cx, args: Vec<Value>) -> Result<Value> {
203    let refs = refs_from_args(cx, args, "control/abort")?;
204    let [prompt_ref, value_ref] = refs.as_slice() else {
205        return Err(arity_error("control/abort", "prompt value"));
206    };
207    let prompt_ref = prompt_ref.clone();
208    let value_ref = value_ref.clone();
209    let result = abort(
210        cx,
211        ControlAbort::new(prompt_ref, value_ref, default_control_result_shape()),
212    )?;
213    control_result_value(cx, result)
214}
215
216fn call_resume(cx: &mut Cx, args: Vec<Value>) -> Result<Value> {
217    if args.len() != 2 {
218        return Err(arity_error("control/resume", "continuation value"));
219    }
220    let continuation = continuation_ref(cx, &args[0])?;
221    let value = value_ref(cx, &args[1], "control/resume value")?;
222    let result = resume(
223        cx,
224        ControlResume::new(continuation, value, default_control_result_shape()),
225    )?;
226    control_result_value(cx, result)
227}
228
229fn call_physical_sensing_trace(cx: &mut Cx, args: Vec<Value>) -> Result<Value> {
230    if !args.is_empty() {
231        return Err(arity_error(
232            "control/physical-sensing-trace",
233            "no arguments",
234        ));
235    }
236    cx.factory().expr(physical_sensing_trace_expr())
237}
238
239fn physical_sensing_trace_expr() -> Expr {
240    list(vec![
241        atom_or_i64("physical-sensing-trace"),
242        list(vec![
243            atom_or_i64("id"),
244            atom_or_i64("a30-021-physical-sensing"),
245        ]),
246        list(vec![
247            atom_or_i64("fixture"),
248            list(vec![
249                atom_or_i64("source"),
250                atom_or_i64("synthetic-sensor-stream"),
251            ]),
252            list(vec![atom_or_i64("media"), atom_or_i64("copied-no")]),
253            list(vec![atom_or_i64("device"), atom_or_i64("live-device-none")]),
254        ]),
255        list(vec![
256            atom_or_i64("sensor-stream"),
257            list(vec![
258                atom_or_i64("runner"),
259                atom_or_i64("fake-sensor-stream"),
260            ]),
261            list(vec![
262                atom_or_i64("frame"),
263                atom_or_i64("1"),
264                atom_or_i64("position"),
265                atom_or_i64("22"),
266                atom_or_i64("velocity"),
267                atom_or_i64("3"),
268            ]),
269            list(vec![
270                atom_or_i64("frame"),
271                atom_or_i64("2"),
272                atom_or_i64("position"),
273                atom_or_i64("24"),
274                atom_or_i64("velocity"),
275                atom_or_i64("2"),
276            ]),
277            list(vec![
278                atom_or_i64("frame"),
279                atom_or_i64("3"),
280                atom_or_i64("position"),
281                atom_or_i64("26"),
282                atom_or_i64("velocity"),
283                atom_or_i64("1"),
284            ]),
285        ]),
286        list(vec![
287            atom_or_i64("temporal-average"),
288            list(vec![atom_or_i64("window"), atom_or_i64("3")]),
289            list(vec![atom_or_i64("position"), atom_or_i64("24")]),
290            list(vec![atom_or_i64("velocity"), atom_or_i64("2")]),
291        ]),
292        list(vec![
293            atom_or_i64("controller"),
294            list(vec![atom_or_i64("kind"), atom_or_i64("proportional")]),
295            list(vec![atom_or_i64("setpoint"), atom_or_i64("30")]),
296            list(vec![atom_or_i64("gain"), atom_or_i64("2")]),
297            list(vec![atom_or_i64("deadband"), atom_or_i64("2")]),
298            list(vec![atom_or_i64("hysteresis"), atom_or_i64("enabled")]),
299        ]),
300        list(vec![
301            atom_or_i64("control-output"),
302            list(vec![atom_or_i64("error"), atom_or_i64("6")]),
303            list(vec![atom_or_i64("command"), atom_or_i64("increase-12")]),
304            list(vec![atom_or_i64("clamped"), atom_or_i64("no")]),
305            list(vec![
306                atom_or_i64("next-state"),
307                atom_or_i64("approach-setpoint"),
308            ]),
309        ]),
310        list(vec![
311            atom_or_i64("answer"),
312            atom_or_i64("increase-actuator-by-12"),
313        ]),
314        list(vec![
315            atom_or_i64("effect-ledger"),
316            list(vec![
317                atom_or_i64("effect"),
318                atom_or_i64("read-fake-sensor-stream"),
319                atom_or_i64("deterministic"),
320            ]),
321            list(vec![
322                atom_or_i64("effect"),
323                atom_or_i64("average-window-three"),
324                atom_or_i64("pass"),
325            ]),
326            list(vec![
327                atom_or_i64("effect"),
328                atom_or_i64("apply-deadband"),
329                atom_or_i64("active"),
330            ]),
331            list(vec![
332                atom_or_i64("effect"),
333                atom_or_i64("emit-control-output"),
334                atom_or_i64("increase-12"),
335            ]),
336        ]),
337    ])
338}
339
340fn list(items: Vec<Expr>) -> Expr {
341    Expr::List(items)
342}
343
344fn atom_or_i64(name: &str) -> Expr {
345    if name.as_bytes().iter().all(u8::is_ascii_digit) {
346        return Expr::Number(NumberLiteral {
347            domain: Symbol::qualified("numbers", "i64"),
348            canonical: name.to_owned(),
349        });
350    }
351    Expr::Symbol(Symbol::new(name))
352}
353
354fn refs_from_args(cx: &mut Cx, args: Vec<Value>, context: &'static str) -> Result<Vec<Ref>> {
355    args.iter()
356        .map(|value| value_ref(cx, value, context))
357        .collect()
358}
359
360fn continuation_ref(cx: &mut Cx, value: &Value) -> Result<Ref> {
361    if let Some(continuation) = value.object().downcast_ref::<ContinuationValue>() {
362        return Ok(continuation.continuation().clone());
363    }
364    value_ref(cx, value, "control continuation")
365}
366
367fn value_ref(cx: &mut Cx, value: &Value, context: &'static str) -> Result<Ref> {
368    if let Some(result) = value.object().downcast_ref::<ControlResultValue>() {
369        return Ok(result.reference().clone());
370    }
371    let expr = value.object().as_expr(cx)?;
372    match expr {
373        Expr::Symbol(symbol) => Ok(Ref::Symbol(symbol)),
374        _ => Err(Error::TypeMismatch {
375            expected: context,
376            found: "non-ref value",
377        }),
378    }
379}
380
381fn optional_bool_arg(cx: &mut Cx, value: Option<&Value>) -> Result<bool> {
382    let Some(value) = value else {
383        return Ok(false);
384    };
385    match value.object().as_expr(cx)? {
386        Expr::Bool(value) => Ok(value),
387        _ => Err(Error::TypeMismatch {
388            expected: "bool",
389            found: "non-bool",
390        }),
391    }
392}
393
394fn control_result_value(cx: &mut Cx, reference: Ref) -> Result<Value> {
395    cx.factory()
396        .opaque(Arc::new(ControlResultValue::new(reference)))
397}
398
399fn arity_error(function: &'static str, expected: &'static str) -> Error {
400    Error::Eval(format!("{function} expects {expected}"))
401}
402
403#[cfg(test)]
404mod tests {
405    use sim_kernel::{Expr, NumberLiteral, Symbol};
406
407    use super::atom_or_i64;
408
409    #[test]
410    fn atom_or_i64_preserves_fixture_atom_policy() {
411        assert_eq!(atom_or_i64("ready"), Expr::Symbol(Symbol::new("ready")));
412        assert_eq!(
413            atom_or_i64("42"),
414            Expr::Number(NumberLiteral {
415                domain: Symbol::qualified("numbers", "i64"),
416                canonical: "42".to_owned(),
417            })
418        );
419    }
420}