1use sim_kernel::{Expr, Symbol};
11
12use crate::kinds::{
13 AT_TICK_KEY, KIND_KEY, OPERATOR_KEY, ORIGIN_KEY, is_known_kind, required_fields,
14};
15
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub enum Operator {
20 Human,
22 Agent,
24}
25
26impl Operator {
27 pub fn symbol(self) -> Symbol {
29 match self {
30 Operator::Human => Symbol::new("human"),
31 Operator::Agent => Symbol::new("agent"),
32 }
33 }
34
35 pub fn from_name(name: &str) -> Option<Self> {
37 match name {
38 "human" => Some(Operator::Human),
39 "agent" => Some(Operator::Agent),
40 _ => None,
41 }
42 }
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub struct Origin {
48 pub operator: Operator,
50 pub at_tick: u64,
52}
53
54impl Origin {
55 pub fn human(tick: u64) -> Self {
57 Self {
58 operator: Operator::Human,
59 at_tick: tick,
60 }
61 }
62
63 pub fn agent(tick: u64) -> Self {
65 Self {
66 operator: Operator::Agent,
67 at_tick: tick,
68 }
69 }
70
71 fn to_expr(self) -> Expr {
72 sim_value::build::map(vec![
73 (OPERATOR_KEY, Expr::Symbol(self.operator.symbol())),
74 (AT_TICK_KEY, sim_value::build::uint(self.at_tick)),
75 ])
76 }
77}
78
79#[derive(Clone, Debug, PartialEq, Eq)]
82pub struct IntentError {
83 pub path: Vec<String>,
85 pub message: String,
87}
88
89impl IntentError {
90 fn at(path: &[&str], message: impl Into<String>) -> Self {
91 Self {
92 path: path.iter().map(|segment| (*segment).to_owned()).collect(),
93 message: message.into(),
94 }
95 }
96
97 pub fn path_string(&self) -> String {
99 if self.path.is_empty() {
100 "<root>".to_owned()
101 } else {
102 self.path.join(".")
103 }
104 }
105}
106
107impl core::fmt::Display for IntentError {
108 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
109 write!(f, "{}: {}", self.path_string(), self.message)
110 }
111}
112
113pub fn intent(kind_name: &str, origin: Origin, fields: Vec<(&str, Expr)>) -> Expr {
115 let mut pairs = Vec::with_capacity(fields.len() + 2);
116 pairs.push((
117 sim_value::build::sym(KIND_KEY),
118 Expr::Symbol(crate::kinds::intent_kind(kind_name)),
119 ));
120 pairs.push((sim_value::build::sym(ORIGIN_KEY), origin.to_expr()));
121 for (key, value) in fields {
122 pairs.push((sim_value::build::sym(key), value));
123 }
124 Expr::Map(pairs)
125}
126
127fn entry<'a>(entries: &'a [(Expr, Expr)], name: &str) -> Option<&'a Expr> {
128 entries.iter().find_map(|(key, value)| {
129 matches!(key, Expr::Symbol(symbol) if &*symbol.name == name && symbol.namespace.is_none())
130 .then_some(value)
131 })
132}
133
134pub fn intent_kind_of(expr: &Expr) -> Option<Symbol> {
136 let Expr::Map(entries) = expr else {
137 return None;
138 };
139 match entry(entries, KIND_KEY) {
140 Some(Expr::Symbol(kind)) => Some(kind.clone()),
141 _ => None,
142 }
143}
144
145pub fn field<'a>(expr: &'a Expr, name: &str) -> Option<&'a Expr> {
147 sim_value::access::field(expr, name)
148}
149
150pub fn origin(expr: &Expr) -> Option<Origin> {
152 let origin = field(expr, ORIGIN_KEY)?;
153 let Expr::Map(entries) = origin else {
154 return None;
155 };
156 let operator = match entry(entries, OPERATOR_KEY) {
157 Some(Expr::Symbol(symbol)) => Operator::from_name(&symbol.name)?,
158 _ => return None,
159 };
160 let at_tick = match entry(entries, AT_TICK_KEY) {
161 Some(Expr::Number(number)) => number.canonical.parse::<u64>().ok()?,
162 _ => return None,
163 };
164 Some(Origin { operator, at_tick })
165}
166
167pub fn validate_intent(expr: &Expr) -> Result<(), IntentError> {
170 let Expr::Map(entries) = expr else {
171 return Err(IntentError::at(&[], "an Intent must be a map"));
172 };
173 let kind = match entry(entries, KIND_KEY) {
174 Some(Expr::Symbol(kind)) if is_known_kind(kind) => kind.clone(),
175 Some(Expr::Symbol(kind)) => {
176 return Err(IntentError::at(
177 &[KIND_KEY],
178 format!("unrecognized Intent kind '{kind}'"),
179 ));
180 }
181 Some(_) => {
182 return Err(IntentError::at(
183 &[KIND_KEY],
184 "Intent 'kind' must be a symbol",
185 ));
186 }
187 None => return Err(IntentError::at(&[], "Intent is missing a 'kind' tag")),
188 };
189 validate_origin(entries)?;
190 for required in required_fields(&kind.name) {
191 let Some(value) = entry(entries, required) else {
192 return Err(IntentError::at(
193 &[required],
194 format!("Intent '{kind}' is missing required field '{required}'"),
195 ));
196 };
197 if *required == "path" && !matches!(value, Expr::List(_)) {
198 return Err(IntentError::at(
199 &["path"],
200 "edit-field 'path' must be a list of segments",
201 ));
202 }
203 }
204 Ok(())
205}
206
207fn validate_origin(entries: &[(Expr, Expr)]) -> Result<(), IntentError> {
208 let Some(origin) = entry(entries, ORIGIN_KEY) else {
209 return Err(IntentError::at(
210 &[ORIGIN_KEY],
211 "Intent is missing an 'origin'",
212 ));
213 };
214 let Expr::Map(origin_entries) = origin else {
215 return Err(IntentError::at(
216 &[ORIGIN_KEY],
217 "Intent 'origin' must be a map",
218 ));
219 };
220 match entry(origin_entries, OPERATOR_KEY) {
221 Some(Expr::Symbol(symbol)) if Operator::from_name(&symbol.name).is_some() => {}
222 _ => {
223 return Err(IntentError::at(
224 &[ORIGIN_KEY, OPERATOR_KEY],
225 "origin 'operator' must be 'human' or 'agent'",
226 ));
227 }
228 }
229 match entry(origin_entries, AT_TICK_KEY) {
230 Some(Expr::Number(_)) => Ok(()),
231 _ => Err(IntentError::at(
232 &[ORIGIN_KEY, AT_TICK_KEY],
233 "origin 'at-tick' must be a number",
234 )),
235 }
236}
237
238pub fn resolve_targets(expr: &Expr, is_known: impl Fn(&Expr) -> bool) -> Result<(), IntentError> {
242 for (label, target) in referenced_targets(expr) {
243 if !is_known(&target) {
244 return Err(IntentError {
245 path: vec![label],
246 message: "Intent references an unknown target".to_owned(),
247 });
248 }
249 }
250 Ok(())
251}
252
253pub fn referenced_targets(expr: &Expr) -> Vec<(String, Expr)> {
256 let Some(kind) = intent_kind_of(expr) else {
257 return Vec::new();
258 };
259 let mut refs = Vec::new();
260 let mut single = |name: &str| {
261 if let Some(value) = field(expr, name) {
262 refs.push((name.to_owned(), value.clone()));
263 }
264 };
265 match &*kind.name {
266 "tap" | "edit-field" | "invoke" | "scrub" | "set-param" | "performance-event"
267 | "piano-roll-edit" | "player-rack-edit" | "arranger-edit" => single("target"),
268 "move" => single("node"),
269 "unwire" => single("edge"),
270 "create" => single("class"),
271 "open" => single("value"),
272 "approve" | "reject" | "ask" | "split-mission" | "pause-agent" | "rerun-validation"
273 | "replay-cassette" => single("mission"),
274 "open-source" => single("location"),
275 "select" | "delete" => {
276 if let Some(Expr::List(items)) = field(expr, "targets") {
277 for (index, item) in items.iter().enumerate() {
278 refs.push((format!("targets[{index}]"), item.clone()));
279 }
280 }
281 }
282 "wire" => {
283 for end in ["from", "to"] {
284 if let Some(Expr::Map(port)) = field(expr, end)
285 && let Some(node) = entry(port, "node")
286 {
287 refs.push((format!("{end}.node"), node.clone()));
288 }
289 }
290 }
291 _ => {}
292 }
293 refs
294}