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, 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, transpile_old_sketch_to_new, transpile_old_sketch_to_new_with_execution, 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, PlaneKind, 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    // Re-export trim module items
159    pub use crate::frontend::{
160        FrontendState, SetProgramOutcome,
161        api::{
162            Error, Expr, Face, File, FileId, LifecycleApi, Number, Object, ObjectId, ObjectKind, Plane, ProjectId,
163            Result, SceneGraph, SceneGraphDelta, Settings, SourceDelta, SourceRef, Version,
164        },
165        sketch::{
166            Arc, ArcCtor, Circle, CircleCtor, Coincident, Constraint, Distance, ExistingSegmentCtor, Freedom,
167            Horizontal, Line, LineCtor, LinesEqualLength, NewSegmentInfo, Parallel, Perpendicular, Point, Point2d,
168            PointCtor, Segment, SegmentCtor, Sketch, SketchApi, SketchCtor, StartOrEnd, TangentArcCtor, Vertical,
169        },
170        trim::{
171            ArcPoint, AttachToEndpoint, CoincidentData, ConstraintToMigrate, Coords2d, EndpointChanged, LineEndpoint,
172            TrimDirection, TrimItem, TrimOperation, TrimTermination, TrimTerminations, arc_arc_intersection,
173            execute_trim_loop_with_context, get_next_trim_spawn, get_position_coords_for_line,
174            get_position_coords_from_arc, get_trim_spawn_terminations, is_point_on_arc, is_point_on_line_segment,
175            line_arc_intersection, line_segment_intersection, perpendicular_distance_to_segment,
176            project_point_onto_arc, project_point_onto_segment,
177        },
178    };
179}
180
181#[cfg(feature = "cli")]
182use clap::ValueEnum;
183use serde::{Deserialize, Serialize};
184
185use crate::exec::WarningLevel;
186#[allow(unused_imports)]
187use crate::log::{log, logln};
188
189lazy_static::lazy_static! {
190
191    pub static ref IMPORT_FILE_EXTENSIONS: Vec<String> = {
192        let mut import_file_extensions = vec!["stp".to_string(), "glb".to_string(), "fbxb".to_string()];
193        #[cfg(feature = "cli")]
194        let named_extensions = kittycad::types::FileImportFormat::value_variants()
195            .iter()
196            .map(|x| format!("{x}"))
197            .collect::<Vec<String>>();
198        #[cfg(not(feature = "cli"))]
199        let named_extensions = vec![]; // We don't really need this outside of the CLI.
200        // Add all the default import formats.
201        import_file_extensions.extend_from_slice(&named_extensions);
202        import_file_extensions
203    };
204
205    pub static ref RELEVANT_FILE_EXTENSIONS: Vec<String> = {
206        let mut relevant_extensions = IMPORT_FILE_EXTENSIONS.clone();
207        relevant_extensions.push("kcl".to_string());
208        relevant_extensions
209    };
210}
211
212#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
213pub struct Program {
214    #[serde(flatten)]
215    pub ast: parsing::ast::types::Node<parsing::ast::types::Program>,
216    // The ui doesn't need to know about this.
217    // It's purely used for saving the contents of the original file, so we can use it for errors.
218    // Because in the case of the root file, we don't want to read the file from disk again.
219    #[serde(skip)]
220    pub original_file_contents: String,
221}
222
223#[cfg(any(test, feature = "lsp-test-util"))]
224pub use lsp::test_util::copilot_lsp_server;
225#[cfg(any(test, feature = "lsp-test-util"))]
226pub use lsp::test_util::kcl_lsp_server;
227
228impl Program {
229    pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationError>), KclError> {
230        let module_id = ModuleId::default();
231        let (ast, errs) = parsing::parse_str(input, module_id).0?;
232
233        Ok((
234            ast.map(|ast| Program {
235                ast,
236                original_file_contents: input.to_string(),
237            }),
238            errs,
239        ))
240    }
241
242    pub fn parse_no_errs(input: &str) -> Result<Program, KclError> {
243        let module_id = ModuleId::default();
244        let ast = parsing::parse_str(input, module_id).parse_errs_as_err()?;
245
246        Ok(Program {
247            ast,
248            original_file_contents: input.to_string(),
249        })
250    }
251
252    pub fn compute_digest(&mut self) -> parsing::ast::digest::Digest {
253        self.ast.compute_digest()
254    }
255
256    /// Get the meta settings for the kcl file from the annotations.
257    pub fn meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
258        self.ast.meta_settings()
259    }
260
261    /// Change the meta settings for the kcl file.
262    pub fn change_default_units(
263        &self,
264        length_units: Option<kittycad_modeling_cmds::units::UnitLength>,
265    ) -> Result<Self, KclError> {
266        Ok(Self {
267            ast: self.ast.change_default_units(length_units)?,
268            original_file_contents: self.original_file_contents.clone(),
269        })
270    }
271
272    pub fn change_experimental_features(&self, warning_level: Option<WarningLevel>) -> Result<Self, KclError> {
273        Ok(Self {
274            ast: self.ast.change_experimental_features(warning_level)?,
275            original_file_contents: self.original_file_contents.clone(),
276        })
277    }
278
279    pub fn is_empty_or_only_settings(&self) -> bool {
280        self.ast.is_empty_or_only_settings()
281    }
282
283    pub fn lint_all(&self) -> Result<Vec<lint::Discovered>, anyhow::Error> {
284        self.ast.lint_all()
285    }
286
287    pub fn lint<'a>(&'a self, rule: impl lint::Rule<'a>) -> Result<Vec<lint::Discovered>, anyhow::Error> {
288        self.ast.lint(rule)
289    }
290
291    #[cfg(feature = "artifact-graph")]
292    pub fn node_path_from_range(&self, cached_body_items: usize, range: SourceRange) -> Option<NodePath> {
293        let module_infos = indexmap::IndexMap::new();
294        let programs = crate::execution::ProgramLookup::new(self.ast.clone(), module_infos);
295        NodePath::from_range(&programs, cached_body_items, range)
296    }
297
298    pub fn recast(&self) -> String {
299        // Use the default options until we integrate into the UI the ability to change them.
300        self.ast.recast_top(&Default::default(), 0)
301    }
302
303    pub fn recast_with_options(&self, options: &FormatOptions) -> String {
304        self.ast.recast_top(options, 0)
305    }
306
307    /// Create an empty program.
308    pub fn empty() -> Self {
309        Self {
310            ast: parsing::ast::types::Node::no_src(parsing::ast::types::Program::default()),
311            original_file_contents: String::new(),
312        }
313    }
314}
315
316#[inline]
317fn try_f64_to_usize(f: f64) -> Option<usize> {
318    let i = f as usize;
319    if i as f64 == f { Some(i) } else { None }
320}
321
322#[inline]
323fn try_f64_to_u32(f: f64) -> Option<u32> {
324    let i = f as u32;
325    if i as f64 == f { Some(i) } else { None }
326}
327
328#[inline]
329fn try_f64_to_u64(f: f64) -> Option<u64> {
330    let i = f as u64;
331    if i as f64 == f { Some(i) } else { None }
332}
333
334#[inline]
335fn try_f64_to_i64(f: f64) -> Option<i64> {
336    let i = f as i64;
337    if i as f64 == f { Some(i) } else { None }
338}
339
340/// Get the version of the KCL library.
341pub fn version() -> &'static str {
342    env!("CARGO_PKG_VERSION")
343}
344
345#[cfg(test)]
346mod test {
347    use super::*;
348
349    #[test]
350    fn convert_int() {
351        assert_eq!(try_f64_to_usize(0.0), Some(0));
352        assert_eq!(try_f64_to_usize(42.0), Some(42));
353        assert_eq!(try_f64_to_usize(0.00000000001), None);
354        assert_eq!(try_f64_to_usize(-1.0), None);
355        assert_eq!(try_f64_to_usize(f64::NAN), None);
356        assert_eq!(try_f64_to_usize(f64::INFINITY), None);
357        assert_eq!(try_f64_to_usize((0.1 + 0.2) * 10.0), None);
358
359        assert_eq!(try_f64_to_u32(0.0), Some(0));
360        assert_eq!(try_f64_to_u32(42.0), Some(42));
361        assert_eq!(try_f64_to_u32(0.00000000001), None);
362        assert_eq!(try_f64_to_u32(-1.0), None);
363        assert_eq!(try_f64_to_u32(f64::NAN), None);
364        assert_eq!(try_f64_to_u32(f64::INFINITY), None);
365        assert_eq!(try_f64_to_u32((0.1 + 0.2) * 10.0), None);
366
367        assert_eq!(try_f64_to_u64(0.0), Some(0));
368        assert_eq!(try_f64_to_u64(42.0), Some(42));
369        assert_eq!(try_f64_to_u64(0.00000000001), None);
370        assert_eq!(try_f64_to_u64(-1.0), None);
371        assert_eq!(try_f64_to_u64(f64::NAN), None);
372        assert_eq!(try_f64_to_u64(f64::INFINITY), None);
373        assert_eq!(try_f64_to_u64((0.1 + 0.2) * 10.0), None);
374
375        assert_eq!(try_f64_to_i64(0.0), Some(0));
376        assert_eq!(try_f64_to_i64(42.0), Some(42));
377        assert_eq!(try_f64_to_i64(0.00000000001), None);
378        assert_eq!(try_f64_to_i64(-1.0), Some(-1));
379        assert_eq!(try_f64_to_i64(f64::NAN), None);
380        assert_eq!(try_f64_to_i64(f64::INFINITY), None);
381        assert_eq!(try_f64_to_i64((0.1 + 0.2) * 10.0), None);
382    }
383}