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