Skip to main content

microcad_lang/eval/
attribute.rs

1// Copyright © 2025-2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4use crate::{
5    Id,
6    builtin::ExporterAccess,
7    eval::{self, *},
8    model::{Attributes, CustomCommand, ExportCommand, MeasureCommand, ResolutionAttribute},
9    parameter,
10    syntax::{self, *},
11};
12use microcad_lang_base::{PushDiag, SrcRef};
13use miette::Diagnostic;
14use std::str::FromStr;
15
16use microcad_core::{Color, Length, RenderResolution, Size2};
17use thiserror::Error;
18
19/// Error type for attributes.
20#[derive(Debug, Error, Diagnostic)]
21pub enum AttributeError {
22    /// Unknown attribute.
23    #[error("Attribute not supported: {0}")]
24    NotSupported(Identifier),
25
26    /// Attribute cannot be assigned to an expression.
27    #[error("Cannot assign attribute to expression `{0}`")]
28    CannotAssignAttribute(String),
29
30    /// The attribute was not found.
31    #[error("Not found: {0}")]
32    NotFound(Identifier),
33
34    /// Invalid command.
35    #[error("Invalid command list for attribute `{0}`")]
36    InvalidCommand(Identifier),
37}
38
39impl Eval<Option<ExportCommand>> for syntax::AttributeCommand {
40    fn eval(&self, context: &mut EvalContext) -> EvalResult<Option<ExportCommand>> {
41        match self {
42            AttributeCommand::Call(call) => {
43                match ArgumentMatch::find_match(
44                    &call.argument_list.eval(context)?,
45                    &[
46                        parameter!(filename: String),
47                        parameter!(resolution: Length = Length::mm(0.1)),
48                        (
49                            Identifier::no_ref("size"),
50                            eval::ParameterValue {
51                                specified_type: Some(Type::Tuple(Box::new(TupleType::new_size2()))),
52                                default_value: Some(Value::Tuple(Box::new(Size2::A4.into()))),
53                                src_ref: SrcRef(None),
54                            },
55                        ),
56                    ]
57                    .into_iter()
58                    .collect(),
59                ) {
60                    Ok(arguments) => {
61                        let filename: std::path::PathBuf =
62                            arguments.get::<String>("filename").into();
63                        let id: Option<Id> = if let Ok(id) = arguments.by_str::<String>("id") {
64                            Some(id.into())
65                        } else {
66                            None
67                        };
68                        let resolution = RenderResolution::new(
69                            arguments.get::<&Value>("resolution").try_scalar()?,
70                        );
71
72                        match context.find_exporter(&filename, &id) {
73                            Ok(exporter) => Ok(Some(ExportCommand {
74                                filename,
75                                exporter,
76                                resolution,
77                            })),
78                            Err(err) => {
79                                context.warning(self, err)?;
80                                Ok(None)
81                            }
82                        }
83                    }
84                    Err(err) => {
85                        context.warning(self, err)?;
86                        Ok(None)
87                    }
88                }
89            }
90            AttributeCommand::Assigment { value, .. } => {
91                let value: Value = value.eval(context)?;
92                match value {
93                    Value::String(filename) => {
94                        let filename = std::path::PathBuf::from(filename);
95                        match context.find_exporter(&filename, &None) {
96                            Ok(exporter) => Ok(Some(ExportCommand {
97                                filename,
98                                resolution: RenderResolution::default(),
99                                exporter,
100                            })),
101                            Err(err) => {
102                                context.warning(self, err)?;
103                                Ok(None)
104                            }
105                        }
106                    }
107                    _ => unimplemented!(),
108                }
109            }
110            _ => Ok(None),
111        }
112    }
113}
114
115impl Eval<Vec<ExportCommand>> for syntax::Attribute {
116    fn eval(&self, context: &mut EvalContext) -> EvalResult<Vec<ExportCommand>> {
117        self.commands
118            .iter()
119            .try_fold(Vec::new(), |mut commands, attribute| {
120                assert_eq!(attribute.id_as_str(), "export");
121                if let Some(export_command) = attribute.eval(context)? {
122                    commands.push(export_command)
123                }
124                Ok(commands)
125            })
126    }
127}
128
129impl Eval<Vec<MeasureCommand>> for syntax::Attribute {
130    fn eval(&self, context: &mut EvalContext) -> EvalResult<Vec<MeasureCommand>> {
131        let mut commands = Vec::new();
132
133        for command in &self.commands {
134            match command {
135                AttributeCommand::Call(_) => match command.id_as_str() {
136                    "width" => commands.push(MeasureCommand::Width),
137                    "height" => commands.push(MeasureCommand::Height),
138                    "size" => commands.push(MeasureCommand::Size),
139                    _ => context
140                        .warning(self, AttributeError::InvalidCommand(command.id().clone()))?,
141                },
142                _ => unimplemented!(),
143            }
144        }
145
146        Ok(commands)
147    }
148}
149
150impl Eval<Vec<CustomCommand>> for syntax::Attribute {
151    fn eval(&self, context: &mut EvalContext) -> EvalResult<Vec<CustomCommand>> {
152        let mut commands = Vec::new();
153        for command in &self.commands {
154            match command {
155                AttributeCommand::Call(call) => {
156                    match context.exporters().exporter_by_id(command.id().id()) {
157                        Ok(exporter) => {
158                            match ArgumentMatch::find_match(
159                                &call.argument_list.eval(context)?,
160                                &exporter.model_parameters(),
161                            ) {
162                                Ok(tuple) => commands.push(CustomCommand::new(command.id(), tuple)),
163                                Err(err) => {
164                                    context.warning(self, err)?;
165                                }
166                            }
167                        }
168                        Err(err) => {
169                            context.warning(self, err)?;
170                        }
171                    }
172                }
173                AttributeCommand::Assigment { name, .. } => {
174                    match context.exporters().exporter_by_id(name.id()) {
175                        Ok(_) => {
176                            commands.push(CustomCommand::new(command.id(), Default::default()))
177                        }
178                        Err(err) => {
179                            context.warning(self, err)?;
180                        }
181                    }
182                }
183                _ => unimplemented!(),
184            }
185        }
186
187        Ok(commands)
188    }
189}
190
191impl Eval<Option<Color>> for syntax::AttributeCommand {
192    fn eval(&self, context: &mut EvalContext) -> EvalResult<Option<Color>> {
193        match self {
194            // Get color from a tuple or string.
195            AttributeCommand::Assigment { value, .. } => {
196                let value: Value = value.eval(context)?;
197                match value {
198                    // Color from string: color = "red"
199                    Value::String(s) => match Color::from_str(&s) {
200                        Ok(color) => Ok(Some(color)),
201                        Err(err) => {
202                            context.warning(self, err)?;
203                            Ok(None)
204                        }
205                    },
206                    // Color from tuple: color = (r = 1.0, g = 1.0, b = 1.0, a = 1.0)
207                    Value::Tuple(tuple) => match Color::try_from(tuple.as_ref()) {
208                        Ok(color) => Ok(Some(color)),
209                        Err(err) => {
210                            context.warning(self, err)?;
211                            Ok(None)
212                        }
213                    },
214                    _ => {
215                        context.warning(
216                            self,
217                            AttributeError::InvalidCommand(Identifier::no_ref("color")),
218                        )?;
219                        Ok(None)
220                    }
221                }
222            }
223            _ => todo!(),
224        }
225    }
226}
227
228impl Eval<Option<ResolutionAttribute>> for syntax::AttributeCommand {
229    fn eval(&self, context: &mut EvalContext) -> EvalResult<Option<ResolutionAttribute>> {
230        match self {
231            AttributeCommand::Assigment { value, .. } => {
232                let value: Value = value.eval(context)?;
233                match value {
234                    Value::Quantity(qty) => match qty.quantity_type {
235                        QuantityType::Scalar => Ok(Some(ResolutionAttribute::Relative(qty.value))),
236                        QuantityType::Length => Ok(Some(ResolutionAttribute::Absolute(qty.value))),
237                        _ => unimplemented!(),
238                    },
239                    _ => todo!("Error handling"),
240                }
241            }
242            _ => {
243                context.warning(
244                    self,
245                    AttributeError::InvalidCommand(Identifier::no_ref("resolution")),
246                )?;
247                Ok(None)
248            }
249        }
250    }
251}
252
253impl Eval<Option<Size2>> for syntax::AttributeCommand {
254    fn eval(&self, _: &mut EvalContext) -> EvalResult<Option<Size2>> {
255        todo!("Get Size2, e.g. `size = (width = 10mm, height = 10mm) from AttributeCommand")
256    }
257}
258
259macro_rules! eval_to_attribute {
260    ($id:ident: $ty:ty) => {
261        impl Eval<Option<$ty>> for syntax::Attribute {
262            fn eval(&self, context: &mut EvalContext) -> EvalResult<Option<$ty>> {
263                let command = self.commands.first().expect("empty attribute");
264                assert_eq!(command.id_ref().to_string(), stringify!($id));
265                match self.single_command() {
266                    Some(command) => Ok(command.eval(context)?),
267                    None => {
268                        context.warning(self, AttributeError::InvalidCommand(command.id()))?;
269                        Ok(None)
270                    }
271                }
272            }
273        }
274    };
275}
276
277eval_to_attribute!(color: Color);
278eval_to_attribute!(resolution: ResolutionAttribute);
279eval_to_attribute!(size: Size2);
280
281impl Eval<Vec<crate::model::Attribute>> for syntax::Attribute {
282    fn eval(&self, context: &mut EvalContext) -> EvalResult<Vec<crate::model::Attribute>> {
283        use crate::model::Attribute as Attr;
284        self.commands
285            .iter()
286            .map(|command| {
287                Ok(match command.id_as_str() {
288                    "color" => match self.eval(context)? {
289                        Some(color) => vec![Attr::Color(color)],
290                        None => Default::default(),
291                    },
292                    "resolution" => match self.eval(context)? {
293                        Some(resolution) => vec![Attr::Resolution(resolution)],
294                        None => Default::default(),
295                    },
296                    "size" => match self.eval(context)? {
297                        Some(size) => vec![Attr::Size(size)],
298                        None => Default::default(),
299                    },
300                    "export" => {
301                        let exports: Vec<ExportCommand> = self.eval(context)?;
302                        exports.iter().cloned().map(Attr::Export).collect()
303                    }
304                    "measure" => {
305                        let measures: Vec<MeasureCommand> = self.eval(context)?;
306                        measures.iter().cloned().map(Attr::Measure).collect()
307                    }
308                    _ => {
309                        let commands: Vec<CustomCommand> = self.eval(context)?;
310                        commands.iter().cloned().map(Attr::Custom).collect()
311                    }
312                })
313            })
314            .flat_map(|res| match res {
315                Ok(res) => res.into_iter().map(Ok).collect(),
316                Err(err) => vec![Err(err)],
317            })
318            .collect()
319    }
320}
321
322impl Eval<crate::model::Attributes> for AttributeList {
323    fn eval(&self, context: &mut EvalContext) -> EvalResult<crate::model::Attributes> {
324        Ok(Attributes(self.iter().try_fold(
325            Vec::new(),
326            |mut attributes, attribute| -> EvalResult<_> {
327                attributes.append(&mut attribute.eval(context)?);
328                Ok(attributes)
329            },
330        )?))
331    }
332}