1use 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
23pub(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(crate) const WARN_REGION_LIVENESS: &str = "regionLiveness";
81pub(super) const WARN_VALUES: [&str; 14] = [
82 WARN_UNKNOWN_UNITS,
83 WARN_ANGLE_UNITS,
84 WARN_UNKNOWN_ATTR,
85 WARN_MOD_RETURN_VALUE,
86 WARN_DEPRECATED,
87 WARN_IGNORED_Z_AXIS,
88 WARN_SOLVER,
89 WARN_SHOULD_BE_PERCENTAGE,
90 WARN_INVALID_MATH,
91 WARN_UNNECESSARY_CLOSE,
92 WARN_NOT_YET_SUPPORTED,
93 WARN_CSG_NO_INTERSECTION,
94 WARN_OVER_CONSTRAINED_SKETCH,
95 WARN_REGION_LIVENESS,
96];
97
98#[derive(Clone, Copy, Eq, PartialEq, Debug, Deserialize, Serialize, ts_rs::TS)]
99#[ts(export)]
100#[serde(tag = "type")]
101pub enum WarningLevel {
102 Allow,
103 Warn,
104 Deny,
105}
106
107impl WarningLevel {
108 pub(crate) fn severity(self) -> Option<Severity> {
109 match self {
110 WarningLevel::Allow => None,
111 WarningLevel::Warn => Some(Severity::Warning),
112 WarningLevel::Deny => Some(Severity::Error),
113 }
114 }
115
116 pub(crate) fn as_str(self) -> &'static str {
117 match self {
118 WarningLevel::Allow => WARN_ALLOW,
119 WarningLevel::Warn => WARN_WARN,
120 WarningLevel::Deny => WARN_DENY,
121 }
122 }
123}
124
125impl FromStr for WarningLevel {
126 type Err = ();
127
128 fn from_str(s: &str) -> Result<Self, Self::Err> {
129 match s {
130 WARN_ALLOW => Ok(Self::Allow),
131 WARN_WARN => Ok(Self::Warn),
132 WARN_DENY => Ok(Self::Deny),
133 _ => Err(()),
134 }
135 }
136}
137
138#[derive(Clone, Copy, Eq, PartialEq, Debug, Default)]
139pub enum Impl {
140 #[default]
141 Kcl,
142 KclConstrainable,
143 Rust,
144 RustConstrainable,
145 RustConstraint,
146 Primitive,
147}
148
149impl FromStr for Impl {
150 type Err = ();
151
152 fn from_str(s: &str) -> Result<Self, Self::Err> {
153 match s {
154 IMPL_RUST => Ok(Self::Rust),
155 IMPL_CONSTRAINT => Ok(Self::RustConstraint),
156 IMPL_CONSTRAINABLE => Ok(Self::KclConstrainable),
157 IMPL_RUST_CONSTRAINABLE => Ok(Self::RustConstrainable),
158 IMPL_KCL => Ok(Self::Kcl),
159 IMPL_PRIMITIVE => Ok(Self::Primitive),
160 _ => Err(()),
161 }
162 }
163}
164
165pub(crate) fn settings_completion_text() -> String {
166 format!("@{SETTINGS}({SETTINGS_UNIT_LENGTH} = mm, {SETTINGS_VERSION} = 1.0)")
167}
168
169pub(super) fn is_significant(attr: &&Node<Annotation>) -> bool {
170 match attr.name() {
171 Some(name) => SIGNIFICANT_ATTRS.contains(&name),
172 None => true,
173 }
174}
175
176pub(super) fn expect_properties<'a>(
177 for_key: &'static str,
178 annotation: &'a Node<Annotation>,
179) -> Result<&'a [Node<ObjectProperty>], KclError> {
180 assert_eq!(annotation.name().unwrap(), for_key);
181 Ok(&**annotation.properties.as_ref().ok_or_else(|| {
182 KclError::new_semantic(KclErrorDetails::new(
183 format!("Empty `{for_key}` annotation"),
184 vec![annotation.as_source_range()],
185 ))
186 })?)
187}
188
189pub(super) fn expect_ident(expr: &Expr) -> Result<&str, KclError> {
190 if let Expr::Name(name) = expr
191 && let Some(name) = name.local_ident()
192 {
193 return Ok(*name);
194 }
195
196 Err(KclError::new_semantic(KclErrorDetails::new(
197 "Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(),
198 vec![expr.into()],
199 )))
200}
201
202pub(super) fn many_of(
203 expr: &Expr,
204 of: &[&'static str],
205 source_range: SourceRange,
206) -> Result<Vec<&'static str>, KclError> {
207 const UNEXPECTED_MSG: &str = "Unexpected warnings value, expected a name or array of names, e.g., `unknownUnits` or `[unknownUnits, deprecated]`";
208
209 let values = match expr {
210 Expr::Name(name) => {
211 if let Some(name) = name.local_ident() {
212 vec![*name]
213 } else {
214 return Err(KclError::new_semantic(KclErrorDetails::new(
215 UNEXPECTED_MSG.to_owned(),
216 vec![expr.into()],
217 )));
218 }
219 }
220 Expr::ArrayExpression(e) => {
221 let mut result = Vec::new();
222 for e in &e.elements {
223 if let Expr::Name(name) = e
224 && let Some(name) = name.local_ident()
225 {
226 result.push(*name);
227 continue;
228 }
229 return Err(KclError::new_semantic(KclErrorDetails::new(
230 UNEXPECTED_MSG.to_owned(),
231 vec![e.into()],
232 )));
233 }
234 result
235 }
236 _ => {
237 return Err(KclError::new_semantic(KclErrorDetails::new(
238 UNEXPECTED_MSG.to_owned(),
239 vec![expr.into()],
240 )));
241 }
242 };
243
244 values
245 .into_iter()
246 .map(|v| {
247 of.iter()
248 .find(|vv| **vv == v)
249 .ok_or_else(|| {
250 KclError::new_semantic(KclErrorDetails::new(
251 format!("Unexpected warning value: `{v}`; accepted values: {}", of.join(", "),),
252 vec![source_range],
253 ))
254 })
255 .copied()
256 })
257 .collect::<Result<Vec<&str>, KclError>>()
258}
259
260pub(super) fn expect_number(expr: &Expr) -> Result<String, KclError> {
262 if let Expr::Literal(lit) = expr
263 && let LiteralValue::Number { .. } = &lit.value
264 {
265 return Ok(lit.raw.clone());
266 }
267
268 Err(KclError::new_semantic(KclErrorDetails::new(
269 "Unexpected settings value, expected a number, e.g., `1.0`".to_owned(),
270 vec![expr.into()],
271 )))
272}
273
274#[derive(Debug, Clone, Eq, PartialEq)]
275pub struct FnAttrs {
276 pub impl_: Impl,
277 pub deprecated: bool,
278 pub deprecated_since: Option<VersionConstraint>,
281 pub experimental: bool,
282 pub include_in_feature_tree: bool,
283}
284
285impl Default for FnAttrs {
286 fn default() -> Self {
287 Self {
288 impl_: Impl::default(),
289 deprecated: false,
290 deprecated_since: None,
291 experimental: false,
292 include_in_feature_tree: true,
293 }
294 }
295}
296
297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
308pub struct VersionConstraint(Vec<u32>);
309
310impl VersionConstraint {
311 pub fn parse(s: &str) -> Option<Self> {
314 let parts: Vec<u32> = s
315 .split('.')
316 .map(|p| p.parse::<u32>().ok())
317 .collect::<Option<Vec<_>>>()?;
318 if parts.is_empty() { None } else { Some(Self(parts)) }
319 }
320}
321
322impl fmt::Display for VersionConstraint {
323 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324 let mut first = true;
325 for n in &self.0 {
326 if !first {
327 f.write_str(".")?;
328 }
329 write!(f, "{n}")?;
330 first = false;
331 }
332 Ok(())
333 }
334}
335
336pub(crate) fn version_ge(version: &str, constraint: &VersionConstraint) -> bool {
340 let Some(parsed): Option<Vec<u32>> = version.split('.').map(|p| p.parse::<u32>().ok()).collect() else {
341 return false;
342 };
343 parsed >= constraint.0
344}
345
346pub(super) fn get_fn_attrs(
347 annotations: &[Node<Annotation>],
348 source_range: SourceRange,
349) -> Result<Option<FnAttrs>, KclError> {
350 let mut found_attrs = false;
351 let mut fn_attrs = FnAttrs::default();
352 for attr in annotations {
353 if attr.name.is_some() || attr.properties.is_none() {
354 continue;
355 }
356 for p in attr.properties.as_ref().unwrap() {
357 if &*p.key.name == IMPL
358 && let Some(s) = p.value.ident_name()
359 {
360 found_attrs = true;
361 fn_attrs.impl_ = Impl::from_str(s).map_err(|_| {
362 KclError::new_semantic(KclErrorDetails::new(
363 format!(
364 "Invalid value for {} attribute, expected one of: {}",
365 IMPL,
366 IMPL_VALUES.join(", ")
367 ),
368 vec![source_range],
369 ))
370 })?;
371 continue;
372 }
373
374 if &*p.key.name == DEPRECATED
375 && let Some(b) = p.value.literal_bool()
376 {
377 found_attrs = true;
378 fn_attrs.deprecated = b;
379 continue;
380 }
381
382 if &*p.key.name == DEPRECATED_SINCE {
383 let Some(s) = p.value.literal_str() else {
384 return Err(KclError::new_semantic(KclErrorDetails::new(
385 format!("Expected a version string for {DEPRECATED_SINCE}, e.g., \"2.0\""),
386 vec![source_range],
387 )));
388 };
389 let Some(constraint) = VersionConstraint::parse(s) else {
390 return Err(KclError::new_semantic(KclErrorDetails::new(
391 format!(
392 "Invalid version string for {DEPRECATED_SINCE}: `{s}`; expected a dotted integer version, e.g., \"2.0\""
393 ),
394 vec![source_range],
395 )));
396 };
397 found_attrs = true;
398 fn_attrs.deprecated_since = Some(constraint);
399 continue;
400 }
401
402 if &*p.key.name == DOC_CATEGORY {
404 continue;
405 }
406
407 if &*p.key.name == EXPERIMENTAL
408 && let Some(b) = p.value.literal_bool()
409 {
410 found_attrs = true;
411 fn_attrs.experimental = b;
412 continue;
413 }
414
415 if &*p.key.name == INCLUDE_IN_FEATURE_TREE
416 && let Some(b) = p.value.literal_bool()
417 {
418 found_attrs = true;
419 fn_attrs.include_in_feature_tree = b;
420 continue;
421 }
422
423 return Err(KclError::new_semantic(KclErrorDetails::new(
424 format!(
425 "Invalid attribute, expected one of: {IMPL}, {DEPRECATED}, {DEPRECATED_SINCE}, {DOC_CATEGORY}, {EXPERIMENTAL}, {INCLUDE_IN_FEATURE_TREE}, found `{}`",
426 &*p.key.name,
427 ),
428 vec![source_range],
429 )));
430 }
431 }
432
433 Ok(if found_attrs { Some(fn_attrs) } else { None })
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 fn vc(s: &str) -> VersionConstraint {
441 VersionConstraint::parse(s).unwrap()
442 }
443
444 #[test]
445 fn version_constraint_parse_handles_typical_inputs() {
446 assert_eq!(VersionConstraint::parse("1.0"), Some(VersionConstraint(vec![1, 0])));
447 assert_eq!(VersionConstraint::parse("2"), Some(VersionConstraint(vec![2])));
448 assert_eq!(
449 VersionConstraint::parse("2.1.3"),
450 Some(VersionConstraint(vec![2, 1, 3]))
451 );
452 assert_eq!(VersionConstraint::parse(""), None);
453 assert_eq!(VersionConstraint::parse("1.x"), None);
454 assert_eq!(VersionConstraint::parse("1.-1"), None);
455 }
456
457 #[test]
458 fn version_constraint_display_round_trips() {
459 assert_eq!(vc("1.0").to_string(), "1.0");
460 assert_eq!(vc("2.1.3").to_string(), "2.1.3");
461 assert_eq!(vc("2").to_string(), "2");
462 }
463
464 #[test]
465 fn version_ge_compares_components_numerically() {
466 assert!(version_ge("1.0", &vc("1.0")));
467 assert!(version_ge("2.0", &vc("1.0")));
468 assert!(version_ge("2.0", &vc("2.0")));
469 assert!(version_ge("10.0", &vc("2.0")));
470 assert!(version_ge("2.1", &vc("2.0")));
471 assert!(!version_ge("1.0", &vc("2.0")));
472 assert!(!version_ge("2.0", &vc("2.1")));
473 assert!(!version_ge("1.99", &vc("2.0")));
474 assert!(!version_ge("bogus", &vc("1.0")));
476 }
477}