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