Skip to main content

adam_lang/module/
compile.rs

1//! Materializes borrowed checked plans into immutable, repeatable native factories.
2//!
3//! ```
4//! use adam_lang::{ModuleCompiler, ModuleSet, TypeRegistry};
5//! let path = "example".parse()?;
6//! let mut modules = ModuleSet::new();
7//! modules.insert_source(path, "example.adm2", "sheet reusable() {}")?;
8//! let compiled = ModuleCompiler::new(TypeRegistry::new(), cel_parser::OpLookup::new())
9//!     .compile(modules).map_err(|mut errors| errors.remove(0))?;
10//! assert!(compiled.template(&"example".parse()?, "reusable").is_some());
11//! # Ok::<(), adam_lang::ModuleDiagnostic>(())
12//! ```
13
14use std::{collections::HashSet, rc::Rc};
15
16use adam_rs::{CellKind, RelationshipSlot};
17use cel_parser::{Expr, OpLookup};
18use cel_runtime::{DynSegment, ValueType};
19
20use super::{
21    analysis::{self, Body, DefinitionPlan, Kind},
22    *,
23};
24use crate::{
25    CompositionBuilder, TypeRegistry,
26    cell_value::{CellValue, RawFrame},
27    composition::{AuthoredItem, AuthoredRole, CellSlot, MethodRecipe},
28    type_registry::TypeShape,
29};
30
31/// Shares the immutable, noncloneable operation environment between owning factories.
32pub(crate) struct Environment {
33    pub(crate) types: Rc<TypeRegistry>,
34    pub(crate) lookup: OpLookup,
35}
36
37/// Owns the type and operation environments for one collection-wide compilation.
38///
39/// Compilation does not execute user expressions or construct live graphs.
40/// See [`Self::new`] for an example.
41pub struct ModuleCompiler {
42    environment: Rc<Environment>,
43}
44
45impl ModuleCompiler {
46    /// Takes ownership of the immutable environments without cloning either.
47    ///
48    /// # Examples
49    /// ```
50    /// use adam_lang::{ModuleCompiler, ModuleSet, TypeRegistry};
51    /// let mut modules = ModuleSet::new();
52    /// modules.insert_source("example".parse()?, "example.adm2", "sheet reusable() {}")?;
53    /// let compiled = ModuleCompiler::new(TypeRegistry::new(), cel_parser::OpLookup::new())
54    ///     .compile(modules).map_err(|mut errors| errors.remove(0))?;
55    /// assert!(compiled.template(&"example".parse()?, "reusable").is_some());
56    /// # Ok::<(), adam_lang::ModuleDiagnostic>(())
57    /// ```
58    pub fn new(types: TypeRegistry, lookup: OpLookup) -> Self {
59        Self {
60            environment: Rc::new(Environment {
61                types: Rc::new(types),
62                lookup,
63            }),
64        }
65    }
66
67    /// Checks the collection once and materializes every definition in dependency order.
68    ///
69    /// All definitions, including unused definitions and inactive branches, must check.
70    /// Initializers, defaults, policies, operations and matches remain unexecuted.
71    /// Whole-graph solvability and live-root construction are not compilation checks.
72    /// # Errors
73    /// Returns source-owning diagnostics for invalid collections or recipe preparation.
74    /// No partial compiled collection escapes on failure.
75    /// - Complexity: Borrowed analysis plus expression emission, native recipe validation
76    ///   and O(n) owning syntax/metadata storage in authored nodes, not expanded uses.
77    ///
78    /// # Examples
79    /// See [`Self::new`].
80    pub fn compile(self, modules: ModuleSet) -> Result<CompiledModules, Vec<ModuleDiagnostic>> {
81        compile(self.environment, modules)
82    }
83}
84
85/// Retains one root's own declaration, never an included private name.
86///
87/// Native slots belong to the corresponding prepared root's immutable recipe.
88/// See [`CompiledModules::prepared_root`].
89#[derive(Clone, Debug)]
90pub struct RootCell {
91    /// Retains the authored root-local name.
92    pub name: String,
93    /// Retains the native kind after private-output promotion.
94    pub kind: CellKind,
95    /// Retains the complete language shape.
96    pub shape: TypeShape,
97    /// Identifies the local cell in the root occurrence.
98    pub slot: adam_rs::CellSlot,
99    /// Retains the exact declaration source.
100    pub label: SourceLabel,
101}
102
103/// Owns a lowered root and its public-name assembly inputs, without constructing it.
104///
105/// [`CompiledModules::instantiate_root`] uses the same native instantiator as components.
106/// See [`CompiledModules::prepared_root`].
107pub struct PreparedRoot {
108    pub(crate) template: TypedTemplate,
109    cells: Vec<RootCell>,
110}
111
112impl PreparedRoot {
113    /// Borrows authored root-local declarations in source order.
114    ///
115    /// Included private cells never enter this slice.
116    /// # Examples
117    /// See [`CompiledModules::prepared_root`].
118    pub fn cells(&self) -> &[RootCell] {
119        &self.cells
120    }
121
122    /// Borrows the genuine root definition label.
123    ///
124    /// # Examples
125    /// See [`CompiledModules::prepared_root`].
126    pub fn definition_label(&self) -> &SourceLabel {
127        self.template.definition_label()
128    }
129}
130
131/// Owns resolved immutable exports and explicitly selectable prepared roots.
132///
133/// Imported aliases do not become exports. Factories and diagnostics retain their
134/// actual source owners independently of this collection.
135/// See [`ModuleCompiler::new`] for an example.
136pub struct CompiledModules {
137    templates: HashMap<ModulePath, HashMap<String, TypedTemplate>>,
138    roots: HashMap<ModulePath, Vec<(String, PreparedRoot)>>,
139    sources: Vec<SourceId>,
140    /// Shares the immutable entry with subsequent compilations and parser conveniences.
141    pub(crate) environment: Rc<Environment>,
142}
143
144impl CompiledModules {
145    /// Constructs one explicitly selected root without propagation.
146    ///
147    /// Only authored root declarations enter the ordered public maps. Included
148    /// private cells retain shapes and provenance but never become public names.
149    /// The complete combined sheet passes native guard-independence validation.
150    ///
151    /// # Errors
152    /// Returns an owning diagnostic for an unknown selection, construction failure,
153    /// or invalid combined graph. No partial live sheet escapes on failure.
154    /// - Complexity: Native instantiation and validation plus O(c + a + s) root
155    ///   declarations, authored records and retained source identities.
156    ///
157    /// # Examples
158    /// ```
159    /// let mut modules = adam_lang::ModuleSet::new();
160    /// let path = "app".parse()?;
161    /// modules.insert_source(path, "app.adm2", "sheet app { cell x: i32 = 7; }")?;
162    /// let compiled = adam_lang::ModuleCompiler::new(adam_lang::TypeRegistry::new(),
163    ///     cel_parser::OpLookup::new()).compile(modules).map_err(|mut e| e.remove(0))?;
164    /// let parsed = compiled.instantiate_root(&"app".parse()?, "app")?;
165    /// assert_eq!(*parsed.read::<i32>(parsed.cell_names["x"].0).unwrap(), 7);
166    /// # Ok::<(), adam_lang::ModuleDiagnostic>(())
167    /// ```
168    pub fn instantiate_root(
169        &self,
170        module: &ModulePath,
171        name: &str,
172    ) -> Result<crate::ParsedSheet, ModuleDiagnostic> {
173        let root = self.prepared_root(module, name).ok_or_else(|| {
174            crate::composition::diagnostic(format!("unknown root `{module}::{name}`"))
175        })?;
176        let mut destination = crate::ParsedSheet::new();
177        let instance = root.template.instantiate(&mut destination, &[])?;
178        let occurrence = instance.occurrences()[0].id();
179        for cell in root.cells() {
180            let id = instance
181                .cell(occurrence, &cell.slot)
182                .expect("prepared root owns each authored local slot");
183            destination
184                .cell_names
185                .insert(cell.name.clone(), (id, cell.shape.clone()));
186            if cell.kind == adam_rs::CellKind::Out {
187                destination.output_names.insert(cell.name.clone(), id);
188            }
189        }
190        destination.retain_root_spans(&instance);
191        let mut sources: HashSet<_> = destination.sources.iter().cloned().collect();
192        for source in &self.sources {
193            if sources.insert(source.clone()) {
194                destination.sources.push(source.clone());
195            }
196        }
197        destination.sheet.validate().map_err(|error| {
198            destination.validation_diagnostic(error, root.definition_label(), &instance)
199        })?;
200        Ok(destination)
201    }
202
203    /// Borrows source identities from supplied files and imported immutable templates.
204    ///
205    /// Includes empty source modules. Independent sources keep their ownership identity
206    /// even when file names, text or original collection indices coincide.
207    /// # Examples
208    /// ```
209    /// let mut modules = adam_lang::ModuleSet::new();
210    /// modules.insert_source("empty".parse()?, "empty.adm2", "")?;
211    /// let compiled = adam_lang::ModuleCompiler::new(adam_lang::TypeRegistry::new(),
212    ///     cel_parser::OpLookup::new()).compile(modules).map_err(|mut e| e.remove(0))?;
213    /// assert_eq!(compiled.sources()[0].file_name(), "empty.adm2");
214    /// # Ok::<(), adam_lang::ModuleDiagnostic>(())
215    /// ```
216    pub fn sources(&self) -> &[SourceId] {
217        &self.sources
218    }
219
220    /// Creates a compiler sharing only this collection's immutable environments.
221    ///
222    /// New compilations retain independent module collections and construction frames.
223    /// # Examples
224    /// ```
225    /// let compiled = adam_lang::ModuleCompiler::new(adam_lang::TypeRegistry::new(),
226    ///     cel_parser::OpLookup::new()).compile(adam_lang::ModuleSet::new())
227    ///     .map_err(|mut errors| errors.remove(0))?;
228    /// let other = compiled.compiler().compile(adam_lang::ModuleSet::new());
229    /// assert!(other.is_ok());
230    /// # Ok::<(), adam_lang::ModuleDiagnostic>(())
231    /// ```
232    pub fn compiler(&self) -> ModuleCompiler {
233        ModuleCompiler {
234            environment: self.environment.clone(),
235        }
236    }
237
238    /// Borrows a declared reusable export, excluding roots and imported aliases.
239    ///
240    /// - Complexity: Expected O(p + n) module-path and definition-name bytes.
241    /// # Examples
242    /// See [`ModuleCompiler::new`].
243    pub fn template(&self, module: &ModulePath, name: &str) -> Option<&TypedTemplate> {
244        self.templates.get(module)?.get(name)
245    }
246
247    /// Enumerates explicitly selectable root names in authored order.
248    ///
249    /// An unknown module has no roots; no arbitrary root is selected.
250    /// - Complexity: O(r) traversal for r roots.
251    /// # Examples
252    /// See [`Self::prepared_root`].
253    pub fn roots<'a>(&'a self, module: &ModulePath) -> impl Iterator<Item = &'a str> {
254        self.roots
255            .get(module)
256            .into_iter()
257            .flatten()
258            .map(|(name, _)| name.as_str())
259    }
260
261    /// Borrows one explicit root's lowered recipe metadata without instantiating it.
262    ///
263    /// - Complexity: O(r + p + n) root/name comparisons and module-path bytes.
264    /// # Examples
265    /// ```
266    /// let mut modules = adam_lang::ModuleSet::new();
267    /// let path = "app".parse()?;
268    /// modules.insert_source(path, "app.adm2", "sheet first { cell x: i32; } sheet second {}")?;
269    /// let compiled = adam_lang::ModuleCompiler::new(adam_lang::TypeRegistry::new(),
270    ///     cel_parser::OpLookup::new()).compile(modules).map_err(|mut e| e.remove(0))?;
271    /// let path = "app".parse()?;
272    /// assert_eq!(compiled.roots(&path).collect::<Vec<_>>(), ["first", "second"]);
273    /// assert_eq!(compiled.prepared_root(&path, "first").unwrap().cells()[0].name, "x");
274    /// # Ok::<(), adam_lang::ModuleDiagnostic>(())
275    /// ```
276    pub fn prepared_root(&self, module: &ModulePath, name: &str) -> Option<&PreparedRoot> {
277        self.roots
278            .get(module)?
279            .iter()
280            .find(|(root, _)| root == name)
281            .map(|(_, root)| root)
282    }
283}
284
285/// Owns only the syntax and planned descriptors read by one repeatable emitter.
286struct Expression {
287    syntax: Expr,
288    inputs: Vec<(String, ValueType)>,
289    candidate: Option<ValueType>,
290    descriptor: ValueType,
291    label: SourceLabel,
292    environment: Rc<Environment>,
293}
294
295impl Expression {
296    /// Copies immutable checked syntax/bindings without retaining the module collection.
297    ///
298    /// - Complexity: O(e + i + t) AST nodes, input names and descriptor nodes.
299    fn own(
300        expression: &analysis::Expression<'_>,
301        plan: &DefinitionPlan<'_>,
302        environment: &Rc<Environment>,
303    ) -> Self {
304        Self {
305            syntax: expression.syntax.clone(),
306            inputs: expression
307                .inputs
308                .iter()
309                .map(|id| {
310                    let cell = &plan.cells[*id];
311                    (
312                        cell.name.to_owned(),
313                        environment.types.value_type(&cell.shape),
314                    )
315                })
316                .collect(),
317            candidate: expression
318                .candidate
319                .map(|id| environment.types.value_type(&plan.cells[id].shape)),
320            descriptor: expression.descriptor.clone(),
321            label: expression.label.clone(),
322            environment: environment.clone(),
323        }
324    }
325
326    /// Emits fresh executable resources using only the already planned positional reads.
327    ///
328    /// # Errors
329    /// Propagates operation-emission errors, without executing the resulting segment.
330    /// - Complexity: CEL emission plus O(nr) positional name comparisons.
331    fn emit(&self) -> anyhow::Result<DynSegment> {
332        analysis::Emission {
333            syntax: &self.syntax,
334            inputs: self
335                .inputs
336                .iter()
337                .map(|(name, descriptor)| (name.as_str(), descriptor.clone())),
338            candidate: self.candidate.clone(),
339            descriptor: &self.descriptor,
340            label: &self.label,
341        }
342        .emit(&self.environment.types, &self.environment.lookup)
343        .map_err(|error| anyhow::anyhow!("{error}"))
344    }
345
346    /// Emits and evaluates a checked literal once during native construction.
347    ///
348    /// # Errors
349    /// Propagates literal-operation/frame errors without losing their original cause.
350    /// - Complexity: Literal emission, frame preparation and execution costs.
351    fn key(&self) -> anyhow::Result<CellValue> {
352        let mut frame =
353            RawFrame::new(self.emit()?, &[], &self.descriptor, &self.environment.types)?;
354        frame.call(&[]).map(CellValue)
355    }
356}
357
358/// Compiles with a shared immutable entry, including future AdamParser root lowering.
359///
360/// # Errors
361/// Returns collection-wide checked or owning-preparation diagnostics.
362/// - Complexity: As [`ModuleCompiler::compile`].
363pub(crate) fn compile(
364    environment: Rc<Environment>,
365    modules: ModuleSet,
366) -> Result<CompiledModules, Vec<ModuleDiagnostic>> {
367    let checked = analysis::analyze(&modules, &environment.types, &environment.lookup)?;
368    debug_assert!(std::ptr::eq(checked.types, environment.types.as_ref()));
369    debug_assert!(std::ptr::eq(checked.lookup, &environment.lookup));
370    let mut completed: Vec<Option<(TypedTemplate, Vec<RootCell>)>> =
371        (0..checked.definitions.len()).map(|_| None).collect();
372    // The resolver already supplies dependency postorder; no second sort or expansion runs.
373    for &id in &checked.resolved.order {
374        let plan = &checked.definitions[id];
375        let result = if let Some(native) = plan.native {
376            Ok((native.clone(), Vec::new()))
377        } else {
378            materialize(plan, &checked.definitions, &environment, &completed)
379        };
380        completed[id] = Some(result.map_err(|mut error| {
381            error.labels.push(plan.label.clone());
382            vec![error]
383        })?);
384    }
385    let mut result = CompiledModules {
386        templates: HashMap::new(),
387        roots: HashMap::new(),
388        sources: modules.sources.sources().to_vec(),
389        environment: environment.clone(),
390    };
391    let mut seen_sources = result.sources.iter().cloned().collect();
392    let mut seen_definitions = HashSet::new();
393    for (id, &(module, local)) in checked.resolved.definitions.iter().enumerate() {
394        let entry = &modules.entries[module];
395        let name = &entry.definitions[local].name;
396        let (template, cells) = completed[id]
397            .take()
398            .expect("dependency order covers all definitions");
399        template.retain_sources(
400            &mut seen_definitions,
401            &mut seen_sources,
402            &mut result.sources,
403        );
404        if checked.definitions[id].component {
405            result
406                .templates
407                .entry(entry.path.clone())
408                .or_default()
409                .insert(name.clone(), template);
410        } else {
411            result
412                .roots
413                .entry(entry.path.clone())
414                .or_default()
415                .push((name.clone(), PreparedRoot { template, cells }));
416        }
417    }
418    Ok(result)
419}
420
421/// Maps checked definition-local ordinals to native slots without allocating graph cells.
422///
423/// - Precondition: Every ordinal names an already declared local slot.
424/// - Complexity: O(i) handles.
425fn slots(ordinals: &[usize], cells: &[CellSlot]) -> Vec<CellSlot> {
426    debug_assert!(ordinals.iter().all(|id| *id < cells.len()));
427    ordinals.iter().map(|id| cells[*id].clone()).collect()
428}
429
430/// Constructs a method using the checked explicit output-pattern flag.
431///
432/// # Errors
433/// Propagates native recipe preparation errors.
434/// - Complexity: Expression ownership/emission plus native method preparation.
435fn method(
436    builder: &CompositionBuilder,
437    plan: &DefinitionPlan<'_>,
438    environment: &Rc<Environment>,
439    cells: &[CellSlot],
440    outputs: &[usize],
441    destructure: bool,
442    expression: &analysis::Expression<'_>,
443) -> Result<MethodRecipe, ModuleDiagnostic> {
444    let inputs = slots(&expression.inputs, cells);
445    let outputs = slots(outputs, cells);
446    let owned = Expression::own(expression, plan, environment);
447    builder.method_pattern(&inputs, &outputs, destructure, move || owned.emit())
448}
449
450/// Lowers every checked body form in authored order through the native recipe adapters.
451///
452/// Roots use this identical lowering and retain only their own public-name assembly inputs.
453/// # Errors
454/// Propagates recipe preparation errors with owning definition/item labels.
455/// - Complexity: O(n) local handles/metadata plus retained emission and native validation.
456fn materialize(
457    plan: &DefinitionPlan<'_>,
458    definitions: &[DefinitionPlan<'_>],
459    environment: &Rc<Environment>,
460    completed: &[Option<(TypedTemplate, Vec<RootCell>)>],
461) -> Result<(TypedTemplate, Vec<RootCell>), ModuleDiagnostic> {
462    let mut builder = CompositionBuilder::with_types(environment.types.clone());
463    builder.set_label(plan.label.clone());
464    let mut cells = Vec::with_capacity(plan.cells.len());
465    let mut authored = Vec::with_capacity(plan.formals + plan.items.len());
466    for cell in &plan.cells[..plan.formals] {
467        let slot = builder.parameter(cell.name, cell.shape.clone())?;
468        authored.push(AuthoredItem {
469            role: AuthoredRole::Parameter,
470            parent: None,
471            label: cell.label.clone(),
472            cell: Some(slot.native().clone()),
473            relationship: None,
474            method_index: None,
475            requirement_name: None,
476            expression: None,
477        });
478        cells.push(slot);
479    }
480    let mut groups: Vec<Option<RelationshipSlot>> = vec![None; plan.items.len()];
481    let mut pending_conditional = None;
482    // Native declarations and groups are created once, at their authored positions.
483    for (ordinal, item) in plan.items.iter().enumerate() {
484        let mut metadata = AuthoredItem {
485            role: AuthoredRole::Cell,
486            parent: item.parent.map(|p| p + plan.formals),
487            label: item.label.clone(),
488            cell: None,
489            relationship: None,
490            method_index: None,
491            requirement_name: None,
492            expression: None,
493        };
494        let lower = (|| -> Result<(), ModuleDiagnostic> {
495            match &item.body {
496                Body::Cell { cell, initializer } => {
497                    debug_assert_eq!(*cell, cells.len());
498                    let declaration = &plan.cells[*cell];
499                    let source = declaration.kind == Kind::Source;
500                    metadata.role = if source {
501                        AuthoredRole::Source
502                    } else {
503                        AuthoredRole::Cell
504                    };
505                    metadata.expression = initializer
506                        .as_ref()
507                        .map(|expression| expression.label.clone());
508                    let slot = if let Some(expression) = initializer {
509                        let owned = Expression::own(expression, plan, environment);
510                        if source {
511                            builder.source(
512                                declaration.name,
513                                declaration.shape.clone(),
514                                move || owned.emit(),
515                            )?
516                        } else {
517                            builder.cell(
518                                declaration.name,
519                                declaration.shape.clone(),
520                                move || owned.emit(),
521                            )?
522                        }
523                    } else {
524                        builder.default_private(
525                            declaration.name,
526                            declaration.shape.clone(),
527                            source,
528                        )?
529                    };
530                    metadata.cell = Some(slot.native().clone());
531                    metadata.label = declaration.label.clone();
532                    cells.push(slot);
533                }
534                Body::Out { cell, writer } => {
535                    metadata.role = AuthoredRole::Out;
536                    metadata.expression = Some(writer.label.clone());
537                    debug_assert_eq!(*cell, cells.len());
538                    let declaration = &plan.cells[*cell];
539                    let slot = builder.default_private(
540                        declaration.name,
541                        declaration.shape.clone(),
542                        false,
543                    )?;
544                    metadata.cell = Some(slot.native().clone());
545                    metadata.label = declaration.label.clone();
546                    cells.push(slot);
547                    let recipe =
548                        method(&builder, plan, environment, &cells, &[*cell], false, writer)?;
549                    metadata.relationship = Some(builder.out(recipe)?);
550                    metadata.method_index = Some(0);
551                }
552                Body::Filter {
553                    cell,
554                    expression,
555                    range,
556                } => {
557                    metadata.role = if *range {
558                        AuthoredRole::RangeFilter
559                    } else {
560                        AuthoredRole::Filter
561                    };
562                    metadata.expression = Some(expression.label.clone());
563                    let inputs = slots(&expression.inputs, &cells);
564                    let owned = Expression::own(expression, plan, environment);
565                    builder.filter(&cells[*cell], &inputs, move || owned.emit())?;
566                    metadata.cell = Some(cells[*cell].native().clone());
567                }
568                Body::Requirement {
569                    cell,
570                    name,
571                    expression,
572                } => {
573                    metadata.role = AuthoredRole::Requirement;
574                    metadata.expression = Some(expression.label.clone());
575                    let inputs = slots(&expression.inputs, &cells);
576                    let owned = Expression::own(expression, plan, environment);
577                    builder.require(&cells[*cell], &inputs, move || owned.emit())?;
578                    metadata.cell = Some(cells[*cell].native().clone());
579                    metadata.requirement_name = name.map(str::to_owned);
580                }
581                Body::Relationship { methods } => {
582                    metadata.role = AuthoredRole::Relationship;
583                    let recipes = methods
584                        .clone()
585                        .map(|id| match &plan.items[id].body {
586                            Body::Method {
587                                outputs,
588                                destructure,
589                                expression,
590                            } => method(
591                                &builder,
592                                plan,
593                                environment,
594                                &cells,
595                                outputs,
596                                *destructure,
597                                expression,
598                            ),
599                            _ => unreachable!("checked relationship contains only methods"),
600                        })
601                        .collect::<Result<Vec<_>, _>>()?;
602                    let group = builder.relationship(recipes)?;
603                    metadata.relationship = Some(group.clone());
604                    groups[ordinal] = Some(group);
605                }
606                Body::Method { expression, .. } => {
607                    metadata.role = AuthoredRole::Method;
608                    metadata.expression = Some(expression.label.clone());
609                    let parent = item.parent.expect("method has relationship parent");
610                    let Body::Relationship { methods } = &plan.items[parent].body else {
611                        unreachable!()
612                    };
613                    metadata.relationship = groups[parent].clone();
614                    metadata.method_index = Some(ordinal - methods.start);
615                }
616                Body::Use {
617                    target,
618                    bindings,
619                    syntax,
620                } => {
621                    metadata.role = AuthoredRole::Use;
622                    if let Some(location) = &item.label.location {
623                        metadata.label = label(&location.source, syntax.span, "component use");
624                    }
625                    let child = &completed[*target]
626                        .as_ref()
627                        .expect("dependency materialized")
628                        .0;
629                    let signature = &definitions[*target];
630                    let actuals = signature.cells[..signature.formals]
631                        .iter()
632                        .zip(bindings)
633                        .map(|(formal, id)| (formal.name, cells[*id].clone()))
634                        .collect::<Vec<_>>();
635                    let inclusion = builder.include(child, &actuals)?;
636                    groups[ordinal] = Some(inclusion.relationships()[0].clone());
637                    metadata.relationship = groups[ordinal].clone();
638                }
639                Body::Conditional { expression } => {
640                    metadata.role = AuthoredRole::Conditional;
641                    metadata.expression = Some(expression.label.clone());
642                    pending_conditional = Some(ordinal);
643                }
644                Body::Branch { syntax } => {
645                    metadata.role = AuthoredRole::Branch;
646                    if let Some(syntax) = syntax
647                        && let Some(location) = &item.label.location
648                    {
649                        metadata.expression =
650                            Some(label(&location.source, syntax.literal_span, "branch key"));
651                    }
652                }
653            }
654            Ok(())
655        })();
656        lower.map_err(|mut error| {
657            error.labels.push(item.label.clone());
658            error
659        })?;
660        authored.push(metadata);
661        // A conditional's group registration follows the last authored branch member.
662        // Its members already occupy their correct native declaration positions.
663        let next_conditional = plan
664            .items
665            .get(ordinal + 1)
666            .is_none_or(|next| next.parent.is_none());
667        if next_conditional && let Some(id) = pending_conditional.take() {
668            lower_conditional(&mut builder, plan, id, environment, &cells, &groups)?;
669        }
670    }
671    let locals = if plan.component {
672        Vec::new()
673    } else {
674        plan.cells
675            .iter()
676            .zip(&cells)
677            .map(|(cell, slot)| RootCell {
678                name: cell.name.to_owned(),
679                shape: cell.shape.clone(),
680                slot: slot.native().clone(),
681                label: cell.label.clone(),
682                kind: match cell.kind {
683                    Kind::Cell => CellKind::Cell,
684                    Kind::Source => CellKind::Source,
685                    Kind::Out => CellKind::Out,
686                    Kind::Parameter => unreachable!("root has no formals"),
687                },
688            })
689            .collect()
690    };
691    builder.set_authored(authored);
692    Ok((builder.finish()?, locals))
693}
694
695/// Attaches a conditional's already lowered native groups and deferred literal keys.
696///
697/// # Errors
698/// Propagates native conditional preparation or retained literal-emission errors.
699/// - Complexity: O(b + n) branches and local body scan, plus expression ownership/emission.
700fn lower_conditional(
701    builder: &mut CompositionBuilder,
702    plan: &DefinitionPlan<'_>,
703    id: usize,
704    environment: &Rc<Environment>,
705    cells: &[CellSlot],
706    groups: &[Option<RelationshipSlot>],
707) -> Result<(), ModuleDiagnostic> {
708    let Body::Conditional { expression } = &plan.items[id].body else {
709        unreachable!()
710    };
711    let inputs = slots(&expression.inputs, cells);
712    let owned = Expression::own(expression, plan, environment);
713    let shape = expression.shape.clone();
714    let mut keys = Vec::new();
715    let mut branches = Vec::new();
716    let mut default = Vec::new();
717    let mut current_branch = None;
718    for (item_id, item) in plan
719        .items
720        .iter()
721        .enumerate()
722        .skip(id + 1)
723        .take_while(|(_, item)| item.parent.is_some())
724    {
725        if let Body::Branch { syntax } = &item.body {
726            if let Some(syntax) = syntax {
727                let literal = cel_parser::literal_expr(&syntax.literal)
728                    .map_err(crate::composition::diagnostic)?;
729                let syntax = if syntax.negated {
730                    Expr::Op {
731                        name: "-".into(),
732                        operands: vec![literal],
733                        span: syntax.literal_span,
734                    }
735                } else {
736                    literal
737                };
738                keys.push(Expression {
739                    syntax,
740                    inputs: Vec::new(),
741                    candidate: None,
742                    descriptor: owned.descriptor.clone(),
743                    label: item.label.clone(),
744                    environment: environment.clone(),
745                });
746                branches.push(Vec::new());
747                current_branch = Some(branches.len() - 1);
748            } else {
749                current_branch = None;
750            }
751        } else if let Some(group) = &groups[item_id] {
752            if let Some(branch) = current_branch {
753                branches[branch].push(group.clone());
754            } else {
755                default.push(group.clone());
756            }
757        }
758    }
759    builder.conditional_owned(
760        &inputs,
761        shape,
762        move || owned.emit(),
763        move || {
764            keys.iter()
765                .map(|key| key.key().map(|value| vec![value]))
766                .collect()
767        },
768        branches,
769        default,
770    )
771}
772
773#[cfg(test)]
774mod tests {
775    use super::*;
776
777    /// Exercises the exact Task 6 recipe/slot handoff without implementing root assembly.
778    #[test]
779    fn prepared_roots_use_component_lowering_without_private_name_flattening() {
780        let path: ModulePath = "roots".parse().unwrap();
781        let mut modules = ModuleSet::new();
782        modules
783            .insert_source(
784                path.clone(),
785                "roots.adm2",
786                "sheet hidden(a: i32, b: i32) { cell private: i32 = 4;
787                relationship { b := a + private; } }
788             sheet root { source input: i32 = 3; cell output: i32;
789                relationship hidden(a: input, b: output);
790                out answer := output; }",
791            )
792            .unwrap();
793        let compiled = ModuleCompiler::new(TypeRegistry::new(), OpLookup::new())
794            .compile(modules)
795            .unwrap();
796        let root = compiled.prepared_root(&path, "root").unwrap();
797        let mut destination = crate::ParsedSheet::new();
798        let instance = root.template.instantiate(&mut destination, &[]).unwrap();
799        let root_id = instance.occurrences()[0].id();
800        let output = instance.cell(root_id, &root.cells()[1].slot).unwrap();
801        let answer = instance.cell(root_id, &root.cells()[2].slot).unwrap();
802        assert_eq!(
803            root.cells()
804                .iter()
805                .map(|cell| cell.name.as_str())
806                .collect::<Vec<_>>(),
807            ["input", "output", "answer"]
808        );
809        assert_eq!(root.cells()[2].kind, CellKind::Out);
810        assert_eq!(destination.cell_kind(answer), Some(CellKind::Out));
811        drop((instance, compiled));
812        destination.propagate().unwrap();
813        assert_eq!(*destination.read::<i32>(output).unwrap(), 7);
814        assert_eq!(*destination.read::<i32>(answer).unwrap(), 7);
815        assert!(destination.cell_names.is_empty());
816    }
817}