Skip to main content

endbasic_core/
mem.rs

1// EndBASIC
2// Copyright 2026 Julio Merino
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Memory representation and related types.
18
19use crate::ExprType;
20use crate::num::{U24, unchecked_u24_as_usize};
21use std::convert::TryFrom;
22use std::hash::Hash;
23
24/// Data for a multidimensional array stored on the heap.
25#[derive(Clone, Debug)]
26pub(crate) struct ArrayData {
27    /// Size of each dimension.
28    pub(crate) dimensions: Vec<usize>,
29
30    /// Flattened row-major storage of element values as `u64` (matching register representation).
31    pub(crate) values: Vec<u64>,
32}
33
34impl ArrayData {
35    /// Computes the flat index into `values` for the given `subscripts`, with bounds checking.
36    pub(crate) fn flat_index(&self, subscripts: &[i32]) -> Result<usize, String> {
37        debug_assert_eq!(
38            subscripts.len(),
39            self.dimensions.len(),
40            "Invalid number of subscripts; guaranteed valid by the compiler"
41        );
42
43        let mut offset = 0;
44        let mut multiplier = 1;
45        for (s, d) in subscripts.iter().zip(&self.dimensions) {
46            let Ok(s) = usize::try_from(*s) else {
47                return Err(format!("Subscript {} cannot be negative", s));
48            };
49            if s >= *d {
50                return Err(format!("Subscript {} exceeds limit of {}", s, d));
51            }
52            offset += s * multiplier;
53            multiplier *= d;
54        }
55        Ok(offset)
56    }
57}
58
59/// A typed scalar value, used both in the compile-time constant pool and as a
60/// return value when inspecting global variables after execution.
61///
62/// Only scalar types that can be hashed are included here.  Arrays are never
63/// stored as `ConstantDatum`.
64#[derive(Clone, Debug)]
65pub enum ConstantDatum {
66    /// A boolean value.
67    Boolean(bool),
68
69    /// A double-precision floating-point value.
70    Double(f64),
71
72    /// A 32-bit signed integer value.
73    Integer(i32),
74
75    /// A string value.
76    Text(String),
77}
78
79impl PartialEq for ConstantDatum {
80    fn eq(&self, other: &Self) -> bool {
81        match (self, other) {
82            (Self::Boolean(a), Self::Boolean(b)) => a == b,
83            (Self::Double(a), Self::Double(b)) => a.to_bits() == b.to_bits(),
84            (Self::Integer(a), Self::Integer(b)) => a == b,
85            (Self::Text(a), Self::Text(b)) => a == b,
86            _ => false,
87        }
88    }
89}
90
91impl Eq for ConstantDatum {}
92
93impl Hash for ConstantDatum {
94    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
95        std::mem::discriminant(self).hash(state);
96        match self {
97            Self::Boolean(b) => b.hash(state),
98            Self::Double(d) => d.to_bits().hash(state),
99            Self::Integer(i) => i.hash(state),
100            Self::Text(s) => s.hash(state),
101        }
102    }
103}
104
105impl From<bool> for ConstantDatum {
106    fn from(value: bool) -> Self {
107        Self::Boolean(value)
108    }
109}
110
111impl From<f64> for ConstantDatum {
112    fn from(value: f64) -> Self {
113        Self::Double(value)
114    }
115}
116
117impl From<i32> for ConstantDatum {
118    fn from(value: i32) -> Self {
119        Self::Integer(value)
120    }
121}
122
123impl From<&str> for ConstantDatum {
124    fn from(value: &str) -> Self {
125        Self::Text(value.to_owned())
126    }
127}
128
129impl From<String> for ConstantDatum {
130    fn from(value: String) -> Self {
131        Self::Text(value)
132    }
133}
134
135impl ConstantDatum {
136    /// Returns the type of the constant datum.
137    pub(crate) fn etype(&self) -> ExprType {
138        match self {
139            Self::Boolean(..) => ExprType::Boolean,
140            Self::Double(..) => ExprType::Double,
141            Self::Integer(..) => ExprType::Integer,
142            Self::Text(..) => ExprType::Text,
143        }
144    }
145
146    /// Formats this value as EndBASIC source code.
147    pub fn as_source(&self) -> String {
148        match self {
149            Self::Boolean(v) => {
150                if *v {
151                    "TRUE".to_owned()
152                } else {
153                    "FALSE".to_owned()
154                }
155            }
156            Self::Double(v) => {
157                let mut s = format!("{}", v);
158                if !s.contains('.') && !s.contains('e') && !s.contains('E') {
159                    s.push_str(".0");
160                }
161                s
162            }
163            Self::Integer(v) => format!("{}", v),
164            Self::Text(v) => format!("\"{}\"", v.replace('"', "\"\"")),
165        }
166    }
167
168    /// Formats this value for display in disassembly output.
169    pub(crate) fn as_disassembly(&self) -> String {
170        match self {
171            Self::Boolean(v) => {
172                if *v {
173                    "TRUE".to_owned()
174                } else {
175                    "FALSE".to_owned()
176                }
177            }
178            Self::Double(v) => v.to_string(),
179            Self::Integer(v) => format!("{}", v),
180            Self::Text(v) => format!("\"{}\"", v.replace('"', "\"\"")),
181        }
182    }
183
184    /// Decodes a raw register `value` of `etype` into a constant datum.
185    pub(crate) fn from_raw(
186        value: u64,
187        etype: ExprType,
188        constants: &[ConstantDatum],
189        heap: &Heap,
190    ) -> Self {
191        match etype {
192            ExprType::Boolean => Self::Boolean(value != 0),
193            ExprType::Double => Self::Double(f64::from_bits(value)),
194            ExprType::Integer => Self::Integer(value as i32),
195            ExprType::Text => {
196                let ptr = DatumPtr::from(value);
197                Self::Text(ptr.resolve_string(constants, heap).to_owned())
198            }
199        }
200    }
201}
202
203/// A heap-allocated value used at runtime.
204///
205/// Only types that require heap allocation are included here.  Scalars other than
206/// `Text` live directly in registers and never appear on the heap.
207#[derive(Clone, Debug)]
208pub(crate) enum HeapDatum {
209    /// An array value.
210    Array(ArrayData),
211
212    /// A string value.
213    Text(String),
214}
215
216/// Error reported when the heap cannot grow any further.
217#[derive(Debug, thiserror::Error)]
218#[error("Out of heap space")]
219pub(crate) struct HeapOverflowError;
220
221/// Heap-allocated data used at runtime.
222pub(crate) struct Heap {
223    data: Vec<HeapDatum>,
224    max_entries: U24,
225}
226
227impl Heap {
228    /// Creates a new empty heap.
229    pub(crate) fn new(max_entries: U24) -> Self {
230        Self { data: vec![HeapDatum::Text(String::new())], max_entries }
231    }
232
233    /// Removes all entries from the heap.
234    pub(crate) fn clear(&mut self) {
235        *self = Self::new(self.max_entries);
236    }
237
238    /// Returns the number of entries in the heap.
239    pub(crate) fn len(&self) -> usize {
240        self.data.len()
241    }
242
243    /// Returns the pointer to the shared empty string.
244    pub(crate) fn empty_text_ptr(&self) -> u64 {
245        debug_assert!(matches!(self.data.first(), Some(HeapDatum::Text(s)) if s.is_empty()));
246        DatumPtr::for_heap(0)
247    }
248
249    /// Allocates `datum` in the heap and returns its pointer.
250    pub(crate) fn push(&mut self, datum: HeapDatum) -> Result<u64, HeapOverflowError> {
251        if matches!(&datum, HeapDatum::Text(s) if s.is_empty()) {
252            return Ok(self.empty_text_ptr());
253        }
254
255        let index = U24::try_from(self.len()).map_err(|_| HeapOverflowError)?;
256        if self.len() >= unchecked_u24_as_usize(self.max_entries) {
257            return Err(HeapOverflowError);
258        }
259        self.data.push(datum);
260        Ok(DatumPtr::for_heap(u32::from(index)))
261    }
262
263    /// Returns the heap entry at `index`.
264    pub(crate) fn get(&self, index: usize) -> &HeapDatum {
265        &self.data[index]
266    }
267
268    /// Returns the heap entry at `index` for mutation.
269    pub(crate) fn get_mut(&mut self, index: usize) -> &mut HeapDatum {
270        &mut self.data[index]
271    }
272}
273
274/// Tagged pointers for constant and heap addresses.
275///
276/// A `DatumPtr` indexes into the constant pool or the heap, where datum values live.
277/// The encoding uses the sign of the lower 32 bits of a `u64`: positive values are
278/// constant pool indices, and negative values (two's complement) are heap indices.
279///
280/// This is distinct from `TaggedRegisterRef`, which points to a register in the register
281/// file rather than to data storage.
282#[derive(Clone, Copy)]
283pub(crate) enum DatumPtr {
284    /// A pointer to an entry in the constants pool.
285    Constant(U24),
286
287    /// A pointer to an entry in the heap.
288    Heap(U24),
289}
290
291impl From<u64> for DatumPtr {
292    fn from(value: u64) -> Self {
293        let signed_value = value as i32;
294        if signed_value < 0 {
295            DatumPtr::Heap(U24::try_from((-signed_value - 1) as u32).unwrap())
296        } else {
297            DatumPtr::Constant(U24::try_from(signed_value as u32).unwrap())
298        }
299    }
300}
301
302impl DatumPtr {
303    /// Creates a new pointer for a heap `index` and returns its `u64` representation.
304    pub(crate) fn for_heap(index: u32) -> u64 {
305        let raw = index as i32;
306        let raw = -raw - 1;
307        raw as u64
308    }
309
310    /// Resolves this pointer and returns the string it points to.
311    ///
312    /// Panics if the pointed-to datum is not a `Text` value.
313    pub(crate) fn resolve_string<'b>(
314        &self,
315        constants: &'b [ConstantDatum],
316        heap: &'b Heap,
317    ) -> &'b str {
318        match self {
319            DatumPtr::Constant(index) => match &constants[unchecked_u24_as_usize(*index)] {
320                ConstantDatum::Text(s) => s,
321                _ => panic!("Constant pointer does not point to a Text value"),
322            },
323            DatumPtr::Heap(index) => match heap.get(unchecked_u24_as_usize(*index)) {
324                HeapDatum::Text(s) => s,
325                _ => panic!("Heap pointer does not point to a Text value"),
326            },
327        }
328    }
329
330    /// Extracts the heap index from this pointer.
331    ///
332    /// Panics if this is not a heap pointer.
333    pub(crate) fn heap_index(&self) -> usize {
334        match self {
335            DatumPtr::Heap(index) => unchecked_u24_as_usize(*index),
336            DatumPtr::Constant(_) => panic!("Expected a heap pointer"),
337        }
338    }
339}
340
341#[cfg(test)]
342mod tests {
343    use super::*;
344
345    #[test]
346    fn test_constant_datum_from_scalars() {
347        assert_eq!(ConstantDatum::Boolean(true), ConstantDatum::from(true));
348        assert_eq!(ConstantDatum::Double(3.25), ConstantDatum::from(3.25));
349        assert_eq!(ConstantDatum::Integer(-7), ConstantDatum::from(-7));
350    }
351
352    #[test]
353    fn test_constant_datum_from_str() {
354        let mut text = "hello".to_owned();
355        let datum = ConstantDatum::from(text.as_str());
356        text.clear();
357
358        assert_eq!(ConstantDatum::Text("hello".to_owned()), datum);
359    }
360
361    #[test]
362    fn test_constant_datum_from_string() {
363        assert_eq!(
364            ConstantDatum::Text("hello".to_owned()),
365            ConstantDatum::from("hello".to_owned())
366        );
367    }
368
369    #[test]
370    fn test_constant_datum_from_raw() {
371        assert_eq!(
372            ConstantDatum::Boolean(true),
373            ConstantDatum::from_raw(1, ExprType::Boolean, &[], &Heap::new(U24::from(64)))
374        );
375        assert_eq!(
376            ConstantDatum::Double(3.25),
377            ConstantDatum::from_raw(
378                3.25f64.to_bits(),
379                ExprType::Double,
380                &[],
381                &Heap::new(U24::from(64))
382            )
383        );
384        assert_eq!(
385            ConstantDatum::Integer(-7),
386            ConstantDatum::from_raw(
387                (-7i32) as u64,
388                ExprType::Integer,
389                &[],
390                &Heap::new(U24::from(64))
391            )
392        );
393
394        let constants = vec![ConstantDatum::Text("hello".to_owned())];
395        assert_eq!(
396            ConstantDatum::Text("hello".to_owned()),
397            ConstantDatum::from_raw(0, ExprType::Text, &constants, &Heap::new(U24::from(64)))
398        );
399    }
400}