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    pub use crate::execution::ArtifactCommand;
142    pub use crate::execution::DefaultPlanes;
143    pub use crate::execution::IdGenerator;
144    pub use crate::execution::KclValue;
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::engine::conn_wasm::EngineCommandManager;
158    pub use crate::engine::conn_wasm::EngineConnection;
159    pub use crate::engine::conn_wasm::ResponseContext;
160    pub use crate::fs::wasm::FileManager;
161    pub use crate::fs::wasm::FileSystemManager;
162}
163
164pub mod mock_engine {
165    pub use crate::engine::conn_mock::EngineConnection;
166}
167
168#[cfg(not(target_arch = "wasm32"))]
169pub mod native_engine {
170    pub use crate::engine::conn::EngineConnection;
171}
172
173pub mod std_utils {
174    pub use crate::std::utils::TangentialArcInfoInput;
175    pub use crate::std::utils::get_tangential_arc_to_info;
176    pub use crate::std::utils::is_points_ccw_wasm;
177    pub use crate::std::utils::untyped_point_to_unit;
178}
179
180pub mod pretty {
181    pub use crate::fmt::format_number_literal;
182    pub use crate::fmt::format_number_value;
183    pub use crate::fmt::human_display_number;
184    pub use crate::parsing::token::NumericSuffix;
185}
186
187pub mod front {
188    pub use crate::frontend::MAX_SKETCH_CHECKPOINTS;
189    pub(crate) use crate::frontend::modify::find_defined_names;
190    pub(crate) use crate::frontend::modify::next_free_name_using_max;
191    pub use crate::frontend::sketch::ExecResult;
192    pub use crate::frontend::{
193        FrontendState,
194        SetProgramOutcome,
195        api::{
196            Cap, CapKind, EditSketchOutcome, Error, Expr, Face, File, FileId, LifecycleApi, NewSketchOutcome, Number,
197            Object, ObjectId, ObjectKind, Plane, ProjectId, RestoreSketchCheckpointOutcome, Result, SceneGraph,
198            SceneGraphDelta, Settings, SketchCheckpointId, SketchMutationOutcome, SourceDelta, SourceRef, Version,
199            Wall,
200        },
201        sketch::{
202            Angle, Arc, ArcCtor, Circle, CircleCtor, Coincident, Constraint, ControlPointSpline,
203            ControlPointSplineCtor, Distance, EqualRadius, ExistingSegmentCtor, Fixed, FixedPoint, Freedom, Horizontal,
204            Line, LineCtor, LinesEqualLength, Midpoint, NewSegmentInfo, Parallel, Perpendicular, Point, Point2d,
205            PointCtor, Segment, SegmentCtor, Sketch, SketchApi, SketchCtor, StartOrEnd, Symmetric, Tangent, Vertical,
206        },
207        // Re-export trim module items
208        trim::{
209            ArcPoint, AttachToEndpoint, CoincidentData, ConstraintToMigrate, Coords2d, EndpointChanged, LineEndpoint,
210            TrimDirection, TrimItem, TrimOperation, TrimTermination, TrimTerminations, arc_arc_intersection,
211            execute_trim_loop_with_context, get_next_trim_spawn, get_position_coords_for_line,
212            get_position_coords_from_arc, get_trim_spawn_terminations, is_point_on_arc, is_point_on_line_segment,
213            line_arc_intersection, line_segment_intersection, perpendicular_distance_to_segment,
214            project_point_onto_arc, project_point_onto_segment,
215        },
216    };
217}
218
219#[cfg(feature = "cli")]
220use clap::ValueEnum;
221use serde::Deserialize;
222use serde::Serialize;
223
224use crate::exec::WarningLevel;
225#[allow(unused_imports)]
226use crate::log::log;
227#[allow(unused_imports)]
228use crate::log::logln;
229
230lazy_static::lazy_static! {
231
232    pub static ref IMPORT_FILE_EXTENSIONS: Vec<String> = {
233        let mut import_file_extensions = vec!["stp".to_string(), "glb".to_string(), "fbxb".to_string()];
234        #[cfg(feature = "cli")]
235        let named_extensions = kittycad::types::FileImportFormat::value_variants()
236            .iter()
237            .map(|x| format!("{x}"))
238            .collect::<Vec<String>>();
239        #[cfg(not(feature = "cli"))]
240        let named_extensions = vec![]; // We don't really need this outside of the CLI.
241        // Add all the default import formats.
242        import_file_extensions.extend_from_slice(&named_extensions);
243        import_file_extensions
244    };
245
246    pub static ref RELEVANT_FILE_EXTENSIONS: Vec<String> = {
247        let mut relevant_extensions = IMPORT_FILE_EXTENSIONS.clone();
248        relevant_extensions.push("kcl".to_string());
249        relevant_extensions.push("md".to_string());
250        relevant_extensions
251    };
252}
253
254#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
255pub struct Program {
256    #[serde(flatten)]
257    pub ast: parsing::ast::types::Node<parsing::ast::types::Program>,
258    // The ui doesn't need to know about this.
259    // It's purely used for saving the contents of the original file, so we can use it for errors.
260    // Because in the case of the root file, we don't want to read the file from disk again.
261    #[serde(skip)]
262    pub original_file_contents: String,
263}
264
265#[cfg(any(test, feature = "lsp-test-util"))]
266pub use lsp::test_util::copilot_lsp_server;
267#[cfg(any(test, feature = "lsp-test-util"))]
268pub use lsp::test_util::kcl_lsp_server;
269
270impl Program {
271    pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationIssue>), KclError> {
272        let module_id = ModuleId::default();
273        let (ast, errs) = parsing::parse_str(input, module_id).0?;
274
275        Ok((
276            ast.map(|ast| Program {
277                ast,
278                original_file_contents: input.to_string(),
279            }),
280            errs,
281        ))
282    }
283
284    pub fn parse_no_errs(input: &str) -> Result<Program, KclError> {
285        let module_id = ModuleId::default();
286        let ast = parsing::parse_str(input, module_id).parse_errs_as_err()?;
287
288        Ok(Program {
289            ast,
290            original_file_contents: input.to_string(),
291        })
292    }
293
294    pub fn compute_digest(&mut self) -> parsing::ast::digest::Digest {
295        self.ast.compute_digest()
296    }
297
298    /// Get the meta settings for the kcl file from the annotations.
299    pub fn meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
300        self.ast.meta_settings()
301    }
302
303    /// Change the meta settings for the kcl file.
304    pub fn change_default_units(
305        &self,
306        length_units: Option<kittycad_modeling_cmds::units::UnitLength>,
307    ) -> Result<Self, KclError> {
308        Ok(Self {
309            ast: self.ast.change_default_units(length_units)?,
310            original_file_contents: self.original_file_contents.clone(),
311        })
312    }
313
314    pub fn change_kcl_version(&self, kcl_version: Option<String>) -> Result<Self, KclError> {
315        Ok(Self {
316            ast: self.ast.change_kcl_version(kcl_version)?,
317            original_file_contents: self.original_file_contents.clone(),
318        })
319    }
320
321    pub fn change_experimental_features(&self, warning_level: Option<WarningLevel>) -> Result<Self, KclError> {
322        Ok(Self {
323            ast: self.ast.change_experimental_features(warning_level)?,
324            original_file_contents: self.original_file_contents.clone(),
325        })
326    }
327
328    pub fn is_empty_or_only_settings(&self) -> bool {
329        self.ast.is_empty_or_only_settings()
330    }
331
332    pub fn lint_all(&self) -> Result<Vec<lint::Discovered>, anyhow::Error> {
333        self.ast.lint_all()
334    }
335
336    pub fn lint<'a>(&'a self, rule: impl lint::Rule<'a>) -> Result<Vec<lint::Discovered>, anyhow::Error> {
337        self.ast.lint(rule)
338    }
339
340    pub fn node_path_from_range(&self, cached_body_items: usize, range: SourceRange) -> Option<NodePath> {
341        let module_infos = indexmap::IndexMap::new();
342        let programs = crate::execution::ProgramLookup::new(self.ast.clone(), module_infos);
343        NodePath::from_range(&programs, cached_body_items, range)
344    }
345
346    /// Fill node paths and consume the input so that the program without paths
347    /// isn't accidentally used. Filling node paths happens automatically during
348    /// parsing. Calling this is only needed after the caller invalidates the
349    /// node paths such as by mutating an AST or by making a round-trip through
350    /// serialization.
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}