Skip to main content

copperlace/render/
state.rs

1use std::collections::{HashMap, HashSet};
2
3use super::error::RenderError;
4use super::ruleset::RuleSet;
5use super::value::{CopperlaceNumber, CopperlaceValue, StructuredNode};
6
7/// Initial variable bindings for one render operation.
8///
9/// Values in this map are available before top-level `context` defaults and
10/// named rules. A render may still update them with overwrite bindings such as
11/// `{alias:=rule}`.
12pub type RenderContext = HashMap<String, String>;
13
14/// Render-time options that affect rule expansion behavior.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
16pub struct RenderOptions {
17    /// Maximum recursive re-entries allowed for one rule name.
18    ///
19    /// A value of `0` preserves the default behavior: re-entering a rule that is
20    /// already on the call stack returns `CircularRuleReference`. Values greater
21    /// than zero allow that many recursive re-entries before recursive calls
22    /// return an empty string.
23    pub max_recursion_depth: usize,
24}
25
26/// Mutable state for one render operation.
27///
28/// `RuleSet::render_rule` creates a fresh state for each call. The state tracks
29/// per-render bindings, the rule call stack used for cycle detection, and the
30/// random number generator used by choice nodes.
31pub struct RenderState<'a> {
32    pub(crate) ruleset: &'a RuleSet,
33    context: RenderContext,
34    scopes: Vec<IterationScope<'a>>,
35    pub(crate) options: RenderOptions,
36    pub(crate) call_stack: Vec<String>,
37    scoped_call_stack: Vec<usize>,
38    pub(crate) unique_choices: HashMap<String, HashSet<usize>>,
39    pub(crate) rng: rand::rngs::ThreadRng,
40}
41
42#[derive(Clone, Copy)]
43pub(crate) struct LoopMetadata {
44    pub(crate) index: usize,
45    pub(crate) length: usize,
46}
47
48struct IterationScope<'a> {
49    variable_name: String,
50    value: IterationValue<'a>,
51    metadata: LoopMetadata,
52    bindings: RenderContext,
53}
54
55enum ScopedValue<'a> {
56    Text(String),
57    Node(&'a StructuredNode),
58    Runtime(CopperlaceValue),
59    MetadataObject,
60}
61
62pub(crate) enum IterationValue<'a> {
63    Compiled(&'a StructuredNode),
64    Runtime(CopperlaceValue),
65}
66
67impl<'a> RenderState<'a> {
68    /// Creates an empty render state for a ruleset.
69    pub fn new(ruleset: &'a RuleSet) -> Self {
70        Self::with_context(ruleset, RenderContext::new())
71    }
72
73    /// Creates a render state with initial variable bindings.
74    pub fn with_context(ruleset: &'a RuleSet, context: RenderContext) -> Self {
75        Self::with_context_and_options(ruleset, context, RenderOptions::default())
76    }
77
78    /// Creates a render state with initial variable bindings and render options.
79    pub fn with_context_and_options(
80        ruleset: &'a RuleSet,
81        context: RenderContext,
82        options: RenderOptions,
83    ) -> Self {
84        RenderState {
85            ruleset,
86            context,
87            scopes: Vec::new(),
88            options,
89            call_stack: Vec::new(),
90            scoped_call_stack: Vec::new(),
91            unique_choices: HashMap::new(),
92            rng: rand::rngs::ThreadRng::default(),
93        }
94    }
95
96    pub(crate) fn resolve_bound_text(&mut self, name: &str) -> Result<Option<String>, RenderError> {
97        let Some(value) = self.scoped_value(name)? else {
98            return Ok(None);
99        };
100
101        match value {
102            ScopedValue::Text(value) => Ok(Some(value)),
103            ScopedValue::Node(node) => self.render_scoped_node(name, node).map(Some),
104            ScopedValue::Runtime(value) => value.to_rendered_text().map(Some),
105            ScopedValue::MetadataObject => Err(RenderError::UnsupportedValue("object".to_string())),
106        }
107    }
108
109    pub(crate) fn contains_bound_value(&self, name: &str) -> Result<bool, RenderError> {
110        self.scoped_value(name).map(|value| value.is_some())
111    }
112
113    pub(crate) fn bind(
114        &mut self,
115        name: &str,
116        value: String,
117        overwrite: bool,
118    ) -> Result<(), RenderError> {
119        if overwrite && self.is_immutable_loop_name(name) {
120            return Err(RenderError::ImmutableLoopBinding(name.to_string()));
121        }
122
123        if let Some(scope) = self.scopes.last_mut() {
124            scope.bindings.insert(name.to_string(), value);
125        } else {
126            self.context.insert(name.to_string(), value);
127        }
128        Ok(())
129    }
130
131    pub(crate) fn ensure_mutable_binding(&self, name: &str) -> Result<(), RenderError> {
132        if self.is_immutable_loop_name(name) {
133            return Err(RenderError::ImmutableLoopBinding(name.to_string()));
134        }
135        Ok(())
136    }
137
138    pub(crate) fn cache_context_default(&mut self, name: &str, value: String) {
139        self.context.insert(name.to_string(), value);
140    }
141
142    pub(crate) fn iterable_elements(
143        &self,
144        source: &str,
145    ) -> Result<Vec<IterationValue<'a>>, RenderError> {
146        let value = match self.scoped_value(source)? {
147            Some(value) => value,
148            None => {
149                if let Some(node) = self.ruleset.structured_context_node(source) {
150                    ScopedValue::Node(node)
151                } else {
152                    ScopedValue::Node(self.ruleset.structured_node(source)?)
153                }
154            }
155        };
156
157        match value {
158            ScopedValue::Node(StructuredNode::Array(values)) => values
159                .iter()
160                .map(|entry| {
161                    entry
162                        .value_node()
163                        .map(IterationValue::Compiled)
164                        .ok_or_else(|| RenderError::UnsupportedIterationSource {
165                            source: source.to_string(),
166                            value_type: "array with generated entries".to_string(),
167                        })
168                })
169                .collect(),
170            ScopedValue::Node(node) => Err(RenderError::UnsupportedIterationSource {
171                source: source.to_string(),
172                value_type: node.value_type().to_string(),
173            }),
174            ScopedValue::Text(_) => Err(RenderError::UnsupportedIterationSource {
175                source: source.to_string(),
176                value_type: "string".to_string(),
177            }),
178            ScopedValue::MetadataObject => Err(RenderError::UnsupportedIterationSource {
179                source: source.to_string(),
180                value_type: "object".to_string(),
181            }),
182            ScopedValue::Runtime(CopperlaceValue::Array(values)) => {
183                Ok(values.into_iter().map(IterationValue::Runtime).collect())
184            }
185            ScopedValue::Runtime(value) => Err(RenderError::UnsupportedIterationSource {
186                source: source.to_string(),
187                value_type: value.value_type().to_string(),
188            }),
189        }
190    }
191
192    pub(crate) fn push_iteration_scope(
193        &mut self,
194        variable_name: &str,
195        value: IterationValue<'a>,
196        metadata: LoopMetadata,
197    ) {
198        self.scopes.push(IterationScope {
199            variable_name: variable_name.to_string(),
200            value,
201            metadata,
202            bindings: RenderContext::new(),
203        });
204    }
205
206    pub(crate) fn pop_iteration_scope(&mut self) {
207        self.scopes.pop();
208    }
209
210    fn scoped_value(&self, name: &str) -> Result<Option<ScopedValue<'a>>, RenderError> {
211        for scope in self.scopes.iter().rev() {
212            if name == scope.variable_name {
213                return Ok(Some(match &scope.value {
214                    IterationValue::Compiled(value) => ScopedValue::Node(value),
215                    IterationValue::Runtime(value) => ScopedValue::Runtime(value.clone()),
216                }));
217            }
218            if let Some(path) = name
219                .strip_prefix(&scope.variable_name)
220                .and_then(|suffix| suffix.strip_prefix('.'))
221            {
222                return match &scope.value {
223                    IterationValue::Compiled(value) => value
224                        .child(path)
225                        .map(ScopedValue::Node)
226                        .map(Some)
227                        .ok_or_else(|| RenderError::UnknownRule(name.to_string())),
228                    IterationValue::Runtime(value) => value
229                        .child(path)
230                        .cloned()
231                        .map(ScopedValue::Runtime)
232                        .map(Some)
233                        .ok_or_else(|| RenderError::UnknownRule(name.to_string())),
234                };
235            }
236
237            if name == "loop" {
238                return Ok(Some(ScopedValue::MetadataObject));
239            }
240            if let Some(field) = name.strip_prefix("loop.") {
241                return self.metadata_value(name, field, scope.metadata).map(Some);
242            }
243
244            if let Some(value) = scope.bindings.get(name) {
245                return Ok(Some(ScopedValue::Text(value.clone())));
246            }
247        }
248
249        Ok(self.context.get(name).cloned().map(ScopedValue::Text))
250    }
251
252    fn metadata_value(
253        &self,
254        full_name: &str,
255        field: &str,
256        metadata: LoopMetadata,
257    ) -> Result<ScopedValue<'a>, RenderError> {
258        let value = match field {
259            "index" => {
260                CopperlaceValue::Number(CopperlaceNumber::Unsigned((metadata.index + 1) as u64))
261            }
262            "index0" => CopperlaceValue::Number(CopperlaceNumber::Unsigned(metadata.index as u64)),
263            "length" => CopperlaceValue::Number(CopperlaceNumber::Unsigned(metadata.length as u64)),
264            "first" => CopperlaceValue::Boolean(metadata.index == 0),
265            "last" => CopperlaceValue::Boolean(metadata.index + 1 == metadata.length),
266            _ => return Err(RenderError::UnknownRule(full_name.to_string())),
267        };
268        Ok(ScopedValue::Runtime(value))
269    }
270
271    fn render_scoped_node(
272        &mut self,
273        name: &str,
274        node: &'a StructuredNode,
275    ) -> Result<String, RenderError> {
276        let identity = node as *const StructuredNode as usize;
277        let existing_calls = self
278            .scoped_call_stack
279            .iter()
280            .filter(|entry| **entry == identity)
281            .count();
282        if self.options.max_recursion_depth == 0 && existing_calls > 0 {
283            let mut cycle = self.call_stack.clone();
284            cycle.push(name.to_string());
285            return Err(RenderError::CircularRuleReference(cycle));
286        }
287        if existing_calls > self.options.max_recursion_depth {
288            return Ok(String::new());
289        }
290
291        self.scoped_call_stack.push(identity);
292        self.call_stack.push(name.to_string());
293        let result = node.generate_text(self);
294        self.call_stack.pop();
295        self.scoped_call_stack.pop();
296        result
297    }
298
299    fn is_immutable_loop_name(&self, name: &str) -> bool {
300        self.scopes.iter().rev().any(|scope| {
301            name == scope.variable_name
302                || name.starts_with(&format!("{}.", scope.variable_name))
303                || name == "loop"
304                || name.starts_with("loop.")
305        })
306    }
307
308    pub(crate) fn used_unique_choice_indices(&self, rule_name: &str) -> Option<&HashSet<usize>> {
309        self.unique_choices.get(rule_name)
310    }
311
312    pub(crate) fn mark_unique_choice_index(&mut self, rule_name: &str, index: usize) {
313        self.unique_choices
314            .entry(rule_name.to_string())
315            .or_default()
316            .insert(index);
317    }
318}