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