Skip to main content

rscad_openscad/eval/
value.rs

1//! OpenSCAD runtime values and their operator semantics.
2
3use std::sync::Arc;
4
5/// An OpenSCAD numeric range `[start : step : end]` (inclusive end).
6#[derive(Clone, Copy, Debug, PartialEq)]
7pub struct RangeValue {
8    pub start: f64,
9    pub step: f64,
10    pub end: f64,
11}
12
13/// A dynamically-typed OpenSCAD value.
14///
15/// Vectors are `Arc`-shared: values are cheap to clone during evaluation and
16/// remain `Send` so evaluation can run on a dedicated big-stack thread.
17#[derive(Clone, Debug, PartialEq)]
18pub enum Value {
19    Undef,
20    Bool(bool),
21    Number(f64),
22    String(String),
23    Vector(Arc<Vec<Value>>),
24    Range(RangeValue),
25}
26
27impl Value {
28    pub fn vector(items: Vec<Value>) -> Self {
29        Value::Vector(Arc::new(items))
30    }
31
32    /// OpenSCAD truthiness: `false`, `0`, `""`, `[]`, and `undef` are falsy.
33    pub fn truthy(&self) -> bool {
34        match self {
35            Value::Undef => false,
36            Value::Bool(b) => *b,
37            Value::Number(n) => *n != 0.0,
38            Value::String(s) => !s.is_empty(),
39            Value::Vector(v) => !v.is_empty(),
40            Value::Range(_) => true,
41        }
42    }
43
44    pub fn as_number(&self) -> Option<f64> {
45        match self {
46            Value::Number(n) => Some(*n),
47            _ => None,
48        }
49    }
50
51    pub fn as_vector(&self) -> Option<&[Value]> {
52        match self {
53            Value::Vector(v) => Some(v),
54            _ => None,
55        }
56    }
57
58    /// Interpret as a flat numeric vector.
59    pub fn as_numbers(&self) -> Option<Vec<f64>> {
60        self.as_vector()?
61            .iter()
62            .map(Value::as_number)
63            .collect::<Option<Vec<_>>>()
64    }
65
66    /// Interpret as a 2D/3D point, zero-extending to three components.
67    pub fn as_vec3_lenient(&self) -> Option<[f64; 3]> {
68        let nums = self.as_numbers()?;
69        match nums.len() {
70            2 => Some([nums[0], nums[1], 0.0]),
71            3 => Some([nums[0], nums[1], nums[2]]),
72            _ => None,
73        }
74    }
75
76    pub fn type_name(&self) -> &'static str {
77        match self {
78            Value::Undef => "undef",
79            Value::Bool(_) => "bool",
80            Value::Number(_) => "number",
81            Value::String(_) => "string",
82            Value::Vector(_) => "vector",
83            Value::Range(_) => "range",
84        }
85    }
86
87    /// Format for `echo`/diagnostics: strings are quoted.
88    pub fn echo_format(&self) -> String {
89        self.format(true)
90    }
91
92    /// Format for `str()`: strings are unquoted.
93    pub fn str_format(&self) -> String {
94        self.format(false)
95    }
96
97    fn format(&self, quote_strings: bool) -> String {
98        match self {
99            Value::Undef => "undef".to_string(),
100            Value::Bool(b) => b.to_string(),
101            Value::Number(n) => format_number(*n),
102            Value::String(s) if quote_strings => format!("\"{s}\""),
103            Value::String(s) => s.clone(),
104            Value::Vector(v) => {
105                let items: Vec<String> = v.iter().map(|x| x.format(true)).collect();
106                format!("[{}]", items.join(", "))
107            }
108            Value::Range(r) => format!(
109                "[{} : {} : {}]",
110                format_number(r.start),
111                format_number(r.step),
112                format_number(r.end)
113            ),
114        }
115    }
116}
117
118pub fn format_number(n: f64) -> String {
119    if n == f64::INFINITY {
120        "inf".to_string()
121    } else if n == f64::NEG_INFINITY {
122        "-inf".to_string()
123    } else if n.is_nan() {
124        "nan".to_string()
125    } else {
126        format!("{n}")
127    }
128}
129
130/// Errors from value operations; the caller downgrades these to warnings and
131/// substitutes `undef`, matching OpenSCAD's lenient arithmetic.
132pub type OpResult = core::result::Result<Value, String>;
133
134fn both_numbers(a: &Value, b: &Value) -> Option<(f64, f64)> {
135    Some((a.as_number()?, b.as_number()?))
136}
137
138fn elementwise(a: &[Value], b: &[Value], op: fn(&Value, &Value) -> OpResult) -> OpResult {
139    if a.len() != b.len() {
140        return Err(format!(
141            "vector length mismatch: {} vs {}",
142            a.len(),
143            b.len()
144        ));
145    }
146    a.iter()
147        .zip(b)
148        .map(|(x, y)| op(x, y))
149        .collect::<Result<Vec<_>, _>>()
150        .map(Value::vector)
151}
152
153pub fn add(a: &Value, b: &Value) -> OpResult {
154    match (a, b) {
155        (Value::Number(x), Value::Number(y)) => Ok(Value::Number(x + y)),
156        (Value::Vector(x), Value::Vector(y)) => elementwise(x, y, add),
157        _ => Err(format!(
158            "cannot add {} and {}",
159            a.type_name(),
160            b.type_name()
161        )),
162    }
163}
164
165pub fn sub(a: &Value, b: &Value) -> OpResult {
166    match (a, b) {
167        (Value::Number(x), Value::Number(y)) => Ok(Value::Number(x - y)),
168        (Value::Vector(x), Value::Vector(y)) => elementwise(x, y, sub),
169        _ => Err(format!(
170            "cannot subtract {} from {}",
171            b.type_name(),
172            a.type_name()
173        )),
174    }
175}
176
177/// Is this a flat vector of numbers?
178fn is_flat_numeric(v: &[Value]) -> bool {
179    v.iter().all(|x| matches!(x, Value::Number(_)))
180}
181
182/// Is this a vector of equal-length numeric vectors (a matrix)?
183fn matrix_dims(v: &[Value]) -> Option<(usize, usize)> {
184    let rows = v.len();
185    let first = v.first()?.as_vector()?;
186    let cols = first.len();
187    for row in v {
188        let row = row.as_vector()?;
189        if row.len() != cols || !is_flat_numeric(row) {
190            return None;
191        }
192    }
193    Some((rows, cols))
194}
195
196fn dot(a: &[Value], b: &[Value]) -> OpResult {
197    if a.len() != b.len() {
198        return Err(format!(
199            "vector length mismatch in dot product: {} vs {}",
200            a.len(),
201            b.len()
202        ));
203    }
204    let mut acc = 0.0;
205    for (x, y) in a.iter().zip(b) {
206        match both_numbers(x, y) {
207            Some((x, y)) => acc += x * y,
208            None => return Err("dot product requires numeric vectors".into()),
209        }
210    }
211    Ok(Value::Number(acc))
212}
213
214fn scale(v: &[Value], s: f64) -> OpResult {
215    v.iter()
216        .map(|x| mul(x, &Value::Number(s)))
217        .collect::<Result<Vec<_>, _>>()
218        .map(Value::vector)
219}
220
221pub fn mul(a: &Value, b: &Value) -> OpResult {
222    match (a, b) {
223        (Value::Number(x), Value::Number(y)) => Ok(Value::Number(x * y)),
224        (Value::Number(s), Value::Vector(v)) => scale(v, *s),
225        (Value::Vector(v), Value::Number(s)) => scale(v, *s),
226        (Value::Vector(x), Value::Vector(y)) => {
227            let (fx, fy) = (is_flat_numeric(x), is_flat_numeric(y));
228            match (fx, fy) {
229                (true, true) => dot(x, y),
230                // matrix * vector
231                (false, true) => {
232                    matrix_dims(x).ok_or("left operand is not a matrix")?;
233                    x.iter()
234                        .map(|row| dot(row.as_vector().expect("BUG: matrix_dims checked"), y))
235                        .collect::<Result<Vec<_>, _>>()
236                        .map(Value::vector)
237                }
238                // vector * matrix
239                (true, false) => {
240                    let (rows, cols) = matrix_dims(y).ok_or("right operand is not a matrix")?;
241                    if x.len() != rows {
242                        return Err(format!(
243                            "vector*matrix dimension mismatch: {} vs {rows}",
244                            x.len()
245                        ));
246                    }
247                    let mut out = Vec::with_capacity(cols);
248                    for c in 0..cols {
249                        let mut acc = 0.0;
250                        for (r, xv) in x.iter().enumerate() {
251                            let cell = y[r].as_vector().expect("BUG: matrix_dims checked")[c]
252                                .as_number()
253                                .expect("BUG: matrix_dims checked");
254                            acc += xv.as_number().expect("BUG: is_flat_numeric checked") * cell;
255                        }
256                        out.push(Value::Number(acc));
257                    }
258                    Ok(Value::vector(out))
259                }
260                // matrix * matrix
261                (false, false) => {
262                    let (_ar, ac) = matrix_dims(x).ok_or("left operand is not a matrix")?;
263                    let (br, bc) = matrix_dims(y).ok_or("right operand is not a matrix")?;
264                    if ac != br {
265                        return Err(format!("matrix dimension mismatch: {ac} vs {br}"));
266                    }
267                    let mut out = Vec::with_capacity(x.len());
268                    for row in x.iter() {
269                        let row = row.as_vector().expect("BUG: matrix_dims checked");
270                        let mut out_row = Vec::with_capacity(bc);
271                        for c in 0..bc {
272                            let mut acc = 0.0;
273                            for (k, rv) in row.iter().enumerate() {
274                                let cell = y[k].as_vector().expect("BUG: matrix_dims checked")[c]
275                                    .as_number()
276                                    .expect("BUG: matrix_dims checked");
277                                acc += rv.as_number().expect("BUG: matrix_dims checked") * cell;
278                            }
279                            out_row.push(Value::Number(acc));
280                        }
281                        out.push(Value::vector(out_row));
282                    }
283                    Ok(Value::vector(out))
284                }
285            }
286        }
287        _ => Err(format!(
288            "cannot multiply {} and {}",
289            a.type_name(),
290            b.type_name()
291        )),
292    }
293}
294
295pub fn div(a: &Value, b: &Value) -> OpResult {
296    match (a, b) {
297        (Value::Number(x), Value::Number(y)) => Ok(Value::Number(x / y)),
298        (Value::Vector(v), Value::Number(s)) => v
299            .iter()
300            .map(|x| div(x, &Value::Number(*s)))
301            .collect::<Result<Vec<_>, _>>()
302            .map(Value::vector),
303        _ => Err(format!(
304            "cannot divide {} by {}",
305            a.type_name(),
306            b.type_name()
307        )),
308    }
309}
310
311pub fn rem(a: &Value, b: &Value) -> OpResult {
312    match both_numbers(a, b) {
313        Some((x, y)) => Ok(Value::Number(x % y)),
314        None => Err(format!(
315            "cannot take {} modulo {}",
316            a.type_name(),
317            b.type_name()
318        )),
319    }
320}
321
322pub fn neg(a: &Value) -> OpResult {
323    match a {
324        Value::Number(n) => Ok(Value::Number(-n)),
325        Value::Vector(v) => v
326            .iter()
327            .map(neg)
328            .collect::<Result<Vec<_>, _>>()
329            .map(Value::vector),
330        _ => Err(format!("cannot negate {}", a.type_name())),
331    }
332}
333
334/// Ordering comparison (`<`, `>`, `<=`, `>=`): numbers and strings only.
335pub fn compare(a: &Value, b: &Value) -> core::result::Result<core::cmp::Ordering, String> {
336    match (a, b) {
337        (Value::Number(x), Value::Number(y)) => x
338            .partial_cmp(y)
339            .ok_or_else(|| "cannot compare NaN".to_string()),
340        (Value::String(x), Value::String(y)) => Ok(x.cmp(y)),
341        _ => Err(format!(
342            "cannot compare {} with {}",
343            a.type_name(),
344            b.type_name()
345        )),
346    }
347}
348
349/// Index into a vector (`v[i]`) or string (`s[i]`).
350pub fn index(base: &Value, idx: &Value) -> OpResult {
351    let i = idx
352        .as_number()
353        .ok_or_else(|| format!("index must be a number, got {}", idx.type_name()))?;
354    if i.fract() != 0.0 || i < 0.0 {
355        return Err(format!("invalid index {i}"));
356    }
357    let i = i as usize;
358    match base {
359        Value::Vector(v) => v
360            .get(i)
361            .cloned()
362            .ok_or_else(|| format!("index {i} out of bounds (len {})", v.len())),
363        Value::String(s) => s
364            .chars()
365            .nth(i)
366            .map(|c| Value::String(c.to_string()))
367            .ok_or_else(|| format!("index {i} out of bounds (len {})", s.chars().count())),
368        _ => Err(format!("cannot index {}", base.type_name())),
369    }
370}
371
372/// Iteration over a value, as used by `for` and list comprehensions.
373///
374/// Ranges step numerically (empty when the step direction never reaches the
375/// end), vectors yield elements, strings yield 1-char strings, and any other
376/// value yields itself once.
377pub fn iterate(v: &Value) -> Vec<Value> {
378    match v {
379        Value::Range(r) => {
380            let mut out = Vec::new();
381            if r.step == 0.0 || r.step.is_nan() || r.start.is_nan() || r.end.is_nan() {
382                return out;
383            }
384            let count = ((r.end - r.start) / r.step).floor();
385            if count < 0.0 || !count.is_finite() {
386                return out;
387            }
388            let count = count as u64;
389            for i in 0..=count {
390                out.push(Value::Number(r.start + r.step * i as f64));
391            }
392            out
393        }
394        Value::Vector(v) => v.iter().cloned().collect(),
395        Value::String(s) => s.chars().map(|c| Value::String(c.to_string())).collect(),
396        Value::Undef => Vec::new(),
397        other => vec![other.clone()],
398    }
399}
400
401/// Number of iterations `iterate` would produce, without materializing.
402pub fn iteration_count(v: &Value) -> u64 {
403    match v {
404        Value::Range(r) => {
405            if r.step == 0.0 || r.step.is_nan() || r.start.is_nan() || r.end.is_nan() {
406                return 0;
407            }
408            let count = ((r.end - r.start) / r.step).floor();
409            if count < 0.0 || !count.is_finite() {
410                0
411            } else {
412                count as u64 + 1
413            }
414        }
415        Value::Vector(v) => v.len() as u64,
416        Value::String(s) => s.chars().count() as u64,
417        Value::Undef => 0,
418        _ => 1,
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    fn num(n: f64) -> Value {
427        Value::Number(n)
428    }
429    fn vec3(a: f64, b: f64, c: f64) -> Value {
430        Value::vector(vec![num(a), num(b), num(c)])
431    }
432
433    #[test]
434    fn truthiness() {
435        assert!(!Value::Undef.truthy());
436        assert!(!num(0.0).truthy());
437        assert!(!Value::String(String::new()).truthy());
438        assert!(!Value::vector(vec![]).truthy());
439        assert!(num(0.5).truthy());
440        assert!(Value::Bool(true).truthy());
441    }
442
443    #[test]
444    fn vector_arithmetic() {
445        assert_eq!(
446            add(&vec3(1.0, 2.0, 3.0), &vec3(4.0, 5.0, 6.0)),
447            Ok(vec3(5.0, 7.0, 9.0))
448        );
449        assert_eq!(
450            sub(&vec3(4.0, 5.0, 6.0), &vec3(1.0, 2.0, 3.0)),
451            Ok(vec3(3.0, 3.0, 3.0))
452        );
453        assert_eq!(
454            mul(&num(2.0), &vec3(1.0, 2.0, 3.0)),
455            Ok(vec3(2.0, 4.0, 6.0))
456        );
457        // dot product
458        assert_eq!(
459            mul(&vec3(1.0, 2.0, 3.0), &vec3(4.0, 5.0, 6.0)),
460            Ok(num(32.0))
461        );
462        assert!(add(&vec3(1.0, 2.0, 3.0), &Value::vector(vec![num(1.0)])).is_err());
463    }
464
465    #[test]
466    fn matrix_vector_product() {
467        let m = Value::vector(vec![
468            Value::vector(vec![num(1.0), num(0.0)]),
469            Value::vector(vec![num(0.0), num(2.0)]),
470        ]);
471        let v = Value::vector(vec![num(3.0), num(4.0)]);
472        assert_eq!(mul(&m, &v), Ok(Value::vector(vec![num(3.0), num(8.0)])));
473    }
474
475    #[test]
476    fn range_iteration() {
477        let r = Value::Range(RangeValue {
478            start: 0.0,
479            step: 2.0,
480            end: 5.0,
481        });
482        let vals: Vec<f64> = iterate(&r).iter().filter_map(Value::as_number).collect();
483        assert_eq!(vals, vec![0.0, 2.0, 4.0]);
484        // reversed range with positive step is empty
485        let r = Value::Range(RangeValue {
486            start: 5.0,
487            step: 1.0,
488            end: 0.0,
489        });
490        assert!(iterate(&r).is_empty());
491        // negative step counts down
492        let r = Value::Range(RangeValue {
493            start: 3.0,
494            step: -1.0,
495            end: 1.0,
496        });
497        assert_eq!(iteration_count(&r), 3);
498    }
499
500    #[test]
501    fn string_index_and_iterate() {
502        let s = Value::String("ab".into());
503        assert_eq!(index(&s, &num(1.0)), Ok(Value::String("b".into())));
504        assert_eq!(iterate(&s).len(), 2);
505    }
506
507    #[test]
508    fn number_formatting() {
509        assert_eq!(num(5.0).str_format(), "5");
510        assert_eq!(num(0.5).str_format(), "0.5");
511        assert_eq!(Value::String("x".into()).echo_format(), "\"x\"");
512        assert_eq!(vec3(1.0, 2.0, 3.0).echo_format(), "[1, 2, 3]");
513    }
514}