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