Skip to main content

kcl_lib/
lib.rs

1//! Rust support for KCL (aka the KittyCAD Language).
2//!
3//! KCL is written in Rust. This crate contains the compiler tooling (e.g. parser, lexer, code generation),
4//! the standard library implementation, generator for the docs, and more.
5#![recursion_limit = "1024"]
6#![allow(clippy::boxed_local)]
7
8#[allow(unused_macros)]
9macro_rules! println {
10    ($($rest:tt)*) => {
11        #[cfg(all(feature = "disable-println", not(test)))]
12        {
13            let _ = format!($($rest)*);
14        }
15        #[cfg(any(not(feature = "disable-println"), test))]
16        std::println!($($rest)*)
17    }
18}
19
20#[allow(unused_macros)]
21macro_rules! eprintln {
22    ($($rest:tt)*) => {
23        #[cfg(all(feature = "disable-println", not(test)))]
24        {
25            let _ = format!($($rest)*);
26        }
27        #[cfg(any(not(feature = "disable-println"), test))]
28        std::eprintln!($($rest)*)
29    }
30}
31
32#[allow(unused_macros)]
33macro_rules! print {
34    ($($rest:tt)*) => {
35        #[cfg(all(feature = "disable-println", not(test)))]
36        {
37            let _ = format!($($rest)*);
38        }
39        #[cfg(any(not(feature = "disable-println"), test))]
40        std::print!($($rest)*)
41    }
42}
43
44#[allow(unused_macros)]
45macro_rules! eprint {
46    ($($rest:tt)*) => {
47        #[cfg(all(feature = "disable-println", not(test)))]
48        {
49            let _ = format!($($rest)*);
50        }
51        #[cfg(any(not(feature = "disable-println"), test))]
52        std::eprint!($($rest)*)
53    }
54}
55#[cfg(feature = "dhat-heap")]
56#[global_allocator]
57static ALLOC: dhat::Alloc = dhat::Alloc;
58
59pub mod collections;
60mod docs;
61mod engine;
62mod errors;
63mod execution;
64mod fmt;
65mod frontend;
66mod fs;
67pub(crate) mod id;
68mod import_format;
69pub mod lint;
70mod log;
71mod lsp_types;
72mod modules;
73mod parsing;
74mod project;
75mod runtime_flags;
76mod settings;
77#[cfg(test)]
78mod simulation_tests;
79pub mod std;
80#[cfg(not(target_arch = "wasm32"))]
81pub mod test_server;
82mod thread;
83#[doc(hidden)]
84pub mod tooling;
85pub mod unit_conversion;
86mod unparser;
87mod util;
88#[cfg(test)]
89mod variant_name;
90pub mod walk;
91#[cfg(target_arch = "wasm32")]
92mod wasm;
93
94/// Internal compiler APIs consumed by the standalone language-server crate.
95#[doc(hidden)]
96pub mod lsp_support {
97    pub mod docs {
98        pub mod kcl_doc {
99            pub use crate::docs::kcl_doc::ArgData;
100            pub use crate::docs::kcl_doc::DocData;
101            pub use crate::docs::kcl_doc::ModData;
102            pub use crate::docs::kcl_doc::walk_stdlib;
103        }
104    }
105
106    pub mod engine {
107        #[cfg(not(target_arch = "wasm32"))]
108        pub use crate::engine::new_zoo_client;
109    }
110
111    pub mod errors {
112        pub use crate::errors::Severity;
113        pub use crate::errors::Suggestion;
114    }
115
116    pub mod execution {
117        pub use crate::execution::ExecutorContext;
118        pub use crate::execution::MockConfig;
119
120        pub mod cache {
121            pub use crate::execution::cache::read_old_memory_var;
122        }
123
124        pub mod typed_path {
125            pub use crate::execution::typed_path::TypedPath;
126        }
127    }
128
129    pub mod fs {
130        pub use crate::fs::FileManager;
131        pub use crate::fs::FileSystemHandle;
132        pub use crate::fs::new_file_system_handle;
133
134        #[cfg(all(target_arch = "wasm32", not(test)))]
135        pub mod wasm {
136            pub use crate::fs::wasm::FileSystemManager;
137        }
138    }
139
140    pub mod parsing {
141        pub use crate::parsing::PIPE_OPERATOR;
142        pub use crate::parsing::parse_str;
143        pub use crate::parsing::parse_tokens;
144
145        pub mod ast {
146            pub mod types {
147                pub use crate::parsing::ast::types::*;
148            }
149        }
150
151        pub mod token {
152            pub use crate::parsing::token::LexerMode;
153            pub use crate::parsing::token::RESERVED_WORDS;
154            pub use crate::parsing::token::TokenStream;
155            pub use crate::parsing::token::lex;
156
157            pub mod adapter {
158                pub use crate::parsing::token::adapter::lex_with_diagnostics;
159            }
160        }
161    }
162}
163
164pub use engine::AsyncTasks;
165pub use engine::EngineBatchContext;
166pub use engine::EngineStats;
167pub use errors::BacktraceItem;
168pub use errors::BacktraceItemKind;
169pub use errors::CompilationIssue;
170pub use errors::CompilationIssueReport;
171pub use errors::ConnectionError;
172pub use errors::ExecError;
173pub use errors::IsRetryable;
174pub use errors::KclError;
175pub use errors::KclErrorWithOutputs;
176pub use errors::Report;
177pub use errors::ReportWithOutputs;
178pub use errors::render_compilation_issue_miette;
179pub use execution::ConstraintKind;
180pub use execution::EdgeRefactorMeta;
181pub use execution::EnvironmentRef;
182pub use execution::ExecOutcome;
183pub use execution::ExecState;
184pub use execution::ExecutionCallbacks;
185pub use execution::ExecutorContext;
186pub use execution::ExecutorSettings;
187pub use execution::KclValueView;
188pub use execution::KclVersion;
189pub use execution::LegacyAngleRefactorMeta;
190pub use execution::MetaSettings;
191pub use execution::MockConfig;
192pub use execution::OperationCallbackArgs;
193pub use execution::Point2d;
194pub use execution::RefactorMetadata;
195pub use execution::SegmentDragAnchor;
196pub use execution::SketchConstraintReport;
197pub use execution::SketchConstraintStatus;
198pub use execution::bust_cache;
199pub use execution::clear_mem_cache;
200pub use execution::typed_path::TypedPath;
201pub use fs::FileSystem;
202pub use fs::FileSystemHandle;
203pub use fs::in_memory::InMemoryFiles;
204pub use fs::new_file_system_handle;
205pub use kcl_error;
206pub use kcl_error::SourceRange;
207pub use lsp_types::IntoDiagnostic;
208pub use lsp_types::LspSuggestion;
209pub use lsp_types::ToLspRange;
210pub use modules::ModuleId;
211pub use parsing::ast::types::FormatOptions;
212pub use parsing::ast::types::NodePath;
213pub use parsing::ast::types::NodePathExt;
214pub use parsing::ast::types::Program as AstProgram;
215pub use parsing::ast::types::Step as NodePathStep;
216pub use project::ProjectManager;
217pub use runtime_flags::KclRuntimeFlags;
218pub use runtime_flags::RuntimeFlag;
219pub use runtime_flags::kcl_runtime_flags;
220pub use runtime_flags::set_kcl_runtime_flags;
221pub use settings::types::Configuration;
222pub use settings::types::project::ProjectConfiguration;
223#[cfg(not(target_arch = "wasm32"))]
224pub use unparser::recast_dir;
225#[cfg(not(target_arch = "wasm32"))]
226pub use unparser::walk_dir;
227
228pub mod engine_connection {
229    pub use crate::engine::engine_manager::EngineManager;
230    pub use crate::engine::engine_manager::EngineTransport;
231    pub use crate::engine::engine_manager::ResponseInformation;
232    pub use crate::engine::engine_manager::SocketHealth;
233    pub use crate::engine::engine_manager::TransportCloseError;
234}
235
236// Rather than make executor public and make lots of it pub(crate), just re-export into a new module.
237// Ideally we wouldn't export these things at all, they should only be used for testing.
238pub mod exec {
239    pub use kcl_api::NumericType;
240    pub use kcl_api::UnitAngle;
241    pub use kcl_api::UnitLength;
242    pub use kcl_api::UnitType;
243
244    pub use crate::execution::ArtifactCommand;
245    pub use crate::execution::DefaultPlanes;
246    pub use crate::execution::IdGenerator;
247    pub use crate::execution::KclObjectKind;
248    pub use crate::execution::KclValue;
249    pub use crate::execution::KclValueView;
250    pub use crate::execution::Operation;
251    pub use crate::execution::PlaneKind;
252    pub use crate::execution::Sketch;
253    pub use crate::execution::annotations::WarningLevel;
254    pub use crate::util::RetryConfig;
255    pub use crate::util::execute_with_retries;
256}
257
258#[cfg(target_arch = "wasm32")]
259pub mod wasm_engine {
260    pub use crate::engine::conn_wasm::EngineCommandManager;
261    pub use crate::engine::conn_wasm::EngineConnection;
262    pub use crate::engine::conn_wasm::ResponseContext;
263    pub use crate::fs::wasm::FileManager;
264    pub use crate::fs::wasm::FileSystemManager;
265}
266
267pub mod std_utils {
268    pub use crate::std::utils::TangentialArcInfoInput;
269    pub use crate::std::utils::get_tangential_arc_to_info;
270    pub use crate::std::utils::is_points_ccw_wasm;
271    pub use crate::std::utils::untyped_point_to_unit;
272}
273
274pub mod pretty {
275    pub use crate::fmt::format_number_literal;
276    pub use crate::fmt::format_number_value;
277    pub use crate::fmt::human_display_number;
278    pub use crate::parsing::token::NumericSuffix;
279}
280
281pub mod front {
282    pub use crate::frontend::MAX_SKETCH_CHECKPOINTS;
283    pub(crate) use crate::frontend::modify::find_defined_names;
284    pub(crate) use crate::frontend::modify::next_free_name_using_max;
285    pub use crate::frontend::sketch::ExecResult;
286    pub use crate::frontend::{
287        EditConstraintOptions,
288        EditDistanceConstraintLabelPositionOptions,
289        EditSegmentsOptions,
290        FrontendState,
291        SetProgramOutcome,
292        api::{
293            Cap, CapKind, EditSketchOutcome, Error, Expr, Face, File, FileId, LifecycleApi, NewSketchOutcome, Number,
294            Object, ObjectId, ObjectKind, Plane, ProjectId, RestoreSketchCheckpointOutcome, Result, SceneGraph,
295            SceneGraphDelta, Settings, SketchCheckpointId, SketchMutationOutcome, SourceDelta, SourceRef, Version,
296            Wall,
297        },
298        sketch::{
299            Angle, Arc, ArcCtor, ArcDirection, Circle, CircleCtor, Coincident, Constraint, ConstraintLabelPositionEdit,
300            ControlPointSpline, ControlPointSplineCtor, Distance, EqualRadius, ExistingSegmentCtor, Fixed, FixedPoint,
301            Freedom, Horizontal, Line, LineCtor, LinesEqualLength, Midpoint, NewSegmentInfo, Parallel, Perpendicular,
302            Point, Point2d, PointCtor, Segment, SegmentCtor, Sketch, SketchApi, SketchCtor, StartOrEnd, Symmetric,
303            Tangent, Vertical,
304        },
305        // Re-export trim module items
306        trim::{
307            ArcPoint, AttachToEndpoint, CoincidentData, ConstraintToMigrate, Coords2d, EndpointChanged, LineEndpoint,
308            TrimDirection, TrimItem, TrimOperation, TrimTermination, TrimTerminations, execute_trim_loop_with_context,
309            get_next_trim_spawn, get_position_coords_for_line, get_position_coords_from_arc, is_point_on_line_segment,
310            line_segment_intersection, perpendicular_distance_to_segment, project_point_onto_arc,
311            project_point_onto_segment,
312        },
313    };
314}
315
316use serde::Deserialize;
317use serde::Serialize;
318
319use crate::exec::WarningLevel;
320#[allow(unused_imports)]
321use crate::log::log;
322#[allow(unused_imports)]
323use crate::log::logln;
324
325lazy_static::lazy_static! {
326
327    pub static ref IMPORT_FILE_EXTENSIONS: Vec<String> = {
328        import_format::IMPORT_FILE_EXTENSION_FORMATS
329            .iter()
330            .map(|(extension, _)| (*extension).to_owned())
331            .collect()
332    };
333
334    pub static ref RELEVANT_FILE_EXTENSIONS: Vec<String> = {
335        let mut relevant_extensions = IMPORT_FILE_EXTENSIONS.clone();
336        relevant_extensions.push("kcl".to_string());
337        relevant_extensions.push("md".to_string());
338        relevant_extensions
339    };
340}
341
342#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
343pub struct Program {
344    #[serde(flatten)]
345    pub ast: parsing::ast::types::Node<parsing::ast::types::Program>,
346    // The ui doesn't need to know about this.
347    // It's purely used for saving the contents of the original file, so we can use it for errors.
348    // Because in the case of the root file, we don't want to read the file from disk again.
349    #[serde(skip)]
350    pub original_file_contents: String,
351}
352
353impl Program {
354    pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationIssue>), KclError> {
355        let module_id = ModuleId::default();
356        let (ast, errs) = parsing::parse_str(input, module_id).0?;
357
358        Ok((
359            ast.map(|ast| Program {
360                ast,
361                original_file_contents: input.to_string(),
362            }),
363            errs,
364        ))
365    }
366
367    pub fn parse_no_errs(input: &str) -> Result<Program, KclError> {
368        let module_id = ModuleId::default();
369        let ast = parsing::parse_str(input, module_id).parse_errs_as_err()?;
370
371        Ok(Program {
372            ast,
373            original_file_contents: input.to_string(),
374        })
375    }
376
377    pub fn compute_digest(&mut self) -> parsing::ast::digest::Digest {
378        self.ast.compute_digest()
379    }
380
381    /// Get the meta settings for the kcl file from the annotations.
382    pub fn meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
383        self.ast.meta_settings()
384    }
385
386    /// Resolve the entry point's language version before connecting or executing.
387    /// Missing settings use the legacy version; invalid settings are errors.
388    pub fn language_version(&self) -> Result<KclVersion, KclError> {
389        Ok(self.meta_settings()?.unwrap_or_default().kcl_version)
390    }
391
392    /// Change the meta settings for the kcl file.
393    pub fn change_default_units(
394        &self,
395        length_units: Option<kittycad_modeling_cmds::units::UnitLength>,
396    ) -> Result<Self, KclError> {
397        Ok(Self {
398            ast: self.ast.change_default_units(length_units)?,
399            original_file_contents: self.original_file_contents.clone(),
400        })
401    }
402
403    pub fn change_kcl_version(&self, kcl_version: Option<String>) -> Result<Self, KclError> {
404        Ok(Self {
405            ast: self.ast.change_kcl_version(kcl_version)?,
406            original_file_contents: self.original_file_contents.clone(),
407        })
408    }
409
410    pub fn change_experimental_features(&self, warning_level: Option<WarningLevel>) -> Result<Self, KclError> {
411        Ok(Self {
412            ast: self.ast.change_experimental_features(warning_level)?,
413            original_file_contents: self.original_file_contents.clone(),
414        })
415    }
416
417    pub fn is_empty_or_only_settings(&self) -> bool {
418        self.ast.is_empty_or_only_settings()
419    }
420
421    pub fn lint_all(&self) -> Result<Vec<lint::Discovered>, anyhow::Error> {
422        self.ast.lint_all()
423    }
424
425    pub fn lint_all_with_options(&self, options: lint::LintOptions) -> Result<Vec<lint::Discovered>, anyhow::Error> {
426        self.ast.lint_all_with_options(options)
427    }
428
429    pub fn lint<'a>(&'a self, rule: impl lint::Rule<'a>) -> Result<Vec<lint::Discovered>, anyhow::Error> {
430        self.ast.lint(rule)
431    }
432
433    pub fn node_path_from_range(&self, cached_body_items: usize, range: SourceRange) -> Option<NodePath> {
434        let module_infos = indexmap::IndexMap::new();
435        let programs = crate::execution::ProgramLookup::new(self.ast.clone(), module_infos);
436        NodePath::from_range(&programs, cached_body_items, range)
437    }
438
439    /// Fill node paths and consume the input so that the program without paths
440    /// isn't accidentally used. Filling node paths happens automatically during
441    /// parsing. Calling this is only needed after the caller invalidates the
442    /// node paths such as by mutating an AST or by making a round-trip through
443    /// serialization.
444    pub fn fill_node_paths(mut self) -> Program {
445        parsing::ast::types::fill_node_paths(&mut self.ast);
446        self
447    }
448
449    pub fn recast(&self) -> String {
450        // Use the default options until we integrate into the UI the ability to change them.
451        self.ast.recast_top(&Default::default(), 0)
452    }
453
454    pub fn recast_with_options(&self, options: &FormatOptions) -> String {
455        self.ast.recast_top(options, 0)
456    }
457
458    /// Create an empty program.
459    pub fn empty() -> Self {
460        Self {
461            ast: parsing::ast::types::Node::no_src(parsing::ast::types::Program::default()),
462            original_file_contents: String::new(),
463        }
464    }
465}
466
467#[inline]
468fn try_f64_to_usize(f: f64) -> Option<usize> {
469    let i = f as usize;
470    if i as f64 == f { Some(i) } else { None }
471}
472
473#[inline]
474fn try_f64_to_u32(f: f64) -> Option<u32> {
475    let i = f as u32;
476    if i as f64 == f { Some(i) } else { None }
477}
478
479#[inline]
480fn try_f64_to_u64(f: f64) -> Option<u64> {
481    let i = f as u64;
482    if i as f64 == f { Some(i) } else { None }
483}
484
485#[inline]
486fn try_f64_to_i64(f: f64) -> Option<i64> {
487    let i = f as i64;
488    if i as f64 == f { Some(i) } else { None }
489}
490
491/// Get the version of the KCL library.
492pub fn version() -> &'static str {
493    env!("CARGO_PKG_VERSION")
494}
495
496#[cfg(test)]
497mod test {
498    use super::*;
499
500    #[test]
501    fn entry_point_language_version() {
502        for (code, expected) in [
503            ("", KclVersion::V1),
504            ("@settings(defaultLengthUnit = mm)", KclVersion::V1),
505            ("@settings(kclVersion = 2.0)", KclVersion::V2),
506            ("@settings(kclVersion = \"3.0-preview\")", KclVersion::V3Preview),
507        ] {
508            assert_eq!(
509                Program::parse_no_errs(code).unwrap().language_version().unwrap(),
510                expected
511            );
512        }
513        let invalid = Program::parse_no_errs("@settings(kclVersion = 99.0)").unwrap();
514        invalid.language_version().unwrap_err();
515    }
516
517    #[test]
518    fn proprietary_file_extensions_use_real_suffixes() {
519        for extension in ["sat", "sab", "catpart", "prt", "ipt", "x_t", "x_b", "sldprt"] {
520            assert!(IMPORT_FILE_EXTENSIONS.iter().any(|candidate| candidate == extension));
521        }
522    }
523
524    #[test]
525    fn convert_int() {
526        assert_eq!(try_f64_to_usize(0.0), Some(0));
527        assert_eq!(try_f64_to_usize(42.0), Some(42));
528        assert_eq!(try_f64_to_usize(0.00000000001), None);
529        assert_eq!(try_f64_to_usize(-1.0), None);
530        assert_eq!(try_f64_to_usize(f64::NAN), None);
531        assert_eq!(try_f64_to_usize(f64::INFINITY), None);
532        assert_eq!(try_f64_to_usize((0.1 + 0.2) * 10.0), None);
533
534        assert_eq!(try_f64_to_u32(0.0), Some(0));
535        assert_eq!(try_f64_to_u32(42.0), Some(42));
536        assert_eq!(try_f64_to_u32(0.00000000001), None);
537        assert_eq!(try_f64_to_u32(-1.0), None);
538        assert_eq!(try_f64_to_u32(f64::NAN), None);
539        assert_eq!(try_f64_to_u32(f64::INFINITY), None);
540        assert_eq!(try_f64_to_u32((0.1 + 0.2) * 10.0), None);
541
542        assert_eq!(try_f64_to_u64(0.0), Some(0));
543        assert_eq!(try_f64_to_u64(42.0), Some(42));
544        assert_eq!(try_f64_to_u64(0.00000000001), None);
545        assert_eq!(try_f64_to_u64(-1.0), None);
546        assert_eq!(try_f64_to_u64(f64::NAN), None);
547        assert_eq!(try_f64_to_u64(f64::INFINITY), None);
548        assert_eq!(try_f64_to_u64((0.1 + 0.2) * 10.0), None);
549
550        assert_eq!(try_f64_to_i64(0.0), Some(0));
551        assert_eq!(try_f64_to_i64(42.0), Some(42));
552        assert_eq!(try_f64_to_i64(0.00000000001), None);
553        assert_eq!(try_f64_to_i64(-1.0), Some(-1));
554        assert_eq!(try_f64_to_i64(f64::NAN), None);
555        assert_eq!(try_f64_to_i64(f64::INFINITY), None);
556        assert_eq!(try_f64_to_i64((0.1 + 0.2) * 10.0), None);
557    }
558}