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