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