Skip to main content

microcad_lang/eval/
workbench.rs

1// Copyright © 2024-2026 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! Workbench definition syntax element evaluation
5
6use crate::{eval::*, model::*, render::Hashed, syntax::*};
7
8impl WorkbenchDefinition {
9    /// Try to evaluate a single call into a [`Model`].
10    ///
11    /// - `arguments`: Single argument tuple (will not be multiplied).
12    /// - `init`: Initializer to call with given `arguments`.
13    /// - `context`: Current evaluation context.
14    fn eval_to_model<'a>(
15        &'a self,
16        call_src_ref: SrcRef,
17        creator: Creator,
18        init: Option<&'a InitDefinition>,
19        context: &mut EvalContext,
20    ) -> EvalResult<Model> {
21        log::debug!(
22            "Evaluating model of `{id:?}` {kind}",
23            id = self.id,
24            kind = self.kind
25        );
26
27        let arguments = creator.arguments.clone();
28
29        // copy all arguments which are part of the building plan into properties
30        let (mut properties, non_properties): (Vec<_>, Vec<_>) = arguments
31            .named_iter()
32            .map(|(id, value)| (id.clone(), value.clone()))
33            .partition(|(id, _)| self.plan.contains_key(id));
34
35        // create uninitialized values for all missing building plan properties
36        let missing: Vec<_> = self
37            .plan
38            .iter()
39            .filter(|param| !properties.iter().any(|(id, _)| param.id == *id))
40            .map(|param| param.id.clone())
41            .collect();
42        missing
43            .into_iter()
44            .for_each(|id| properties.push((id, Value::None)));
45
46        log::trace!("Properties: {properties:?}");
47        log::trace!("Non-Properties: {non_properties:?}");
48
49        // Create model
50        let model = ModelBuilder::new(
51            Element::Workpiece(Workpiece {
52                kind: *self.kind,
53                // copy all arguments which are part of the building plan to properties
54                properties: properties.into_iter().collect(),
55                creator: Hashed::new(creator),
56            }),
57            call_src_ref,
58        )
59        .attributes(self.attribute_list.eval(context)?)
60        .build();
61
62        context.scope(
63            StackFrame::Workbench(model, self.id.clone(), Default::default()),
64            |context| {
65                let model = context.get_model()?;
66
67                // run init code
68                if let Some(init) = init {
69                    log::trace!(
70                        "Initializing`{id:?}` {kind}",
71                        id = self.id,
72                        kind = self.kind
73                    );
74                    if let Err(err) = init.eval(non_properties.into_iter().collect(), context) {
75                        context.error(&self.src_ref(), err)?;
76                    }
77                }
78
79                // At this point, all properties must have a value
80                log::trace!("Run body`{id:?}` {kind}", id = self.id, kind = self.kind);
81                model.append_children(self.body.statements.eval(context)?);
82
83                // We have to deduce the output type of this model, otherwise the model is incomplete.
84                {
85                    let model_ = model.borrow();
86                    match &*model_.element {
87                        Element::Workpiece(workpiece) => {
88                            let output_type = model.deduce_output_type();
89
90                            let result = workpiece.check_output_type(output_type);
91                            match result {
92                                Ok(()) => {}
93                                Err(EvalError::WorkbenchNoOutput(..)) => {
94                                    context.warning(&self.src_ref(), result.expect_err("Error"))?;
95                                }
96                                result => {
97                                    context.error(&self.src_ref(), result.expect_err("Error"))?;
98                                }
99                            }
100                        }
101                        _ => panic!("A workbench must produce a workpiece."),
102                    }
103                }
104
105                Ok(model)
106            },
107        )
108    }
109}
110
111impl WorkbenchDefinition {
112    /// Evaluate the call of a workbench with given arguments.
113    ///
114    /// - `args`: Arguments which will be matched with the building plan and the initializers using parameter multiplicity.
115    /// - `context`: Current evaluation context.
116    ///
117    /// Return evaluated nodes (multiple nodes might be created by parameter multiplicity).
118    pub fn call(
119        &self,
120        call_src_ref: SrcRef,
121        symbol: Symbol,
122        arguments: &ArgumentValueList,
123        context: &mut EvalContext,
124    ) -> EvalResult<Model> {
125        log::debug!(
126            "{call} workbench {kind} {id:?}({arguments:?})",
127            call = crate::mark!(CALL),
128            id = self.id,
129            kind = self.kind
130        );
131
132        // prepare empty result model
133        let mut models = Models::default();
134
135        // match all initializations starting with the building plan
136        let matches: Vec<_> = std::iter::once((
137            None,
138            self.plan
139                .eval(context)
140                .and_then(|params| ArgumentMatch::find_multi_match(arguments, &params)),
141        ))
142        // chain the inits
143        .chain(self.inits().map(|init| {
144            (
145                Some(init),
146                init.parameters
147                    .eval(context)
148                    .and_then(|params| ArgumentMatch::find_multi_match(arguments, &params)),
149            )
150        }))
151        // debug inspection of all matches/non-matches
152        .inspect(|(i, m)| {
153            let result = match m {
154                Ok(m) => format!(
155                    "{match_} [{priority:>10}]",
156                    priority = m.priority,
157                    match_ = crate::mark!(MATCH)
158                ),
159                Err(_) => crate::mark!(NO_MATCH),
160            };
161            if let Some(i) = i {
162                log::debug!("{result} {}::init({})", symbol.full_name(), i.parameters)
163            } else {
164                log::debug!("{result} {}({})", symbol.full_name(), self.plan)
165            }
166        })
167        // filter out non-matching
168        .filter_map(|(i, m)| if let Ok(m) = m { Some((i, m)) } else { None })
169        .collect();
170
171        // find hightest priority matches
172        let matches = Priority::high_to_low().iter().find_map(|priority| {
173            let matches: Vec<_> = matches
174                .iter()
175                .filter(|(_, m)| m.priority == *priority)
176                .collect();
177            if matches.is_empty() {
178                None
179            } else {
180                Some(matches)
181            }
182        });
183
184        if let Some(mut matches) = matches {
185            if matches.len() > 1 {
186                let ambiguous = matches
187                    .iter()
188                    .map(|(init, _)| match init {
189                        Some(init) => {
190                            format!(
191                                "{name}::init({params})",
192                                name = symbol.full_name(),
193                                params = init.parameters
194                            )
195                        }
196                        None => format!(
197                            "{name:?}({params})",
198                            name = symbol.full_name(),
199                            params = self.plan
200                        ),
201                    })
202                    .collect::<Vec<_>>();
203                log::debug!(
204                    "{match_} Ambiguous initialization: {name}({arguments})\nCould be one of:\n{ambiguous}",
205                    name = symbol.full_name(),
206                    ambiguous = ambiguous.join("\n"),
207                    match_ = crate::mark!(AMBIGUOUS)
208                );
209                context.error(
210                    arguments,
211                    EvalError::AmbiguousInitialization {
212                        src_ref: call_src_ref,
213                        name: self.id.clone(),
214                        actual_params: arguments.to_string(),
215                        ambiguous_params: ambiguous,
216                    },
217                )?;
218            } else if let Some(matched) = matches.pop() {
219                let what = if matched.0.is_none() {
220                    "Building plan"
221                } else {
222                    "Initializer"
223                };
224                log::debug!(
225                    "{match_} {what}: {}",
226                    matched
227                        .1
228                        .args
229                        .iter()
230                        .map(|m| format!("{m:?}"))
231                        .collect::<Vec<_>>()
232                        .join("\n"),
233                    match_ = crate::mark!(MATCH!)
234                );
235
236                // evaluate models for all multiplicity matches
237                for arguments in matched.1.args.iter() {
238                    models.push(self.eval_to_model(
239                        call_src_ref.clone(),
240                        Creator::new(symbol.clone(), arguments.clone()),
241                        matched.0,
242                        context,
243                    )?);
244                }
245            }
246        } else {
247            log::debug!(
248                "{match_} Neither the building plan nor any initializer matches arguments",
249                match_ = crate::mark!(NO_MATCH!)
250            );
251            context.error(
252                arguments,
253                EvalError::NoInitializationFound {
254                    src_ref: call_src_ref,
255                    name: self.id.clone(),
256                    actual_params: arguments.to_string(),
257                    possible_params: self.possible_params(),
258                },
259            )?;
260        }
261
262        Ok(models.to_multiplicity(self.src_ref()))
263    }
264}