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