Skip to main content

brink_format/
value.rs

1use std::sync::Arc;
2
3use serde::{Deserialize, Serialize};
4
5use crate::id::DefinitionId;
6
7/// The runtime type of a [`Value`].
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub enum ValueType {
10    Int,
11    Float,
12    Bool,
13    String,
14    List,
15    DivertTarget,
16    VariablePointer,
17    TempPointer,
18    Null,
19    FragmentRef,
20}
21
22/// A runtime value in the ink VM.
23///
24/// Heap-allocating variants (`String`, `List`) are wrapped in `Arc` so that
25/// cloning a `Value` is always O(1) — a refcount bump, not a deep copy.
26/// This matches C#'s reference-type semantics and makes call-frame cloning
27/// (during `fork_thread`) essentially free. Atomic refcounts are used so
28/// `Value` can flow through Bevy's parallel scheduler.
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
30pub enum Value {
31    Int(i32),
32    Float(f32),
33    Bool(bool),
34    String(Arc<str>),
35    List(Arc<ListValue>),
36    DivertTarget(DefinitionId),
37    /// A reference to a global variable, used for `ref` parameters.
38    VariablePointer(DefinitionId),
39    /// A runtime-only pointer to a temp in a specific call frame.
40    /// Used for `ref` parameters that target temp variables.
41    TempPointer {
42        slot: u16,
43        frame_depth: u16,
44    },
45    Null,
46    /// A reference to a fragment in the output buffer's fragment store.
47    /// Fragments preserve structural output parts for locale re-rendering.
48    FragmentRef(u32),
49}
50
51impl Value {
52    /// Return the type discriminant for this value.
53    pub fn value_type(&self) -> ValueType {
54        match self {
55            Self::Int(_) => ValueType::Int,
56            Self::Float(_) => ValueType::Float,
57            Self::Bool(_) => ValueType::Bool,
58            Self::String(_) => ValueType::String,
59            Self::List(_) => ValueType::List,
60            Self::DivertTarget(_) => ValueType::DivertTarget,
61            Self::VariablePointer(_) => ValueType::VariablePointer,
62            Self::TempPointer { .. } => ValueType::TempPointer,
63            Self::Null => ValueType::Null,
64            Self::FragmentRef(_) => ValueType::FragmentRef,
65        }
66    }
67
68    /// Extract an `i32` if this value is an [`Int`](Self::Int).
69    ///
70    /// Strict: does not coerce floats or booleans. Returns `None` for any
71    /// other variant. For binding authors that want to read an integer
72    /// argument from ink.
73    pub fn as_int(&self) -> Option<i32> {
74        match self {
75            Self::Int(i) => Some(*i),
76            _ => None,
77        }
78    }
79
80    /// Extract an `f32` if this value is numeric.
81    ///
82    /// Lenient on the int → float direction only: an [`Int`](Self::Int) is
83    /// widened to `f32` (matching ink's implicit int→float promotion), but a
84    /// float is never truncated to an int by [`as_int`](Self::as_int).
85    pub fn as_float(&self) -> Option<f32> {
86        match self {
87            Self::Float(f) => Some(*f),
88            #[expect(
89                clippy::cast_precision_loss,
90                reason = "int->float promotion matches ink coercion semantics"
91            )]
92            Self::Int(i) => Some(*i as f32),
93            _ => None,
94        }
95    }
96
97    /// Extract a `bool` if this value is a [`Bool`](Self::Bool).
98    ///
99    /// Strict: does not treat nonzero numbers as truthy. Use the VM's own
100    /// truthiness rules if you need ink-style coercion.
101    pub fn as_bool(&self) -> Option<bool> {
102        match self {
103            Self::Bool(b) => Some(*b),
104            _ => None,
105        }
106    }
107
108    /// Borrow the string contents if this value is a [`String`](Self::String).
109    pub fn as_str(&self) -> Option<&str> {
110        match self {
111            Self::String(s) => Some(s),
112            _ => None,
113        }
114    }
115}
116
117impl From<i32> for Value {
118    fn from(v: i32) -> Self {
119        Self::Int(v)
120    }
121}
122
123impl From<f32> for Value {
124    fn from(v: f32) -> Self {
125        Self::Float(v)
126    }
127}
128
129impl From<bool> for Value {
130    fn from(v: bool) -> Self {
131        Self::Bool(v)
132    }
133}
134
135impl From<&str> for Value {
136    fn from(v: &str) -> Self {
137        Self::String(Arc::from(v))
138    }
139}
140
141impl From<String> for Value {
142    fn from(v: String) -> Self {
143        Self::String(Arc::from(v))
144    }
145}
146
147impl From<Arc<str>> for Value {
148    fn from(v: Arc<str>) -> Self {
149        Self::String(v)
150    }
151}
152
153impl From<()> for Value {
154    /// The unit type maps to [`Null`](Self::Null) — the natural return for a
155    /// fire-and-forget external that produces no value.
156    fn from((): ()) -> Self {
157        Self::Null
158    }
159}
160
161/// An ink list value: a set of list items plus their origin list definitions.
162#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
163pub struct ListValue {
164    /// The active items in this list (each a `ListItem` `DefinitionId`).
165    pub items: Vec<DefinitionId>,
166    /// The origin list definitions this value was derived from.
167    pub origins: Vec<DefinitionId>,
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use crate::id::DefinitionTag;
174
175    #[test]
176    fn value_type_discriminant() {
177        assert_eq!(Value::Int(0).value_type(), ValueType::Int);
178        assert_eq!(Value::Float(0.0).value_type(), ValueType::Float);
179        assert_eq!(Value::Bool(true).value_type(), ValueType::Bool);
180        assert_eq!(Value::String("".into()).value_type(), ValueType::String);
181        assert_eq!(Value::Null.value_type(), ValueType::Null);
182
183        let list = ListValue {
184            items: vec![],
185            origins: vec![],
186        };
187        assert_eq!(Value::List(list.into()).value_type(), ValueType::List);
188
189        let target = DefinitionId::new(DefinitionTag::Address, 1);
190        assert_eq!(
191            Value::DivertTarget(target).value_type(),
192            ValueType::DivertTarget
193        );
194    }
195
196    #[test]
197    fn from_impls_roundtrip() {
198        assert_eq!(Value::from(7_i32), Value::Int(7));
199        assert_eq!(Value::from(1.5_f32), Value::Float(1.5));
200        assert_eq!(Value::from(true), Value::Bool(true));
201        assert_eq!(Value::from("hi"), Value::String("hi".into()));
202        assert_eq!(Value::from(String::from("hi")), Value::String("hi".into()));
203        assert_eq!(Value::from(()), Value::Null);
204    }
205
206    #[test]
207    fn accessors_are_strict_except_int_to_float() {
208        assert_eq!(Value::Int(3).as_int(), Some(3));
209        assert_eq!(Value::Float(3.0).as_int(), None);
210        assert_eq!(Value::Bool(true).as_int(), None);
211
212        // int->float promotion is allowed (matches ink coercion);
213        // float->int truncation is not.
214        assert_eq!(Value::Int(3).as_float(), Some(3.0));
215        assert_eq!(Value::Float(2.5).as_float(), Some(2.5));
216
217        assert_eq!(Value::Bool(true).as_bool(), Some(true));
218        assert_eq!(Value::Int(1).as_bool(), None);
219
220        assert_eq!(Value::String("x".into()).as_str(), Some("x"));
221        assert_eq!(Value::Int(1).as_str(), None);
222    }
223}