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