Skip to main content

microcad_lang/eval/
workbench.rs

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