Skip to main content

sim_kernel/
control.rs

1//! Control policy: the contract for delimited prompts, capture, and resume.
2//!
3//! The kernel defines the prompt/capture/abort/resume records and the
4//! [`ControlPolicy`] trait; libraries implement the concrete control behavior.
5
6use std::sync::Arc;
7
8use crate::{
9    capability::{
10        CapabilityName, control_capture_capability, control_multishot_capability,
11        control_prompt_capability, control_resume_capability,
12    },
13    datum::Datum,
14    datum_store::DatumStore,
15    effect::{
16        Effect, effect_abort_op_key, effect_control_abort_kind, effect_control_capture_kind,
17        effect_control_prompt_kind, effect_control_resume_kind, effect_resume_op_key,
18        resolve_effect,
19    },
20    env::Cx,
21    error::{Diagnostic, Result, Severity},
22    id::Symbol,
23    op::core_any_ref,
24    ref_id::{ContentId, Coordinate, HandleId, Ref},
25};
26
27/// Record describing a delimited control prompt to enter.
28#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct ControlPrompt {
30    /// Reference identifying the prompt boundary.
31    pub prompt: Ref,
32    /// Input value supplied to the prompt body.
33    pub input: Ref,
34    /// Shape the prompt result must satisfy.
35    pub result_shape: Ref,
36}
37
38impl ControlPrompt {
39    /// Build a prompt record from its boundary, input, and result shape.
40    pub fn new(prompt: Ref, input: Ref, result_shape: Ref) -> Self {
41        Self {
42            prompt,
43            input,
44            result_shape,
45        }
46    }
47}
48
49/// Record describing a capture of the continuation up to a prompt.
50#[derive(Clone, Debug, PartialEq, Eq)]
51pub struct ControlCapture {
52    /// Reference identifying the prompt boundary captured up to.
53    pub prompt: Ref,
54    /// Reference to the captured continuation.
55    pub continuation: Ref,
56    /// Value delivered to the captured continuation.
57    pub value: Ref,
58    /// Shape the resumed result must satisfy.
59    pub result_shape: Ref,
60    /// Whether the continuation may be resumed more than once.
61    pub multishot: bool,
62}
63
64impl ControlCapture {
65    /// Build a single-shot capture record.
66    pub fn new(prompt: Ref, continuation: Ref, value: Ref, result_shape: Ref) -> Self {
67        Self {
68            prompt,
69            continuation,
70            value,
71            result_shape,
72            multishot: false,
73        }
74    }
75
76    /// Mark the capture as resumable more than once.
77    pub fn multishot(mut self) -> Self {
78        self.multishot = true;
79        self
80    }
81}
82
83/// Record describing an abort that unwinds to a prompt with a value.
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct ControlAbort {
86    /// Reference identifying the prompt boundary to unwind to.
87    pub prompt: Ref,
88    /// Value delivered as the prompt result.
89    pub value: Ref,
90    /// Shape the prompt result must satisfy.
91    pub result_shape: Ref,
92}
93
94impl ControlAbort {
95    /// Build an abort record from its prompt, value, and result shape.
96    pub fn new(prompt: Ref, value: Ref, result_shape: Ref) -> Self {
97        Self {
98            prompt,
99            value,
100            result_shape,
101        }
102    }
103}
104
105/// Record describing a resume of a captured continuation with a value.
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct ControlResume {
108    /// Reference to the continuation being resumed.
109    pub continuation: Ref,
110    /// Value delivered to the resumed continuation.
111    pub value: Ref,
112    /// Shape the resumed result must satisfy.
113    pub result_shape: Ref,
114}
115
116impl ControlResume {
117    /// Build a resume record from its continuation, value, and result shape.
118    pub fn new(continuation: Ref, value: Ref, result_shape: Ref) -> Self {
119        Self {
120            continuation,
121            value,
122            result_shape,
123        }
124    }
125}
126
127/// Policy implementing delimited control: prompts, capture, abort, and resume.
128///
129/// The kernel defines this contract and the records it consumes; libraries
130/// supply the concrete continuation machinery. Unsupported operations report an
131/// "unsupported" control result rather than failing hard.
132pub trait ControlPolicy: Send + Sync {
133    /// Stable name identifying the policy in diagnostics.
134    fn name(&self) -> &'static str;
135
136    /// Enter a prompt boundary; the default is a no-op.
137    fn enter_prompt(&self, _cx: &mut Cx, _prompt: &ControlPrompt) -> Result<()> {
138        Ok(())
139    }
140
141    /// Capture the continuation up to a prompt; defaults to unsupported.
142    fn capture(&self, cx: &mut Cx, _capture: &ControlCapture) -> Result<Ref> {
143        unsupported_control_result(cx, self.name(), effect_control_capture_kind())
144    }
145
146    /// Abort to a prompt with a value; defaults to unsupported.
147    fn abort(&self, cx: &mut Cx, _abort: &ControlAbort) -> Result<Ref> {
148        unsupported_control_result(cx, self.name(), effect_control_abort_kind())
149    }
150
151    /// Resume a captured continuation with a value; defaults to unsupported.
152    fn resume(&self, cx: &mut Cx, _resume: &ControlResume) -> Result<Ref> {
153        unsupported_control_result(cx, self.name(), effect_control_resume_kind())
154    }
155}
156
157/// Shared, reference-counted handle to a [`ControlPolicy`].
158pub type ControlPolicyRef = Arc<dyn ControlPolicy>;
159
160/// Control policy that supports prompts but rejects capture, abort, and resume.
161#[derive(Default)]
162pub struct NoopControlPolicy;
163
164impl ControlPolicy for NoopControlPolicy {
165    fn name(&self) -> &'static str {
166        "noop-control"
167    }
168}
169
170/// Run `body` inside a control prompt, emitting the prompt effect first.
171pub fn prompt<F>(cx: &mut Cx, prompt: ControlPrompt, body: F) -> Result<Ref>
172where
173    F: FnOnce(&mut Cx) -> Result<Ref>,
174{
175    let effect = prompt_effect(cx.fresh_handle(), &prompt);
176    resolve_effect(cx, effect, |cx, _effect| {
177        let policy = cx.control_policy_ref();
178        policy.enter_prompt(cx, &prompt)?;
179        body(cx)
180    })
181}
182
183/// Capture the continuation up to a prompt via the active control policy.
184pub fn capture(cx: &mut Cx, capture: ControlCapture) -> Result<Ref> {
185    let effect = capture_effect(cx, &capture)?;
186    resolve_effect(cx, effect, |cx, _effect| {
187        let policy = cx.control_policy_ref();
188        policy.capture(cx, &capture)
189    })
190}
191
192/// Abort to a prompt with a value via the active control policy.
193pub fn abort(cx: &mut Cx, abort: ControlAbort) -> Result<Ref> {
194    let effect = abort_effect(cx.fresh_handle(), &abort);
195    resolve_effect(cx, effect, |cx, _effect| {
196        let policy = cx.control_policy_ref();
197        policy.abort(cx, &abort)
198    })
199}
200
201/// Resume a captured continuation with a value via the active control policy.
202pub fn resume(cx: &mut Cx, resume: ControlResume) -> Result<Ref> {
203    let effect = resume_effect(cx.fresh_handle(), &resume);
204    resolve_effect(cx, effect, |cx, _effect| {
205        let policy = cx.control_policy_ref();
206        policy.resume(cx, &resume)
207    })
208}
209
210/// Build the capability-gated effect that requests a control prompt.
211pub fn prompt_effect(id: crate::HandleId, prompt: &ControlPrompt) -> Effect {
212    Effect::new(
213        id,
214        effect_control_prompt_kind(),
215        prompt.prompt.clone(),
216        prompt.input.clone(),
217        prompt.result_shape.clone(),
218        effect_resume_op_key(),
219        effect_abort_op_key(),
220    )
221    .requiring(control_prompt_capability())
222}
223
224/// Build the capability-gated effect that requests a continuation capture.
225pub fn capture_effect(cx: &mut Cx, capture: &ControlCapture) -> Result<Effect> {
226    let input = intern_control_input(
227        cx,
228        control_capture_status(),
229        vec![
230            (
231                Symbol::new("continuation"),
232                ref_datum(capture.continuation.clone()),
233            ),
234            (Symbol::new("value"), ref_datum(capture.value.clone())),
235            (Symbol::new("multishot"), Datum::Bool(capture.multishot)),
236        ],
237    )?;
238    Ok(Effect::new(
239        cx.fresh_handle(),
240        effect_control_capture_kind(),
241        capture.prompt.clone(),
242        input,
243        capture.result_shape.clone(),
244        effect_resume_op_key(),
245        effect_abort_op_key(),
246    )
247    .with_requirements(control_requirements(
248        control_capture_capability(),
249        capture.multishot,
250    )))
251}
252
253/// Build the capability-gated effect that requests an abort.
254pub fn abort_effect(id: crate::HandleId, abort: &ControlAbort) -> Effect {
255    Effect::new(
256        id,
257        effect_control_abort_kind(),
258        abort.prompt.clone(),
259        abort.value.clone(),
260        abort.result_shape.clone(),
261        effect_resume_op_key(),
262        effect_abort_op_key(),
263    )
264    .requiring(control_capture_capability())
265}
266
267/// Build the capability-gated effect that requests a resume.
268pub fn resume_effect(id: crate::HandleId, resume: &ControlResume) -> Effect {
269    Effect::new(
270        id,
271        effect_control_resume_kind(),
272        resume.continuation.clone(),
273        resume.value.clone(),
274        resume.result_shape.clone(),
275        effect_resume_op_key(),
276        effect_abort_op_key(),
277    )
278    .requiring(control_resume_capability())
279}
280
281/// Intern a control result recording a captured continuation and value.
282pub fn captured_control_result(cx: &mut Cx, continuation: Ref, value: Ref) -> Result<Ref> {
283    intern_control_result(
284        cx,
285        control_captured_status(),
286        vec![
287            (Symbol::new("continuation"), ref_datum(continuation)),
288            (Symbol::new("value"), ref_datum(value)),
289        ],
290    )
291}
292
293/// Intern a control result recording an abort to a prompt with a value.
294pub fn aborted_control_result(cx: &mut Cx, prompt: Ref, value: Ref) -> Result<Ref> {
295    intern_control_result(
296        cx,
297        control_aborted_status(),
298        vec![
299            (Symbol::new("prompt"), ref_datum(prompt)),
300            (Symbol::new("value"), ref_datum(value)),
301        ],
302    )
303}
304
305/// Intern a control result recording a resumed continuation and value.
306pub fn resumed_control_result(cx: &mut Cx, continuation: Ref, value: Ref) -> Result<Ref> {
307    intern_control_result(
308        cx,
309        control_resumed_status(),
310        vec![
311            (Symbol::new("continuation"), ref_datum(continuation)),
312            (Symbol::new("value"), ref_datum(value)),
313        ],
314    )
315}
316
317/// Intern an "unsupported" control result and push its diagnostic, for
318/// policies that do not implement `operation`.
319pub fn unsupported_control_result(
320    cx: &mut Cx,
321    policy: &'static str,
322    operation: Symbol,
323) -> Result<Ref> {
324    let diagnostic = unsupported_control_diagnostic(policy, operation);
325    cx.push_diagnostic(diagnostic.clone());
326    intern_control_result(
327        cx,
328        control_unsupported_status(),
329        vec![(Symbol::new("diagnostic"), diagnostic_datum(diagnostic))],
330    )
331}
332
333/// Build the diagnostic reported when `policy` cannot perform `operation`.
334pub fn unsupported_control_diagnostic(policy: &'static str, operation: Symbol) -> Diagnostic {
335    let mut diagnostic = Diagnostic::error(format!(
336        "control policy {policy} does not support {operation}"
337    ));
338    diagnostic.code = Some(control_unsupported_status());
339    diagnostic
340}
341
342/// Read the status symbol of an interned control result, if `result` is one.
343pub fn control_result_status(cx: &Cx, result: &Ref) -> Result<Option<Symbol>> {
344    let Ref::Content(id) = result else {
345        return Ok(None);
346    };
347    let Some(Datum::Node { tag, fields }) = cx.datum_store().get(id)? else {
348        return Ok(None);
349    };
350    if tag != &control_result_tag() {
351        return Ok(None);
352    }
353    Ok(fields.iter().find_map(|(field, value)| {
354        if field == &Symbol::new("status")
355            && let Datum::Symbol(status) = value
356        {
357            return Some(status.clone());
358        }
359        None
360    }))
361}
362
363/// Status symbol naming a prompt-entry control operation.
364pub fn control_prompt_status() -> Symbol {
365    control_symbol("prompt")
366}
367
368/// Status symbol naming a capture control operation.
369pub fn control_capture_status() -> Symbol {
370    control_symbol("capture")
371}
372
373/// Status symbol for a control result that captured a continuation.
374pub fn control_captured_status() -> Symbol {
375    control_symbol("captured")
376}
377
378/// Status symbol for a control result that aborted to a prompt.
379pub fn control_aborted_status() -> Symbol {
380    control_symbol("aborted")
381}
382
383/// Status symbol for a control result that resumed a continuation.
384pub fn control_resumed_status() -> Symbol {
385    control_symbol("resumed")
386}
387
388/// Status symbol for a control result the policy did not support.
389pub fn control_unsupported_status() -> Symbol {
390    control_symbol("unsupported")
391}
392
393/// The default prompt boundary reference.
394pub fn default_control_prompt() -> Ref {
395    Ref::Symbol(control_symbol("default-prompt"))
396}
397
398/// The default control result shape (the open `core` any shape).
399pub fn default_control_result_shape() -> Ref {
400    core_any_ref()
401}
402
403fn control_requirements(primary: CapabilityName, multishot: bool) -> Vec<CapabilityName> {
404    let mut requires = vec![primary];
405    if multishot {
406        requires.push(control_multishot_capability());
407    }
408    requires
409}
410
411fn intern_control_input(
412    cx: &mut Cx,
413    operation: Symbol,
414    mut fields: Vec<(Symbol, Datum)>,
415) -> Result<Ref> {
416    fields.insert(0, (Symbol::new("operation"), Datum::Symbol(operation)));
417    let id = cx.datum_store_mut().intern(Datum::Node {
418        tag: control_input_tag(),
419        fields,
420    })?;
421    Ok(Ref::Content(id))
422}
423
424fn intern_control_result(
425    cx: &mut Cx,
426    status: Symbol,
427    mut fields: Vec<(Symbol, Datum)>,
428) -> Result<Ref> {
429    fields.insert(0, (Symbol::new("status"), Datum::Symbol(status)));
430    let id = cx.datum_store_mut().intern(Datum::Node {
431        tag: control_result_tag(),
432        fields,
433    })?;
434    Ok(Ref::Content(id))
435}
436
437fn diagnostic_datum(diagnostic: Diagnostic) -> Datum {
438    Datum::Node {
439        tag: core_symbol("Diagnostic"),
440        fields: vec![
441            (
442                Symbol::new("severity"),
443                Datum::Symbol(severity_symbol(diagnostic.severity)),
444            ),
445            (Symbol::new("message"), Datum::String(diagnostic.message)),
446            (
447                Symbol::new("code"),
448                diagnostic.code.map_or(Datum::Nil, Datum::Symbol),
449            ),
450        ],
451    }
452}
453
454fn severity_symbol(severity: Severity) -> Symbol {
455    match severity {
456        Severity::Error => core_symbol("error"),
457        Severity::Warning => core_symbol("warning"),
458        Severity::Info => core_symbol("info"),
459        Severity::Note => core_symbol("note"),
460    }
461}
462
463fn ref_datum(reference: Ref) -> Datum {
464    match reference {
465        Ref::Symbol(symbol) => Datum::Node {
466            tag: core_symbol("ref"),
467            fields: vec![
468                (Symbol::new("kind"), Datum::Symbol(core_symbol("symbol"))),
469                (Symbol::new("symbol"), Datum::Symbol(symbol)),
470            ],
471        },
472        Ref::Content(content) => Datum::Node {
473            tag: core_symbol("ref"),
474            fields: vec![
475                (Symbol::new("kind"), Datum::Symbol(core_symbol("content"))),
476                (Symbol::new("content"), content_id_datum(content)),
477            ],
478        },
479        Ref::Handle(handle) => Datum::Node {
480            tag: core_symbol("ref"),
481            fields: vec![
482                (Symbol::new("kind"), Datum::Symbol(core_symbol("handle"))),
483                (Symbol::new("id"), handle_id_datum(handle)),
484            ],
485        },
486        Ref::Coord(coordinate) => coordinate_datum(coordinate),
487    }
488}
489
490fn coordinate_datum(coordinate: Coordinate) -> Datum {
491    Datum::Node {
492        tag: core_symbol("ref"),
493        fields: vec![
494            (Symbol::new("kind"), Datum::Symbol(core_symbol("coord"))),
495            (Symbol::new("space"), Datum::Symbol(coordinate.space)),
496            (Symbol::new("ordinal"), content_id_datum(coordinate.ordinal)),
497        ],
498    }
499}
500
501fn content_id_datum(content: ContentId) -> Datum {
502    Datum::Node {
503        tag: core_symbol("content-id"),
504        fields: vec![
505            (Symbol::new("algorithm"), Datum::Symbol(content.algorithm)),
506            (Symbol::new("bytes"), Datum::Bytes(content.bytes.to_vec())),
507        ],
508    }
509}
510
511fn handle_id_datum(handle: HandleId) -> Datum {
512    Datum::Bytes(handle.0.to_be_bytes().to_vec())
513}
514
515fn control_input_tag() -> Symbol {
516    core_symbol("ControlInput")
517}
518
519fn control_result_tag() -> Symbol {
520    core_symbol("ControlResult")
521}
522
523fn control_symbol(name: &str) -> Symbol {
524    Symbol::qualified("control", name)
525}
526
527fn core_symbol(name: &str) -> Symbol {
528    Symbol::qualified("core", name)
529}
530
531#[cfg(test)]
532mod tests;