Skip to main content

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}