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