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,
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
255    };
256}
257
258#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
259pub struct Program {
260    #[serde(flatten)]
261    pub ast: parsing::ast::types::Node<parsing::ast::types::Program>,
262    // The ui doesn't need to know about this.
263    // It's purely used for saving the contents of the original file, so we can use it for errors.
264    // Because in the case of the root file, we don't want to read the file from disk again.
265    #[serde(skip)]
266    pub original_file_contents: String,
267}
268
269#[cfg(any(test, feature = "lsp-test-util"))]
270pub use lsp::test_util::copilot_lsp_server;
271#[cfg(any(test, feature = "lsp-test-util"))]
272pub use lsp::test_util::kcl_lsp_server;
273
274impl Program {
275    pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationIssue>), KclError> {
276        let module_id = ModuleId::default();
277        let (ast, errs) = parsing::parse_str(input, module_id).0?;
278
279        Ok((
280            ast.map(|ast| Program {
281                ast,
282                original_file_contents: input.to_string(),
283            }),
284            errs,
285        ))
286    }
287
288    pub fn parse_no_errs(input: &str) -> Result<Program, KclError> {
289        let module_id = ModuleId::default();
290        let ast = parsing::parse_str(input, module_id).parse_errs_as_err()?;
291
292        Ok(Program {
293            ast,
294            original_file_contents: input.to_string(),
295        })
296    }
297
298    pub fn compute_digest(&mut self) -> parsing::ast::digest::Digest {
299        self.ast.compute_digest()
300    }
301
302    /// Get the meta settings for the kcl file from the annotations.
303    pub fn meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
304        self.ast.meta_settings()
305    }
306
307    /// Change the meta settings for the kcl file.
308    pub fn change_default_units(
309        &self,
310        length_units: Option<kittycad_modeling_cmds::units::UnitLength>,
311    ) -> Result<Self, KclError> {
312        Ok(Self {
313            ast: self.ast.change_default_units(length_units)?,
314            original_file_contents: self.original_file_contents.clone(),
315        })
316    }
317
318    pub fn change_experimental_features(&self, warning_level: Option<WarningLevel>) -> Result<Self, KclError> {
319        Ok(Self {
320            ast: self.ast.change_experimental_features(warning_level)?,
321            original_file_contents: self.original_file_contents.clone(),
322        })
323    }
324
325    pub fn is_empty_or_only_settings(&self) -> bool {
326        self.ast.is_empty_or_only_settings()
327    }
328
329    pub fn lint_all(&self) -> Result<Vec<lint::Discovered>, anyhow::Error> {
330        self.ast.lint_all()
331    }
332
333    pub fn lint<'a>(&'a self, rule: impl lint::Rule<'a>) -> Result<Vec<lint::Discovered>, anyhow::Error> {
334        self.ast.lint(rule)
335    }
336
337    #[cfg(feature = "artifact-graph")]
338    pub fn node_path_from_range(&self, cached_body_items: usize, range: SourceRange) -> Option<NodePath> {
339        let module_infos = indexmap::IndexMap::new();
340        let programs = crate::execution::ProgramLookup::new(self.ast.clone(), module_infos);
341        NodePath::from_range(&programs, cached_body_items, range)
342    }
343
344    /// Fill node paths and consume the input so that the program without paths
345    /// isn't accidentally used. Filling node paths happens automatically during
346    /// parsing. Calling this is only needed after the caller invalidates the
347    /// node paths such as by mutating an AST or by making a round-trip through
348    /// serialization.
349    #[cfg(feature = "artifact-graph")]
350    pub fn fill_node_paths(mut self) -> Program {
351        parsing::ast::types::fill_node_paths(&mut self.ast);
352        self
353    }
354
355    pub fn recast(&self) -> String {
356        // Use the default options until we integrate into the UI the ability to change them.
357        self.ast.recast_top(&Default::default(), 0)
358    }
359
360    pub fn recast_with_options(&self, options: &FormatOptions) -> String {
361        self.ast.recast_top(options, 0)
362    }
363
364    /// Create an empty program.
365    pub fn empty() -> Self {
366        Self {
367            ast: parsing::ast::types::Node::no_src(parsing::ast::types::Program::default()),
368            original_file_contents: String::new(),
369        }
370    }
371}
372
373#[inline]
374fn try_f64_to_usize(f: f64) -> Option<usize> {
375    let i = f as usize;
376    if i as f64 == f { Some(i) } else { None }
377}
378
379#[inline]
380fn try_f64_to_u32(f: f64) -> Option<u32> {
381    let i = f as u32;
382    if i as f64 == f { Some(i) } else { None }
383}
384
385#[inline]
386fn try_f64_to_u64(f: f64) -> Option<u64> {
387    let i = f as u64;
388    if i as f64 == f { Some(i) } else { None }
389}
390
391#[inline]
392fn try_f64_to_i64(f: f64) -> Option<i64> {
393    let i = f as i64;
394    if i as f64 == f { Some(i) } else { None }
395}
396
397/// Get the version of the KCL library.
398pub fn version() -> &'static str {
399    env!("CARGO_PKG_VERSION")
400}
401
402#[cfg(test)]
403mod test {
404    use super::*;
405
406    #[test]
407    fn convert_int() {
408        assert_eq!(try_f64_to_usize(0.0), Some(0));
409        assert_eq!(try_f64_to_usize(42.0), Some(42));
410        assert_eq!(try_f64_to_usize(0.00000000001), None);
411        assert_eq!(try_f64_to_usize(-1.0), None);
412        assert_eq!(try_f64_to_usize(f64::NAN), None);
413        assert_eq!(try_f64_to_usize(f64::INFINITY), None);
414        assert_eq!(try_f64_to_usize((0.1 + 0.2) * 10.0), None);
415
416        assert_eq!(try_f64_to_u32(0.0), Some(0));
417        assert_eq!(try_f64_to_u32(42.0), Some(42));
418        assert_eq!(try_f64_to_u32(0.00000000001), None);
419        assert_eq!(try_f64_to_u32(-1.0), None);
420        assert_eq!(try_f64_to_u32(f64::NAN), None);
421        assert_eq!(try_f64_to_u32(f64::INFINITY), None);
422        assert_eq!(try_f64_to_u32((0.1 + 0.2) * 10.0), None);
423
424        assert_eq!(try_f64_to_u64(0.0), Some(0));
425        assert_eq!(try_f64_to_u64(42.0), Some(42));
426        assert_eq!(try_f64_to_u64(0.00000000001), None);
427        assert_eq!(try_f64_to_u64(-1.0), None);
428        assert_eq!(try_f64_to_u64(f64::NAN), None);
429        assert_eq!(try_f64_to_u64(f64::INFINITY), None);
430        assert_eq!(try_f64_to_u64((0.1 + 0.2) * 10.0), None);
431
432        assert_eq!(try_f64_to_i64(0.0), Some(0));
433        assert_eq!(try_f64_to_i64(42.0), Some(42));
434        assert_eq!(try_f64_to_i64(0.00000000001), None);
435        assert_eq!(try_f64_to_i64(-1.0), Some(-1));
436        assert_eq!(try_f64_to_i64(f64::NAN), None);
437        assert_eq!(try_f64_to_i64(f64::INFINITY), None);
438        assert_eq!(try_f64_to_i64((0.1 + 0.2) * 10.0), None);
439    }
440}