2023-11-15 11:41:12 -06:00
|
|
|
//! Rust support for KCL (aka the KittyCAD Language).
|
|
|
|
//!
|
|
|
|
//! KCL is written in Rust. This crate contains the compiler tooling (e.g. parser, lexer, code generation),
|
|
|
|
//! the standard library implementation, a LSP implementation, generator for the docs, and more.
|
2023-09-19 14:20:14 -07:00
|
|
|
#![recursion_limit = "1024"]
|
2024-06-21 19:54:18 -07:00
|
|
|
#![allow(clippy::boxed_local)]
|
2023-09-19 14:20:14 -07:00
|
|
|
|
2024-06-23 19:19:24 -07:00
|
|
|
#[allow(unused_macros)]
|
2024-06-19 19:38:56 -07:00
|
|
|
macro_rules! println {
|
|
|
|
($($rest:tt)*) => {
|
2025-03-15 10:08:39 -07:00
|
|
|
#[cfg(all(feature = "disable-println", not(test)))]
|
2024-11-28 11:27:17 +13:00
|
|
|
{
|
|
|
|
let _ = format!($($rest)*);
|
|
|
|
}
|
2025-03-15 10:08:39 -07:00
|
|
|
#[cfg(any(not(feature = "disable-println"), test))]
|
2024-06-19 19:38:56 -07:00
|
|
|
std::println!($($rest)*)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-28 11:27:17 +13:00
|
|
|
#[allow(unused_macros)]
|
|
|
|
macro_rules! eprintln {
|
|
|
|
($($rest:tt)*) => {
|
2025-03-15 10:08:39 -07:00
|
|
|
#[cfg(all(feature = "disable-println", not(test)))]
|
2024-11-28 11:27:17 +13:00
|
|
|
{
|
|
|
|
let _ = format!($($rest)*);
|
|
|
|
}
|
2025-03-15 10:08:39 -07:00
|
|
|
#[cfg(any(not(feature = "disable-println"), test))]
|
2024-11-28 11:27:17 +13:00
|
|
|
std::eprintln!($($rest)*)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(unused_macros)]
|
|
|
|
macro_rules! print {
|
|
|
|
($($rest:tt)*) => {
|
2025-03-15 10:08:39 -07:00
|
|
|
#[cfg(all(feature = "disable-println", not(test)))]
|
2024-11-28 11:27:17 +13:00
|
|
|
{
|
|
|
|
let _ = format!($($rest)*);
|
|
|
|
}
|
2025-03-15 10:08:39 -07:00
|
|
|
#[cfg(any(not(feature = "disable-println"), test))]
|
2024-11-28 11:27:17 +13:00
|
|
|
std::print!($($rest)*)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(unused_macros)]
|
|
|
|
macro_rules! eprint {
|
|
|
|
($($rest:tt)*) => {
|
2025-03-15 10:08:39 -07:00
|
|
|
#[cfg(all(feature = "disable-println", not(test)))]
|
2024-11-28 11:27:17 +13:00
|
|
|
{
|
|
|
|
let _ = format!($($rest)*);
|
|
|
|
}
|
2025-03-15 10:08:39 -07:00
|
|
|
#[cfg(any(not(feature = "disable-println"), test))]
|
2024-11-28 11:27:17 +13:00
|
|
|
std::eprint!($($rest)*)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#[cfg(feature = "dhat-heap")]
|
|
|
|
#[global_allocator]
|
|
|
|
static ALLOC: dhat::Alloc = dhat::Alloc;
|
|
|
|
|
2024-11-20 15:19:25 +13:00
|
|
|
mod coredump;
|
|
|
|
mod docs;
|
|
|
|
mod engine;
|
|
|
|
mod errors;
|
2024-12-07 07:16:04 +13:00
|
|
|
mod execution;
|
2024-11-20 15:19:25 +13:00
|
|
|
mod fs;
|
2024-06-11 19:23:35 -04:00
|
|
|
pub mod lint;
|
2024-11-28 11:27:17 +13:00
|
|
|
mod log;
|
2024-11-20 15:19:25 +13:00
|
|
|
mod lsp;
|
2025-02-11 13:52:46 +13:00
|
|
|
mod modules;
|
2024-12-05 17:56:49 +13:00
|
|
|
mod parsing;
|
2024-11-20 15:19:25 +13:00
|
|
|
mod settings;
|
KCL: New simulation test pipeline (#4351)
The idea behind this is to test all the various stages of executing KCL
separately, i.e.
- Start with a program
- Tokenize it
- Parse those tokens into an AST
- Recast the AST
- Execute the AST, outputting
- a PNG of the rendered model
- serialized program memory
Each of these steps reads some input and writes some output to disk.
The output of one step becomes the input to the next step. These
intermediate artifacts are also snapshotted (like expectorate or 2020)
to ensure we're aware of any changes to how KCL works. A change could
be a bug, or it could be harmless, or deliberate, but keeping it checked
into the repo means we can easily track changes.
Note: UUIDs sent back by the engine are currently nondeterministic, so
they would break all the snapshot tests. So, the snapshots use a regex
filter and replace anything that looks like a uuid with [uuid] when
writing program memory to a snapshot. In the future I hope our UUIDs will
be seedable and easy to make deterministic. At that point, we can stop
filtering the UUIDs.
We run this pipeline on many different KCL programs. Each keeps its
inputs (KCL programs), outputs (PNG, program memory snapshot) and
intermediate artifacts (AST, token lists, etc) in that directory.
I also added a new `just` command to easily generate these tests.
You can run `just new-sim-test gear $(cat gear.kcl)` to set up a new
gear test directory and generate all the intermediate artifacts for the
first time. This doesn't need any macros, it just appends some new lines
of normal Rust source code to `tests.rs`, so it's easy to see exactly
what the code is doing.
This uses `cargo insta` for convenient snapshot testing of artifacts
as JSON, and `twenty-twenty` for snapshotting PNGs.
This was heavily inspired by Predrag Gruevski's talk at EuroRust 2024
about deterministic simulation testing, and how it can both reduce bugs
and also reduce testing/CI time. Very grateful to him for chatting with
me about this over the last couple of weeks.
2024-10-30 12:14:17 -05:00
|
|
|
#[cfg(test)]
|
|
|
|
mod simulation_tests;
|
2024-12-03 16:39:51 +13:00
|
|
|
mod source_range;
|
2025-01-16 11:10:36 -05:00
|
|
|
pub mod std;
|
2024-07-31 09:54:46 -05:00
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
2024-06-18 14:38:25 -05:00
|
|
|
pub mod test_server;
|
2024-11-20 15:19:25 +13:00
|
|
|
mod thread;
|
2024-09-07 12:51:35 -04:00
|
|
|
mod unparser;
|
2025-03-08 04:04:57 +13:00
|
|
|
mod walk;
|
2024-03-12 13:37:47 -07:00
|
|
|
#[cfg(target_arch = "wasm32")]
|
2024-11-20 15:19:25 +13:00
|
|
|
mod wasm;
|
|
|
|
|
|
|
|
pub use coredump::CoreDump;
|
|
|
|
pub use engine::{EngineManager, ExecutionKind};
|
2025-02-26 21:15:52 -08:00
|
|
|
pub use errors::{
|
|
|
|
CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs, Report, ReportWithOutputs,
|
|
|
|
};
|
2025-02-12 10:22:56 +13:00
|
|
|
pub use execution::{
|
|
|
|
bust_cache, clear_mem_cache, ExecOutcome, ExecState, ExecutorContext, ExecutorSettings, MetaSettings, Point2d,
|
|
|
|
};
|
2024-12-03 16:39:51 +13:00
|
|
|
pub use lsp::{
|
|
|
|
copilot::Backend as CopilotLspBackend,
|
|
|
|
kcl::{Backend as KclLspBackend, Server as KclLspServerSubCommand},
|
|
|
|
};
|
2025-02-11 13:52:46 +13:00
|
|
|
pub use modules::ModuleId;
|
2024-12-10 18:50:22 -08:00
|
|
|
pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions};
|
2024-11-20 15:19:25 +13:00
|
|
|
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
|
2025-02-11 13:52:46 +13:00
|
|
|
pub use source_range::SourceRange;
|
2024-11-20 15:19:25 +13:00
|
|
|
|
|
|
|
// Rather than make executor public and make lots of it pub(crate), just re-export into a new module.
|
|
|
|
// Ideally we wouldn't export these things at all, they should only be used for testing.
|
|
|
|
pub mod exec {
|
2025-02-13 11:59:57 +13:00
|
|
|
pub use crate::execution::{ArtifactCommand, DefaultPlanes, IdGenerator, KclValue, PlaneType, Sketch};
|
2024-11-20 15:19:25 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(target_arch = "wasm32")]
|
|
|
|
pub mod wasm_engine {
|
2024-12-03 16:39:51 +13:00
|
|
|
pub use crate::{
|
|
|
|
coredump::wasm::{CoreDumpManager, CoreDumper},
|
|
|
|
engine::conn_wasm::{EngineCommandManager, EngineConnection},
|
2025-03-15 10:08:39 -07:00
|
|
|
fs::wasm::{FileManager, FileSystemManager},
|
2024-12-03 16:39:51 +13:00
|
|
|
};
|
2024-11-20 15:19:25 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
|
|
pub mod native_engine {
|
|
|
|
pub use crate::engine::conn::EngineConnection;
|
|
|
|
}
|
|
|
|
|
|
|
|
pub mod std_utils {
|
|
|
|
pub use crate::std::utils::{get_tangential_arc_to_info, is_points_ccw_wasm, TangentialArcInfoInput};
|
|
|
|
}
|
|
|
|
|
2025-01-31 10:45:39 -05:00
|
|
|
pub mod pretty {
|
2025-01-31 13:11:15 -08:00
|
|
|
pub use crate::{parsing::token::NumericSuffix, unparser::format_number};
|
2025-01-31 10:45:39 -05:00
|
|
|
}
|
|
|
|
|
2024-12-03 16:39:51 +13:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
2024-11-28 11:27:17 +13:00
|
|
|
#[allow(unused_imports)]
|
|
|
|
use crate::log::{log, logln};
|
2024-11-20 15:19:25 +13:00
|
|
|
|
|
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
|
|
pub struct Program {
|
|
|
|
#[serde(flatten)]
|
2024-12-05 19:51:06 -08:00
|
|
|
pub ast: parsing::ast::types::Node<parsing::ast::types::Program>,
|
2025-02-27 14:34:01 -08:00
|
|
|
// The ui doesn't need to know about this.
|
|
|
|
// It's purely used for saving the contents of the original file, so we can use it for errors.
|
|
|
|
// Because in the case of the root file, we don't want to read the file from disk again.
|
|
|
|
#[serde(skip)]
|
|
|
|
pub original_file_contents: String,
|
2024-11-20 15:19:25 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
#[cfg(any(test, feature = "lsp-test-util"))]
|
|
|
|
pub use lsp::test_util::copilot_lsp_server;
|
|
|
|
#[cfg(any(test, feature = "lsp-test-util"))]
|
|
|
|
pub use lsp::test_util::kcl_lsp_server;
|
|
|
|
|
|
|
|
impl Program {
|
2024-12-06 13:57:31 +13:00
|
|
|
pub fn parse(input: &str) -> Result<(Option<Program>, Vec<CompilationError>), KclError> {
|
|
|
|
let module_id = ModuleId::default();
|
2024-12-10 14:26:53 +13:00
|
|
|
let tokens = parsing::token::lex(input, module_id)?;
|
2024-12-06 13:57:31 +13:00
|
|
|
let (ast, errs) = parsing::parse_tokens(tokens).0?;
|
|
|
|
|
2025-02-27 14:34:01 -08:00
|
|
|
Ok((
|
|
|
|
ast.map(|ast| Program {
|
|
|
|
ast,
|
|
|
|
original_file_contents: input.to_string(),
|
|
|
|
}),
|
|
|
|
errs,
|
|
|
|
))
|
2024-12-06 13:57:31 +13:00
|
|
|
}
|
|
|
|
|
|
|
|
pub fn parse_no_errs(input: &str) -> Result<Program, KclError> {
|
2024-11-20 15:19:25 +13:00
|
|
|
let module_id = ModuleId::default();
|
2024-12-10 14:26:53 +13:00
|
|
|
let tokens = parsing::token::lex(input, module_id)?;
|
2024-12-05 17:56:49 +13:00
|
|
|
let ast = parsing::parse_tokens(tokens).parse_errs_as_err()?;
|
2024-11-20 15:19:25 +13:00
|
|
|
|
2025-02-27 14:34:01 -08:00
|
|
|
Ok(Program {
|
|
|
|
ast,
|
|
|
|
original_file_contents: input.to_string(),
|
|
|
|
})
|
2024-11-20 15:19:25 +13:00
|
|
|
}
|
|
|
|
|
2024-12-05 17:56:49 +13:00
|
|
|
pub fn compute_digest(&mut self) -> parsing::ast::digest::Digest {
|
2024-11-20 15:19:25 +13:00
|
|
|
self.ast.compute_digest()
|
|
|
|
}
|
|
|
|
|
2025-01-31 13:11:15 -08:00
|
|
|
/// Get the meta settings for the kcl file from the annotations.
|
2025-02-06 12:37:13 -05:00
|
|
|
pub fn meta_settings(&self) -> Result<Option<crate::MetaSettings>, KclError> {
|
2025-02-06 19:54:58 -05:00
|
|
|
self.ast.meta_settings()
|
2025-01-31 13:11:15 -08:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Change the meta settings for the kcl file.
|
2025-02-27 14:34:01 -08:00
|
|
|
pub fn change_meta_settings(&self, settings: crate::MetaSettings) -> Result<Self, KclError> {
|
2025-01-31 13:11:15 -08:00
|
|
|
Ok(Self {
|
|
|
|
ast: self.ast.change_meta_settings(settings)?,
|
2025-02-27 14:34:01 -08:00
|
|
|
original_file_contents: self.original_file_contents.clone(),
|
2025-01-31 13:11:15 -08:00
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2024-11-20 15:19:25 +13:00
|
|
|
pub fn lint_all(&self) -> Result<Vec<lint::Discovered>, anyhow::Error> {
|
|
|
|
self.ast.lint_all()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn lint<'a>(&'a self, rule: impl lint::Rule<'a>) -> Result<Vec<lint::Discovered>, anyhow::Error> {
|
|
|
|
self.ast.lint(rule)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn recast(&self) -> String {
|
|
|
|
// Use the default options until we integrate into the UI the ability to change them.
|
|
|
|
self.ast.recast(&Default::default(), 0)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn recast_with_options(&self, options: &FormatOptions) -> String {
|
|
|
|
self.ast.recast(options, 0)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-11-25 10:50:43 +13:00
|
|
|
#[inline]
|
|
|
|
fn try_f64_to_usize(f: f64) -> Option<usize> {
|
|
|
|
let i = f as usize;
|
|
|
|
if i as f64 == f {
|
|
|
|
Some(i)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn try_f64_to_u32(f: f64) -> Option<u32> {
|
|
|
|
let i = f as u32;
|
|
|
|
if i as f64 == f {
|
|
|
|
Some(i)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn try_f64_to_u64(f: f64) -> Option<u64> {
|
|
|
|
let i = f as u64;
|
|
|
|
if i as f64 == f {
|
|
|
|
Some(i)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn try_f64_to_i64(f: f64) -> Option<i64> {
|
|
|
|
let i = f as i64;
|
|
|
|
if i as f64 == f {
|
|
|
|
Some(i)
|
|
|
|
} else {
|
|
|
|
None
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-02-12 15:55:34 -08:00
|
|
|
/// Get the version of the KCL library.
|
|
|
|
pub fn version() -> &'static str {
|
|
|
|
env!("CARGO_PKG_VERSION")
|
|
|
|
}
|
|
|
|
|
2024-11-25 10:50:43 +13:00
|
|
|
#[cfg(test)]
|
|
|
|
mod test {
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn convert_int() {
|
|
|
|
assert_eq!(try_f64_to_usize(0.0), Some(0));
|
|
|
|
assert_eq!(try_f64_to_usize(42.0), Some(42));
|
|
|
|
assert_eq!(try_f64_to_usize(0.00000000001), None);
|
|
|
|
assert_eq!(try_f64_to_usize(-1.0), None);
|
|
|
|
assert_eq!(try_f64_to_usize(f64::NAN), None);
|
|
|
|
assert_eq!(try_f64_to_usize(f64::INFINITY), None);
|
|
|
|
assert_eq!(try_f64_to_usize((0.1 + 0.2) * 10.0), None);
|
|
|
|
|
|
|
|
assert_eq!(try_f64_to_u32(0.0), Some(0));
|
|
|
|
assert_eq!(try_f64_to_u32(42.0), Some(42));
|
|
|
|
assert_eq!(try_f64_to_u32(0.00000000001), None);
|
|
|
|
assert_eq!(try_f64_to_u32(-1.0), None);
|
|
|
|
assert_eq!(try_f64_to_u32(f64::NAN), None);
|
|
|
|
assert_eq!(try_f64_to_u32(f64::INFINITY), None);
|
|
|
|
assert_eq!(try_f64_to_u32((0.1 + 0.2) * 10.0), None);
|
|
|
|
|
|
|
|
assert_eq!(try_f64_to_u64(0.0), Some(0));
|
|
|
|
assert_eq!(try_f64_to_u64(42.0), Some(42));
|
|
|
|
assert_eq!(try_f64_to_u64(0.00000000001), None);
|
|
|
|
assert_eq!(try_f64_to_u64(-1.0), None);
|
|
|
|
assert_eq!(try_f64_to_u64(f64::NAN), None);
|
|
|
|
assert_eq!(try_f64_to_u64(f64::INFINITY), None);
|
|
|
|
assert_eq!(try_f64_to_u64((0.1 + 0.2) * 10.0), None);
|
|
|
|
|
|
|
|
assert_eq!(try_f64_to_i64(0.0), Some(0));
|
|
|
|
assert_eq!(try_f64_to_i64(42.0), Some(42));
|
|
|
|
assert_eq!(try_f64_to_i64(0.00000000001), None);
|
|
|
|
assert_eq!(try_f64_to_i64(-1.0), Some(-1));
|
|
|
|
assert_eq!(try_f64_to_i64(f64::NAN), None);
|
|
|
|
assert_eq!(try_f64_to_i64(f64::INFINITY), None);
|
|
|
|
assert_eq!(try_f64_to_i64((0.1 + 0.2) * 10.0), None);
|
|
|
|
}
|
|
|
|
}
|