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