axon/effects/value.rs
1//! Runtime value representation for AXON algebraic effects.
2//!
3//! At runtime every value is a tagged dynamic [`Value`]. The Python
4//! frontend's typechecker (Fase 23.c) already proves that values flow
5//! through compatible types, so the runtime does no type checking — it
6//! only carries enough discriminant to print + serialise + compare.
7//!
8//! [`Value`] is intentionally small + cheap to clone. One-shot
9//! continuations (D2) consume their captured value at most once, so
10//! avoidable copies are minimal in the hot path.
11
12use std::collections::BTreeMap;
13
14use serde::{Deserialize, Serialize};
15
16/// A runtime value flowing through perform / resume / abort sites.
17///
18/// `Symbol` carries an unresolved identifier (e.g. a step output name
19/// like `"Extract.output"`); the runtime treats it as opaque since
20/// resolving it would require the surrounding flow's environment.
21/// Production runs supply concrete values via the IR-loading layer or
22/// via `EffectRuntime::bind_global`.
23///
24/// `Sentinel::Unit` and `Sentinel::Never` are the well-known unit /
25/// bottom sentinels. `Never` should never appear at runtime in a
26/// well-typed program; its presence indicates the typechecker missed
27/// a `resume` on a `Never`-returning operation (a compiler bug).
28#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
29#[serde(untagged)]
30pub enum Value {
31 Unit,
32 Bool(bool),
33 Int(i64),
34 Float(f64),
35 String(String),
36 Map(BTreeMap<String, Value>),
37 List(Vec<Value>),
38 /// Symbolic reference (an identifier string in the IR — e.g.
39 /// `token`, `state.last`). The runtime treats it opaquely; tests
40 /// may resolve it via the `EffectRuntime::bind_global` table.
41 Symbol(String),
42}
43
44impl Value {
45 /// True iff this value is the unit sentinel.
46 pub fn is_unit(&self) -> bool {
47 matches!(self, Value::Unit)
48 }
49
50 /// Construct from a string — used when the IR carries an argument
51 /// expression as raw text (consistent with Python convention for
52 /// `arguments: list[str]` on perform / forward sites).
53 pub fn from_argument_text(s: &str) -> Self {
54 // Heuristic: bare identifiers / dotted paths become Symbols;
55 // quoted literals become Strings. The IR already strips quotes
56 // at parse time, so anything passed here is interpreted as a
57 // symbolic reference unless it parses as a number or `true`/`false`.
58 if let Ok(b) = s.parse::<bool>() {
59 return Value::Bool(b);
60 }
61 if let Ok(i) = s.parse::<i64>() {
62 return Value::Int(i);
63 }
64 if let Ok(f) = s.parse::<f64>() {
65 return Value::Float(f);
66 }
67 Value::Symbol(s.to_string())
68 }
69
70 /// Best-effort string render for diagnostics + traces.
71 pub fn render(&self) -> String {
72 match self {
73 Value::Unit => "()".to_string(),
74 Value::Bool(b) => b.to_string(),
75 Value::Int(i) => i.to_string(),
76 Value::Float(f) => f.to_string(),
77 Value::String(s) => format!("{s:?}"),
78 Value::List(items) => {
79 let parts: Vec<String> = items.iter().map(|v| v.render()).collect();
80 format!("[{}]", parts.join(", "))
81 }
82 Value::Map(m) => {
83 let parts: Vec<String> =
84 m.iter().map(|(k, v)| format!("{k}: {}", v.render())).collect();
85 format!("{{{}}}", parts.join(", "))
86 }
87 Value::Symbol(s) => s.clone(),
88 }
89 }
90}
91
92impl Default for Value {
93 fn default() -> Self {
94 Value::Unit
95 }
96}