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