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