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