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