Skip to main content

hara_native/work/
plan.rs

1//! Portable, closure-free work-plan execution.
2//!
3//! A [`WorkPlan`] is data that can cross a Hara package boundary.  Leaf
4//! behavior is referenced by a stable target name and supplied by a local
5//! [`WorkRegistry`]; closures are intentionally never part of the encoded
6//! plan.  The implementation is single-threaded because guest [`Value`]s are
7//! evaluator-thread values, just like [`super::WorkHost`].
8
9use super::{WorkContext, WorkHost, WorkOptions, WorkRun};
10use crate::core::{Promise, PromiseRejection, PromiseState, Value};
11use crate::lang::protocol::INamespaced;
12use std::cell::RefCell;
13use std::collections::HashMap;
14use std::rc::Rc;
15
16const VERSION_KEY: &str = "work/plan-version";
17const OP_KEY: &str = "work/op";
18const TARGET_KEY: &str = "work/target";
19const CHILDREN_KEY: &str = "work/children";
20const CHILD_KEY: &str = "work/child";
21const SOURCE_KEY: &str = "work/source";
22const CONTINUATION_KEY: &str = "work/continuation-target";
23const CLEANUP_KEY: &str = "work/cleanup";
24const INITIAL_KEY: &str = "work/initial";
25const REDUCER_KEY: &str = "work/reducer";
26const SELECTOR_KEY: &str = "work/selector";
27const CHOICES_KEY: &str = "work/choices";
28const WAIT_KEY: &str = "work/wait";
29const MAXIMUM_DEPTH_KEY: &str = "work/maximum-depth";
30const NODES_KEY: &str = "work/nodes";
31const ORDER_KEY: &str = "work/order";
32const PROCESS_KEY: &str = "work/process";
33
34fn key(name: &str) -> Value {
35    Value::Keyword(name.into())
36}
37
38fn map(entries: impl IntoIterator<Item = (&'static str, Value)>) -> Value {
39    Value::Map(
40        entries
41            .into_iter()
42            .map(|(name, value)| (key(name), value))
43            .collect(),
44    )
45}
46
47fn field(value: &Value, name: &str) -> Option<Value> {
48    match value {
49        Value::Map(fields) => fields.get(&key(name)).cloned(),
50        Value::OrderedMap(fields) => fields.get(&key(name)).cloned(),
51        Value::SortedMap(fields) => fields.get(&key(name)).cloned(),
52        _ => None,
53    }
54}
55
56fn vector(value: Value, name: &str) -> Result<Vec<Value>, String> {
57    match value {
58        Value::Vector(values) => Ok(values.into_iter().collect()),
59        Value::Tuple(values) => Ok(values.into_iter().collect()),
60        _ => Err(format!("work/plan-invalid: {name} must be a vector")),
61    }
62}
63
64pub fn target_name(value: Value) -> Result<String, String> {
65    match value {
66        Value::String(value) => non_blank_target(value),
67        Value::Keyword(value) => non_blank_target(value.to_string()),
68        Value::Symbol(value) => non_blank_target(value.to_string()),
69        _ => Err("work/plan-invalid: work target must be a string, keyword, or symbol".into()),
70    }
71}
72
73fn non_blank_target(value: String) -> Result<String, String> {
74    if value.trim().is_empty() {
75        Err("work/plan-invalid: work target cannot be blank".into())
76    } else {
77        Ok(value)
78    }
79}
80
81fn plan_error(message: impl Into<String>) -> PromiseRejection {
82    PromiseRejection::Value(map([
83        ("code", Value::Keyword("work/plan-error".into())),
84        ("message", Value::String(message.into())),
85        ("retryable", Value::Bool(false)),
86    ]))
87}
88
89fn truthy(value: &Value) -> bool {
90    !matches!(value, Value::Nil | Value::Bool(false))
91}
92
93/// The operations accepted by the portable work-plan v1 envelope.
94#[derive(Clone, Copy, Debug, PartialEq, Eq)]
95pub enum WorkOperation {
96    Pure,
97    Step,
98    Chain,
99    Each,
100    Filter,
101    Fold,
102    All,
103    Choose,
104    Graph,
105    Batch,
106    Bind,
107    Ensure,
108    Await,
109}
110
111impl WorkOperation {
112    pub const VERSION: i64 = 1;
113
114    pub fn as_str(self) -> &'static str {
115        match self {
116            Self::Pure => "pure",
117            Self::Step => "step",
118            Self::Chain => "chain",
119            Self::Each => "each",
120            Self::Filter => "filter",
121            Self::Fold => "fold",
122            Self::All => "all",
123            Self::Choose => "choose",
124            Self::Graph => "graph",
125            Self::Batch => "batch",
126            Self::Bind => "bind",
127            Self::Ensure => "ensure",
128            Self::Await => "await",
129        }
130    }
131
132    pub fn parse(value: &Value) -> Result<Self, String> {
133        let Value::Keyword(value) = value else {
134            return Err("work/plan-invalid: work operation must be a keyword".into());
135        };
136        match value.get_name() {
137            "pure" => Ok(Self::Pure),
138            "step" => Ok(Self::Step),
139            "chain" => Ok(Self::Chain),
140            "each" => Ok(Self::Each),
141            "filter" => Ok(Self::Filter),
142            "fold" => Ok(Self::Fold),
143            "all" => Ok(Self::All),
144            "choose" => Ok(Self::Choose),
145            "graph" => Ok(Self::Graph),
146            "batch" => Ok(Self::Batch),
147            "bind" => Ok(Self::Bind),
148            "ensure" => Ok(Self::Ensure),
149            "await" => Ok(Self::Await),
150            _ => Err(format!("work/plan-unsupported: {}", value.as_str())),
151        }
152    }
153}
154
155/// A canonical v1 plan backed by a Hara portable value.
156#[derive(Clone, Debug)]
157pub struct WorkPlan {
158    value: Value,
159}
160
161impl WorkPlan {
162    pub fn from_value(value: Value) -> Result<Self, String> {
163        validate_plan(&value)?;
164        Ok(Self { value })
165    }
166
167    pub fn value(&self) -> Value {
168        self.value.clone()
169    }
170
171    pub fn operation(&self) -> WorkOperation {
172        WorkOperation::parse(&field(&self.value, OP_KEY).expect("validated plan has operation"))
173            .expect("validated plan has supported operation")
174    }
175
176    pub fn encode_hta(&self) -> Result<Vec<u8>, String> {
177        crate::hta::encode(&self.value)
178    }
179
180    pub fn decode_hta(bytes: &[u8]) -> Result<Self, String> {
181        Self::from_value(crate::hta::decode_canonical(bytes)?)
182    }
183
184    pub fn pure(target: impl Into<String>) -> Result<Self, String> {
185        Self::leaf(WorkOperation::Pure, target)
186    }
187
188    pub fn step(target: impl Into<String>) -> Result<Self, String> {
189        Self::leaf(WorkOperation::Step, target)
190    }
191
192    pub fn leaf(operation: WorkOperation, target: impl Into<String>) -> Result<Self, String> {
193        match operation {
194            WorkOperation::Pure | WorkOperation::Step => Self::from_value(map([
195                (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
196                (OP_KEY, Value::Keyword(operation.as_str().into())),
197                (TARGET_KEY, Value::String(non_blank_target(target.into())?)),
198            ])),
199            _ => Err("work/plan-invalid: only pure and step are leaf operations".into()),
200        }
201    }
202
203    pub fn chain(children: Vec<Self>) -> Result<Self, String> {
204        Self::children(WorkOperation::Chain, children)
205    }
206
207    pub fn all(children: Vec<Self>) -> Result<Self, String> {
208        Self::children(WorkOperation::All, children)
209    }
210
211    pub fn each(child: Self) -> Result<Self, String> {
212        Self::child(WorkOperation::Each, child)
213    }
214
215    pub fn filter(child: Self) -> Result<Self, String> {
216        Self::child(WorkOperation::Filter, child)
217    }
218
219    pub fn children(operation: WorkOperation, children: Vec<Self>) -> Result<Self, String> {
220        Self::from_value(map([
221            (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
222            (OP_KEY, Value::Keyword(operation.as_str().into())),
223            (
224                CHILDREN_KEY,
225                Value::Vector(children.into_iter().map(|child| child.value).collect()),
226            ),
227        ]))
228    }
229
230    pub fn child(operation: WorkOperation, child: Self) -> Result<Self, String> {
231        Self::from_value(map([
232            (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
233            (OP_KEY, Value::Keyword(operation.as_str().into())),
234            (CHILD_KEY, child.value),
235        ]))
236    }
237
238    pub fn fold(initial: Value, reducer: Self) -> Result<Self, String> {
239        Self::from_value(map([
240            (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
241            (OP_KEY, Value::Keyword("fold".into())),
242            (INITIAL_KEY, initial),
243            (REDUCER_KEY, reducer.value),
244        ]))
245    }
246
247    pub fn choose(selector: Self, choices: Value) -> Result<Self, String> {
248        Self::from_value(map([
249            (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
250            (OP_KEY, Value::Keyword("choose".into())),
251            (SELECTOR_KEY, selector.value),
252            (CHOICES_KEY, choices),
253        ]))
254    }
255
256    pub fn graph(graph: Value) -> Result<Self, String> {
257        Self::generic(WorkOperation::Graph, graph)
258    }
259
260    pub fn batch(stages: Value) -> Result<Self, String> {
261        Self::generic(WorkOperation::Batch, stages)
262    }
263
264    pub fn bind(source: Self, continuation_target: impl Into<String>) -> Result<Self, String> {
265        Self::from_value(map([
266            (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
267            (OP_KEY, Value::Keyword("bind".into())),
268            (SOURCE_KEY, source.value),
269            (
270                CONTINUATION_KEY,
271                Value::String(non_blank_target(continuation_target.into())?),
272            ),
273        ]))
274    }
275
276    pub fn ensure(body: Self, cleanup: Self) -> Result<Self, String> {
277        Self::from_value(map([
278            (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
279            (OP_KEY, Value::Keyword("ensure".into())),
280            (CHILD_KEY, body.value),
281            (CLEANUP_KEY, cleanup.value),
282        ]))
283    }
284
285    pub fn await_(wait: Value) -> Result<Self, String> {
286        Self::from_value(map([
287            (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
288            (OP_KEY, Value::Keyword("await".into())),
289            (WAIT_KEY, wait),
290        ]))
291    }
292
293    pub fn generic(operation: WorkOperation, fields: Value) -> Result<Self, String> {
294        let fields = map_entries(&fields, "operation fields")?;
295        let output = fields
296            .into_iter()
297            .chain([
298                (key(VERSION_KEY), Value::Number(WorkOperation::VERSION)),
299                (key(OP_KEY), Value::Keyword(operation.as_str().into())),
300            ])
301            .collect();
302        Self::from_value(Value::Map(output))
303    }
304}
305
306/// A named local implementation for a serializable work target.
307pub type WorkTarget = Rc<dyn Fn(Value, WorkContext) -> Result<Value, PromiseRejection>>;
308
309/// Process-local bindings for the otherwise portable target names in a plan.
310#[derive(Clone, Default)]
311pub struct WorkRegistry {
312    targets: Rc<RefCell<HashMap<String, WorkTarget>>>,
313}
314
315impl WorkRegistry {
316    pub fn bind(&self, name: impl Into<String>, target: WorkTarget) -> Result<(), String> {
317        let name = non_blank_target(name.into())?;
318        self.targets.borrow_mut().insert(name, target);
319        Ok(())
320    }
321
322    pub fn unbind(&self, name: &str) -> bool {
323        self.targets.borrow_mut().remove(name).is_some()
324    }
325
326    pub fn reset(&self) {
327        self.targets.borrow_mut().clear();
328    }
329
330    pub fn target(&self, name: &str) -> Option<WorkTarget> {
331        self.targets.borrow().get(name).cloned()
332    }
333
334    pub fn target_names(&self) -> Vec<String> {
335        let mut names = self.targets.borrow().keys().cloned().collect::<Vec<_>>();
336        names.sort();
337        names
338    }
339
340    pub fn identity(&self) -> usize {
341        Rc::as_ptr(&self.targets) as usize
342    }
343}
344
345pub type SuspensionTarget = Rc<dyn Fn(Value, WorkContext) -> Result<Value, PromiseRejection>>;
346
347/// Evaluates plans against explicit targets and an optional suspension bridge.
348#[derive(Clone, Default)]
349pub struct WorkRuntime {
350    registry: WorkRegistry,
351    suspension: Option<SuspensionTarget>,
352}
353
354impl WorkRuntime {
355    pub fn new(registry: WorkRegistry) -> Self {
356        Self {
357            registry,
358            suspension: None,
359        }
360    }
361
362    pub fn registry(&self) -> WorkRegistry {
363        self.registry.clone()
364    }
365
366    pub fn with_suspension(mut self, suspension: SuspensionTarget) -> Self {
367        self.suspension = Some(suspension);
368        self
369    }
370
371    pub fn reset(&self) {
372        self.registry.reset();
373    }
374
375    pub fn evaluate(
376        &self,
377        plan: &WorkPlan,
378        input: Value,
379        context: WorkContext,
380    ) -> Result<Value, PromiseRejection> {
381        execute(self.clone(), plan.value(), input, context, 0)
382    }
383}
384
385impl WorkHost {
386    /// Submit a portable plan through the existing lifecycle host.
387    pub fn submit_plan(
388        &self,
389        runtime: WorkRuntime,
390        plan: WorkPlan,
391        input: Value,
392        options: WorkOptions,
393    ) -> Result<WorkRun, String> {
394        self.submit_scoped_rejection(options, move |context| {
395            runtime.evaluate(&plan, input, context)
396        })
397    }
398
399    /// Cancels and forgets all runs, restores admission, and is idempotent.
400    pub fn reset(&self) {
401        self.kill();
402        let mut host = self.inner.borrow_mut();
403        host.queue.clear();
404        host.runs.clear();
405        host.next_id = 1;
406        host.started = true;
407    }
408}
409
410fn validate_plan(value: &Value) -> Result<(), String> {
411    let (Value::Map(_) | Value::OrderedMap(_) | Value::SortedMap(_)) = value else {
412        return Err("work/plan-invalid: plan must be a map".into());
413    };
414    if field(value, VERSION_KEY) != Some(Value::Number(WorkOperation::VERSION)) {
415        return Err("work/plan-invalid: unsupported plan version".into());
416    }
417    let operation =
418        WorkOperation::parse(&field(value, OP_KEY).ok_or("work/plan-invalid: missing operation")?)?;
419    match operation {
420        WorkOperation::Pure | WorkOperation::Step => {
421            target_name(
422                field(value, TARGET_KEY).ok_or("work/plan-invalid: leaf requires target")?,
423            )?;
424        }
425        WorkOperation::Chain | WorkOperation::All => {
426            for child in vector(
427                field(value, CHILDREN_KEY).ok_or("work/plan-invalid: missing children")?,
428                CHILDREN_KEY,
429            )? {
430                validate_plan(&child)?;
431            }
432        }
433        WorkOperation::Each | WorkOperation::Filter => {
434            validate_plan(&field(value, CHILD_KEY).ok_or("work/plan-invalid: missing child")?)?;
435        }
436        WorkOperation::Fold => {
437            validate_plan(&field(value, REDUCER_KEY).ok_or("work/plan-invalid: missing reducer")?)?;
438        }
439        WorkOperation::Choose => {
440            validate_plan(
441                &field(value, SELECTOR_KEY).ok_or("work/plan-invalid: missing selector")?,
442            )?;
443            let choices = field(value, CHOICES_KEY).ok_or("work/plan-invalid: missing choices")?;
444            for choice in map_values(&choices, CHOICES_KEY)? {
445                validate_plan(&choice)?;
446            }
447        }
448        WorkOperation::Bind => {
449            validate_plan(
450                &field(value, SOURCE_KEY).ok_or("work/plan-invalid: missing bind source")?,
451            )?;
452            target_name(
453                field(value, CONTINUATION_KEY)
454                    .ok_or("work/plan-invalid: missing continuation target")?,
455            )?;
456        }
457        WorkOperation::Ensure => {
458            validate_plan(
459                &field(value, CHILD_KEY).ok_or("work/plan-invalid: missing ensure body")?,
460            )?;
461            validate_plan(&field(value, CLEANUP_KEY).ok_or("work/plan-invalid: missing cleanup")?)?;
462        }
463        WorkOperation::Await => {
464            if field(value, WAIT_KEY).is_none() {
465                return Err("work/plan-invalid: await requires a wait descriptor".into());
466            }
467        }
468        WorkOperation::Graph => {
469            let nodes = field(value, NODES_KEY).ok_or("work/plan-invalid: missing graph nodes")?;
470            for child in map_values(&nodes, NODES_KEY)? {
471                validate_plan(&child)?;
472            }
473            for id in vector(
474                field(value, ORDER_KEY).ok_or("work/plan-invalid: missing graph order")?,
475                ORDER_KEY,
476            )? {
477                let child = map_lookup(&nodes, &id)
478                    .ok_or("work/plan-invalid: graph order refers to an unknown node")?;
479                validate_plan(&child)?;
480            }
481        }
482        WorkOperation::Batch => {
483            validate_plan(
484                &field(value, PROCESS_KEY).ok_or("work/plan-invalid: missing batch process")?,
485            )?;
486        }
487    }
488    Ok(())
489}
490
491fn map_values(value: &Value, name: &str) -> Result<Vec<Value>, String> {
492    match value {
493        Value::Map(entries) => Ok(entries.iter().map(|(_, value)| value.clone()).collect()),
494        Value::OrderedMap(entries) => Ok(entries.iter().map(|(_, value)| value.clone()).collect()),
495        Value::SortedMap(entries) => Ok(entries.iter().map(|(_, value)| value.clone()).collect()),
496        _ => Err(format!("work/plan-invalid: {name} must be a map")),
497    }
498}
499
500fn map_entries(value: &Value, name: &str) -> Result<Vec<(Value, Value)>, String> {
501    match value {
502        Value::Map(entries) => Ok(entries
503            .iter()
504            .map(|(key, value)| (key.clone(), value.clone()))
505            .collect()),
506        Value::OrderedMap(entries) => Ok(entries
507            .iter()
508            .map(|(key, value)| (key.clone(), value.clone()))
509            .collect()),
510        Value::SortedMap(entries) => Ok(entries
511            .iter()
512            .map(|(key, value)| (key.clone(), value.clone()))
513            .collect()),
514        _ => Err(format!("work/plan-invalid: {name} must be a map")),
515    }
516}
517
518fn map_lookup(value: &Value, lookup: &Value) -> Option<Value> {
519    match value {
520        Value::Map(entries) => entries.get(lookup).cloned(),
521        Value::OrderedMap(entries) => entries.get(lookup).cloned(),
522        Value::SortedMap(entries) => entries.get(lookup).cloned(),
523        _ => None,
524    }
525}
526
527fn then(
528    value: Value,
529    continuation: Rc<dyn Fn(Value) -> Result<Value, PromiseRejection>>,
530) -> Result<Value, PromiseRejection> {
531    let Value::Promise(source) = value else {
532        return continuation(value);
533    };
534    let output = Promise::new();
535    let destination = output.clone();
536    source.on_settle(Rc::new(move |state| match state {
537        PromiseState::Fulfilled(value) => match continuation(value) {
538            Ok(Value::Promise(value)) => {
539                destination.adopt(&value);
540            }
541            Ok(value) => {
542                destination.resolve(value);
543            }
544            Err(error) => {
545                destination.reject_rejection(error);
546            }
547        },
548        PromiseState::Rejected(error) => {
549            destination.reject_rejection(error);
550        }
551        PromiseState::Pending => {}
552    }));
553    Ok(Value::Promise(output))
554}
555
556fn settle(
557    value: Value,
558    fulfilled: Rc<dyn Fn(Value) -> Result<Value, PromiseRejection>>,
559    rejected: Rc<dyn Fn(PromiseRejection) -> Result<Value, PromiseRejection>>,
560) -> Result<Value, PromiseRejection> {
561    let Value::Promise(source) = value else {
562        return fulfilled(value);
563    };
564    let output = Promise::new();
565    let destination = output.clone();
566    source.on_settle(Rc::new(move |state| {
567        let next = match state {
568            PromiseState::Fulfilled(value) => fulfilled(value),
569            PromiseState::Rejected(error) => rejected(error),
570            PromiseState::Pending => return,
571        };
572        match next {
573            Ok(Value::Promise(value)) => {
574                destination.adopt(&value);
575            }
576            Ok(value) => {
577                destination.resolve(value);
578            }
579            Err(error) => {
580                destination.reject_rejection(error);
581            }
582        }
583    }));
584    Ok(Value::Promise(output))
585}
586
587fn execute(
588    runtime: WorkRuntime,
589    value: Value,
590    input: Value,
591    context: WorkContext,
592    bind_depth: usize,
593) -> Result<Value, PromiseRejection> {
594    validate_plan(&value).map_err(plan_error)?;
595    let operation = WorkOperation::parse(&field(&value, OP_KEY).expect("validated plan has op"))
596        .map_err(plan_error)?;
597    context.check_cancelled()?;
598    let _ = context.emit(
599        Value::Keyword("work/node-started".into()),
600        map([("operation", Value::Keyword(operation.as_str().into()))]),
601    );
602    let result = match operation {
603        WorkOperation::Pure | WorkOperation::Step => {
604            execute_target(&runtime, &value, input, context.clone())
605        }
606        WorkOperation::Chain => execute_chain(runtime, value, input, context.clone(), bind_depth),
607        WorkOperation::All => execute_all(runtime, value, input, context.clone(), bind_depth),
608        WorkOperation::Each => {
609            execute_each(runtime, value, input, context.clone(), bind_depth, false)
610        }
611        WorkOperation::Filter => {
612            execute_each(runtime, value, input, context.clone(), bind_depth, true)
613        }
614        WorkOperation::Fold => execute_fold(runtime, value, input, context.clone(), bind_depth),
615        WorkOperation::Choose => execute_choose(runtime, value, input, context.clone(), bind_depth),
616        WorkOperation::Bind => execute_bind(runtime, value, input, context.clone(), bind_depth),
617        WorkOperation::Ensure => execute_ensure(runtime, value, input, context.clone(), bind_depth),
618        WorkOperation::Await => execute_await(&runtime, value, context.clone()),
619        WorkOperation::Graph => execute_graph(runtime, value, input, context.clone(), bind_depth),
620        WorkOperation::Batch => execute_batch(runtime, value, input, context.clone(), bind_depth),
621    };
622    if result.is_ok() {
623        let _ = context.emit(
624            Value::Keyword("work/node-completed".into()),
625            map([("operation", Value::Keyword(operation.as_str().into()))]),
626        );
627    }
628    result
629}
630
631fn execute_target(
632    runtime: &WorkRuntime,
633    value: &Value,
634    input: Value,
635    context: WorkContext,
636) -> Result<Value, PromiseRejection> {
637    let target = target_name(field(value, TARGET_KEY).expect("validated leaf has target"))
638        .map_err(plan_error)?;
639    runtime
640        .registry
641        .target(&target)
642        .ok_or_else(|| plan_error(format!("work/target-unavailable: {target}")))?(input, context)
643}
644
645fn execute_chain(
646    runtime: WorkRuntime,
647    value: Value,
648    input: Value,
649    context: WorkContext,
650    depth: usize,
651) -> Result<Value, PromiseRejection> {
652    let children = vector(
653        field(&value, CHILDREN_KEY).expect("validated chain children"),
654        CHILDREN_KEY,
655    )
656    .map_err(plan_error)?;
657    fn next(
658        runtime: WorkRuntime,
659        children: Rc<Vec<Value>>,
660        index: usize,
661        input: Value,
662        context: WorkContext,
663        depth: usize,
664    ) -> Result<Value, PromiseRejection> {
665        let Some(child) = children.get(index).cloned() else {
666            return Ok(input);
667        };
668        let runtime_next = runtime.clone();
669        let children_next = children.clone();
670        let context_next = context.clone();
671        let value = execute(runtime, child, input, context, depth)?;
672        then(
673            value,
674            Rc::new(move |resolved| {
675                next(
676                    runtime_next.clone(),
677                    children_next.clone(),
678                    index + 1,
679                    resolved,
680                    context_next.clone(),
681                    depth,
682                )
683            }),
684        )
685    }
686    next(runtime, Rc::new(children), 0, input, context, depth)
687}
688
689fn execute_all(
690    runtime: WorkRuntime,
691    value: Value,
692    input: Value,
693    context: WorkContext,
694    depth: usize,
695) -> Result<Value, PromiseRejection> {
696    let children = vector(
697        field(&value, CHILDREN_KEY).expect("validated all children"),
698        CHILDREN_KEY,
699    )
700    .map_err(plan_error)?;
701    fn next(
702        runtime: WorkRuntime,
703        children: Rc<Vec<Value>>,
704        index: usize,
705        input: Value,
706        context: WorkContext,
707        depth: usize,
708        output: Vec<Value>,
709    ) -> Result<Value, PromiseRejection> {
710        let Some(child) = children.get(index).cloned() else {
711            return Ok(Value::Vector(output.into_iter().collect()));
712        };
713        let runtime_next = runtime.clone();
714        let children_next = children.clone();
715        let context_next = context.clone();
716        let value = execute(runtime, child, input.clone(), context, depth)?;
717        then(
718            value,
719            Rc::new(move |resolved| {
720                let mut next_output = output.clone();
721                next_output.push(resolved);
722                next(
723                    runtime_next.clone(),
724                    children_next.clone(),
725                    index + 1,
726                    input.clone(),
727                    context_next.clone(),
728                    depth,
729                    next_output,
730                )
731            }),
732        )
733    }
734    next(
735        runtime,
736        Rc::new(children),
737        0,
738        input,
739        context,
740        depth,
741        Vec::new(),
742    )
743}
744
745fn sequence_input(value: Value) -> Result<Vec<Value>, PromiseRejection> {
746    vector(value, "work input").map_err(plan_error)
747}
748
749fn execute_each(
750    runtime: WorkRuntime,
751    value: Value,
752    input: Value,
753    context: WorkContext,
754    depth: usize,
755    filtering: bool,
756) -> Result<Value, PromiseRejection> {
757    let child = field(&value, CHILD_KEY).expect("validated child");
758    let values = sequence_input(input)?;
759    fn next(
760        runtime: WorkRuntime,
761        child: Value,
762        values: Rc<Vec<Value>>,
763        index: usize,
764        context: WorkContext,
765        depth: usize,
766        filtering: bool,
767        output: Vec<Value>,
768    ) -> Result<Value, PromiseRejection> {
769        let Some(item) = values.get(index).cloned() else {
770            return Ok(Value::Vector(output.into_iter().collect()));
771        };
772        let runtime_next = runtime.clone();
773        let child_next = child.clone();
774        let values_next = values.clone();
775        let context_next = context.clone();
776        let value = execute(runtime, child, item.clone(), context, depth)?;
777        then(
778            value,
779            Rc::new(move |resolved| {
780                let mut next_output = output.clone();
781                if !filtering || truthy(&resolved) {
782                    next_output.push(if filtering { item.clone() } else { resolved });
783                }
784                next(
785                    runtime_next.clone(),
786                    child_next.clone(),
787                    values_next.clone(),
788                    index + 1,
789                    context_next.clone(),
790                    depth,
791                    filtering,
792                    next_output,
793                )
794            }),
795        )
796    }
797    next(
798        runtime,
799        child,
800        Rc::new(values),
801        0,
802        context,
803        depth,
804        filtering,
805        Vec::new(),
806    )
807}
808
809fn execute_fold(
810    runtime: WorkRuntime,
811    value: Value,
812    input: Value,
813    context: WorkContext,
814    depth: usize,
815) -> Result<Value, PromiseRejection> {
816    let reducer = field(&value, REDUCER_KEY).expect("validated reducer");
817    let initial = field(&value, INITIAL_KEY).unwrap_or(Value::Nil);
818    let values = sequence_input(input)?;
819    fn next(
820        runtime: WorkRuntime,
821        reducer: Value,
822        values: Rc<Vec<Value>>,
823        index: usize,
824        accumulator: Value,
825        context: WorkContext,
826        depth: usize,
827    ) -> Result<Value, PromiseRejection> {
828        let Some(item) = values.get(index).cloned() else {
829            return Ok(accumulator);
830        };
831        let runtime_next = runtime.clone();
832        let reducer_next = reducer.clone();
833        let values_next = values.clone();
834        let context_next = context.clone();
835        let request = map([("acc", accumulator), ("item", item)]);
836        let value = execute(runtime, reducer, request, context, depth)?;
837        then(
838            value,
839            Rc::new(move |resolved| {
840                next(
841                    runtime_next.clone(),
842                    reducer_next.clone(),
843                    values_next.clone(),
844                    index + 1,
845                    resolved,
846                    context_next.clone(),
847                    depth,
848                )
849            }),
850        )
851    }
852    next(
853        runtime,
854        reducer,
855        Rc::new(values),
856        0,
857        initial,
858        context,
859        depth,
860    )
861}
862
863fn execute_choose(
864    runtime: WorkRuntime,
865    value: Value,
866    input: Value,
867    context: WorkContext,
868    depth: usize,
869) -> Result<Value, PromiseRejection> {
870    let selector = field(&value, SELECTOR_KEY).expect("validated selector");
871    let choices = field(&value, CHOICES_KEY).expect("validated choices");
872    let runtime_next = runtime.clone();
873    let context_next = context.clone();
874    let selected = execute(runtime, selector, input.clone(), context, depth)?;
875    then(
876        selected,
877        Rc::new(move |selected| {
878            let child =
879                map_lookup(&choices, &selected).ok_or_else(|| plan_error("work/choice-missing"))?;
880            execute(
881                runtime_next.clone(),
882                child,
883                input.clone(),
884                context_next.clone(),
885                depth,
886            )
887        }),
888    )
889}
890
891fn execute_graph(
892    runtime: WorkRuntime,
893    value: Value,
894    input: Value,
895    context: WorkContext,
896    depth: usize,
897) -> Result<Value, PromiseRejection> {
898    let nodes = field(&value, NODES_KEY).expect("validated graph nodes");
899    let order = vector(
900        field(&value, ORDER_KEY).expect("validated graph order"),
901        ORDER_KEY,
902    )
903    .map_err(plan_error)?;
904    fn next(
905        runtime: WorkRuntime,
906        nodes: Value,
907        order: Rc<Vec<Value>>,
908        index: usize,
909        input: Value,
910        context: WorkContext,
911        depth: usize,
912        output: Vec<(Value, Value)>,
913    ) -> Result<Value, PromiseRejection> {
914        let Some(id) = order.get(index).cloned() else {
915            return Ok(Value::Map(output.into_iter().collect()));
916        };
917        let child = map_lookup(&nodes, &id).expect("validated graph node");
918        let runtime_next = runtime.clone();
919        let nodes_next = nodes.clone();
920        let order_next = order.clone();
921        let input_next = input.clone();
922        let context_next = context.clone();
923        let value = execute(runtime, child, input, context, depth)?;
924        then(
925            value,
926            Rc::new(move |resolved| {
927                let mut next_output = output.clone();
928                next_output.push((id.clone(), resolved));
929                next(
930                    runtime_next.clone(),
931                    nodes_next.clone(),
932                    order_next.clone(),
933                    index + 1,
934                    input_next.clone(),
935                    context_next.clone(),
936                    depth,
937                    next_output,
938                )
939            }),
940        )
941    }
942    next(
943        runtime,
944        nodes,
945        Rc::new(order),
946        0,
947        input,
948        context,
949        depth,
950        Vec::new(),
951    )
952}
953
954fn execute_batch(
955    runtime: WorkRuntime,
956    value: Value,
957    input: Value,
958    context: WorkContext,
959    depth: usize,
960) -> Result<Value, PromiseRejection> {
961    let each = map([
962        (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
963        (OP_KEY, Value::Keyword("each".into())),
964        (
965            CHILD_KEY,
966            field(&value, PROCESS_KEY).expect("validated batch process"),
967        ),
968    ]);
969    execute_each(runtime, each, input, context, depth, false)
970}
971
972fn execute_bind(
973    runtime: WorkRuntime,
974    value: Value,
975    input: Value,
976    context: WorkContext,
977    depth: usize,
978) -> Result<Value, PromiseRejection> {
979    let maximum = field(&value, MAXIMUM_DEPTH_KEY)
980        .and_then(|value| match value {
981            Value::Number(value) if value > 0 => Some(value as usize),
982            _ => None,
983        })
984        .unwrap_or(64);
985    if depth >= maximum {
986        return Err(plan_error("work/bind-depth-exceeded"));
987    }
988    let source = field(&value, SOURCE_KEY).expect("validated source");
989    let target = target_name(field(&value, CONTINUATION_KEY).expect("validated continuation"))
990        .map_err(plan_error)?;
991    let runtime_next = runtime.clone();
992    let context_next = context.clone();
993    let source_value = execute(runtime, source, input, context, depth)?;
994    then(
995        source_value,
996        Rc::new(move |resolved| {
997            let produced = runtime_next
998                .registry
999                .target(&target)
1000                .ok_or_else(|| plan_error(format!("work/target-unavailable: {target}")))?(
1001                resolved.clone(),
1002                context_next.clone(),
1003            )?;
1004            let plan = WorkPlan::from_value(produced)
1005                .map_err(|_| plan_error("work/bind-target-returned-non-plan"))?;
1006            execute(
1007                runtime_next.clone(),
1008                plan.value(),
1009                resolved,
1010                context_next.clone(),
1011                depth + 1,
1012            )
1013        }),
1014    )
1015}
1016
1017fn finish_ensure(
1018    runtime: WorkRuntime,
1019    cleanup: Value,
1020    input: Value,
1021    context: WorkContext,
1022    depth: usize,
1023    body_status: &'static str,
1024    body_result: Value,
1025    body_error: Option<PromiseRejection>,
1026) -> Result<Value, PromiseRejection> {
1027    let cleanup_value = map([
1028        ("work/body-status", Value::Keyword(body_status.into())),
1029        ("work/body-result", body_result.clone()),
1030        ("work/input", input),
1031    ]);
1032    let cleanup_result = execute(runtime, cleanup, cleanup_value, context, depth)?;
1033    then(
1034        cleanup_result,
1035        Rc::new(move |_| match &body_error {
1036            Some(error) => Err(error.clone()),
1037            None => Ok(body_result.clone()),
1038        }),
1039    )
1040}
1041
1042fn execute_ensure(
1043    runtime: WorkRuntime,
1044    value: Value,
1045    input: Value,
1046    context: WorkContext,
1047    depth: usize,
1048) -> Result<Value, PromiseRejection> {
1049    let body = field(&value, CHILD_KEY).expect("validated ensure body");
1050    let cleanup = field(&value, CLEANUP_KEY).expect("validated cleanup");
1051    let result = execute(runtime.clone(), body, input.clone(), context.clone(), depth);
1052    match result {
1053        Ok(result) => {
1054            let completed_runtime = runtime.clone();
1055            let completed_cleanup = cleanup.clone();
1056            let completed_input = input.clone();
1057            let completed_context = context.clone();
1058            settle(
1059                result,
1060                Rc::new(move |body_result| {
1061                    finish_ensure(
1062                        completed_runtime.clone(),
1063                        completed_cleanup.clone(),
1064                        completed_input.clone(),
1065                        completed_context.clone(),
1066                        depth,
1067                        "completed",
1068                        body_result,
1069                        None,
1070                    )
1071                }),
1072                Rc::new(move |body_error| {
1073                    finish_ensure(
1074                        runtime.clone(),
1075                        cleanup.clone(),
1076                        input.clone(),
1077                        context.clone(),
1078                        depth,
1079                        "failed",
1080                        Value::Nil,
1081                        Some(body_error),
1082                    )
1083                }),
1084            )
1085        }
1086        Err(body_error) => finish_ensure(
1087            runtime,
1088            cleanup,
1089            input,
1090            context,
1091            depth,
1092            "failed",
1093            Value::Nil,
1094            Some(body_error),
1095        ),
1096    }
1097}
1098
1099fn execute_await(
1100    runtime: &WorkRuntime,
1101    value: Value,
1102    context: WorkContext,
1103) -> Result<Value, PromiseRejection> {
1104    let wait = field(&value, WAIT_KEY).expect("validated await");
1105    runtime
1106        .suspension
1107        .as_ref()
1108        .ok_or_else(|| plan_error("work/suspension-unavailable"))?(wait, context)
1109}
1110
1111#[cfg(test)]
1112mod tests {
1113    use super::*;
1114    use std::cell::Cell;
1115
1116    fn registry() -> WorkRegistry {
1117        let registry = WorkRegistry::default();
1118        registry
1119            .bind(
1120                "fixture/inc",
1121                Rc::new(|value, _| match value {
1122                    Value::Number(value) => Ok(Value::Number(value + 1)),
1123                    _ => Err(plan_error("fixture expects number")),
1124                }),
1125            )
1126            .unwrap();
1127        registry
1128            .bind(
1129                "fixture/double",
1130                Rc::new(|value, _| match value {
1131                    Value::Number(value) => Ok(Value::Number(value * 2)),
1132                    _ => Err(plan_error("fixture expects number")),
1133                }),
1134            )
1135            .unwrap();
1136        registry
1137    }
1138
1139    #[test]
1140    fn hta_round_trip_is_canonical_for_closure_free_plans() {
1141        let plan = WorkPlan::chain(vec![
1142            WorkPlan::pure("fixture/inc").unwrap(),
1143            WorkPlan::step("fixture/double").unwrap(),
1144        ])
1145        .unwrap();
1146        let bytes = plan.encode_hta().unwrap();
1147        assert_eq!(
1148            bytes,
1149            WorkPlan::decode_hta(&bytes).unwrap().encode_hta().unwrap()
1150        );
1151    }
1152
1153    #[test]
1154    fn named_targets_execute_through_the_existing_host_lifecycle() {
1155        let host = WorkHost::new();
1156        let runtime = WorkRuntime::new(registry());
1157        let plan = WorkPlan::chain(vec![
1158            WorkPlan::pure("fixture/inc").unwrap(),
1159            WorkPlan::step("fixture/double").unwrap(),
1160        ])
1161        .unwrap();
1162        let run = host
1163            .submit_plan(
1164                runtime,
1165                plan,
1166                Value::Number(4),
1167                WorkOptions::with_id("plan-run").unwrap(),
1168            )
1169            .unwrap();
1170        assert_eq!(
1171            run.work_result().wait_state(),
1172            PromiseState::Fulfilled(Value::Number(10))
1173        );
1174        assert_eq!(
1175            run.work_status().state,
1176            super::super::WorkRunState::Completed
1177        );
1178    }
1179
1180    #[test]
1181    fn missing_targets_fail_closed_and_reset_is_idempotent() {
1182        let host = WorkHost::new();
1183        let runtime = WorkRuntime::default();
1184        let run = host
1185            .submit_plan(
1186                runtime,
1187                WorkPlan::pure("missing").unwrap(),
1188                Value::Nil,
1189                WorkOptions::default(),
1190            )
1191            .unwrap();
1192        assert!(matches!(
1193            run.work_result().wait_state(),
1194            PromiseState::Rejected(_)
1195        ));
1196        host.reset();
1197        host.reset();
1198        assert_eq!(host.status().run_count, 0);
1199        assert!(host.started());
1200    }
1201
1202    #[test]
1203    fn keyword_targets_use_their_stable_names() {
1204        let plan = WorkPlan::from_value(map([
1205            (VERSION_KEY, Value::Number(WorkOperation::VERSION)),
1206            (OP_KEY, Value::Keyword("pure".into())),
1207            (TARGET_KEY, Value::Keyword("fixture/inc".into())),
1208        ]))
1209        .unwrap();
1210        let host = WorkHost::new();
1211        let run = host
1212            .submit_plan(
1213                WorkRuntime::new(registry()),
1214                plan,
1215                Value::Number(4),
1216                WorkOptions::default(),
1217            )
1218            .unwrap();
1219        assert_eq!(
1220            run.work_result().wait_state(),
1221            PromiseState::Fulfilled(Value::Number(5))
1222        );
1223    }
1224
1225    #[test]
1226    fn ensure_runs_cleanup_for_completed_and_failed_bodies() {
1227        let registry = registry();
1228        let cleanup_count = Rc::new(Cell::new(0));
1229        let cleanup_count_next = cleanup_count.clone();
1230        registry
1231            .bind(
1232                "fixture/cleanup",
1233                Rc::new(move |_, _| {
1234                    cleanup_count_next.set(cleanup_count_next.get() + 1);
1235                    Ok(Value::Nil)
1236                }),
1237            )
1238            .unwrap();
1239        registry
1240            .bind(
1241                "fixture/fail",
1242                Rc::new(|_, _| Err(plan_error("fixture fails"))),
1243            )
1244            .unwrap();
1245
1246        let host = WorkHost::new();
1247        let runtime = WorkRuntime::new(registry);
1248        let cleanup = WorkPlan::step("fixture/cleanup").unwrap();
1249        let completed = host
1250            .submit_plan(
1251                runtime.clone(),
1252                WorkPlan::ensure(WorkPlan::pure("fixture/inc").unwrap(), cleanup.clone()).unwrap(),
1253                Value::Number(4),
1254                WorkOptions::with_id("ensure-completed").unwrap(),
1255            )
1256            .unwrap();
1257        assert_eq!(
1258            completed.work_result().wait_state(),
1259            PromiseState::Fulfilled(Value::Number(5))
1260        );
1261
1262        let failed = host
1263            .submit_plan(
1264                runtime,
1265                WorkPlan::ensure(WorkPlan::pure("fixture/fail").unwrap(), cleanup).unwrap(),
1266                Value::Number(4),
1267                WorkOptions::with_id("ensure-failed").unwrap(),
1268            )
1269            .unwrap();
1270        assert!(matches!(
1271            failed.work_result().wait_state(),
1272            PromiseState::Rejected(_)
1273        ));
1274        assert_eq!(cleanup_count.get(), 2);
1275    }
1276
1277    #[test]
1278    fn graph_and_batch_execute_their_data_owned_children() {
1279        let runtime = WorkRuntime::new(registry());
1280        let host = WorkHost::new();
1281        let graph = WorkPlan::graph(map([
1282            (
1283                "work/nodes",
1284                Value::Map(
1285                    [(
1286                        Value::Keyword("increment".into()),
1287                        WorkPlan::pure("fixture/inc").unwrap().value(),
1288                    )]
1289                    .into_iter()
1290                    .collect(),
1291                ),
1292            ),
1293            (
1294                "work/order",
1295                Value::Vector([Value::Keyword("increment".into())].into_iter().collect()),
1296            ),
1297        ]))
1298        .unwrap();
1299        let graph_result = host
1300            .submit_plan(
1301                runtime.clone(),
1302                graph,
1303                Value::Number(4),
1304                WorkOptions::with_id("plan-graph").unwrap(),
1305            )
1306            .unwrap()
1307            .work_result()
1308            .wait_state();
1309        let PromiseState::Fulfilled(graph_result) = graph_result else {
1310            panic!("graph plan should succeed");
1311        };
1312        assert_eq!(
1313            map_lookup(&graph_result, &Value::Keyword("increment".into())),
1314            Some(Value::Number(5))
1315        );
1316
1317        let batch = WorkPlan::batch(map([(
1318            "work/process",
1319            WorkPlan::step("fixture/double").unwrap().value(),
1320        )]))
1321        .unwrap();
1322        assert_eq!(
1323            host.submit_plan(
1324                runtime,
1325                batch,
1326                Value::Vector([Value::Number(2), Value::Number(3)].into_iter().collect()),
1327                WorkOptions::with_id("plan-batch").unwrap()
1328            )
1329            .unwrap()
1330            .work_result()
1331            .wait_state(),
1332            PromiseState::Fulfilled(Value::Vector(
1333                [Value::Number(4), Value::Number(6)].into_iter().collect()
1334            ))
1335        );
1336
1337        let invalid = WorkPlan::graph(map([
1338            (
1339                "work/nodes",
1340                Value::Map(
1341                    [(Value::Keyword("unused".into()), Value::Number(1))]
1342                        .into_iter()
1343                        .collect(),
1344                ),
1345            ),
1346            (
1347                "work/order",
1348                Value::Vector(Vec::new().into_iter().collect()),
1349            ),
1350        ]))
1351        .unwrap_err();
1352        assert!(invalid.contains("plan must be a map"));
1353    }
1354}