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 coredump;
61mod docs;
62mod engine;
63mod errors;
64mod execution;
65mod fmt;
66mod frontend;
67mod fs;
68#[cfg(feature = "artifact-graph")]
69pub(crate) mod id;
70pub mod lint;
71mod log;
72mod lsp;
73mod modules;
74mod parsing;
75mod project;
76mod settings;
77#[cfg(test)]
78mod simulation_tests;
79pub mod std;
80#[cfg(not(target_arch = "wasm32"))]
81pub mod test_server;
82mod thread;
83mod unparser;
84#[cfg(test)]
85mod variant_name;
86pub mod walk;
87#[cfg(target_arch = "wasm32")]
88mod wasm;
89
90pub use coredump::CoreDump;
91pub use engine::{AsyncTasks, EngineManager, EngineStats};
92pub use errors::{
93    BacktraceItem, CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs, Report,
94    ReportWithOutputs,
95};
96pub use execution::{
97    ExecOutcome, ExecState, ExecutorContext, ExecutorSettings, MetaSettings, MockConfig, Point2d, bust_cache,
98    clear_mem_cache, transpile_all_old_sketches_to_new, transpile_old_sketch_to_new, transpile_old_sketch_to_new_ast,
99    transpile_old_sketch_to_new_with_execution, typed_path::TypedPath,
100};
101pub use kcl_error::SourceRange;
102pub use lsp::{
103    ToLspRange,
104    copilot::Backend as CopilotLspBackend,
105    kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
106};
107pub use modules::ModuleId;
108pub use parsing::ast::types::{FormatOptions, NodePath, Step as NodePathStep};
109pub use project::ProjectManager;
110pub use settings::types::{Configuration, project::ProjectConfiguration};
111#[cfg(not(target_arch = "wasm32"))]
112pub use unparser::{recast_dir, walk_dir};
113
114// Rather than make executor public and make lots of it pub(crate), just re-export into a new module.
115// Ideally we wouldn't export these things at all, they should only be used for testing.
116pub mod exec {
117    #[cfg(feature = "artifact-graph")]
118    pub use crate::execution::{ArtifactCommand, Operation};
119    pub use crate::execution::{
120        DefaultPlanes, IdGenerator, KclValue, PlaneKind, Sketch,
121        annotations::WarningLevel,
122        types::{NumericType, UnitType},
123    };
124}
125
126#[cfg(target_arch = "wasm32")]
127pub mod wasm_engine {
128    pub use crate::{
129        coredump::wasm::{CoreDumpManager, CoreDumper},
130        engine::conn_wasm::{EngineCommandManager, EngineConnection, ResponseContext},
131        fs::wasm::{FileManager, FileSystemManager},
132    };
133}
134
135pub mod mock_engine {
136    pub use crate::engine::conn_mock::EngineConnection;
137}
138
139#[cfg(not(target_arch = "wasm32"))]
140pub mod native_engine {
141    pub use crate::engine::conn::EngineConnection;
142}
143
144pub mod std_utils {
145    pub use crate::std::utils::{
146        TangentialArcInfoInput, get_tangential_arc_to_info, is_points_ccw_wasm, untyped_point_to_unit,
147    };
148}
149
150pub mod pretty {
151    pub use crate::{
152        fmt::{format_number_literal, format_number_value, human_display_number},
153        parsing::token::NumericSuffix,
154    };
155}
156
157pub mod front {
158    pub(crate) use crate::frontend::modify::{find_defined_names, next_free_name_using_max};
159    // Re-export trim module items
160    pub use crate::frontend::{
161        FrontendState, SetProgramOutcome,
162        api::{
163            Error, Expr, Face, File, FileId, LifecycleApi, Number, Object, ObjectId, ObjectKind, Plane, ProjectId,
164            Result, SceneGraph, SceneGraphDelta, Settings, SourceDelta, SourceRef, Version,
165        },
166        sketch::{
167            Angle, Arc, ArcCtor, Circle, CircleCtor, Coincident, Constraint, Distance, ExistingSegmentCtor, Freedom,
168            Horizontal, Line, LineCtor, LinesEqualLength, NewSegmentInfo, Parallel, Perpendicular, Point, Point2d,
169            PointCtor, Segment, SegmentCtor, Sketch, SketchApi, SketchCtor, StartOrEnd, TangentArcCtor, Vertical,
170        },
171        trim::{
172            ArcPoint, AttachToEndpoint, CoincidentData, ConstraintToMigrate, Coords2d, EndpointChanged, LineEndpoint,
173            TrimDirection, TrimItem, TrimOperation, TrimTermination, TrimTerminations, arc_arc_intersection,
174            execute_trim_loop_with_context, get_next_trim_spawn, get_position_coords_for_line,
175            get_position_coords_from_arc, get_trim_spawn_terminations, is_point_on_arc, is_point_on_line_segment,
176            line_arc_intersection, line_segment_intersection, perpendicular_distance_to_segment,
177            project_point_onto_arc, project_point_onto_segment,
178        },
179    };
180}
181
182#[cfg(feature = "cli")]
183use clap::ValueEnum;
184use serde::{Deserialize, Serialize};
185
186use crate::exec::WarningLevel;
187#[allow(unused_imports)]
188use crate::log::{log, logln};
189
190lazy_static::lazy_static! {
191
192    pub static ref IMPORT_FILE_EXTENSIONS: Vec<String> = {
193        let mut import_file_extensions = vec!["stp".to_string(), "glb".to_string(), "fbxb".to_string()];
194        #[cfg(feature = "cli")]
195        let named_extensions = kittycad::types::FileImportFormat::value_variants()
196            .iter()
197            .map(|x| format!("{x}"))
198            .collect::<Vec<String>>();
199        #[cfg(not(feature = "cli"))]
200        let named_extensions = vec![]; // We don't really need this outside of the CLI.
201        // Add all the default import formats.
202        import_file_extensions.extend_from_slice(&named_extensions);
203        import_file_extensions
204    };
205
206    pub static ref RELEVANT_FILE_EXTENSIONS: Vec<String> = {
207        let mut relevant_extensions = IMPORT_FILE_EXTENSIONS.clone();
208        relevant_extensions.push("kcl".to_string());
209        relevant_extensions
210    };
211}
212
213#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
214pub struct Program {
215    #[serde(flatten)]
216    pub ast: parsing::ast::types::Node<parsing::ast::types::Program>,
217    // The ui doesn't need to know about this.
218    // It's purely used for saving the contents of the original file, so we can use it for errors.
219    // Because in the case of the root file, we don't want to read the file from disk again.
220    #[serde(skip)]
221    pub original_file_contents: String,
222}
223
224#[cfg(any(test, feature = "lsp-test-util"))]
225pub use lsp::test_util::copilot_lsp_server;
226#[cfg(any(test, feature = "lsp-test-util"))]
227pub use lsp::test_util::kcl_lsp_server;
228
229impl Program {
230    pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationError>), KclError> {
231        let module_id = ModuleId::default();
232        let (ast, errs) = parsing::parse_str(input, module_id).0?;
233
234        Ok((
235            ast.map(|ast| Program {
236                ast,
237                original_file_contents: input.to_string(),
238            }),
239            errs,
240        ))
241    }
242
243    pub fn parse_no_errs(input: &str) -> Result<Program, KclError> {
244        let module_id = ModuleId::default();
245        let ast = parsing::parse_str(input, module_id).parse_errs_as_err()?;
246
247        Ok(Program {
248            ast,
249            original_file_contents: input.to_string(),
250        })
251    }
252
253    pub fn compute_digest(&mut self) -> parsing::ast::digest::Digest {
254        self.ast.compute_digest()
255    }
256
257    /// Get the meta settings for the kcl file from the annotations.
258    pub fn meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
259        self.ast.meta_settings()
260    }
261
262    /// Change the meta settings for the kcl file.
263    pub fn change_default_units(
264        &self,
265        length_units: Option<kittycad_modeling_cmds::units::UnitLength>,
266    ) -> Result<Self, KclError> {
267        Ok(Self {
268            ast: self.ast.change_default_units(length_units)?,
269            original_file_contents: self.original_file_contents.clone(),
270        })
271    }
272
273    pub fn change_experimental_features(&self, warning_level: Option<WarningLevel>) -> Result<Self, KclError> {
274        Ok(Self {
275            ast: self.ast.change_experimental_features(warning_level)?,
276            original_file_contents: self.original_file_contents.clone(),
277        })
278    }
279
280    pub fn is_empty_or_only_settings(&self) -> bool {
281        self.ast.is_empty_or_only_settings()
282    }
283
284    pub fn lint_all(&self) -> Result<Vec<lint::Discovered>, anyhow::Error> {
285        self.ast.lint_all()
286    }
287
288    pub fn lint<'a>(&'a self, rule: impl lint::Rule<'a>) -> Result<Vec<lint::Discovered>, anyhow::Error> {
289        self.ast.lint(rule)
290    }
291
292    #[cfg(feature = "artifact-graph")]
293    pub fn node_path_from_range(&self, cached_body_items: usize, range: SourceRange) -> Option<NodePath> {
294        let module_infos = indexmap::IndexMap::new();
295        let programs = crate::execution::ProgramLookup::new(self.ast.clone(), module_infos);
296        NodePath::from_range(&programs, cached_body_items, range)
297    }
298
299    pub fn recast(&self) -> String {
300        // Use the default options until we integrate into the UI the ability to change them.
301        self.ast.recast_top(&Default::default(), 0)
302    }
303
304    pub fn recast_with_options(&self, options: &FormatOptions) -> String {
305        self.ast.recast_top(options, 0)
306    }
307
308    /// Create an empty program.
309    pub fn empty() -> Self {
310        Self {
311            ast: parsing::ast::types::Node::no_src(parsing::ast::types::Program::default()),
312            original_file_contents: String::new(),
313        }
314    }
315}
316
317#[inline]
318fn try_f64_to_usize(f: f64) -> Option<usize> {
319    let i = f as usize;
320    if i as f64 == f { Some(i) } else { None }
321}
322
323#[inline]
324fn try_f64_to_u32(f: f64) -> Option<u32> {
325    let i = f as u32;
326    if i as f64 == f { Some(i) } else { None }
327}
328
329#[inline]
330fn try_f64_to_u64(f: f64) -> Option<u64> {
331    let i = f as u64;
332    if i as f64 == f { Some(i) } else { None }
333}
334
335#[inline]
336fn try_f64_to_i64(f: f64) -> Option<i64> {
337    let i = f as i64;
338    if i as f64 == f { Some(i) } else { None }
339}
340
341/// Get the version of the KCL library.
342pub fn version() -> &'static str {
343    env!("CARGO_PKG_VERSION")
344}
345
346#[cfg(test)]
347mod test {
348    use super::*;
349
350    #[test]
351    fn convert_int() {
352        assert_eq!(try_f64_to_usize(0.0), Some(0));
353        assert_eq!(try_f64_to_usize(42.0), Some(42));
354        assert_eq!(try_f64_to_usize(0.00000000001), None);
355        assert_eq!(try_f64_to_usize(-1.0), None);
356        assert_eq!(try_f64_to_usize(f64::NAN), None);
357        assert_eq!(try_f64_to_usize(f64::INFINITY), None);
358        assert_eq!(try_f64_to_usize((0.1 + 0.2) * 10.0), None);
359
360        assert_eq!(try_f64_to_u32(0.0), Some(0));
361        assert_eq!(try_f64_to_u32(42.0), Some(42));
362        assert_eq!(try_f64_to_u32(0.00000000001), None);
363        assert_eq!(try_f64_to_u32(-1.0), None);
364        assert_eq!(try_f64_to_u32(f64::NAN), None);
365        assert_eq!(try_f64_to_u32(f64::INFINITY), None);
366        assert_eq!(try_f64_to_u32((0.1 + 0.2) * 10.0), None);
367
368        assert_eq!(try_f64_to_u64(0.0), Some(0));
369        assert_eq!(try_f64_to_u64(42.0), Some(42));
370        assert_eq!(try_f64_to_u64(0.00000000001), None);
371        assert_eq!(try_f64_to_u64(-1.0), None);
372        assert_eq!(try_f64_to_u64(f64::NAN), None);
373        assert_eq!(try_f64_to_u64(f64::INFINITY), None);
374        assert_eq!(try_f64_to_u64((0.1 + 0.2) * 10.0), None);
375
376        assert_eq!(try_f64_to_i64(0.0), Some(0));
377        assert_eq!(try_f64_to_i64(42.0), Some(42));
378        assert_eq!(try_f64_to_i64(0.00000000001), None);
379        assert_eq!(try_f64_to_i64(-1.0), Some(-1));
380        assert_eq!(try_f64_to_i64(f64::NAN), None);
381        assert_eq!(try_f64_to_i64(f64::INFINITY), None);
382        assert_eq!(try_f64_to_i64((0.1 + 0.2) * 10.0), None);
383    }
384}