1use super::checks::{Below, Checks, Hook, Site, Stamp};
6use super::fingerprint::Fingerprint;
7use super::lower::Lower;
8use super::ops::{CallSite, Callee, CondKind, CondSite, FanSite, LoopSite, Op, Policy, TaskMeta};
9use crate::base::any_rejected;
10use crate::base::typo::near_miss;
11use crate::base::{AuthoredPath, Issue, IssueCode, PathSeg, StepKind};
12use crate::def::{
13 self, ForEachDef, GroupDef, Logic, LoopDef, ParseOptions, Rollout, Status, StepDef, TaskDef,
14 WorkflowDef, WorkflowFailurePolicy,
15};
16use crate::expr::path::MAX_PATH_SEGMENTS;
17use crate::expr::{CompileCx, Expr, ExprEngine, Path};
18use crate::function::{Entry, FunctionSpec, Prepare, Registry};
19use datavalue::OwnedDataValue;
20use serde_json::Value;
21use std::fmt;
22use std::str::FromStr;
23use std::sync::Arc;
24
25pub trait ResourceOracle: Send + Sync + 'static {
51 fn resolve(&self, kind: &str, name: &str) -> std::result::Result<(), String>;
62}
63
64#[derive(Clone, Default)]
66#[non_exhaustive]
67pub struct CompileOptions {
68 pub resources: Option<Arc<dyn ResourceOracle>>,
71}
72
73impl fmt::Debug for CompileOptions {
74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 f.debug_struct("CompileOptions")
77 .field("resources", &self.resources.is_some())
78 .finish_non_exhaustive()
79 }
80}
81
82impl CompileOptions {
83 #[must_use]
85 pub fn resources(mut self, oracle: Arc<dyn ResourceOracle>) -> Self {
86 self.resources = Some(oracle);
87 self
88 }
89}
90
91impl Registry {
92 pub fn compiler(&self, options: CompileOptions) -> Compiler {
94 Compiler {
95 registry: self.clone(),
96 options,
97 }
98 }
99}
100
101#[derive(Clone, Debug)]
124pub struct Compiler {
125 pub(super) registry: Registry,
126 pub(super) options: CompileOptions,
127}
128
129impl Compiler {
130 pub fn registry(&self) -> &Registry {
132 &self.registry
133 }
134
135 pub fn check(&self, def: &WorkflowDef) -> Vec<Issue> {
138 match self.compile_one(def) {
139 Ok(compiled) => compiled.issues,
140 Err(issues) => issues,
141 }
142 }
143
144 pub fn check_steps(&self, steps: &Value, root: &str) -> Vec<Issue> {
160 let root = match AuthoredPath::from_str(root) {
161 Ok(root) => root,
162 Err(error) => {
163 return vec![Issue::new(
164 IssueCode::ParseFailed,
165 AuthoredPath::root(),
166 error.to_string(),
167 )];
168 }
169 };
170 let Some((setup, _)) = def::fragment_place(&root) else {
171 let message = format!(
172 "`{root}` holds no steps: a fragment sits at a `tasks` list, such as `tasks` or \
173 `tasks[2].tasks`, or at `loop.setup`"
174 );
175 return vec![Issue::new(IssueCode::ParseFailed, root, message)];
176 };
177 let (parsed, mut issues) = def::parse_steps(steps, &root, ParseOptions::default());
178 if let Some(steps) = parsed {
179 let mut checks = Checks::new(self.registry.exprs());
180 let mut walk = Walk::new(self, "", &mut checks);
181 walk.steps(&steps, &root, setup);
182 issues.append(&mut walk.issues);
183 }
184 issues
185 }
186
187 pub fn compile_one(&self, def: &WorkflowDef) -> std::result::Result<Compiled, Vec<Issue>> {
193 let mut checks = Checks::new(self.registry.exprs());
194 let (workflow, issues) = self.compile_with(def, &mut checks);
195 if any_rejected(&issues) {
196 return Err(issues);
197 }
198 if let Err(broken) = workflow.check_invariants() {
200 panic!(
201 "the lowering of `{}` broke an invariant: {broken}\n{}",
202 workflow.id,
203 workflow.listing(),
204 );
205 }
206 Ok(Compiled { workflow, issues })
207 }
208
209 fn compile_with(
211 &self,
212 def: &WorkflowDef,
213 hook: &mut dyn Hook,
214 ) -> (CompiledWorkflow, Vec<Issue>) {
215 let mut walk = Walk::new(self, &def.id, hook);
216 let root = AuthoredPath::root();
217 let cond = def
220 .condition
221 .as_ref()
222 .and_then(|raw| walk.cond(raw, root.join("condition"), None));
223 let body = root.join("tasks");
226 match &def.r#loop {
227 None => {
228 walk.lower.workflow_cond(cond);
229 walk.steps(&def.tasks, &body, false);
230 }
231 Some(config) => {
232 let lp = walk.r#loop(config);
233 if !config.setup.is_empty() || config.over.is_some() {
236 walk.lower.workflow_cond(cond);
237 }
238 walk.steps(&config.setup, &root.join("loop").join("setup"), true);
239 let sweep = walk.lower.loop_init(lp);
240 walk.lower.workflow_cond(cond);
241 walk.steps(&def.tasks, &body, false);
242 walk.lower.loop_next(lp, sweep);
243 }
244 }
245 walk.hook.workflow(def, &mut walk.issues);
247 let workflow = CompiledWorkflow {
248 registry: self.registry.clone(),
249 id: def.id.clone(),
250 channel: def.channel.clone(),
251 priority: def.priority,
252 status: def.status,
253 version: def.version,
254 rollout: def.rollout,
255 on_failure: def.on_failure,
256 ops: walk.lower.finish(),
257 conds: walk.conds,
258 calls: walk.calls,
259 fans: walk.fans,
260 loops: walk.loops,
261 tasks: walk.tasks,
262 fingerprint: walk.hook.fingerprint(),
263 };
264 (workflow, walk.issues)
265 }
266}
267
268#[derive(Debug)]
270#[non_exhaustive]
271pub struct Compiled {
272 pub workflow: CompiledWorkflow,
274 pub issues: Vec<Issue>,
276}
277
278pub struct CompiledWorkflow {
280 pub(crate) registry: Registry,
283 pub(crate) id: String,
284 pub(crate) channel: String,
285 pub(crate) priority: u32,
286 pub(crate) status: Status,
287 pub(crate) version: u32,
288 pub(crate) rollout: Option<Rollout>,
289 pub(crate) on_failure: WorkflowFailurePolicy,
290 pub(crate) ops: Vec<Op>,
291 pub(crate) conds: Vec<CondSite>,
292 pub(crate) calls: Vec<CallSite>,
293 pub(crate) fans: Vec<FanSite>,
294 pub(crate) loops: Vec<LoopSite>,
295 pub(crate) tasks: Vec<TaskMeta>,
296 pub(crate) fingerprint: Fingerprint,
297}
298
299impl CompiledWorkflow {
300 pub fn id(&self) -> &str {
302 &self.id
303 }
304
305 pub fn fingerprint(&self) -> Fingerprint {
308 self.fingerprint
309 }
310}
311
312impl fmt::Debug for CompiledWorkflow {
313 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315 f.debug_struct("CompiledWorkflow")
316 .field("id", &self.id)
317 .field("ops", &self.ops.len())
318 .field("conds", &self.conds.len())
319 .field("calls", &self.calls.len())
320 .field("fans", &self.fans.len())
321 .field("loops", &self.loops.len())
322 .field("tasks", &self.tasks.len())
323 .finish_non_exhaustive()
324 }
325}
326
327pub(crate) struct CalleeAt<'a> {
330 pub(crate) name: &'a str,
332 pub(crate) workflow_id: &'a str,
334 pub(crate) step: (StepKind, &'a str),
336 pub(crate) input_at: &'a AuthoredPath,
338}
339
340pub(crate) fn compile_callee(
348 exprs: &ExprEngine,
349 entry: Entry<'_>,
350 input: &Value,
351 at: &CalleeAt<'_>,
352 hook: &mut dyn Hook,
353 issues: &mut Vec<Issue>,
354) -> Option<Callee> {
355 let callee = {
356 let mut cx = CompileCx::new(exprs, issues)
357 .at_path(at.input_at.clone())
358 .for_step(at.step.0, at.step.1);
359 let before = cx.rejected();
360 let callee = match entry {
361 Entry::Builtin(kind) => kind.compile(input, &mut cx).map(Callee::Builtin),
362 Entry::Host(f) => {
363 if let Some(fields) = &f.spec().runtime_fields {
364 cx = cx.with_runtime_fields(fields);
365 }
366 let prepare = Prepare::new(at.name, at.workflow_id, at.step.1, f.spec());
367 f.compile(input, &mut cx, &prepare).map(Callee::Host)
368 }
369 };
370 if callee.is_none() && cx.rejected() == before {
373 refused(&mut cx, at.name);
374 }
375 callee
376 }?;
377 callee.visit(&mut Below {
378 hook,
379 issues,
380 root: at.input_at,
381 step: Some(at.step),
382 });
383 Some(callee)
384}
385
386pub(crate) fn refused(cx: &mut CompileCx<'_>, name: &str) {
389 cx.issue(
390 IssueCode::InputParse,
391 format!("the config of `{name}` refused its input without reporting why"),
392 );
393}
394
395pub(super) fn next(len: usize) -> u32 {
397 u32::try_from(len).expect("a workflow holds fewer than 2^32 steps")
398}
399
400fn lookup<'v>(value: &'v Value, path: &AuthoredPath) -> Option<&'v Value> {
402 path.segs().iter().try_fold(value, |value, seg| match seg {
403 PathSeg::Key(key) => value.get(&**key),
404 PathSeg::Index(index) => value.get(*index),
405 })
406}
407
408struct Walk<'w> {
411 registry: &'w Registry,
412 resources: Option<&'w dyn ResourceOracle>,
413 workflow_id: &'w str,
415 hook: &'w mut dyn Hook,
416 issues: Vec<Issue>,
417 conds: Vec<CondSite>,
418 calls: Vec<CallSite>,
419 fans: Vec<FanSite>,
420 loops: Vec<LoopSite>,
421 tasks: Vec<TaskMeta>,
422 lower: Lower,
423}
424
425impl<'w> Walk<'w> {
426 fn new(compiler: &'w Compiler, workflow_id: &'w str, hook: &'w mut dyn Hook) -> Self {
427 Self {
428 registry: &compiler.registry,
429 resources: compiler.options.resources.as_deref(),
430 workflow_id,
431 hook,
432 issues: Vec::new(),
433 conds: Vec::new(),
434 calls: Vec::new(),
435 fans: Vec::new(),
436 loops: Vec::new(),
437 tasks: Vec::new(),
438 lower: Lower::default(),
439 }
440 }
441
442 fn report(&mut self, issue: Issue, step: Stamp<'_>) {
444 self.issues.push(match step {
445 Some((kind, id)) => issue.with_step(kind, Some(id)),
446 None => issue,
447 });
448 }
449
450 fn expr(
453 &mut self,
454 raw: &Logic,
455 at: &AuthoredPath,
456 records: bool,
457 step: Stamp<'_>,
458 ) -> Option<Expr> {
459 match self.registry.exprs().compile::<OwnedDataValue>(&raw.0) {
460 Ok(expr) => {
461 let site = Site {
462 at,
463 records,
464 step,
465 field: None,
466 };
467 self.hook
468 .expr(site, expr.raw(), Some(&expr), &mut self.issues);
469 Some(expr)
470 }
471 Err(error) => {
472 let issue = Issue::new(IssueCode::TemplateCompile, at.clone(), error.message());
473 self.report(issue, step);
474 None
475 }
476 }
477 }
478
479 fn cond(&mut self, raw: &Logic, at: AuthoredPath, step: Stamp<'_>) -> Option<u32> {
484 let expr = self.expr(raw, &at, false, step)?;
485 if expr.is_trivially_true() {
486 return None;
487 }
488 let row = next(self.conds.len());
489 self.conds.push(CondSite {
490 expr,
491 step: step.map(|(_, id)| id.to_owned()),
492 path: at,
493 });
494 Some(row)
495 }
496
497 fn path(&mut self, dotted: &str, at: AuthoredPath, step: Stamp<'_>) -> Option<Path> {
501 match Path::from_str(dotted) {
502 Ok(path) => Some(path),
503 Err(error) => {
504 let issue = Issue::new(IssueCode::InvalidPath, at, error.message());
505 self.report(issue, step);
506 None
507 }
508 }
509 }
510
511 fn r#loop(&mut self, config: &LoopDef) -> u32 {
518 let at = AuthoredPath::root().join("loop");
519 let over = config
520 .over
521 .as_ref()
522 .and_then(|raw| self.expr(raw, &at.join("over"), true, None));
523 let mut slot = |slot: &Option<String>, key: &str| {
524 slot.as_ref()
525 .and_then(|slot| self.path(&format!("temp_data.{slot}"), at.join(key), None))
526 };
527 let counter = slot(&config.counter, "counter");
528 let item = slot(&config.r#as, "as");
529 let scratch = slot(&config.scratch, "scratch");
530 let row = next(self.loops.len());
531 self.loops.push(LoopSite {
532 init: config.init,
533 increment: config.increment,
534 max: config.max,
535 counter,
536 over,
537 r#as: item,
538 scratch,
539 });
540 row
541 }
542
543 fn steps(&mut self, steps: &[StepDef], at: &AuthoredPath, setup: bool) {
547 for (i, step) in steps.iter().enumerate() {
548 let at = at.join(i);
549 let kind = step.kind(setup);
550 match step {
551 StepDef::Group(group) => self.group(&at, kind, group, setup),
552 StepDef::Task(task) => self.task(&at, kind, task),
553 }
554 }
555 }
556
557 fn group(&mut self, at: &AuthoredPath, kind: StepKind, group: &GroupDef, setup: bool) {
560 let cond = group
561 .condition
562 .as_ref()
563 .and_then(|raw| self.cond(raw, at.join("condition"), Some((kind, &group.id))));
564 let open = self.lower.open(cond, CondKind::Group);
565 self.steps(&group.tasks, &at.join("tasks"), setup);
566 if group.terminal {
567 self.lower.halt();
568 }
569 self.lower.close(open);
570 }
571
572 fn task(&mut self, at: &AuthoredPath, kind: StepKind, task: &TaskDef) {
575 let step = (kind, task.id.as_str());
576 let cond = task
577 .condition
578 .as_ref()
579 .and_then(|raw| self.cond(raw, at.join("condition"), Some(step)));
580 let fan = task.for_each.as_ref().and_then(|config| {
581 let fan = self.fan(config, &at.join("for_each"), step)?;
582 let row = next(self.fans.len());
583 self.fans.push(fan);
584 Some(row)
585 });
586 let row = next(self.tasks.len());
587 self.tasks.push(TaskMeta {
588 id: task.id.clone(),
589 function: task.function.name.clone(),
590 kind,
591 path: at.clone(),
592 });
593 let site = self.callee(task, at, step).map(|callee| {
594 let site = next(self.calls.len());
595 self.calls.push(CallSite {
596 task: row,
597 callee,
598 policy: Policy {
599 on_failure: task.on_failure,
600 on_rejection: task.on_rejection,
601 terminal: task.terminal,
602 },
603 });
604 site
605 });
606 let open = self.lower.open(cond, CondKind::Task);
607 match (site, task.for_each.is_some(), fan) {
608 (Some(site), false, _) => self.lower.call(site),
609 (Some(site), true, Some(fan)) => self.lower.fan_out(site, fan),
610 _ => {}
613 }
614 self.lower.close(open);
615 }
616
617 fn fan(
625 &mut self,
626 config: &ForEachDef,
627 at: &AuthoredPath,
628 step: (StepKind, &str),
629 ) -> Option<FanSite> {
630 let over = self.expr(&config.over, &at.join("over"), true, Some(step));
631 let item = format!("temp_data.{}", config.r#as);
632 let bound = self
633 .path(&item, at.join("as"), Some(step))
634 .and_then(|item_path| {
635 let index = self.path(&format!("{item}_index"), at.join("as"), Some(step))?;
636 Some((item_path, index))
637 });
638 let mut context = |path: &Option<String>, key: &str| {
639 path.as_ref()
640 .and_then(|path| self.path(path, at.join(key), Some(step)))
641 };
642 let collect = context(&config.collect, "collect");
643 let into = context(&config.into, "into").and_then(|into| {
644 if into.segs().len() < MAX_PATH_SEGMENTS {
645 return Some(into);
646 }
647 let message = format!(
648 "path `{into}` has {MAX_PATH_SEGMENTS} segments, and each element's result \
649 lands one segment below it; a path holds at most {MAX_PATH_SEGMENTS}"
650 );
651 let issue = Issue::new(IssueCode::InvalidPath, at.join("into"), message);
652 self.report(issue, Some(step));
653 None
654 });
655 let (over, (item, index)) = over.zip(bound)?;
656 Some(FanSite {
657 over,
658 r#as: item,
659 index,
660 max_concurrency: config.max_concurrency,
661 collect,
662 into,
663 })
664 }
665
666 fn callee(
672 &mut self,
673 task: &TaskDef,
674 at: &AuthoredPath,
675 step: (StepKind, &str),
676 ) -> Option<Callee> {
677 let registry = self.registry;
678 let name = &task.function.name;
679 let Some(entry) = registry.entry(name) else {
680 let names: Vec<&str> = registry.catalogue().map(|f| f.name).collect();
681 let message = match near_miss(name, &names) {
682 Some(near) => format!("no function `{name}` is registered; did you mean `{near}`?"),
683 None => format!("no function `{name}` is registered"),
684 };
685 let issue = Issue::new(
686 IssueCode::UnknownFunction,
687 at.join("function").join("name"),
688 message,
689 );
690 self.report(issue, Some(step));
691 return None;
692 };
693 let input_at = at.join("function").join("input");
694 let input = &task.function.input;
695 let site = CalleeAt {
696 name,
697 workflow_id: self.workflow_id,
698 step,
699 input_at: &input_at,
700 };
701 let callee = compile_callee(
702 registry.exprs(),
703 entry,
704 input,
705 &site,
706 &mut *self.hook,
707 &mut self.issues,
708 )?;
709 if let Entry::Host(f) = entry {
710 self.references(f.spec(), input, &input_at, step);
711 }
712 Some(callee)
713 }
714
715 fn references(
720 &mut self,
721 spec: &FunctionSpec,
722 input: &Value,
723 input_at: &AuthoredPath,
724 step: (StepKind, &str),
725 ) {
726 let Some(oracle) = self.resources else {
727 return;
728 };
729 let mut cx = CompileCx::new(self.registry.exprs(), &mut self.issues)
730 .at_path(input_at.clone())
731 .for_step(step.0, step.1);
732 for reference in &spec.references {
733 let Ok(field) = AuthoredPath::from_str(&reference.field) else {
734 continue;
735 };
736 if let Some(Value::String(name)) = lookup(input, &field)
737 && let Err(why) = oracle.resolve(&reference.kind, name)
738 {
739 let kind = &reference.kind;
740 let message = if why.is_empty() {
741 format!("no {kind} named `{name}` resolves")
742 } else {
743 format!("`{name}` does not resolve as a {kind}: {why}")
744 };
745 cx.adopt(Issue::new(IssueCode::UnresolvedReference, field, message));
746 }
747 }
748 }
749}
750
751#[cfg(test)]
752mod tests {
753 use super::*;
754 use crate::base::Result;
755 use crate::def::{FailurePolicy, RejectionPolicy, parse};
756 use crate::function::builtins::{Builtin, BuiltinKind};
757 use crate::function::{Ctx, Function, FunctionConfig, Outcome};
758 use serde_json::json;
759
760 #[derive(FunctionConfig)]
761 struct EchoConfig {
762 #[config(records)]
763 text: Expr<String>,
764 label: Option<Expr<String>>,
765 }
766
767 struct Echo;
768
769 impl Function for Echo {
770 type Config = EchoConfig;
771 async fn call(&self, _ctx: &mut Ctx<'_>, _cfg: &EchoConfig) -> Result<Outcome> {
772 Ok(Outcome::Ok)
773 }
774 }
775
776 type Step = Option<(StepKind, String)>;
778
779 #[derive(Default)]
781 struct Recorder(Vec<(String, bool, Step)>);
782
783 impl Hook for Recorder {
784 fn expr(
785 &mut self,
786 site: Site<'_>,
787 _raw: &Value,
788 _compiled: Option<&dyn crate::expr::ResolveExpr>,
789 _issues: &mut Vec<Issue>,
790 ) {
791 let step = site.step.map(|(kind, id)| (kind, id.to_owned()));
792 self.0.push((site.at.to_string(), site.records, step));
793 }
794 }
795
796 fn compiler() -> Compiler {
797 let mut registry = Registry::default();
798 registry.register("echo", Echo);
799 registry.compiler(CompileOptions::default())
800 }
801
802 fn every_site() -> WorkflowDef {
805 let json = json!({
806 "id": "w", "channel": "orders", "priority": 4, "status": "paused", "version": 3,
807 "on_failure": "continue", "rollout": {"bucket_start": 10, "bucket_end": 60},
808 "condition": {"var": "metadata.go"},
809 "loop": {
810 "counter": "i", "init": 1, "increment": 2, "max": 9,
811 "over": {"var": "data.batches"}, "as": "batch", "scratch": "acc",
812 "setup": [{"id": "s", "condition": {"var": "data.s"},
813 "function": {"name": "log", "input": {"message": "start"}}}],
814 },
815 "tasks": [
816 {"id": "g", "condition": {"var": "data.g"}, "tasks": [
817 {"id": "f", "condition": {"var": "data.f"},
818 "for_each": {"over": {"var": "data.items"}, "as": "item",
819 "max_concurrency": 2, "collect": "temp_data.price",
820 "into": "temp_data.acc.prices"},
821 "on_failure": "continue", "on_rejection": "halt",
822 "function": {"name": "echo", "input": {
823 "text": {"var": "temp_data.item"}, "label": "x"}}},
824 ]},
825 {"id": "m", "terminal": true, "function": {"name": "map", "input": {
826 "mappings": [{"path": "data.x", "logic": {"var": "data.y"}}]}}},
827 ],
828 });
829 let parsed = parse(&json, ParseOptions::default());
830 parsed
831 .def
832 .unwrap_or_else(|| panic!("the definition parses: {:?}", parsed.issues))
833 }
834
835 fn task(id: &str) -> Step {
836 Some((StepKind::Task, id.to_owned()))
837 }
838
839 #[test]
840 fn the_hook_sees_every_site_at_its_absolute_path() {
841 let mut recorder = Recorder::default();
842 let (_, issues) = compiler().compile_with(&every_site(), &mut recorder);
843 assert!(issues.is_empty(), "{issues:?}");
844 let site = |at: &str, records: bool, step: Step| (at.to_owned(), records, step);
845 let setup = Some((StepKind::Setup, "s".to_owned()));
846 let group = Some((StepKind::Group, "g".to_owned()));
847 assert_eq!(
848 recorder.0,
849 [
850 site("condition", false, None),
851 site("loop.over", true, None),
852 site("loop.setup[0].condition", false, setup.clone()),
853 site("loop.setup[0].function.input.message", true, setup),
854 site("tasks[0].condition", false, group),
855 site("tasks[0].tasks[0].condition", false, task("f")),
856 site("tasks[0].tasks[0].for_each.over", true, task("f")),
857 site("tasks[0].tasks[0].function.input.text", true, task("f")),
858 site("tasks[0].tasks[0].function.input.label", false, task("f")),
859 site("tasks[1].function.input.mappings[0].path", true, task("m")),
860 site("tasks[1].function.input.mappings[0].logic", true, task("m")),
861 ]
862 );
863 }
864
865 #[test]
866 fn the_compile_walk_visits_the_steps_in_def_walk_order() {
867 let c = |id: &str| json!({"var": format!("data.{id}")});
871 let t = |id: &str| {
872 json!({"id": id, "condition": c(id),
873 "function": {"name": "log", "input": {"message": "m"}}})
874 };
875 let json = json!({"id": "w", "loop": {"max": 2, "setup": [
876 t("s1"),
877 {"id": "sg", "condition": c("sg"), "tasks": [t("s2"), t("s3")]},
878 ]},
879 "tasks": [
880 {"id": "a", "condition": c("a"), "tasks": [
881 {"id": "b", "condition": c("b"), "tasks": [t("t1")]},
882 t("t2"),
883 {"id": "c", "condition": c("c"), "tasks": [
884 {"id": "d", "condition": c("d"), "tasks": [t("t3")]},
885 ]},
886 ]},
887 t("t4"),
888 ],
889 });
890 let def = parse(&json, ParseOptions::default()).def.unwrap();
891 let mut recorder = Recorder::default();
892 let (_, issues) = compiler().compile_with(&def, &mut recorder);
893 assert!(issues.is_empty(), "{issues:?}");
894 let walked: Vec<(String, Step)> = recorder
895 .0
896 .into_iter()
897 .filter_map(|(at, _, step)| Some((at.strip_suffix(".condition")?.to_owned(), step)))
898 .collect();
899 let expected: Vec<(String, Step)> = def
900 .walk()
901 .map(|(path, kind, step)| (path.to_string(), Some((kind, step.id().to_owned()))))
902 .collect();
903 assert_eq!(walked.len(), 12);
904 assert_eq!(walked, expected);
905 }
906
907 #[test]
908 fn check_steps_reaches_the_same_sites_under_its_root() {
909 let steps = json!([{"id": "f", "condition": {"var": "data.f"},
911 "function": {"name": "echo", "input": {"text": "a", "label": {"var": "data.l"}}}}]);
912 let root: AuthoredPath = "tasks[2].tasks".parse().unwrap();
913 let (parsed, issues) = def::parse_steps(&steps, &root, ParseOptions::default());
914 assert!(issues.is_empty(), "{issues:?}");
915 let parsed = parsed.unwrap();
916 let compiler = compiler();
917 let mut recorder = Recorder::default();
918 let mut walk = Walk::new(&compiler, "", &mut recorder);
919 walk.steps(&parsed, &root, false);
920 assert!(walk.issues.is_empty(), "{:?}", walk.issues);
921 let paths: Vec<_> = recorder.0.iter().map(|(at, ..)| at.as_str()).collect();
922 assert_eq!(
923 paths,
924 [
925 "tasks[2].tasks[0].condition",
926 "tasks[2].tasks[0].function.input.text",
927 "tasks[2].tasks[0].function.input.label",
928 ]
929 );
930 }
931
932 #[test]
933 fn the_side_tables_are_filled_in_walk_order() {
934 let compiled = compiler().compile_one(&every_site()).unwrap();
935 assert!(compiled.issues.is_empty(), "{:?}", compiled.issues);
936 let w = &compiled.workflow;
937
938 assert_eq!(w.id(), "w");
940 assert_eq!(w.channel, "orders");
941 assert_eq!((w.priority, w.version), (4, 3));
942 assert_eq!(w.status, Status::Paused);
943 assert_eq!(w.rollout, Some(Rollout::new(10, 60)));
944 assert_eq!(w.on_failure, WorkflowFailurePolicy::Continue);
945
946 let listing = [
949 " 0 Cond workflow else->end",
950 " 1 Cond task s else->3",
951 " 2 Call s log builtin fail->end",
952 " 3 LoopInit exit->end",
953 " 4 Cond workflow else->end",
954 " 5 Cond group g else->8",
955 " 6 Cond task f else->8",
956 " 7 FanOut f echo host as=item fail->9",
957 " 8 Call m map builtin fail->9 terminal",
958 " 9 LoopNext body->4 exit->end",
959 ];
960 let expected: String = listing.iter().map(|line| format!("{line}\n")).collect();
961 assert_eq!(w.listing(), expected, "\n{}", w.listing());
962 assert_eq!(w.check_invariants(), Ok(()));
963
964 let rows: Vec<_> = w
966 .tasks
967 .iter()
968 .map(|t| (&*t.id, &*t.function, t.kind, t.path.to_string()))
969 .collect();
970 assert_eq!(
971 rows,
972 [
973 ("s", "log", StepKind::Setup, "loop.setup[0]".to_owned()),
974 ("f", "echo", StepKind::Task, "tasks[0].tasks[0]".to_owned()),
975 ("m", "map", StepKind::Task, "tasks[1]".to_owned()),
976 ]
977 );
978
979 let calls: Vec<_> = w
981 .calls
982 .iter()
983 .map(|c| {
984 let callee = match &c.callee {
985 Callee::Builtin(b) => Some(b.kind()),
986 Callee::Host(_) => None,
987 };
988 (
989 c.task,
990 callee,
991 c.policy.on_failure,
992 c.policy.on_rejection,
993 c.policy.terminal,
994 )
995 })
996 .collect();
997 assert_eq!(
998 calls,
999 [
1000 (
1001 0,
1002 Some(BuiltinKind::Log),
1003 FailurePolicy::Fail,
1004 RejectionPolicy::Continue,
1005 false
1006 ),
1007 (
1008 1,
1009 None,
1010 FailurePolicy::Continue,
1011 RejectionPolicy::Halt,
1012 false
1013 ),
1014 (
1015 2,
1016 Some(BuiltinKind::Map),
1017 FailurePolicy::Fail,
1018 RejectionPolicy::Continue,
1019 true
1020 ),
1021 ]
1022 );
1023 assert!(matches!(
1024 &w.calls[2].callee,
1025 Callee::Builtin(Builtin::Map(_))
1026 ));
1027
1028 let conds: Vec<_> = w
1030 .conds
1031 .iter()
1032 .map(|c| (c.step.as_deref(), c.expr.raw().clone()))
1033 .collect();
1034 assert_eq!(
1035 conds,
1036 [
1037 (None, json!({"var": "metadata.go"})),
1038 (Some("s"), json!({"var": "data.s"})),
1039 (Some("g"), json!({"var": "data.g"})),
1040 (Some("f"), json!({"var": "data.f"})),
1041 ]
1042 );
1043
1044 let [fan] = &w.fans[..] else {
1047 panic!("one fan-out");
1048 };
1049 assert_eq!(fan.over.raw(), &json!({"var": "data.items"}));
1050 assert_eq!(fan.r#as.to_string(), "temp_data.item");
1051 assert_eq!(fan.index.to_string(), "temp_data.item_index");
1052 assert_eq!(fan.max_concurrency, 2);
1053 assert_eq!(fan.collect.as_ref().unwrap().to_string(), "temp_data.price");
1054 assert_eq!(
1055 fan.into.as_ref().unwrap().to_string(),
1056 "temp_data.acc.prices"
1057 );
1058
1059 let [lp] = &w.loops[..] else {
1061 panic!("one loop");
1062 };
1063 assert_eq!((lp.init, lp.increment, lp.max), (1, 2, 9));
1064 assert_eq!(lp.counter.as_ref().unwrap().to_string(), "temp_data.i");
1065 assert_eq!(
1066 lp.over.as_ref().unwrap().raw(),
1067 &json!({"var": "data.batches"})
1068 );
1069 assert_eq!(lp.r#as.as_ref().unwrap().to_string(), "temp_data.batch");
1070 assert_eq!(lp.scratch.as_ref().unwrap().to_string(), "temp_data.acc");
1071 }
1072
1073 #[test]
1074 fn a_workflow_without_a_loop_or_slots_has_none() {
1075 let json = json!({"id": "w", "loop": {"max": 2},
1076 "tasks": [{"id": "t", "function": {"name": "echo", "input": {"text": "a"}}}]});
1077 let def = parse(&json, ParseOptions::default()).def.unwrap();
1078 let compiled = compiler().compile_one(&def).unwrap();
1079 let [lp] = &compiled.workflow.loops[..] else {
1080 panic!("one loop");
1081 };
1082 assert!(lp.counter.is_none() && lp.over.is_none());
1083 assert!(lp.r#as.is_none() && lp.scratch.is_none());
1084 assert!(compiled.workflow.conds.is_empty());
1085 assert!(compiled.workflow.fans.is_empty());
1086 }
1087
1088 fn echo_task() -> Value {
1090 json!({"id": "t", "function": {"name": "echo", "input": {"text": "a"}}})
1091 }
1092
1093 #[test]
1094 fn compile_refuses_an_into_with_no_room_for_an_index() {
1095 for (depth, refused) in [(64, true), (63, false)] {
1098 let into = format!("data{}", ".k".repeat(depth - 1));
1099 let mut task = echo_task();
1100 task["for_each"] = json!({"over": {"var": "data.items"}, "as": "item",
1101 "collect": "temp_data.r", "into": into});
1102 let def = parse(
1103 &json!({"id": "w", "tasks": [task]}),
1104 ParseOptions::default(),
1105 )
1106 .def
1107 .unwrap();
1108 let issues = compiler().compile_one(&def).err().unwrap_or_default();
1109 let issues: Vec<_> = issues
1110 .into_iter()
1111 .map(|i| (i.code, i.path.to_string(), i.message))
1112 .collect();
1113 if refused {
1114 assert_eq!(
1115 issues,
1116 [(
1117 IssueCode::InvalidPath,
1118 "tasks[0].for_each.into".to_owned(),
1119 format!(
1120 "path `{into}` has 64 segments, and each element's result lands \
1121 one segment below it; a path holds at most 64"
1122 ),
1123 )]
1124 );
1125 } else {
1126 assert!(issues.is_empty(), "{issues:?}");
1127 }
1128 }
1129 }
1130
1131 #[test]
1132 fn debug_shows_the_id_the_op_count_and_the_table_sizes() {
1133 let compiled = compiler().compile_one(&every_site()).unwrap();
1134 assert_eq!(
1135 format!("{:?}", compiled.workflow),
1136 "CompiledWorkflow { id: \"w\", ops: 10, conds: 4, calls: 3, fans: 1, loops: 1, \
1137 tasks: 3, .. }"
1138 );
1139 }
1140
1141 #[test]
1142 fn debug_shows_the_registry_and_whether_an_oracle_is_set() {
1143 assert_eq!(
1144 format!("{:?}", CompileOptions::default()),
1145 "CompileOptions { resources: false, .. }"
1146 );
1147 let compiler = format!("{:?}", compiler());
1148 assert!(
1149 compiler.starts_with("Compiler { registry: Registry { functions: [\"map\""),
1150 "{compiler}"
1151 );
1152 assert!(compiler.contains("\"echo\"]"), "{compiler}");
1153 }
1154
1155 #[test]
1156 fn a_compiled_workflow_crosses_threads() {
1157 fn send_sync<T: Send + Sync>() {}
1158 send_sync::<CompiledWorkflow>();
1159 send_sync::<Compiler>();
1160 }
1161}