Skip to main content

microcad_lang/eval/
attribute.rs

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