microcad_lang/eval/
workbench.rs

1// Copyright © 2024-2025 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(
64                model,
65                self.id.clone(),
66                non_properties.clone().into_iter().collect(),
67            ),
68            |context| {
69                let model = context.get_model()?;
70
71                // run init code
72                if let Some(init) = init {
73                    log::trace!(
74                        "Initializing`{id:?}` {kind}",
75                        id = self.id,
76                        kind = self.kind
77                    );
78                    if let Err(err) = init.eval(non_properties.into_iter().collect(), context) {
79                        context.error(&self.src_ref_head(), err)?;
80                    }
81                }
82
83                // At this point, all properties must have a value
84                log::trace!("Run body`{id:?}` {kind}", id = self.id, kind = self.kind);
85                model.append_children(self.body.statements.eval(context)?);
86
87                // We have to deduce the output type of this model, otherwise the model is incomplete.
88                {
89                    let model_ = model.borrow();
90                    match &*model_.element {
91                        Element::Workpiece(workpiece) => {
92                            let output_type = model.deduce_output_type();
93
94                            let result = workpiece.check_output_type(output_type);
95                            match result {
96                                Ok(()) => {}
97                                Err(EvalError::WorkbenchNoOutput(..)) => {
98                                    context.warning(
99                                        &self.src_ref_head(),
100                                        result.expect_err("Error"),
101                                    )?;
102                                }
103                                result => {
104                                    context
105                                        .error(&self.src_ref_head(), result.expect_err("Error"))?;
106                                }
107                            }
108                        }
109                        _ => panic!("A workbench must produce a workpiece."),
110                    }
111                }
112
113                Ok(model)
114            },
115        )
116    }
117}
118
119impl WorkbenchDefinition {
120    /// Evaluate the call of a workbench with given arguments.
121    ///
122    /// - `args`: Arguments which will be matched with the building plan and the initializers using parameter multiplicity.
123    /// - `context`: Current evaluation context.
124    ///
125    /// Return evaluated nodes (multiple nodes might be created by parameter multiplicity).
126    pub fn call(
127        &self,
128        call_src_ref: SrcRef,
129        symbol: Symbol,
130        arguments: &ArgumentValueList,
131        context: &mut EvalContext,
132    ) -> EvalResult<Model> {
133        log::debug!(
134            "Workbench {call} {kind} {id:?}({arguments:?})",
135            call = crate::mark!(CALL),
136            id = self.id,
137            kind = self.kind
138        );
139
140        // prepare models
141        let mut models = Models::default();
142        // prepare building plan
143        let plan = self.plan.eval(context)?;
144
145        // try to match arguments with the building plan
146        match ArgumentMatch::find_multi_match(arguments, &plan) {
147            Ok(matches) => {
148                log::debug!(
149                    "Building plan matches: {}",
150                    matches
151                        .iter()
152                        .map(|m| format!("{m:?}"))
153                        .collect::<Vec<_>>()
154                        .join("\n")
155                );
156                // evaluate models for all multiplicity matches
157                for arguments in matches {
158                    models.push(self.eval_to_model(
159                        call_src_ref.clone(),
160                        Creator::new(symbol.clone(), arguments),
161                        None,
162                        context,
163                    )?);
164                }
165            }
166            _ => {
167                log::trace!("Building plan did not match, finding initializer");
168
169                // at the end: check if initialization was successful
170                let mut initialized = false;
171
172                // find an initializer that matches the arguments
173                for init in self.inits() {
174                    if let Ok(matches) =
175                        ArgumentMatch::find_multi_match(arguments, &init.parameters.eval(context)?)
176                    {
177                        log::debug!(
178                            "Initializer matches: {}",
179                            matches
180                                .iter()
181                                .map(|m| format!("{m:?}"))
182                                .collect::<Vec<_>>()
183                                .join("\n")
184                        );
185                        // evaluate models for all multiplicity matches
186                        for arguments in matches {
187                            models.push(self.eval_to_model(
188                                call_src_ref.clone(),
189                                Creator::new(symbol.clone(), arguments),
190                                Some(init),
191                                context,
192                            )?);
193                        }
194                        initialized = true;
195                        break;
196                    }
197                }
198                if !initialized {
199                    context.error(arguments, EvalError::NoInitializationFound(self.id.clone()))?;
200                }
201            }
202        }
203
204        Ok(models.to_multiplicity(self.src_ref.clone()))
205    }
206}