Skip to main content

appcore_filemaker/
compiler.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: compiler.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/30 05:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/30 05:00:00 by dnettoRaw
8//      ###########      S: 1.0.2-rc
9// =============================================================================
10
11//! Defines bounded compiler contracts and behavior for this crate.
12
13use std::collections::{BTreeMap, BTreeSet};
14use std::sync::Arc;
15
16use crate::compiler_style::apply_named_styles;
17use crate::source::{ComponentSource, ElementSource, TemplateSourceV1};
18use crate::{
19    DataValue, DocumentIr, ErrorCode, FileMakerError, OperationControl, Patch, PatchTransaction,
20    PresetRegistry, ProgressPhase, ResourceLimits, Result, TemplateIr, TemplateResolver,
21};
22
23/// Builder for a compiler with explicit resolvers and limits.
24#[derive(Default)]
25pub struct CompilerBuilder {
26    limits: ResourceLimits,
27    presets: PresetRegistry,
28    template_resolver: Option<Arc<dyn TemplateResolver>>,
29    control: OperationControl,
30}
31
32impl CompilerBuilder {
33    /// Replaces resource limits.
34    #[must_use]
35    pub fn limits(mut self, limits: ResourceLimits) -> Self {
36        self.limits = limits;
37        self
38    }
39
40    /// Replaces the preset registry.
41    #[must_use]
42    pub fn presets(mut self, presets: PresetRegistry) -> Self {
43        self.presets = presets;
44        self
45    }
46
47    /// Supplies the only resolver allowed to load includes.
48    #[must_use]
49    pub fn template_resolver(mut self, resolver: Arc<dyn TemplateResolver>) -> Self {
50        self.template_resolver = Some(resolver);
51        self
52    }
53
54    /// Supplies cooperative cancellation and progress controls.
55    #[must_use]
56    pub fn control(mut self, control: OperationControl) -> Self {
57        self.control = control;
58        self
59    }
60
61    /// Validates configuration and builds a compiler.
62    pub fn build(self) -> Result<Compiler> {
63        self.limits.validate()?;
64        Ok(Compiler {
65            limits: self.limits,
66            presets: self.presets,
67            template_resolver: self.template_resolver,
68            control: self.control,
69        })
70    }
71}
72
73/// Deterministic frontend compiler reusable across data instances.
74pub struct Compiler {
75    limits: ResourceLimits,
76    presets: PresetRegistry,
77    template_resolver: Option<Arc<dyn TemplateResolver>>,
78    control: OperationControl,
79}
80
81impl Compiler {
82    /// Starts configuration with conservative defaults.
83    #[must_use]
84    pub fn builder() -> CompilerBuilder {
85        CompilerBuilder::default()
86    }
87
88    /// Parses, expands includes/components/styles, and returns reusable IR.
89    pub fn compile_template_yaml(&self, bytes: &[u8]) -> Result<TemplateIr> {
90        self.control
91            .checkpoint(ProgressPhase::Compile, 0, Some(4))?;
92        let mut source = TemplateSourceV1::parse_yaml(bytes, &self.limits)?;
93        self.control
94            .checkpoint(ProgressPhase::Compile, 1, Some(4))?;
95        let mut include_stack = BTreeSet::new();
96        let mut include_bytes = 0_usize;
97        self.expand_includes(&mut source, &mut include_stack, &mut include_bytes, 0)?;
98        self.control
99            .checkpoint(ProgressPhase::Compile, 2, Some(4))?;
100        expand_components(&mut source, &self.limits)?;
101        apply_named_styles(&mut source)?;
102        self.control
103            .checkpoint(ProgressPhase::Compile, 3, Some(4))?;
104        let ir = source.to_ir(&self.presets, &self.limits)?;
105        crate::validate_template(&ir, &self.limits).enforce(false)?;
106        self.control
107            .checkpoint(ProgressPhase::Compile, 4, Some(4))?;
108        Ok(ir)
109    }
110
111    /// Binds one data instance and applies ordered patch transactions.
112    pub fn bind(
113        &self,
114        template: &TemplateIr,
115        data: &DataValue,
116        patches: &[Patch],
117    ) -> Result<DocumentIr> {
118        self.control.checkpoint(ProgressPhase::Bind, 0, Some(3))?;
119        data.validate(64, self.limits.max_elements, self.limits.max_text_bytes)?;
120        let resolved_data = crate::data::resolve_computed_fields(
121            &template.data_schema,
122            data,
123            self.limits.max_expression_steps,
124        )?;
125        self.control.checkpoint(ProgressPhase::Bind, 1, Some(3))?;
126        let mut document = DocumentIr {
127            template_id: template.id.clone(),
128            model: template.model,
129            page_size: template.page_size,
130            page_template: template.page_template.clone(),
131            collision: template.collision.clone(),
132            page_collision: template.page_collision.clone(),
133            guides: template.guides.clone(),
134            regions: template.regions.clone(),
135            exclusions: template.exclusions.clone(),
136            ai_policy: template.ai_policy.clone(),
137            elements: crate::compiler_bind::bind_elements(
138                &template.elements,
139                &resolved_data,
140                &self.limits,
141                &self.control,
142            )?,
143        };
144        self.control.checkpoint(ProgressPhase::Bind, 2, Some(3))?;
145        let total_patch_operations = patches.iter().try_fold(0_usize, |total, patch| {
146            total
147                .checked_add(patch.operations.len())
148                .ok_or_else(|| limit_error("runtime patch operation accounting overflow"))
149        })?;
150        if total_patch_operations > self.limits.max_patch_operations {
151            return Err(limit_error(
152                "runtime patches exceed the configured total operation limit",
153            ));
154        }
155        for (index, patch) in patches.iter().enumerate() {
156            self.control.checkpoint(
157                ProgressPhase::Bind,
158                u64::try_from(index).unwrap_or(u64::MAX),
159                u64::try_from(patches.len()).ok(),
160            )?;
161            PatchTransaction::new(&mut document, self.limits.max_patch_operations).apply(patch)?;
162        }
163        self.control.checkpoint(ProgressPhase::Bind, 3, Some(3))?;
164        Ok(document)
165    }
166
167    fn expand_includes(
168        &self,
169        source: &mut TemplateSourceV1,
170        stack: &mut BTreeSet<String>,
171        total_bytes: &mut usize,
172        depth: usize,
173    ) -> Result<()> {
174        self.control.checkpoint(
175            ProgressPhase::Compile,
176            u64::try_from(depth).unwrap_or(u64::MAX),
177            u64::try_from(self.limits.max_include_depth).ok(),
178        )?;
179        if depth > self.limits.max_include_depth {
180            return Err(limit_error("include depth exceeds configured limit"));
181        }
182        let includes = std::mem::take(&mut source.includes);
183        for include in includes {
184            if !stack.insert(include.path.clone()) {
185                return Err(
186                    FileMakerError::new(ErrorCode::DataCycle, "include cycle detected")
187                        .at(include.path),
188                );
189            }
190            let resolver = self.template_resolver.as_ref().ok_or_else(|| {
191                FileMakerError::new(
192                    ErrorCode::AssetInvalid,
193                    "template contains includes but no resolver is configured",
194                )
195            })?;
196            let bytes = resolver.resolve_template(&include.path, self.limits.max_include_bytes)?;
197            *total_bytes = total_bytes
198                .checked_add(bytes.len())
199                .ok_or_else(|| limit_error("include byte accounting overflow"))?;
200            if *total_bytes > self.limits.max_include_bytes {
201                return Err(limit_error("total include bytes exceed configured limit"));
202            }
203            let mut child = TemplateSourceV1::parse_yaml(&bytes, &self.limits)?;
204            if child.model != source.model {
205                return Err(schema_error("included template model does not match root"));
206            }
207            self.expand_includes(&mut child, stack, total_bytes, depth + 1)?;
208            merge_include(source, child, include.namespace.as_deref(), &include.path)?;
209            stack.remove(&include.path);
210        }
211        Ok(())
212    }
213}
214
215fn merge_include(
216    root: &mut TemplateSourceV1,
217    mut child: TemplateSourceV1,
218    namespace: Option<&str>,
219    source_path: &str,
220) -> Result<()> {
221    if child
222        .page
223        .as_ref()
224        .is_some_and(|page| page.has_layer_elements())
225    {
226        return Err(schema_error(
227            "included templates cannot declare page layers; the root owns physical pages",
228        ));
229    }
230    let prefix = namespace
231        .map(|value| format!("{value}/"))
232        .unwrap_or_default();
233    for element in &mut child.elements {
234        namespace_element(element, &prefix, source_path);
235    }
236    namespace_components(&mut child.components, namespace);
237    merge_map(&mut root.components, child.components, "component")?;
238    merge_map(&mut root.styles, child.styles, "style")?;
239    merge_map(&mut root.themes, child.themes, "theme")?;
240    merge_map(&mut root.guides, child.guides, "guide")?;
241    merge_map(&mut root.regions, child.regions, "region")?;
242    merge_map(&mut root.exclusions, child.exclusions, "exclusion")?;
243    merge_map(&mut root.data_schema, child.data_schema, "data field")?;
244    root.elements.extend(child.elements);
245    Ok(())
246}
247
248fn merge_map<T>(
249    target: &mut BTreeMap<String, T>,
250    source: BTreeMap<String, T>,
251    label: &str,
252) -> Result<()> {
253    for (name, value) in source {
254        if target.insert(name.clone(), value).is_some() {
255            return Err(schema_error(format!(
256                "duplicate {label} `{name}` after include expansion"
257            )));
258        }
259    }
260    Ok(())
261}
262
263fn namespace_components(
264    components: &mut BTreeMap<String, ComponentSource>,
265    namespace: Option<&str>,
266) {
267    let Some(namespace) = namespace else {
268        return;
269    };
270    let old = std::mem::take(components);
271    for (name, mut component) in old {
272        let prefix = format!("{namespace}/");
273        for element in &mut component.elements {
274            namespace_element(element, &prefix, "component");
275        }
276        components.insert(format!("{namespace}.{name}"), component);
277    }
278}
279
280fn namespace_element(element: &mut ElementSource, prefix: &str, source_path: &str) {
281    element.id = format!("{prefix}{}", element.id);
282    element.provenance_source = Some(source_path.to_owned());
283    for child in &mut element.children {
284        namespace_element(child, prefix, source_path);
285    }
286    for values in element.slots.values_mut() {
287        for child in values {
288            namespace_element(child, prefix, source_path);
289        }
290    }
291}
292
293fn expand_components(source: &mut TemplateSourceV1, limits: &ResourceLimits) -> Result<()> {
294    let components = source.components.clone();
295    let mut count = 0_usize;
296    source.elements = expand_element_list(
297        std::mem::take(&mut source.elements),
298        &components,
299        limits,
300        &mut count,
301        &mut Vec::new(),
302    )?;
303    if let Some(page) = &mut source.page {
304        for elements in page.element_lists_mut() {
305            *elements = expand_element_list(
306                std::mem::take(elements),
307                &components,
308                limits,
309                &mut count,
310                &mut Vec::new(),
311            )?;
312        }
313    }
314    source.components.clear();
315    Ok(())
316}
317
318fn expand_element_list(
319    elements: Vec<ElementSource>,
320    components: &BTreeMap<String, ComponentSource>,
321    limits: &ResourceLimits,
322    count: &mut usize,
323    component_stack: &mut Vec<String>,
324) -> Result<Vec<ElementSource>> {
325    let mut expanded = Vec::new();
326    for mut element in elements {
327        *count = count.saturating_add(1);
328        if *count > limits.max_elements {
329            return Err(limit_error("expanded element limit exceeded"));
330        }
331        if element.element_type == "slot" {
332            return Err(schema_error(
333                "slot placeholder is only valid inside a component",
334            ));
335        }
336        if let Some(name) = element.component.clone() {
337            if component_stack.contains(&name) {
338                return Err(FileMakerError::new(
339                    ErrorCode::DataCycle,
340                    "component cycle detected",
341                ));
342            }
343            let component = components
344                .get(&name)
345                .ok_or_else(|| schema_error(format!("component `{name}` was not found")))?;
346            component_stack.push(name.clone());
347            let mut props = component.props.clone();
348            props.extend(element.props.clone());
349            let body =
350                fill_component_slots(component.elements.clone(), &component.slots, &element.slots);
351            let mut body = expand_element_list(body, components, limits, count, component_stack)?;
352            for child in &mut body {
353                prefix_component_child(child, &element.id, &name, &props);
354            }
355            component_stack.pop();
356            element.component = None;
357            "group".clone_into(&mut element.element_type);
358            element.children = body;
359            element.slots.clear();
360        } else {
361            element.children =
362                expand_element_list(element.children, components, limits, count, component_stack)?;
363        }
364        expanded.push(element);
365    }
366    Ok(expanded)
367}
368
369fn fill_component_slots(
370    elements: Vec<ElementSource>,
371    defaults: &BTreeMap<String, Vec<ElementSource>>,
372    supplied: &BTreeMap<String, Vec<ElementSource>>,
373) -> Vec<ElementSource> {
374    let mut result = Vec::new();
375    for mut element in elements {
376        if element.element_type == "slot" {
377            let name = element.text.as_deref().unwrap_or("default");
378            result.extend(
379                supplied
380                    .get(name)
381                    .or_else(|| defaults.get(name))
382                    .cloned()
383                    .unwrap_or_default(),
384            );
385        } else {
386            element.children = fill_component_slots(element.children, defaults, supplied);
387            result.push(element);
388        }
389    }
390    result
391}
392
393fn prefix_component_child(
394    element: &mut ElementSource,
395    instance_id: &str,
396    component: &str,
397    props: &BTreeMap<String, serde_json::Value>,
398) {
399    element.id = format!("{instance_id}/{}", element.id);
400    element.provenance_components.push(component.to_owned());
401    if let Some(text) = &mut element.text {
402        *text = substitute_props(text, props);
403    }
404    for child in &mut element.children {
405        prefix_component_child(child, instance_id, component, props);
406    }
407}
408
409fn substitute_props(source: &str, props: &BTreeMap<String, serde_json::Value>) -> String {
410    let mut result = source.to_owned();
411    for (name, value) in props {
412        let replacement = value
413            .as_str()
414            .map_or_else(|| value.to_string(), ToOwned::to_owned);
415        result = result.replace(&format!("{{{{{name}}}}}"), &replacement);
416    }
417    result
418}
419
420fn schema_error(message: impl Into<String>) -> FileMakerError {
421    FileMakerError::new(ErrorCode::SchemaField, message)
422}
423
424fn limit_error(message: impl Into<String>) -> FileMakerError {
425    FileMakerError::new(ErrorCode::LimitExceeded, message)
426}