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