plotnik_lib/engine/
materializer.rs

1//! Replays an effect stream to materialize a `Value`.
2
3use super::effect_stream::{CapturedNode, EffectStream};
4use super::value::Value;
5use crate::ir::{DataFieldId, EffectOp, VariantTagId};
6use std::collections::BTreeMap;
7
8/// A container being built on the materializer's value stack.
9enum Container<'tree> {
10    Array(Vec<Value<'tree>>),
11    Object(BTreeMap<DataFieldId, Value<'tree>>),
12    Variant(VariantTagId),
13}
14
15pub struct Materializer<'a, 'tree> {
16    /// The current value being processed.
17    current: Option<Value<'tree>>,
18    /// A stack of containers (arrays, objects, variants) being built.
19    stack: Vec<Container<'tree>>,
20    /// An iterator over the captured nodes from the effect stream.
21    nodes: std::slice::Iter<'a, CapturedNode<'tree>>,
22}
23
24impl<'a, 'tree> Materializer<'a, 'tree> {
25    /// Creates a new materializer for a given effect stream.
26    fn new(stream: &'a EffectStream<'tree>) -> Self {
27        Self {
28            current: None,
29            stack: Vec::new(),
30            nodes: stream.nodes().iter(),
31        }
32    }
33
34    /// Consumes the materializer and returns the final value.
35    fn finish(mut self) -> Value<'tree> {
36        self.current.take().unwrap_or(Value::Null)
37    }
38
39    /// Replays an effect stream to produce a final `Value`.
40    pub fn materialize(stream: &'a EffectStream<'tree>) -> Value<'tree> {
41        let mut materializer = Materializer::new(stream);
42
43        for op in stream.ops() {
44            materializer.apply_op(*op);
45        }
46
47        materializer.finish()
48    }
49
50    /// Applies a single effect operation to the materializer's state.
51    fn apply_op(&mut self, op: EffectOp) {
52        match op {
53            EffectOp::CaptureNode => {
54                let node = *self.nodes.next().expect("mismatched node capture");
55                self.current = Some(Value::Node(node));
56            }
57            EffectOp::StartObject => {
58                self.stack.push(Container::Object(BTreeMap::new()));
59            }
60            EffectOp::EndObject => match self.stack.pop() {
61                Some(Container::Object(obj)) => self.current = Some(Value::Object(obj)),
62                _ => panic!("invalid EndObject operation"),
63            },
64            EffectOp::Field(id) => {
65                let value = self.current.take().unwrap_or(Value::Null);
66                if let Some(Container::Object(map)) = self.stack.last_mut() {
67                    map.insert(id, value);
68                } else {
69                    panic!("invalid Field operation without object on stack");
70                }
71            }
72            EffectOp::StartArray => {
73                self.stack.push(Container::Array(Vec::new()));
74            }
75            EffectOp::EndArray => match self.stack.pop() {
76                Some(Container::Array(arr)) => self.current = Some(Value::Array(arr)),
77                _ => panic!("invalid EndArray operation"),
78            },
79            EffectOp::PushElement => {
80                let value = self.current.take().unwrap_or(Value::Null);
81                if let Some(Container::Array(arr)) = self.stack.last_mut() {
82                    arr.push(value);
83                } else {
84                    panic!("invalid PushElement operation without array on stack");
85                }
86            }
87            EffectOp::ClearCurrent => {
88                self.current = None;
89            }
90            EffectOp::StartVariant(tag) => {
91                self.stack.push(Container::Variant(tag));
92            }
93            EffectOp::EndVariant => {
94                let value = self.current.take().unwrap_or(Value::Null);
95                match self.stack.pop() {
96                    Some(Container::Variant(tag)) => {
97                        self.current = Some(Value::Variant {
98                            tag,
99                            value: Box::new(value),
100                        });
101                    }
102                    _ => panic!("invalid EndVariant operation"),
103                }
104            }
105            EffectOp::ToString => {
106                if let Some(Value::Node(node)) = self.current.take() {
107                    self.current = Some(Value::String(node.text().to_string()));
108                } else {
109                    panic!("invalid ToString operation without a node");
110                }
111            }
112        }
113    }
114}
115
116#[cfg(test)]
117mod materializer_tests;