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