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