Skip to main content

kcl_lib/execution/
annotations.rs

1//! Data on available annotations.
2
3use std::fmt;
4use std::str::FromStr;
5
6use kittycad_modeling_cmds::coord::KITTYCAD;
7use kittycad_modeling_cmds::coord::OPENGL;
8use kittycad_modeling_cmds::coord::System;
9use kittycad_modeling_cmds::coord::VULKAN;
10use serde::Deserialize;
11use serde::Serialize;
12
13use crate::KclError;
14use crate::SourceRange;
15use crate::errors::KclErrorDetails;
16use crate::errors::Severity;
17use crate::parsing::ast::types::Annotation;
18use crate::parsing::ast::types::Expr;
19use crate::parsing::ast::types::LiteralValue;
20use crate::parsing::ast::types::Node;
21use crate::parsing::ast::types::ObjectProperty;
22
23/// Annotations which should cause re-execution if they change.
24pub(super) const SIGNIFICANT_ATTRS: [&str; 3] = [SETTINGS, NO_PRELUDE, WARNINGS];
25
26pub(crate) const SETTINGS: &str = "settings";
27pub(crate) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
28pub(crate) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
29pub(crate) const SETTINGS_VERSION: &str = "kclVersion";
30pub(crate) const SETTINGS_EXPERIMENTAL_FEATURES: &str = "experimentalFeatures";
31
32pub(super) const NO_PRELUDE: &str = "no_std";
33pub(crate) const DEPRECATED: &str = "deprecated";
34pub(crate) const DEPRECATED_SINCE: &str = "deprecated_since";
35pub(crate) const DOC_CATEGORY: &str = "doc_category";
36pub(crate) const EXPERIMENTAL: &str = "experimental";
37pub(crate) const INCLUDE_IN_FEATURE_TREE: &str = "feature_tree";
38
39pub(super) const IMPORT_FORMAT: &str = "format";
40pub(super) const IMPORT_COORDS: &str = "coords";
41pub(super) const IMPORT_COORDS_VALUES: [(&str, &System); 3] =
42    [("zoo", KITTYCAD), ("opengl", OPENGL), ("vulkan", VULKAN)];
43pub(super) const IMPORT_LENGTH_UNIT: &str = "lengthUnit";
44
45pub(crate) const IMPL: &str = "impl";
46pub(crate) const IMPL_RUST: &str = "std_rust";
47pub(crate) const IMPL_CONSTRAINT: &str = "std_rust_constraint";
48pub(crate) const IMPL_CONSTRAINABLE: &str = "std_constrainable";
49pub(crate) const IMPL_RUST_CONSTRAINABLE: &str = "std_rust_constrainable";
50pub(crate) const IMPL_KCL: &str = "kcl";
51pub(crate) const IMPL_PRIMITIVE: &str = "primitive";
52pub(super) const IMPL_VALUES: [&str; 6] = [
53    IMPL_RUST,
54    IMPL_KCL,
55    IMPL_PRIMITIVE,
56    IMPL_CONSTRAINT,
57    IMPL_CONSTRAINABLE,
58    IMPL_RUST_CONSTRAINABLE,
59];
60
61pub(crate) const WARNINGS: &str = "warnings";
62pub(crate) const WARN_ALLOW: &str = "allow";
63pub(crate) const WARN_DENY: &str = "deny";
64pub(crate) const WARN_WARN: &str = "warn";
65pub(super) const WARN_LEVELS: [&str; 3] = [WARN_ALLOW, WARN_DENY, WARN_WARN];
66pub(crate) const WARN_UNKNOWN_UNITS: &str = "unknownUnits";
67pub(crate) const WARN_ANGLE_UNITS: &str = "angleUnits";
68pub(crate) const WARN_UNKNOWN_ATTR: &str = "unknownAttribute";
69pub(crate) const WARN_MOD_RETURN_VALUE: &str = "moduleReturnValue";
70pub(crate) const WARN_DEPRECATED: &str = "deprecated";
71pub(crate) const WARN_IGNORED_Z_AXIS: &str = "ignoredZAxis";
72pub(crate) const WARN_SOLVER: &str = "solver";
73pub(crate) const WARN_SHOULD_BE_PERCENTAGE: &str = "shouldBePercentage";
74pub(crate) const WARN_INVALID_MATH: &str = "invalidMath";
75pub(crate) const WARN_CSG_NO_INTERSECTION: &str = "csgNoIntersection";
76pub(crate) const WARN_UNNECESSARY_CLOSE: &str = "unnecessaryClose";
77pub(crate) const WARN_UNUSED_TAGS: &str = "unusedTags";
78pub(crate) const WARN_NOT_YET_SUPPORTED: &str = "notYetSupported";
79pub(crate) const WARN_OVER_CONSTRAINED_SKETCH: &str = "overConstrainedSketch";
80pub(super) const WARN_VALUES: [&str; 13] = [
81    WARN_UNKNOWN_UNITS,
82    WARN_ANGLE_UNITS,
83    WARN_UNKNOWN_ATTR,
84    WARN_MOD_RETURN_VALUE,
85    WARN_DEPRECATED,
86    WARN_IGNORED_Z_AXIS,
87    WARN_SOLVER,
88    WARN_SHOULD_BE_PERCENTAGE,
89    WARN_INVALID_MATH,
90    WARN_UNNECESSARY_CLOSE,
91    WARN_NOT_YET_SUPPORTED,
92    WARN_CSG_NO_INTERSECTION,
93    WARN_OVER_CONSTRAINED_SKETCH,
94];
95
96#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, ts_rs::TS)]
97#[ts(export)]
98#[serde(tag = "type")]
99pub enum WarningLevel {
100    Allow,
101    Warn,
102    Deny,
103}
104
105impl WarningLevel {
106    pub(crate) fn severity(self) -> Option<Severity> {
107        match self {
108            WarningLevel::Allow => None,
109            WarningLevel::Warn => Some(Severity::Warning),
110            WarningLevel::Deny => Some(Severity::Error),
111        }
112    }
113
114    pub(crate) fn as_str(self) -> &'static str {
115        match self {
116            WarningLevel::Allow => WARN_ALLOW,
117            WarningLevel::Warn => WARN_WARN,
118            WarningLevel::Deny => WARN_DENY,
119        }
120    }
121}
122
123impl FromStr for WarningLevel {
124    type Err = ();
125
126    fn from_str(s: &str) -> Result<Self, Self::Err> {
127        match s {
128            WARN_ALLOW => Ok(Self::Allow),
129            WARN_WARN => Ok(Self::Warn),
130            WARN_DENY => Ok(Self::Deny),
131            _ => Err(()),
132        }
133    }
134}
135
136#[derive(Clone, Copy, Eq, PartialEq, Debug, Default)]
137pub enum Impl {
138    #[default]
139    Kcl,
140    KclConstrainable,
141    Rust,
142    RustConstrainable,
143    RustConstraint,
144    Primitive,
145}
146
147impl FromStr for Impl {
148    type Err = ();
149
150    fn from_str(s: &str) -> Result<Self, Self::Err> {
151        match s {
152            IMPL_RUST => Ok(Self::Rust),
153            IMPL_CONSTRAINT => Ok(Self::RustConstraint),
154            IMPL_CONSTRAINABLE => Ok(Self::KclConstrainable),
155            IMPL_RUST_CONSTRAINABLE => Ok(Self::RustConstrainable),
156            IMPL_KCL => Ok(Self::Kcl),
157            IMPL_PRIMITIVE => Ok(Self::Primitive),
158            _ => Err(()),
159        }
160    }
161}
162
163pub(crate) fn settings_completion_text() -> String {
164    format!("@{SETTINGS}({SETTINGS_UNIT_LENGTH} = mm, {SETTINGS_VERSION} = 1.0)")
165}
166
167pub(super) fn is_significant(attr: &&Node<Annotation>) -> bool {
168    match attr.name() {
169        Some(name) => SIGNIFICANT_ATTRS.contains(&name),
170        None => true,
171    }
172}
173
174pub(super) fn expect_properties<'a>(
175    for_key: &'static str,
176    annotation: &'a Node<Annotation>,
177) -> Result<&'a [Node<ObjectProperty>], KclError> {
178    assert_eq!(annotation.name().unwrap(), for_key);
179    Ok(&**annotation.properties.as_ref().ok_or_else(|| {
180        KclError::new_semantic(KclErrorDetails::new(
181            format!("Empty `{for_key}` annotation"),
182            vec![annotation.as_source_range()],
183        ))
184    })?)
185}
186
187pub(super) fn expect_ident(expr: &Expr) -> Result<&str, KclError> {
188    if let Expr::Name(name) = expr
189        && let Some(name) = name.local_ident()
190    {
191        return Ok(*name);
192    }
193
194    Err(KclError::new_semantic(KclErrorDetails::new(
195        "Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(),
196        vec![expr.into()],
197    )))
198}
199
200pub(super) fn many_of(
201    expr: &Expr,
202    of: &[&'static str],
203    source_range: SourceRange,
204) -> Result<Vec<&'static str>, KclError> {
205    const UNEXPECTED_MSG: &str = "Unexpected warnings value, expected a name or array of names, e.g., `unknownUnits` or `[unknownUnits, deprecated]`";
206
207    let values = match expr {
208        Expr::Name(name) => {
209            if let Some(name) = name.local_ident() {
210                vec![*name]
211            } else {
212                return Err(KclError::new_semantic(KclErrorDetails::new(
213                    UNEXPECTED_MSG.to_owned(),
214                    vec![expr.into()],
215                )));
216            }
217        }
218        Expr::ArrayExpression(e) => {
219            let mut result = Vec::new();
220            for e in &e.elements {
221                if let Expr::Name(name) = e
222                    && let Some(name) = name.local_ident()
223                {
224                    result.push(*name);
225                    continue;
226                }
227                return Err(KclError::new_semantic(KclErrorDetails::new(
228                    UNEXPECTED_MSG.to_owned(),
229                    vec![e.into()],
230                )));
231            }
232            result
233        }
234        _ => {
235            return Err(KclError::new_semantic(KclErrorDetails::new(
236                UNEXPECTED_MSG.to_owned(),
237                vec![expr.into()],
238            )));
239        }
240    };
241
242    values
243        .into_iter()
244        .map(|v| {
245            of.iter()
246                .find(|vv| **vv == v)
247                .ok_or_else(|| {
248                    KclError::new_semantic(KclErrorDetails::new(
249                        format!("Unexpected warning value: `{v}`; accepted values: {}", of.join(", "),),
250                        vec![source_range],
251                    ))
252                })
253                .copied()
254        })
255        .collect::<Result<Vec<&str>, KclError>>()
256}
257
258// Returns the unparsed number literal.
259pub(super) fn expect_number(expr: &Expr) -> Result<String, KclError> {
260    if let Expr::Literal(lit) = expr
261        && let LiteralValue::Number { .. } = &lit.value
262    {
263        return Ok(lit.raw.clone());
264    }
265
266    Err(KclError::new_semantic(KclErrorDetails::new(
267        "Unexpected settings value, expected a number, e.g., `1.0`".to_owned(),
268        vec![expr.into()],
269    )))
270}
271
272#[derive(Debug, Clone, Eq, PartialEq)]
273pub struct FnAttrs {
274    pub impl_: Impl,
275    pub deprecated: bool,
276    /// Constraint marking a KCL version at or after which this item is
277    /// deprecated, e.g. "2.0".
278    pub deprecated_since: Option<VersionConstraint>,
279    pub experimental: bool,
280    pub include_in_feature_tree: bool,
281}
282
283impl Default for FnAttrs {
284    fn default() -> Self {
285        Self {
286            impl_: Impl::default(),
287            deprecated: false,
288            deprecated_since: None,
289            experimental: false,
290            include_in_feature_tree: true,
291        }
292    }
293}
294
295/// A constraint on a KCL version, e.g. the threshold that `@(deprecated_since =
296/// "2.0")` describes. Stored as the parsed component list so comparisons are
297/// numeric, not lexical.
298///
299/// Distinct from the concrete `kclVersion` set in `@settings(...)`: this type
300/// represents a version *boundary*, and we expect to grow more constraint kinds
301/// (e.g., "deprecated up until version X") in the future. Comparisons against a
302/// concrete version are expressed via the free `version_*` functions below
303/// rather than `Ord` so the direction of comparison stays explicit at every
304/// call site.
305#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
306pub struct VersionConstraint(Vec<u32>);
307
308impl VersionConstraint {
309    /// Parse a dotted version string like "1.0" or "2.1.3". Returns `None` for empty
310    /// input or any component that doesn't parse as a non-negative integer.
311    pub fn parse(s: &str) -> Option<Self> {
312        let parts: Vec<u32> = s
313            .split('.')
314            .map(|p| p.parse::<u32>().ok())
315            .collect::<Option<Vec<_>>>()?;
316        if parts.is_empty() { None } else { Some(Self(parts)) }
317    }
318}
319
320impl fmt::Display for VersionConstraint {
321    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322        let mut first = true;
323        for n in &self.0 {
324            if !first {
325                f.write_str(".")?;
326            }
327            write!(f, "{n}")?;
328            first = false;
329        }
330        Ok(())
331    }
332}
333
334/// Returns true when the concrete `version` (e.g., from `@settings(kclVersion = ...)`)
335/// is greater than or equal to the `constraint`. Returns false if `version` cannot be
336/// parsed as a dotted integer version.
337pub(crate) fn version_ge(version: &str, constraint: &VersionConstraint) -> bool {
338    let Some(parsed): Option<Vec<u32>> = version.split('.').map(|p| p.parse::<u32>().ok()).collect() else {
339        return false;
340    };
341    parsed >= constraint.0
342}
343
344pub(super) fn get_fn_attrs(
345    annotations: &[Node<Annotation>],
346    source_range: SourceRange,
347) -> Result<Option<FnAttrs>, KclError> {
348    let mut found_attrs = false;
349    let mut fn_attrs = FnAttrs::default();
350    for attr in annotations {
351        if attr.name.is_some() || attr.properties.is_none() {
352            continue;
353        }
354        for p in attr.properties.as_ref().unwrap() {
355            if &*p.key.name == IMPL
356                && let Some(s) = p.value.ident_name()
357            {
358                found_attrs = true;
359                fn_attrs.impl_ = Impl::from_str(s).map_err(|_| {
360                    KclError::new_semantic(KclErrorDetails::new(
361                        format!(
362                            "Invalid value for {} attribute, expected one of: {}",
363                            IMPL,
364                            IMPL_VALUES.join(", ")
365                        ),
366                        vec![source_range],
367                    ))
368                })?;
369                continue;
370            }
371
372            if &*p.key.name == DEPRECATED
373                && let Some(b) = p.value.literal_bool()
374            {
375                found_attrs = true;
376                fn_attrs.deprecated = b;
377                continue;
378            }
379
380            if &*p.key.name == DEPRECATED_SINCE {
381                let Some(s) = p.value.literal_str() else {
382                    return Err(KclError::new_semantic(KclErrorDetails::new(
383                        format!("Expected a version string for {DEPRECATED_SINCE}, e.g., \"2.0\""),
384                        vec![source_range],
385                    )));
386                };
387                let Some(constraint) = VersionConstraint::parse(s) else {
388                    return Err(KclError::new_semantic(KclErrorDetails::new(
389                        format!(
390                            "Invalid version string for {DEPRECATED_SINCE}: `{s}`; expected a dotted integer version, e.g., \"2.0\""
391                        ),
392                        vec![source_range],
393                    )));
394                };
395                found_attrs = true;
396                fn_attrs.deprecated_since = Some(constraint);
397                continue;
398            }
399
400            // doc_category is handled by the docs generator, not execution.
401            if &*p.key.name == DOC_CATEGORY {
402                continue;
403            }
404
405            if &*p.key.name == EXPERIMENTAL
406                && let Some(b) = p.value.literal_bool()
407            {
408                found_attrs = true;
409                fn_attrs.experimental = b;
410                continue;
411            }
412
413            if &*p.key.name == INCLUDE_IN_FEATURE_TREE
414                && let Some(b) = p.value.literal_bool()
415            {
416                found_attrs = true;
417                fn_attrs.include_in_feature_tree = b;
418                continue;
419            }
420
421            return Err(KclError::new_semantic(KclErrorDetails::new(
422                format!(
423                    "Invalid attribute, expected one of: {IMPL}, {DEPRECATED}, {DEPRECATED_SINCE}, {DOC_CATEGORY}, {EXPERIMENTAL}, {INCLUDE_IN_FEATURE_TREE}, found `{}`",
424                    &*p.key.name,
425                ),
426                vec![source_range],
427            )));
428        }
429    }
430
431    Ok(if found_attrs { Some(fn_attrs) } else { None })
432}
433
434#[cfg(test)]
435mod tests {
436    use super::*;
437
438    fn vc(s: &str) -> VersionConstraint {
439        VersionConstraint::parse(s).unwrap()
440    }
441
442    #[test]
443    fn version_constraint_parse_handles_typical_inputs() {
444        assert_eq!(VersionConstraint::parse("1.0"), Some(VersionConstraint(vec![1, 0])));
445        assert_eq!(VersionConstraint::parse("2"), Some(VersionConstraint(vec![2])));
446        assert_eq!(
447            VersionConstraint::parse("2.1.3"),
448            Some(VersionConstraint(vec![2, 1, 3]))
449        );
450        assert_eq!(VersionConstraint::parse(""), None);
451        assert_eq!(VersionConstraint::parse("1.x"), None);
452        assert_eq!(VersionConstraint::parse("1.-1"), None);
453    }
454
455    #[test]
456    fn version_constraint_display_round_trips() {
457        assert_eq!(vc("1.0").to_string(), "1.0");
458        assert_eq!(vc("2.1.3").to_string(), "2.1.3");
459        assert_eq!(vc("2").to_string(), "2");
460    }
461
462    #[test]
463    fn version_ge_compares_components_numerically() {
464        assert!(version_ge("1.0", &vc("1.0")));
465        assert!(version_ge("2.0", &vc("1.0")));
466        assert!(version_ge("2.0", &vc("2.0")));
467        assert!(version_ge("10.0", &vc("2.0")));
468        assert!(version_ge("2.1", &vc("2.0")));
469        assert!(!version_ge("1.0", &vc("2.0")));
470        assert!(!version_ge("2.0", &vc("2.1")));
471        assert!(!version_ge("1.99", &vc("2.0")));
472        // An unparsable concrete version never satisfies the constraint.
473        assert!(!version_ge("bogus", &vc("1.0")));
474    }
475}