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 docs;
61mod engine;
62mod errors;
63mod execution;
64mod fmt;
65mod frontend;
66mod fs;
67pub(crate) mod id;
68pub mod lint;
69mod log;
70mod lsp;
71mod modules;
72mod parsing;
73mod project;
74mod settings;
75#[cfg(test)]
76mod simulation_tests;
77pub mod std;
78#[cfg(not(target_arch = "wasm32"))]
79pub mod test_server;
80mod thread;
81#[doc(hidden)]
82pub mod tooling;
83mod unparser;
84mod util;
85#[cfg(test)]
86mod variant_name;
87pub mod walk;
88#[cfg(target_arch = "wasm32")]
89mod wasm;
90
91pub use engine::AsyncTasks;
92pub use engine::EngineBatchContext;
93pub use engine::EngineManager;
94pub use engine::EngineStats;
95pub use errors::BacktraceItem;
96pub use errors::CompilationIssue;
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::ConstraintKind;
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::SketchConstraintReport;
112pub use execution::SketchConstraintStatus;
113pub use execution::bust_cache;
114pub use execution::clear_mem_cache;
115pub use execution::pre_execute_transpile;
116pub use execution::transpile_all_old_sketches_to_new;
117pub use execution::transpile_old_sketch_to_new;
118pub use execution::transpile_old_sketch_to_new_ast;
119pub use execution::transpile_old_sketch_to_new_with_execution;
120pub use execution::typed_path::TypedPath;
121pub use kcl_error::SourceRange;
122pub use lsp::ToLspRange;
123pub use lsp::copilot::Backend as CopilotLspBackend;
124pub use lsp::kcl::Backend as KclLspBackend;
125pub use lsp::kcl::Server as KclLspServerSubCommand;
126pub use modules::ModuleId;
127pub use parsing::ast::types::FormatOptions;
128pub use parsing::ast::types::NodePath;
129pub use parsing::ast::types::Step as NodePathStep;
130pub use project::ProjectManager;
131pub use settings::types::Configuration;
132pub use settings::types::project::ProjectConfiguration;
133#[cfg(not(target_arch = "wasm32"))]
134pub use unparser::recast_dir;
135#[cfg(not(target_arch = "wasm32"))]
136pub use unparser::walk_dir;
137
138// Rather than make executor public and make lots of it pub(crate), just re-export into a new module.
139// Ideally we wouldn't export these things at all, they should only be used for testing.
140pub mod exec {
141    #[cfg(feature = "artifact-graph")]
142    pub use crate::execution::ArtifactCommand;
143    pub use crate::execution::DefaultPlanes;
144    pub use crate::execution::IdGenerator;
145    pub use crate::execution::KclValue;
146    #[cfg(feature = "artifact-graph")]
147    pub use crate::execution::Operation;
148    pub use crate::execution::PlaneKind;
149    pub use crate::execution::Sketch;
150    pub use crate::execution::annotations::WarningLevel;
151    pub use crate::execution::types::NumericType;
152    pub use crate::execution::types::UnitType;
153    pub use crate::util::RetryConfig;
154    pub use crate::util::execute_with_retries;
155}
156
157#[cfg(target_arch = "wasm32")]
158pub mod wasm_engine {
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, EqualRadius,
205            ExistingSegmentCtor, Fixed, FixedPoint, Freedom, Horizontal, Line, LineCtor, LinesEqualLength, Midpoint,
206            NewSegmentInfo, Parallel, Perpendicular, Point, Point2d, PointCtor, Segment, SegmentCtor, Sketch,
207            SketchApi, SketchCtor, StartOrEnd, Symmetric, Tangent, 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.push("md".to_string());
252        relevant_extensions
253    };
254}
255
256#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
257pub struct Program {
258    #[serde(flatten)]
259    pub ast: parsing::ast::types::Node<parsing::ast::types::Program>,
260    // The ui doesn't need to know about this.
261    // It's purely used for saving the contents of the original file, so we can use it for errors.
262    // Because in the case of the root file, we don't want to read the file from disk again.
263    #[serde(skip)]
264    pub original_file_contents: String,
265}
266
267#[cfg(any(test, feature = "lsp-test-util"))]
268pub use lsp::test_util::copilot_lsp_server;
269#[cfg(any(test, feature = "lsp-test-util"))]
270pub use lsp::test_util::kcl_lsp_server;
271
272impl Program {
273    pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationIssue>), KclError> {
274        let module_id = ModuleId::default();
275        let (ast, errs) = parsing::parse_str(input, module_id).0?;
276
277        Ok((
278            ast.map(|ast| Program {
279                ast,
280                original_file_contents: input.to_string(),
281            }),
282            errs,
283        ))
284    }
285
286    pub fn parse_no_errs(input: &str) -> Result<Program, KclError> {
287        let module_id = ModuleId::default();
288        let ast = parsing::parse_str(input, module_id).parse_errs_as_err()?;
289
290        Ok(Program {
291            ast,
292            original_file_contents: input.to_string(),
293        })
294    }
295
296    pub fn compute_digest(&mut self) -> parsing::ast::digest::Digest {
297        self.ast.compute_digest()
298    }
299
300    /// Get the meta settings for the kcl file from the annotations.
301    pub fn meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
302        self.ast.meta_settings()
303    }
304
305    /// Change the meta settings for the kcl file.
306    pub fn change_default_units(
307        &self,
308        length_units: Option<kittycad_modeling_cmds::units::UnitLength>,
309    ) -> Result<Self, KclError> {
310        Ok(Self {
311            ast: self.ast.change_default_units(length_units)?,
312            original_file_contents: self.original_file_contents.clone(),
313        })
314    }
315
316    pub fn change_kcl_version(&self, kcl_version: Option<String>) -> Result<Self, KclError> {
317        Ok(Self {
318            ast: self.ast.change_kcl_version(kcl_version)?,
319            original_file_contents: self.original_file_contents.clone(),
320        })
321    }
322
323    pub fn change_experimental_features(&self, warning_level: Option<WarningLevel>) -> Result<Self, KclError> {
324        Ok(Self {
325            ast: self.ast.change_experimental_features(warning_level)?,
326            original_file_contents: self.original_file_contents.clone(),
327        })
328    }
329
330    pub fn is_empty_or_only_settings(&self) -> bool {
331        self.ast.is_empty_or_only_settings()
332    }
333
334    pub fn lint_all(&self) -> Result<Vec<lint::Discovered>, anyhow::Error> {
335        self.ast.lint_all()
336    }
337
338    pub fn lint<'a>(&'a self, rule: impl lint::Rule<'a>) -> Result<Vec<lint::Discovered>, anyhow::Error> {
339        self.ast.lint(rule)
340    }
341
342    #[cfg(feature = "artifact-graph")]
343    pub fn node_path_from_range(&self, cached_body_items: usize, range: SourceRange) -> Option<NodePath> {
344        let module_infos = indexmap::IndexMap::new();
345        let programs = crate::execution::ProgramLookup::new(self.ast.clone(), module_infos);
346        NodePath::from_range(&programs, cached_body_items, range)
347    }
348
349    /// Fill node paths and consume the input so that the program without paths
350    /// isn't accidentally used. Filling node paths happens automatically during
351    /// parsing. Calling this is only needed after the caller invalidates the
352    /// node paths such as by mutating an AST or by making a round-trip through
353    /// serialization.
354    #[cfg(feature = "artifact-graph")]
355    pub fn fill_node_paths(mut self) -> Program {
356        parsing::ast::types::fill_node_paths(&mut self.ast);
357        self
358    }
359
360    pub fn recast(&self) -> String {
361        // Use the default options until we integrate into the UI the ability to change them.
362        self.ast.recast_top(&Default::default(), 0)
363    }
364
365    pub fn recast_with_options(&self, options: &FormatOptions) -> String {
366        self.ast.recast_top(options, 0)
367    }
368
369    /// Create an empty program.
370    pub fn empty() -> Self {
371        Self {
372            ast: parsing::ast::types::Node::no_src(parsing::ast::types::Program::default()),
373            original_file_contents: String::new(),
374        }
375    }
376}
377
378#[inline]
379fn try_f64_to_usize(f: f64) -> Option<usize> {
380    let i = f as usize;
381    if i as f64 == f { Some(i) } else { None }
382}
383
384#[inline]
385fn try_f64_to_u32(f: f64) -> Option<u32> {
386    let i = f as u32;
387    if i as f64 == f { Some(i) } else { None }
388}
389
390#[inline]
391fn try_f64_to_u64(f: f64) -> Option<u64> {
392    let i = f as u64;
393    if i as f64 == f { Some(i) } else { None }
394}
395
396#[inline]
397fn try_f64_to_i64(f: f64) -> Option<i64> {
398    let i = f as i64;
399    if i as f64 == f { Some(i) } else { None }
400}
401
402/// Get the version of the KCL library.
403pub fn version() -> &'static str {
404    env!("CARGO_PKG_VERSION")
405}
406
407#[cfg(test)]
408mod test {
409    use super::*;
410
411    #[test]
412    fn convert_int() {
413        assert_eq!(try_f64_to_usize(0.0), Some(0));
414        assert_eq!(try_f64_to_usize(42.0), Some(42));
415        assert_eq!(try_f64_to_usize(0.00000000001), None);
416        assert_eq!(try_f64_to_usize(-1.0), None);
417        assert_eq!(try_f64_to_usize(f64::NAN), None);
418        assert_eq!(try_f64_to_usize(f64::INFINITY), None);
419        assert_eq!(try_f64_to_usize((0.1 + 0.2) * 10.0), None);
420
421        assert_eq!(try_f64_to_u32(0.0), Some(0));
422        assert_eq!(try_f64_to_u32(42.0), Some(42));
423        assert_eq!(try_f64_to_u32(0.00000000001), None);
424        assert_eq!(try_f64_to_u32(-1.0), None);
425        assert_eq!(try_f64_to_u32(f64::NAN), None);
426        assert_eq!(try_f64_to_u32(f64::INFINITY), None);
427        assert_eq!(try_f64_to_u32((0.1 + 0.2) * 10.0), None);
428
429        assert_eq!(try_f64_to_u64(0.0), Some(0));
430        assert_eq!(try_f64_to_u64(42.0), Some(42));
431        assert_eq!(try_f64_to_u64(0.00000000001), None);
432        assert_eq!(try_f64_to_u64(-1.0), None);
433        assert_eq!(try_f64_to_u64(f64::NAN), None);
434        assert_eq!(try_f64_to_u64(f64::INFINITY), None);
435        assert_eq!(try_f64_to_u64((0.1 + 0.2) * 10.0), None);
436
437        assert_eq!(try_f64_to_i64(0.0), Some(0));
438        assert_eq!(try_f64_to_i64(42.0), Some(42));
439        assert_eq!(try_f64_to_i64(0.00000000001), None);
440        assert_eq!(try_f64_to_i64(-1.0), Some(-1));
441        assert_eq!(try_f64_to_i64(f64::NAN), None);
442        assert_eq!(try_f64_to_i64(f64::INFINITY), None);
443        assert_eq!(try_f64_to_i64((0.1 + 0.2) * 10.0), None);
444    }
445}