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, a LSP 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
59mod coredump;
60mod docs;
61mod engine;
62mod errors;
63mod execution;
64mod fmt;
65mod fs;
66pub mod lint;
67mod log;
68mod lsp;
69mod modules;
70mod parsing;
71mod project;
72mod settings;
73#[cfg(test)]
74mod simulation_tests;
75pub mod std;
76#[cfg(not(target_arch = "wasm32"))]
77pub mod test_server;
78mod thread;
79mod unparser;
80#[cfg(test)]
81mod variant_name;
82pub mod walk;
83#[cfg(target_arch = "wasm32")]
84mod wasm;
85
86pub use coredump::CoreDump;
87pub use engine::{AsyncTasks, EngineManager, EngineStats};
88pub use errors::{
89    BacktraceItem, CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs, Report,
90    ReportWithOutputs,
91};
92pub use execution::{
93    ExecOutcome, ExecState, ExecutorContext, ExecutorSettings, MetaSettings, Point2d, bust_cache, clear_mem_cache,
94    typed_path::TypedPath,
95};
96pub use kcl_error::SourceRange;
97pub use lsp::{
98    ToLspRange,
99    copilot::Backend as CopilotLspBackend,
100    kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
101};
102pub use modules::ModuleId;
103pub use parsing::ast::types::{FormatOptions, NodePath, Step as NodePathStep};
104pub use project::ProjectManager;
105pub use settings::types::{Configuration, project::ProjectConfiguration};
106#[cfg(not(target_arch = "wasm32"))]
107pub use unparser::{recast_dir, walk_dir};
108
109// Rather than make executor public and make lots of it pub(crate), just re-export into a new module.
110// Ideally we wouldn't export these things at all, they should only be used for testing.
111pub mod exec {
112    #[cfg(feature = "artifact-graph")]
113    pub use crate::execution::{ArtifactCommand, Operation};
114    pub use crate::execution::{
115        DefaultPlanes, IdGenerator, KclValue, PlaneType, Sketch,
116        annotations::WarningLevel,
117        types::{NumericType, UnitType},
118    };
119}
120
121#[cfg(target_arch = "wasm32")]
122pub mod wasm_engine {
123    pub use crate::{
124        coredump::wasm::{CoreDumpManager, CoreDumper},
125        engine::conn_wasm::{EngineCommandManager, EngineConnection, ResponseContext},
126        fs::wasm::{FileManager, FileSystemManager},
127    };
128}
129
130pub mod mock_engine {
131    pub use crate::engine::conn_mock::EngineConnection;
132}
133
134#[cfg(not(target_arch = "wasm32"))]
135pub mod native_engine {
136    pub use crate::engine::conn::EngineConnection;
137}
138
139pub mod std_utils {
140    pub use crate::std::utils::{TangentialArcInfoInput, get_tangential_arc_to_info, is_points_ccw_wasm};
141}
142
143pub mod pretty {
144    pub use crate::{
145        fmt::{format_number_literal, format_number_value, human_display_number},
146        parsing::token::NumericSuffix,
147    };
148}
149
150#[cfg(feature = "cli")]
151use clap::ValueEnum;
152use serde::{Deserialize, Serialize};
153
154use crate::exec::WarningLevel;
155#[allow(unused_imports)]
156use crate::log::{log, logln};
157
158lazy_static::lazy_static! {
159
160    pub static ref IMPORT_FILE_EXTENSIONS: Vec<String> = {
161        let mut import_file_extensions = vec!["stp".to_string(), "glb".to_string(), "fbxb".to_string()];
162        #[cfg(feature = "cli")]
163        let named_extensions = kittycad::types::FileImportFormat::value_variants()
164            .iter()
165            .map(|x| format!("{x}"))
166            .collect::<Vec<String>>();
167        #[cfg(not(feature = "cli"))]
168        let named_extensions = vec![]; // We don't really need this outside of the CLI.
169        // Add all the default import formats.
170        import_file_extensions.extend_from_slice(&named_extensions);
171        import_file_extensions
172    };
173
174    pub static ref RELEVANT_FILE_EXTENSIONS: Vec<String> = {
175        let mut relevant_extensions = IMPORT_FILE_EXTENSIONS.clone();
176        relevant_extensions.push("kcl".to_string());
177        relevant_extensions
178    };
179}
180
181#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
182pub struct Program {
183    #[serde(flatten)]
184    pub ast: parsing::ast::types::Node<parsing::ast::types::Program>,
185    // The ui doesn't need to know about this.
186    // It's purely used for saving the contents of the original file, so we can use it for errors.
187    // Because in the case of the root file, we don't want to read the file from disk again.
188    #[serde(skip)]
189    pub original_file_contents: String,
190}
191
192#[cfg(any(test, feature = "lsp-test-util"))]
193pub use lsp::test_util::copilot_lsp_server;
194#[cfg(any(test, feature = "lsp-test-util"))]
195pub use lsp::test_util::kcl_lsp_server;
196
197impl Program {
198    pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationError>), KclError> {
199        let module_id = ModuleId::default();
200        let (ast, errs) = parsing::parse_str(input, module_id).0?;
201
202        Ok((
203            ast.map(|ast| Program {
204                ast,
205                original_file_contents: input.to_string(),
206            }),
207            errs,
208        ))
209    }
210
211    pub fn parse_no_errs(input: &str) -> Result<Program, KclError> {
212        let module_id = ModuleId::default();
213        let ast = parsing::parse_str(input, module_id).parse_errs_as_err()?;
214
215        Ok(Program {
216            ast,
217            original_file_contents: input.to_string(),
218        })
219    }
220
221    pub fn compute_digest(&mut self) -> parsing::ast::digest::Digest {
222        self.ast.compute_digest()
223    }
224
225    /// Get the meta settings for the kcl file from the annotations.
226    pub fn meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
227        self.ast.meta_settings()
228    }
229
230    /// Change the meta settings for the kcl file.
231    pub fn change_default_units(
232        &self,
233        length_units: Option<kittycad_modeling_cmds::units::UnitLength>,
234    ) -> Result<Self, KclError> {
235        Ok(Self {
236            ast: self.ast.change_default_units(length_units)?,
237            original_file_contents: self.original_file_contents.clone(),
238        })
239    }
240
241    pub fn change_experimental_features(&self, warning_level: Option<WarningLevel>) -> Result<Self, KclError> {
242        Ok(Self {
243            ast: self.ast.change_experimental_features(warning_level)?,
244            original_file_contents: self.original_file_contents.clone(),
245        })
246    }
247
248    pub fn is_empty_or_only_settings(&self) -> bool {
249        self.ast.is_empty_or_only_settings()
250    }
251
252    pub fn lint_all(&self) -> Result<Vec<lint::Discovered>, anyhow::Error> {
253        self.ast.lint_all()
254    }
255
256    pub fn lint<'a>(&'a self, rule: impl lint::Rule<'a>) -> Result<Vec<lint::Discovered>, anyhow::Error> {
257        self.ast.lint(rule)
258    }
259
260    #[cfg(feature = "artifact-graph")]
261    pub fn node_path_from_range(&self, cached_body_items: usize, range: SourceRange) -> Option<NodePath> {
262        let module_infos = indexmap::IndexMap::new();
263        let programs = crate::execution::ProgramLookup::new(self.ast.clone(), module_infos);
264        NodePath::from_range(&programs, cached_body_items, range)
265    }
266
267    pub fn recast(&self) -> String {
268        // Use the default options until we integrate into the UI the ability to change them.
269        self.ast.recast_top(&Default::default(), 0)
270    }
271
272    pub fn recast_with_options(&self, options: &FormatOptions) -> String {
273        self.ast.recast_top(options, 0)
274    }
275
276    /// Create an empty program.
277    pub fn empty() -> Self {
278        Self {
279            ast: parsing::ast::types::Node::no_src(parsing::ast::types::Program::default()),
280            original_file_contents: String::new(),
281        }
282    }
283}
284
285#[inline]
286fn try_f64_to_usize(f: f64) -> Option<usize> {
287    let i = f as usize;
288    if i as f64 == f { Some(i) } else { None }
289}
290
291#[inline]
292fn try_f64_to_u32(f: f64) -> Option<u32> {
293    let i = f as u32;
294    if i as f64 == f { Some(i) } else { None }
295}
296
297#[inline]
298fn try_f64_to_u64(f: f64) -> Option<u64> {
299    let i = f as u64;
300    if i as f64 == f { Some(i) } else { None }
301}
302
303#[inline]
304fn try_f64_to_i64(f: f64) -> Option<i64> {
305    let i = f as i64;
306    if i as f64 == f { Some(i) } else { None }
307}
308
309/// Get the version of the KCL library.
310pub fn version() -> &'static str {
311    env!("CARGO_PKG_VERSION")
312}
313
314#[cfg(test)]
315mod test {
316    use super::*;
317
318    #[test]
319    fn convert_int() {
320        assert_eq!(try_f64_to_usize(0.0), Some(0));
321        assert_eq!(try_f64_to_usize(42.0), Some(42));
322        assert_eq!(try_f64_to_usize(0.00000000001), None);
323        assert_eq!(try_f64_to_usize(-1.0), None);
324        assert_eq!(try_f64_to_usize(f64::NAN), None);
325        assert_eq!(try_f64_to_usize(f64::INFINITY), None);
326        assert_eq!(try_f64_to_usize((0.1 + 0.2) * 10.0), None);
327
328        assert_eq!(try_f64_to_u32(0.0), Some(0));
329        assert_eq!(try_f64_to_u32(42.0), Some(42));
330        assert_eq!(try_f64_to_u32(0.00000000001), None);
331        assert_eq!(try_f64_to_u32(-1.0), None);
332        assert_eq!(try_f64_to_u32(f64::NAN), None);
333        assert_eq!(try_f64_to_u32(f64::INFINITY), None);
334        assert_eq!(try_f64_to_u32((0.1 + 0.2) * 10.0), None);
335
336        assert_eq!(try_f64_to_u64(0.0), Some(0));
337        assert_eq!(try_f64_to_u64(42.0), Some(42));
338        assert_eq!(try_f64_to_u64(0.00000000001), None);
339        assert_eq!(try_f64_to_u64(-1.0), None);
340        assert_eq!(try_f64_to_u64(f64::NAN), None);
341        assert_eq!(try_f64_to_u64(f64::INFINITY), None);
342        assert_eq!(try_f64_to_u64((0.1 + 0.2) * 10.0), None);
343
344        assert_eq!(try_f64_to_i64(0.0), Some(0));
345        assert_eq!(try_f64_to_i64(42.0), Some(42));
346        assert_eq!(try_f64_to_i64(0.00000000001), None);
347        assert_eq!(try_f64_to_i64(-1.0), Some(-1));
348        assert_eq!(try_f64_to_i64(f64::NAN), None);
349        assert_eq!(try_f64_to_i64(f64::INFINITY), None);
350        assert_eq!(try_f64_to_i64((0.1 + 0.2) * 10.0), None);
351    }
352}