Skip to main content

kcl_lib/execution/
memory.rs

1//! Facade for KCL memory implementations.
2//!
3//! Keeping memory implementations behind this facade lets new memory backends
4//! be added without moving call sites first.
5
6mod arena;
7
8use std::env;
9use std::fmt;
10use std::sync::Arc;
11#[cfg(test)]
12use std::sync::atomic::AtomicU8;
13use std::sync::atomic::AtomicUsize;
14#[cfg(test)]
15use std::sync::atomic::Ordering;
16
17use indexmap::IndexMap;
18use serde::Deserialize;
19use serde::Serialize;
20
21use crate::SourceRange;
22use crate::errors::KclError;
23use crate::execution::KclValue;
24
25/// The distinguished name of the return value of a function.
26pub(crate) const RETURN_NAME: &str = "__return";
27/// Low-budget namespacing for types and modules.
28pub(crate) const TYPE_PREFIX: &str = "__ty_";
29pub(crate) const MODULE_PREFIX: &str = "__mod_";
30pub(crate) const SKETCH_PREFIX: &str = "__sketch_";
31
32pub(crate) const KCL_MEMORY_IMPL_ENV_VAR: &str = "KCL_MEMORY_IMPL";
33
34/// An index pointing to an environment at a point in time.
35///
36/// The first field indexes an environment, the second field is an epoch. An epoch of 0 is indicates
37/// a dummy, error, or placeholder env ref, an epoch of `usize::MAX` represents the current most
38/// recent epoch.
39#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Hash, Eq, ts_rs::TS)]
40pub struct EnvironmentRef(pub(crate) usize, pub(crate) usize);
41
42impl EnvironmentRef {
43    pub fn dummy() -> Self {
44        Self(usize::MAX, 0)
45    }
46
47    pub(crate) fn current(index: usize) -> Self {
48        Self(index, usize::MAX)
49    }
50
51    pub(crate) fn at_epoch(index: usize, epoch: usize) -> Self {
52        Self(index, epoch)
53    }
54
55    pub(crate) fn is_regular(&self) -> bool {
56        self.0 < usize::MAX && self.1 > 0
57    }
58
59    pub(crate) fn index(&self) -> usize {
60        self.0
61    }
62
63    pub(crate) fn epoch(&self) -> usize {
64        self.1
65    }
66
67    pub(crate) fn skip_env(&self) -> bool {
68        self.0 == usize::MAX
69    }
70
71    /// Replace only the env index if it matches `old`.
72    pub fn replace_env(&mut self, old: Self, new: Self) {
73        if self.0 == old.0 {
74            self.0 = new.0;
75        }
76    }
77
78    /// Replace if it matches `old`.
79    pub fn replace_env_and_epoch(&mut self, old: Self, new: Self) {
80        if self.0 == old.0 && self.1 == old.1 {
81            self.0 = new.0;
82            self.1 = new.1;
83        }
84    }
85}
86
87// TODO keep per-stack stats to avoid so many atomic updates
88#[derive(Debug, Default)]
89pub(crate) struct MemoryStats {
90    // Total number of environments created.
91    env_count: AtomicUsize,
92    // Total number of epochs.
93    epoch_count: AtomicUsize,
94    // Total number of values inserted or updated.
95    mutation_count: AtomicUsize,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
99pub(crate) enum MemoryBackendKind {
100    Arena,
101}
102
103impl MemoryBackendKind {
104    pub(crate) fn from_env() -> Self {
105        #[cfg(test)]
106        if let Some(backend) = Self::test_override() {
107            return backend;
108        }
109
110        match env::var(KCL_MEMORY_IMPL_ENV_VAR) {
111            Ok(value) => Self::parse(&value),
112            Err(env::VarError::NotPresent) => Self::Arena,
113            Err(env::VarError::NotUnicode(value)) => {
114                panic!(
115                    "{KCL_MEMORY_IMPL_ENV_VAR} must be valid unicode; got `{}`.",
116                    value.to_string_lossy()
117                )
118            }
119        }
120    }
121
122    fn parse(value: &str) -> Self {
123        if value.trim().is_empty() || value.eq_ignore_ascii_case("arena") {
124            return Self::Arena;
125        }
126
127        panic!("Unsupported {KCL_MEMORY_IMPL_ENV_VAR} value `{value}`. Expected `arena`.",);
128    }
129
130    #[cfg(test)]
131    pub(crate) fn all() -> &'static [Self] {
132        &[Self::Arena]
133    }
134
135    #[cfg(test)]
136    pub(crate) fn override_for_test(backend: Self) -> MemoryBackendOverrideGuard {
137        let previous = TEST_BACKEND_OVERRIDE.swap(backend.test_override_value(), Ordering::SeqCst);
138        MemoryBackendOverrideGuard { previous }
139    }
140
141    #[cfg(test)]
142    fn test_override() -> Option<Self> {
143        match TEST_BACKEND_OVERRIDE.load(Ordering::SeqCst) {
144            2 => Some(Self::Arena),
145            _ => None,
146        }
147    }
148
149    #[cfg(test)]
150    fn test_override_value(self) -> u8 {
151        match self {
152            Self::Arena => 2,
153        }
154    }
155}
156
157#[cfg(test)]
158static TEST_BACKEND_OVERRIDE: AtomicU8 = AtomicU8::new(0);
159
160#[cfg(test)]
161pub(crate) struct MemoryBackendOverrideGuard {
162    previous: u8,
163}
164
165#[cfg(test)]
166impl Drop for MemoryBackendOverrideGuard {
167    fn drop(&mut self) {
168        TEST_BACKEND_OVERRIDE.store(self.previous, Ordering::SeqCst);
169    }
170}
171
172#[derive(Debug)]
173enum ProgramMemoryBackend {
174    Arena(Arc<arena::ProgramMemory>),
175}
176
177#[derive(Debug, Clone)]
178enum StackBackend {
179    Arena(arena::Stack),
180}
181
182/// Switchable KCL memory facade.
183#[derive(Debug)]
184pub(crate) struct ProgramMemory {
185    backend: ProgramMemoryBackend,
186}
187
188/// Switchable KCL stack facade.
189#[derive(Debug, Clone)]
190pub(crate) struct Stack {
191    pub(crate) memory: Arc<ProgramMemory>,
192    backend: StackBackend,
193}
194
195impl fmt::Display for ProgramMemory {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        match &self.backend {
198            ProgramMemoryBackend::Arena(memory) => memory.fmt(f),
199        }
200    }
201}
202
203impl fmt::Display for Stack {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        match &self.backend {
206            StackBackend::Arena(stack) => stack.fmt(f),
207        }
208    }
209}
210
211impl ProgramMemory {
212    #[allow(clippy::new_without_default)]
213    pub fn new() -> Arc<Self> {
214        Self::new_with_backend(MemoryBackendKind::from_env())
215    }
216
217    pub(crate) fn new_with_backend(backend: MemoryBackendKind) -> Arc<Self> {
218        match backend {
219            MemoryBackendKind::Arena => Arc::new(Self {
220                backend: ProgramMemoryBackend::Arena(arena::ProgramMemory::new()),
221            }),
222        }
223    }
224
225    pub fn new_stack(self: Arc<Self>) -> Stack {
226        let backend = match &self.backend {
227            ProgramMemoryBackend::Arena(memory) => StackBackend::Arena(Arc::clone(memory).new_stack()),
228        };
229
230        Stack { memory: self, backend }
231    }
232
233    pub fn set_std(self: &mut Arc<Self>, std: EnvironmentRef) -> Result<(), KclError> {
234        match &self.backend {
235            ProgramMemoryBackend::Arena(memory) => {
236                let mut memory = Arc::clone(memory);
237                memory.set_std(std)
238            }
239        }
240    }
241
242    pub fn requires_std(&self) -> bool {
243        match &self.backend {
244            ProgramMemoryBackend::Arena(memory) => memory.requires_std(),
245        }
246    }
247
248    pub(crate) fn stats(&self) -> &MemoryStats {
249        match &self.backend {
250            ProgramMemoryBackend::Arena(memory) => &memory.stats,
251        }
252    }
253
254    /// The number of environments currently retaining bindings. A test-only
255    /// observable for memory reclamation.
256    #[cfg(test)]
257    pub(crate) fn envs_with_bindings(&self) -> usize {
258        match &self.backend {
259            ProgramMemoryBackend::Arena(memory) => memory.envs_with_bindings(),
260        }
261    }
262
263    pub fn get_from_owned(
264        &self,
265        var: &str,
266        env_ref: EnvironmentRef,
267        source_range: SourceRange,
268        owner: usize,
269    ) -> Result<KclValue, KclError> {
270        match &self.backend {
271            ProgramMemoryBackend::Arena(memory) => memory.get_from_owned(var, env_ref, source_range, owner),
272        }
273    }
274
275    #[cfg(test)]
276    pub fn get_from_unchecked(&self, var: &str, env_ref: EnvironmentRef) -> Result<KclValue, KclError> {
277        match &self.backend {
278            ProgramMemoryBackend::Arena(memory) => memory.get_from(var, env_ref, SourceRange::default(), 0),
279        }
280    }
281}
282
283impl Stack {
284    pub fn deep_clone(&self) -> Result<Stack, KclError> {
285        match &self.backend {
286            StackBackend::Arena(stack) => {
287                let stack = stack.deep_clone()?;
288                Ok(Stack {
289                    memory: Arc::new(ProgramMemory {
290                        backend: ProgramMemoryBackend::Arena(Arc::clone(&stack.memory)),
291                    }),
292                    backend: StackBackend::Arena(stack),
293                })
294            }
295        }
296    }
297
298    #[cfg(test)]
299    pub fn new_for_tests() -> Stack {
300        Self::new_for_tests_with_backend(MemoryBackendKind::from_env())
301    }
302
303    #[cfg(test)]
304    pub(crate) fn new_for_tests_with_backend(backend: MemoryBackendKind) -> Stack {
305        let mut stack = ProgramMemory::new_with_backend(backend).new_stack();
306        stack
307            .push_new_root_env(false)
308            .expect("test stack root environment should be created");
309        let std = stack.current_env_ref();
310        stack
311            .memory
312            .set_std(std)
313            .expect("test standard library prelude should be initialized");
314        stack
315    }
316
317    pub fn current_epoch(&self) -> usize {
318        match &self.backend {
319            StackBackend::Arena(stack) => stack.current_epoch(),
320        }
321    }
322
323    #[cfg(test)]
324    pub(crate) fn current_env_ref(&self) -> EnvironmentRef {
325        match &self.backend {
326            StackBackend::Arena(stack) => stack.current_env_ref(),
327        }
328    }
329
330    pub fn push_new_env_for_call(&mut self, parent: EnvironmentRef) -> Result<(), KclError> {
331        match &mut self.backend {
332            StackBackend::Arena(stack) => stack.push_new_env_for_call(parent),
333        }
334    }
335
336    pub fn push_new_env_for_scope(&mut self) -> Result<(), KclError> {
337        match &mut self.backend {
338            StackBackend::Arena(stack) => stack.push_new_env_for_scope(),
339        }
340    }
341
342    /// Push a stack frame for a block scope, e.g. a KCL 3.0 if-arm body. Unlike
343    /// [`Self::push_new_env_for_scope`], an unreferenced block doesn't pin its
344    /// enclosing frame in memory; see the arena's `push_new_env_for_block`.
345    pub fn push_new_env_for_block(&mut self) -> Result<(), KclError> {
346        match &mut self.backend {
347            StackBackend::Arena(stack) => stack.push_new_env_for_block(),
348        }
349    }
350
351    pub fn push_new_root_env(&mut self, include_prelude: bool) -> Result<(), KclError> {
352        match &mut self.backend {
353            StackBackend::Arena(stack) => stack.push_new_root_env(include_prelude),
354        }
355    }
356
357    pub fn restore_env(&mut self, env: EnvironmentRef) -> Result<(), KclError> {
358        match &mut self.backend {
359            StackBackend::Arena(stack) => stack.restore_env(env),
360        }
361    }
362
363    pub fn pop_env(&mut self) -> Result<EnvironmentRef, KclError> {
364        match &mut self.backend {
365            StackBackend::Arena(stack) => stack.pop_env(),
366        }
367    }
368
369    pub fn pop_and_preserve_env(&mut self) -> Result<EnvironmentRef, KclError> {
370        match &mut self.backend {
371            StackBackend::Arena(stack) => stack.pop_and_preserve_env(),
372        }
373    }
374
375    pub fn squash_env(&mut self, old: EnvironmentRef) -> Result<(), KclError> {
376        match &mut self.backend {
377            StackBackend::Arena(stack) => stack.squash_env(old),
378        }
379    }
380
381    pub fn snapshot(&mut self) -> Result<EnvironmentRef, KclError> {
382        match &mut self.backend {
383            StackBackend::Arena(stack) => stack.snapshot(),
384        }
385    }
386
387    pub fn add(&mut self, key: String, value: KclValue, source_range: SourceRange) -> Result<(), KclError> {
388        match &mut self.backend {
389            StackBackend::Arena(stack) => stack.add(key, value, source_range),
390        }
391    }
392
393    pub fn add_recursive_closure(
394        &mut self,
395        key: String,
396        value: KclValue,
397        placeholder_env_ref: EnvironmentRef,
398        source_range: SourceRange,
399    ) -> Result<KclValue, KclError> {
400        match &mut self.backend {
401            StackBackend::Arena(stack) => stack.add_recursive_closure(key, value, placeholder_env_ref, source_range),
402        }
403    }
404
405    pub fn update(&mut self, key: &str, f: impl Fn(&mut KclValue, usize)) -> Result<(), KclError> {
406        match &mut self.backend {
407            StackBackend::Arena(stack) => stack.update(key, f),
408        }
409    }
410
411    pub fn get(&self, var: &str, source_range: SourceRange) -> Result<KclValue, KclError> {
412        match &self.backend {
413            StackBackend::Arena(stack) => stack.get(var, source_range),
414        }
415    }
416
417    pub fn get_owned(&self, var: &str, source_range: SourceRange) -> Result<KclValue, KclError> {
418        match &self.backend {
419            StackBackend::Arena(stack) => stack.get_owned(var, source_range),
420        }
421    }
422
423    pub fn cur_frame_contains(&self, var: &str) -> Result<bool, KclError> {
424        match &self.backend {
425            StackBackend::Arena(stack) => stack.cur_frame_contains(var),
426        }
427    }
428
429    pub fn get_from_call_stack(&self, key: &str, source_range: SourceRange) -> Result<(usize, KclValue), KclError> {
430        match &self.backend {
431            StackBackend::Arena(stack) => stack.get_from_call_stack(key, source_range),
432        }
433    }
434
435    pub fn find_keys_in_current_env(&self, pred: impl Fn(&KclValue) -> bool) -> Result<Vec<String>, KclError> {
436        match &self.backend {
437            StackBackend::Arena(stack) => stack.find_keys_in_current_env(pred),
438        }
439    }
440
441    pub fn find_all_in_current_env(&self) -> Result<Vec<(String, KclValue)>, KclError> {
442        match &self.backend {
443            StackBackend::Arena(stack) => stack.find_all_in_current_env(),
444        }
445    }
446
447    pub fn find_all_in_env(&self, env: EnvironmentRef) -> Result<Vec<(String, KclValue)>, KclError> {
448        match &self.backend {
449            StackBackend::Arena(stack) => stack.find_all_in_env(env),
450        }
451    }
452
453    pub(crate) fn find_all_in_env_owned(&self, env: EnvironmentRef) -> Result<IndexMap<String, KclValue>, KclError> {
454        Ok(self.find_all_in_env(env)?.into_iter().collect())
455    }
456
457    pub(crate) fn find_var_name_in_all_envs(
458        &self,
459        pred: impl Fn(&KclValue) -> bool,
460    ) -> Result<Option<String>, KclError> {
461        match &self.backend {
462            StackBackend::Arena(stack) => stack.find_var_name_in_all_envs(pred),
463        }
464    }
465
466    pub fn walk_call_stack_with<T>(&self, f: impl FnMut(&KclValue) -> Option<T>) -> Result<Vec<T>, KclError> {
467        match &self.backend {
468            StackBackend::Arena(stack) => stack.walk_call_stack_with(f),
469        }
470    }
471}
472
473#[cfg(test)]
474impl PartialEq for Stack {
475    fn eq(&self, other: &Self) -> bool {
476        let vars = self
477            .find_keys_in_current_env(|_| true)
478            .expect("stack equality should enumerate current env");
479        let vars_other = other
480            .find_keys_in_current_env(|_| true)
481            .expect("stack equality should enumerate other current env");
482        if vars != vars_other {
483            return false;
484        }
485
486        vars.iter().all(|key| {
487            self.get(key, SourceRange::default()).unwrap() == other.get(key, SourceRange::default()).unwrap()
488        })
489    }
490}
491
492#[cfg(test)]
493mod tests {
494    use super::*;
495
496    #[test]
497    fn parses_arena_backend_name() {
498        assert_eq!(MemoryBackendKind::parse("arena"), MemoryBackendKind::Arena);
499        assert_eq!(MemoryBackendKind::parse("ArEnA"), MemoryBackendKind::Arena);
500    }
501
502    #[test]
503    fn empty_backend_name_uses_arena() {
504        assert_eq!(MemoryBackendKind::parse(""), MemoryBackendKind::Arena);
505        assert_eq!(MemoryBackendKind::parse("   "), MemoryBackendKind::Arena);
506    }
507
508    #[test]
509    #[should_panic(expected = "Unsupported KCL_MEMORY_IMPL value `frozen`. Expected `arena`.")]
510    fn unsupported_backend_name_panics() {
511        MemoryBackendKind::parse("frozen");
512    }
513}