Skip to main content

pflow_tokenmodel/
runtime.rs

1//! Runtime execution of token model schemas.
2
3use std::collections::HashMap;
4use std::fmt;
5
6use serde_json::Value;
7
8use crate::error::{Error, Result};
9use crate::schema::Schema;
10use crate::snapshot::{Bindings, BindingsExt, Snapshot};
11
12/// Evaluates guard expressions.
13pub trait GuardEvaluator {
14    fn evaluate(
15        &self,
16        expr: &str,
17        bindings: &Bindings,
18    ) -> std::result::Result<bool, String>;
19
20    fn evaluate_constraint(
21        &self,
22        expr: &str,
23        tokens: &HashMap<String, i64>,
24    ) -> std::result::Result<bool, String>;
25}
26
27/// Describes a failed constraint check.
28#[derive(Debug, Clone)]
29pub struct ConstraintViolation {
30    pub constraint_id: String,
31    pub constraint_expr: String,
32    pub snapshot: Snapshot,
33    pub err: Option<String>,
34}
35
36impl fmt::Display for ConstraintViolation {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        if let Some(err) = &self.err {
39            write!(f, "constraint {} error: {}", self.constraint_id, err)
40        } else {
41            write!(f, "constraint {} violated", self.constraint_id)
42        }
43    }
44}
45
46/// Execution runtime for a schema.
47pub struct Runtime {
48    pub schema: Schema,
49    pub snapshot: Snapshot,
50    pub sequence: u64,
51    pub check_constraints: bool,
52    pub guard_evaluator: Option<Box<dyn GuardEvaluator>>,
53}
54
55impl Runtime {
56    pub fn new(schema: Schema) -> Self {
57        let snapshot = Snapshot::from_schema(&schema);
58        Self {
59            schema,
60            snapshot,
61            sequence: 0,
62            check_constraints: true,
63            guard_evaluator: None,
64        }
65    }
66
67    pub fn clone_runtime(&self) -> Self {
68        Self {
69            schema: self.schema.clone(),
70            snapshot: self.snapshot.clone(),
71            sequence: self.sequence,
72            check_constraints: self.check_constraints,
73            guard_evaluator: None, // Guard evaluator is not cloned
74        }
75    }
76
77    pub fn tokens(&self, state_id: &str) -> i64 {
78        self.snapshot.get_tokens(state_id)
79    }
80
81    pub fn set_tokens(&mut self, state_id: &str, count: i64) {
82        self.snapshot.set_tokens(state_id, count);
83    }
84
85    pub fn data(&self, state_id: &str) -> Option<&Value> {
86        self.snapshot.get_data(state_id)
87    }
88
89    pub fn set_data(&mut self, state_id: &str, value: Value) {
90        self.snapshot.set_data(state_id, value);
91    }
92
93    /// Returns true if an action can execute.
94    pub fn enabled(&self, action_id: &str) -> bool {
95        if self.schema.action_by_id(action_id).is_none() {
96            return false;
97        }
98
99        for arc in self.schema.input_arcs(action_id) {
100            if let Some(st) = self.schema.state_by_id(&arc.source) {
101                if st.is_token() && self.tokens(&arc.source) < 1 {
102                    return false;
103                }
104            }
105        }
106
107        true
108    }
109
110    /// Returns all actions that can execute.
111    pub fn enabled_actions(&self) -> Vec<String> {
112        self.schema
113            .actions
114            .iter()
115            .filter(|a| self.enabled(&a.id))
116            .map(|a| a.id.clone())
117            .collect()
118    }
119
120    /// Executes an action (token semantics only).
121    pub fn execute(&mut self, action_id: &str) -> Result<()> {
122        if !self.enabled(action_id) {
123            return Err(Error::ActionNotEnabled(action_id.to_string()));
124        }
125
126        // Process input arcs
127        for arc in self.schema.input_arcs(action_id) {
128            if let Some(st) = self.schema.state_by_id(&arc.source) {
129                if st.is_token() {
130                    self.snapshot.add_tokens(&arc.source, -1);
131                }
132            }
133        }
134
135        // Process output arcs
136        for arc in self.schema.output_arcs(action_id) {
137            if let Some(st) = self.schema.state_by_id(&arc.target) {
138                if st.is_token() {
139                    self.snapshot.add_tokens(&arc.target, 1);
140                }
141            }
142        }
143
144        self.sequence += 1;
145
146        if self.check_constraints {
147            let violations = self.check_constraints_impl();
148            if let Some(v) = violations.first() {
149                if let Some(err) = &v.err {
150                    return Err(Error::ConstraintEvaluation(
151                        v.constraint_id.clone(),
152                        err.clone(),
153                    ));
154                }
155                return Err(Error::ConstraintViolated(v.constraint_id.clone()));
156            }
157        }
158
159        Ok(())
160    }
161
162    /// Executes an action with variable bindings.
163    pub fn execute_with_bindings(
164        &mut self,
165        action_id: &str,
166        bindings: &Bindings,
167    ) -> Result<()> {
168        let action = self
169            .schema
170            .action_by_id(action_id)
171            .ok_or_else(|| Error::ActionNotFound(action_id.to_string()))?
172            .clone();
173
174        // Evaluate guard
175        if !action.guard.is_empty() {
176            if let Some(evaluator) = &self.guard_evaluator {
177                match evaluator.evaluate(&action.guard, bindings) {
178                    Ok(true) => {}
179                    Ok(false) => return Err(Error::GuardNotSatisfied),
180                    Err(e) => return Err(Error::GuardEvaluation(e)),
181                }
182            }
183        }
184
185        if !self.enabled(action_id) {
186            return Err(Error::ActionNotEnabled(action_id.to_string()));
187        }
188
189        self.apply_arcs(action_id, bindings);
190        self.sequence += 1;
191
192        if self.check_constraints {
193            let violations = self.check_constraints_impl();
194            if let Some(v) = violations.first() {
195                if let Some(err) = &v.err {
196                    return Err(Error::ConstraintEvaluation(
197                        v.constraint_id.clone(),
198                        err.clone(),
199                    ));
200                }
201                return Err(Error::ConstraintViolated(v.constraint_id.clone()));
202            }
203        }
204
205        Ok(())
206    }
207
208    fn apply_arcs(&mut self, action_id: &str, bindings: &Bindings) {
209        // Clone arcs to avoid borrow issues
210        let input_arcs: Vec<_> = self
211            .schema
212            .input_arcs(action_id)
213            .into_iter()
214            .cloned()
215            .collect();
216        let output_arcs: Vec<_> = self
217            .schema
218            .output_arcs(action_id)
219            .into_iter()
220            .cloned()
221            .collect();
222
223        for arc in &input_arcs {
224            let is_token = self
225                .schema
226                .state_by_id(&arc.source)
227                .map(|s| s.is_token())
228                .unwrap_or(false);
229
230            if is_token {
231                self.snapshot.add_tokens(&arc.source, -1);
232            } else {
233                self.apply_data_arc(&arc.source, arc, bindings, false);
234            }
235        }
236
237        for arc in &output_arcs {
238            let is_token = self
239                .schema
240                .state_by_id(&arc.target)
241                .map(|s| s.is_token())
242                .unwrap_or(false);
243
244            if is_token {
245                self.snapshot.add_tokens(&arc.target, 1);
246            } else {
247                self.apply_data_arc(&arc.target, arc, bindings, true);
248            }
249        }
250    }
251
252    fn apply_data_arc(
253        &mut self,
254        state_id: &str,
255        arc: &crate::schema::Arc,
256        bindings: &Bindings,
257        add: bool,
258    ) {
259        let value_name = if arc.value.is_empty() {
260            "amount"
261        } else {
262            &arc.value
263        };
264        let amount = bindings.get_i64(value_name);
265
266        if arc.keys.is_empty() {
267            return;
268        }
269
270        if arc.keys.len() == 1 {
271            let key = bindings.get_string(&arc.keys[0]);
272            if key.is_empty() {
273                return;
274            }
275
276            let current = self.get_map_i64(state_id, &key);
277            let new_val = if add {
278                current + amount
279            } else {
280                current - amount
281            };
282            self.snapshot
283                .set_data_map_value(state_id, &key, Value::Number(new_val.into()));
284        } else if arc.keys.len() == 2 {
285            let key1 = bindings.get_string(&arc.keys[0]);
286            let key2 = bindings.get_string(&arc.keys[1]);
287            if key1.is_empty() || key2.is_empty() {
288                return;
289            }
290
291            // Nested map access
292            let current = self.get_nested_map_i64(state_id, &key1, &key2);
293            let new_val = if add {
294                current + amount
295            } else {
296                current - amount
297            };
298
299            // Build nested structure
300            let entry = self
301                .snapshot
302                .data
303                .entry(state_id.to_string())
304                .or_insert_with(|| Value::Object(Default::default()));
305
306            if let Value::Object(outer) = entry {
307                let nested = outer
308                    .entry(key1)
309                    .or_insert_with(|| Value::Object(Default::default()));
310                if let Value::Object(inner) = nested {
311                    inner.insert(key2, Value::Number(new_val.into()));
312                }
313            }
314        }
315    }
316
317    fn get_map_i64(&self, state_id: &str, key: &str) -> i64 {
318        self.snapshot
319            .get_data_map_value(state_id, key)
320            .and_then(|v| v.as_i64())
321            .unwrap_or(0)
322    }
323
324    fn get_nested_map_i64(&self, state_id: &str, key1: &str, key2: &str) -> i64 {
325        self.snapshot
326            .get_data_map(state_id)
327            .and_then(|m| m.get(key1))
328            .and_then(|v| v.as_object())
329            .and_then(|m| m.get(key2))
330            .and_then(|v| v.as_i64())
331            .unwrap_or(0)
332    }
333
334    fn check_constraints_impl(&self) -> Vec<ConstraintViolation> {
335        let mut violations = Vec::new();
336
337        let evaluator = match &self.guard_evaluator {
338            Some(e) => e,
339            None => return violations,
340        };
341
342        for c in &self.schema.constraints {
343            match evaluator.evaluate_constraint(&c.expr, &self.snapshot.tokens) {
344                Ok(true) => {}
345                Ok(false) => {
346                    violations.push(ConstraintViolation {
347                        constraint_id: c.id.clone(),
348                        constraint_expr: c.expr.clone(),
349                        snapshot: self.snapshot.clone(),
350                        err: None,
351                    });
352                }
353                Err(e) => {
354                    violations.push(ConstraintViolation {
355                        constraint_id: c.id.clone(),
356                        constraint_expr: c.expr.clone(),
357                        snapshot: self.snapshot.clone(),
358                        err: Some(e),
359                    });
360                }
361            }
362        }
363
364        violations
365    }
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use crate::schema::{Action, Arc, Kind, State};
372
373    fn make_simple_schema() -> Schema {
374        let mut s = Schema::new("test");
375        s.add_state(State {
376            id: "ready".into(),
377            kind: Kind::Token,
378            initial: Some(Value::Number(1.into())),
379            typ: "int".into(),
380            exported: false,
381        });
382        s.add_state(State {
383            id: "done".into(),
384            kind: Kind::Token,
385            initial: Some(Value::Number(0.into())),
386            typ: "int".into(),
387            exported: false,
388        });
389        s.add_action(Action {
390            id: "process".into(),
391            guard: String::new(),
392            event_id: String::new(),
393            event_bindings: None,
394        });
395        s.add_arc(Arc {
396            source: "ready".into(),
397            target: "process".into(),
398            keys: vec![],
399            value: String::new(),
400        });
401        s.add_arc(Arc {
402            source: "process".into(),
403            target: "done".into(),
404            keys: vec![],
405            value: String::new(),
406        });
407        s
408    }
409
410    #[test]
411    fn test_runtime_basic() {
412        let schema = make_simple_schema();
413        let mut rt = Runtime::new(schema);
414
415        assert_eq!(rt.tokens("ready"), 1);
416        assert_eq!(rt.tokens("done"), 0);
417        assert!(rt.enabled("process"));
418
419        rt.execute("process").unwrap();
420
421        assert_eq!(rt.tokens("ready"), 0);
422        assert_eq!(rt.tokens("done"), 1);
423        assert!(!rt.enabled("process"));
424    }
425
426    #[test]
427    fn test_runtime_not_enabled() {
428        let schema = make_simple_schema();
429        let mut rt = Runtime::new(schema);
430
431        rt.execute("process").unwrap();
432        let result = rt.execute("process");
433        assert!(result.is_err());
434    }
435
436    #[test]
437    fn test_enabled_actions() {
438        let schema = make_simple_schema();
439        let rt = Runtime::new(schema);
440
441        let enabled = rt.enabled_actions();
442        assert_eq!(enabled, vec!["process"]);
443    }
444
445    #[test]
446    fn test_execute_with_bindings() {
447        let mut schema = Schema::new("erc20");
448        schema.add_data_state(
449            "balances",
450            "map[address]uint256",
451            Some(Value::Object(Default::default())),
452            true,
453        );
454        schema.add_action(Action {
455            id: "transfer".into(),
456            guard: String::new(),
457            event_id: String::new(),
458            event_bindings: None,
459        });
460        schema.add_arc(Arc {
461            source: "balances".into(),
462            target: "transfer".into(),
463            keys: vec!["from".into()],
464            value: String::new(),
465        });
466        schema.add_arc(Arc {
467            source: "transfer".into(),
468            target: "balances".into(),
469            keys: vec!["to".into()],
470            value: String::new(),
471        });
472
473        let mut rt = Runtime::new(schema);
474
475        // Set initial balance
476        rt.snapshot.set_data_map_value(
477            "balances",
478            "alice",
479            Value::Number(100.into()),
480        );
481
482        let mut bindings = Bindings::new();
483        bindings.insert("from".into(), Value::String("alice".into()));
484        bindings.insert("to".into(), Value::String("bob".into()));
485        bindings.insert("amount".into(), Value::Number(30.into()));
486
487        rt.execute_with_bindings("transfer", &bindings).unwrap();
488
489        assert_eq!(rt.get_map_i64("balances", "alice"), 70);
490        assert_eq!(rt.get_map_i64("balances", "bob"), 30);
491    }
492}