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