#[cfg(feature = "artifact-graph")]
use std::collections::BTreeMap;
use std::sync::Arc;
use anyhow::Result;
#[cfg(feature = "artifact-graph")]
pub use artifact::Artifact;
#[cfg(feature = "artifact-graph")]
pub use artifact::ArtifactCommand;
#[cfg(feature = "artifact-graph")]
pub use artifact::ArtifactGraph;
#[cfg(feature = "artifact-graph")]
pub use artifact::CapSubType;
#[cfg(feature = "artifact-graph")]
pub use artifact::CodeRef;
#[cfg(feature = "artifact-graph")]
pub use artifact::SketchBlock;
#[cfg(feature = "artifact-graph")]
pub use artifact::SketchBlockConstraint;
#[cfg(feature = "artifact-graph")]
pub use artifact::SketchBlockConstraintType;
#[cfg(feature = "artifact-graph")]
pub use artifact::StartSketchOnFace;
#[cfg(feature = "artifact-graph")]
pub use artifact::StartSketchOnPlane;
use cache::GlobalState;
pub use cache::bust_cache;
pub use cache::clear_mem_cache;
#[cfg(feature = "artifact-graph")]
pub use cad_op::Group;
pub use cad_op::Operation;
pub use geometry::*;
pub use id_generator::IdGenerator;
pub(crate) use import::PreImportedGeometry;
use indexmap::IndexMap;
pub use kcl_value::KclObjectFields;
pub use kcl_value::KclValue;
use kcmc::ImageFormat;
use kcmc::ModelingCmd;
use kcmc::each_cmd as mcmd;
use kcmc::ok_response::OkModelingCmdResponse;
use kcmc::ok_response::output::TakeSnapshot;
use kcmc::websocket::ModelingSessionData;
use kcmc::websocket::OkWebSocketResponseData;
use kittycad_modeling_cmds::id::ModelingCmdId;
use kittycad_modeling_cmds::{self as kcmc};
pub use memory::EnvironmentRef;
pub(crate) use modeling::ModelingCmdMeta;
use serde::Deserialize;
use serde::Serialize;
pub(crate) use sketch_solve::normalize_to_solver_distance_unit;
pub(crate) use sketch_solve::solver_numeric_type;
pub use sketch_transpiler::pre_execute_transpile;
pub use sketch_transpiler::transpile_all_old_sketches_to_new;
pub use sketch_transpiler::transpile_old_sketch_to_new;
pub use sketch_transpiler::transpile_old_sketch_to_new_ast;
pub use sketch_transpiler::transpile_old_sketch_to_new_with_execution;
pub use state::ExecState;
pub use state::MetaSettings;
pub(crate) use state::ModuleArtifactState;
use uuid::Uuid;
use crate::CompilationIssue;
use crate::ExecError;
use crate::KclErrorWithOutputs;
use crate::NodePath;
use crate::SourceRange;
#[cfg(feature = "artifact-graph")]
use crate::collections::AhashIndexSet;
use crate::engine::EngineManager;
use crate::engine::GridScaleBehavior;
use crate::errors::KclError;
use crate::errors::KclErrorDetails;
use crate::execution::cache::CacheInformation;
use crate::execution::cache::CacheResult;
use crate::execution::import_graph::Universe;
use crate::execution::import_graph::UniverseMap;
use crate::execution::typed_path::TypedPath;
#[cfg(feature = "artifact-graph")]
use crate::front::Number;
use crate::front::Object;
use crate::front::ObjectId;
use crate::fs::FileManager;
use crate::modules::ModuleExecutionOutcome;
use crate::modules::ModuleId;
use crate::modules::ModulePath;
use crate::modules::ModuleRepr;
use crate::parsing::ast::types::Expr;
use crate::parsing::ast::types::ImportPath;
use crate::parsing::ast::types::NodeRef;
pub(crate) mod annotations;
#[cfg(feature = "artifact-graph")]
mod artifact;
pub(crate) mod cache;
mod cad_op;
mod exec_ast;
pub mod fn_call;
#[cfg(test)]
#[cfg(feature = "artifact-graph")]
mod freedom_analysis_tests;
mod geometry;
mod id_generator;
mod import;
mod import_graph;
pub(crate) mod kcl_value;
mod memory;
mod modeling;
mod sketch_solve;
mod sketch_transpiler;
mod state;
pub mod typed_path;
pub(crate) mod types;
pub(crate) const SKETCH_BLOCK_PARAM_ON: &str = "on";
pub(crate) const SKETCH_OBJECT_META: &str = "meta";
pub(crate) const SKETCH_OBJECT_META_SKETCH: &str = "sketch";
macro_rules! control_continue {
($control_flow:expr) => {{
let cf = $control_flow;
if cf.is_some_return() {
return Ok(cf);
} else {
cf.into_value()
}
}};
}
pub(crate) use control_continue;
macro_rules! early_return {
($control_flow:expr) => {{
let cf = $control_flow;
if cf.is_some_return() {
return Err(EarlyReturn::from(cf));
} else {
cf.into_value()
}
}};
}
pub(crate) use early_return;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize)]
pub enum ControlFlowKind {
#[default]
Continue,
Exit,
}
impl ControlFlowKind {
pub fn is_some_return(&self) -> bool {
match self {
ControlFlowKind::Continue => false,
ControlFlowKind::Exit => true,
}
}
}
#[must_use = "You should always handle the control flow value when it is returned"]
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct KclValueControlFlow {
value: KclValue,
pub control: ControlFlowKind,
}
impl KclValue {
pub(crate) fn continue_(self) -> KclValueControlFlow {
KclValueControlFlow {
value: self,
control: ControlFlowKind::Continue,
}
}
pub(crate) fn exit(self) -> KclValueControlFlow {
KclValueControlFlow {
value: self,
control: ControlFlowKind::Exit,
}
}
}
impl KclValueControlFlow {
pub fn is_some_return(&self) -> bool {
self.control.is_some_return()
}
pub(crate) fn into_value(self) -> KclValue {
self.value
}
}
#[must_use = "You should always handle the control flow value when it is returned"]
#[derive(Debug, Clone)]
pub(crate) enum EarlyReturn {
Value(KclValueControlFlow),
Error(KclError),
}
impl From<KclValueControlFlow> for EarlyReturn {
fn from(cf: KclValueControlFlow) -> Self {
EarlyReturn::Value(cf)
}
}
impl From<KclError> for EarlyReturn {
fn from(err: KclError) -> Self {
EarlyReturn::Error(err)
}
}
pub(crate) enum StatementKind<'a> {
Declaration { name: &'a str },
Expression,
}
#[derive(Debug, Clone, Copy)]
pub enum PreserveMem {
Normal,
Always,
}
impl PreserveMem {
fn normal(self) -> bool {
match self {
PreserveMem::Normal => true,
PreserveMem::Always => false,
}
}
}
#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct ExecOutcome {
pub variables: IndexMap<String, KclValue>,
#[cfg(feature = "artifact-graph")]
pub operations: Vec<Operation>,
#[cfg(feature = "artifact-graph")]
pub artifact_graph: ArtifactGraph,
#[cfg(feature = "artifact-graph")]
#[serde(skip)]
pub scene_objects: Vec<Object>,
#[cfg(feature = "artifact-graph")]
#[serde(skip)]
pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
#[cfg(feature = "artifact-graph")]
#[serde(skip)]
pub var_solutions: Vec<(SourceRange, Number)>,
pub issues: Vec<CompilationIssue>,
pub filenames: IndexMap<ModuleId, ModulePath>,
pub default_planes: Option<DefaultPlanes>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum SegmentFreedom {
Free,
Fixed,
Conflict,
Error,
}
impl From<crate::front::Freedom> for SegmentFreedom {
fn from(f: crate::front::Freedom) -> Self {
match f {
crate::front::Freedom::Free => Self::Free,
crate::front::Freedom::Fixed => Self::Fixed,
crate::front::Freedom::Conflict => Self::Conflict,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum ConstraintKind {
FullyConstrained,
UnderConstrained,
OverConstrained,
Error,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SketchConstraintStatus {
pub name: String,
pub status: ConstraintKind,
pub free_count: usize,
pub conflict_count: usize,
pub total_count: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SketchConstraintReport {
pub fully_constrained: Vec<SketchConstraintStatus>,
pub under_constrained: Vec<SketchConstraintStatus>,
pub over_constrained: Vec<SketchConstraintStatus>,
pub errors: Vec<SketchConstraintStatus>,
}
#[cfg(feature = "artifact-graph")]
pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
use crate::front::ObjectKind;
use crate::front::Segment;
let lookup = |id: ObjectId| -> Option<crate::front::Freedom> {
let obj = scene_objects.get(id.0)?;
if let ObjectKind::Segment {
segment: Segment::Point(p),
} = &obj.kind
{
Some(p.freedom())
} else {
None
}
};
let mut fully_constrained = Vec::new();
let mut under_constrained = Vec::new();
let mut over_constrained = Vec::new();
let mut errors = Vec::new();
for obj in scene_objects {
let ObjectKind::Sketch(sketch) = &obj.kind else {
continue;
};
let mut free_count: usize = 0;
let mut conflict_count: usize = 0;
let mut error_count: usize = 0;
let mut total_count: usize = 0;
for &seg_id in &sketch.segments {
let Some(seg_obj) = scene_objects.get(seg_id.0) else {
continue;
};
let ObjectKind::Segment { segment } = &seg_obj.kind else {
continue;
};
if let Segment::Point(p) = segment
&& p.owner.is_some()
{
continue;
}
let freedom = segment
.freedom(lookup)
.map(SegmentFreedom::from)
.unwrap_or(SegmentFreedom::Error);
total_count += 1;
match freedom {
SegmentFreedom::Free => free_count += 1,
SegmentFreedom::Conflict => conflict_count += 1,
SegmentFreedom::Error => error_count += 1,
SegmentFreedom::Fixed => {}
}
}
let status = if error_count > 0 {
ConstraintKind::Error
} else if conflict_count > 0 {
ConstraintKind::OverConstrained
} else if free_count > 0 {
ConstraintKind::UnderConstrained
} else {
ConstraintKind::FullyConstrained
};
let entry = SketchConstraintStatus {
name: obj.label.clone(),
status,
free_count,
conflict_count,
total_count,
};
match status {
ConstraintKind::FullyConstrained => fully_constrained.push(entry),
ConstraintKind::UnderConstrained => under_constrained.push(entry),
ConstraintKind::OverConstrained => over_constrained.push(entry),
ConstraintKind::Error => errors.push(entry),
}
}
SketchConstraintReport {
fully_constrained,
under_constrained,
over_constrained,
errors,
}
}
impl ExecOutcome {
pub fn scene_object_by_id(&self, id: ObjectId) -> Option<&Object> {
#[cfg(feature = "artifact-graph")]
{
debug_assert!(
id.0 < self.scene_objects.len(),
"Requested object ID {} but only have {} objects",
id.0,
self.scene_objects.len()
);
self.scene_objects.get(id.0)
}
#[cfg(not(feature = "artifact-graph"))]
{
let _ = id;
None
}
}
pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
self.issues.iter().filter(|error| error.is_err())
}
#[cfg(feature = "artifact-graph")]
pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
sketch_constraint_report_from_scene_objects(&self.scene_objects)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MockConfig {
pub use_prev_memory: bool,
pub sketch_block_id: Option<ObjectId>,
pub freedom_analysis: bool,
#[cfg(feature = "artifact-graph")]
pub segment_ids_edited: AhashIndexSet<ObjectId>,
}
impl Default for MockConfig {
fn default() -> Self {
Self {
use_prev_memory: true,
sketch_block_id: None,
freedom_analysis: true,
#[cfg(feature = "artifact-graph")]
segment_ids_edited: AhashIndexSet::default(),
}
}
}
impl MockConfig {
pub fn new_sketch_mode(sketch_block_id: ObjectId) -> Self {
Self {
sketch_block_id: Some(sketch_block_id),
..Default::default()
}
}
#[must_use]
pub(crate) fn no_freedom_analysis(mut self) -> Self {
self.freedom_analysis = false;
self
}
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct DefaultPlanes {
pub xy: uuid::Uuid,
pub xz: uuid::Uuid,
pub yz: uuid::Uuid,
pub neg_xy: uuid::Uuid,
pub neg_xz: uuid::Uuid,
pub neg_yz: uuid::Uuid,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct TagIdentifier {
pub value: String,
#[serde(skip)]
pub info: Vec<(usize, TagEngineInfo)>,
#[serde(skip)]
pub meta: Vec<Metadata>,
}
impl TagIdentifier {
pub fn get_info(&self, at_epoch: usize) -> Option<&TagEngineInfo> {
for (e, info) in self.info.iter().rev() {
if *e <= at_epoch {
return Some(info);
}
}
None
}
pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
self.info.last().map(|i| &i.1)
}
pub fn get_all_cur_info(&self) -> Vec<&TagEngineInfo> {
let Some(cur_epoch) = self.info.last().map(|(e, _)| *e) else {
return vec![];
};
self.info
.iter()
.rev()
.take_while(|(e, _)| *e == cur_epoch)
.map(|(_, info)| info)
.collect()
}
pub fn merge_info(&mut self, other: &TagIdentifier) {
assert_eq!(&self.value, &other.value);
for (oe, ot) in &other.info {
if let Some((e, t)) = self.info.last_mut() {
if *e > *oe {
continue;
}
if e == oe {
*t = ot.clone();
continue;
}
}
self.info.push((*oe, ot.clone()));
}
}
pub fn geometry(&self) -> Option<Geometry> {
self.get_cur_info().map(|info| info.geometry.clone())
}
}
impl Eq for TagIdentifier {}
impl std::fmt::Display for TagIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.value)
}
}
impl std::str::FromStr for TagIdentifier {
type Err = KclError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self {
value: s.to_string(),
info: Vec::new(),
meta: Default::default(),
})
}
}
impl Ord for TagIdentifier {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.value.cmp(&other.value)
}
}
impl PartialOrd for TagIdentifier {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl std::hash::Hash for TagIdentifier {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.value.hash(state);
}
}
#[derive(Debug, Clone, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
pub struct TagEngineInfo {
pub id: uuid::Uuid,
pub geometry: Geometry,
pub path: Option<Path>,
pub surface: Option<ExtrudeSurface>,
}
#[derive(Debug, Copy, Clone, Deserialize, Serialize, PartialEq)]
pub enum BodyType {
Root,
Block,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, Eq, Copy)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct Metadata {
pub source_range: SourceRange,
}
impl From<Metadata> for Vec<SourceRange> {
fn from(meta: Metadata) -> Self {
vec![meta.source_range]
}
}
impl From<&Metadata> for SourceRange {
fn from(meta: &Metadata) -> Self {
meta.source_range
}
}
impl From<SourceRange> for Metadata {
fn from(source_range: SourceRange) -> Self {
Self { source_range }
}
}
impl<T> From<NodeRef<'_, T>> for Metadata {
fn from(node: NodeRef<'_, T>) -> Self {
Self {
source_range: SourceRange::new(node.start, node.end, node.module_id),
}
}
}
impl From<&Expr> for Metadata {
fn from(expr: &Expr) -> Self {
Self {
source_range: SourceRange::from(expr),
}
}
}
impl Metadata {
pub fn to_source_ref(meta: &[Metadata], node_path: Option<NodePath>) -> crate::front::SourceRef {
if meta.len() == 1 {
let meta = &meta[0];
return crate::front::SourceRef::Simple {
range: meta.source_range,
node_path,
};
}
crate::front::SourceRef::BackTrace {
ranges: meta.iter().map(|m| (m.source_range, node_path.clone())).collect(),
}
}
}
#[derive(PartialEq, Debug, Default, Clone)]
pub enum ContextType {
#[default]
Live,
Mock,
MockCustomForwarded,
}
#[derive(Debug, Clone)]
pub struct ExecutorContext {
pub engine: Arc<Box<dyn EngineManager>>,
pub fs: Arc<FileManager>,
pub settings: ExecutorSettings,
pub context_type: ContextType,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS)]
#[ts(export)]
pub struct ExecutorSettings {
pub highlight_edges: bool,
pub enable_ssao: bool,
pub show_grid: bool,
pub replay: Option<String>,
pub project_directory: Option<TypedPath>,
pub current_file: Option<TypedPath>,
pub fixed_size_grid: bool,
}
impl Default for ExecutorSettings {
fn default() -> Self {
Self {
highlight_edges: true,
enable_ssao: false,
show_grid: false,
replay: None,
project_directory: None,
current_file: None,
fixed_size_grid: true,
}
}
}
impl From<crate::settings::types::Configuration> for ExecutorSettings {
fn from(config: crate::settings::types::Configuration) -> Self {
Self::from(config.settings)
}
}
impl From<crate::settings::types::Settings> for ExecutorSettings {
fn from(settings: crate::settings::types::Settings) -> Self {
Self {
highlight_edges: settings.modeling.highlight_edges.into(),
enable_ssao: settings.modeling.enable_ssao.into(),
show_grid: settings.modeling.show_scale_grid,
replay: None,
project_directory: None,
current_file: None,
fixed_size_grid: settings.modeling.fixed_size_grid,
}
}
}
impl From<crate::settings::types::project::ProjectConfiguration> for ExecutorSettings {
fn from(config: crate::settings::types::project::ProjectConfiguration) -> Self {
Self::from(config.settings.modeling)
}
}
impl From<crate::settings::types::ModelingSettings> for ExecutorSettings {
fn from(modeling: crate::settings::types::ModelingSettings) -> Self {
Self {
highlight_edges: modeling.highlight_edges.into(),
enable_ssao: modeling.enable_ssao.into(),
show_grid: modeling.show_scale_grid,
replay: None,
project_directory: None,
current_file: None,
fixed_size_grid: true,
}
}
}
impl From<crate::settings::types::project::ProjectModelingSettings> for ExecutorSettings {
fn from(modeling: crate::settings::types::project::ProjectModelingSettings) -> Self {
Self {
highlight_edges: modeling.highlight_edges.into(),
enable_ssao: modeling.enable_ssao.into(),
show_grid: Default::default(),
replay: None,
project_directory: None,
current_file: None,
fixed_size_grid: true,
}
}
}
impl ExecutorSettings {
pub fn with_current_file(&mut self, current_file: TypedPath) {
if current_file.extension() == Some("kcl") {
self.current_file = Some(current_file.clone());
if let Some(parent) = current_file.parent() {
self.project_directory = Some(parent);
} else {
self.project_directory = Some(TypedPath::from(""));
}
} else {
self.project_directory = Some(current_file);
}
}
}
impl ExecutorContext {
pub fn new_with_engine_and_fs(
engine: Arc<Box<dyn EngineManager>>,
fs: Arc<FileManager>,
settings: ExecutorSettings,
) -> Self {
ExecutorContext {
engine,
fs,
settings,
context_type: ContextType::Live,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn new_with_engine(engine: Arc<Box<dyn EngineManager>>, settings: ExecutorSettings) -> Self {
Self::new_with_engine_and_fs(engine, Arc::new(FileManager::new()), settings)
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn new(client: &kittycad::Client, settings: ExecutorSettings) -> Result<Self> {
let pr = std::env::var("ZOO_ENGINE_PR").ok().and_then(|s| s.parse().ok());
let (ws, _headers) = client
.modeling()
.commands_ws(kittycad::modeling::CommandsWsParams {
api_call_id: None,
fps: None,
order_independent_transparency: None,
post_effect: if settings.enable_ssao {
Some(kittycad::types::PostEffectType::Ssao)
} else {
None
},
replay: settings.replay.clone(),
show_grid: if settings.show_grid { Some(true) } else { None },
pool: None,
pr,
unlocked_framerate: None,
webrtc: Some(false),
video_res_width: None,
video_res_height: None,
})
.await?;
let engine: Arc<Box<dyn EngineManager>> =
Arc::new(Box::new(crate::engine::conn::EngineConnection::new(ws).await?));
Ok(Self::new_with_engine(engine, settings))
}
#[cfg(target_arch = "wasm32")]
pub fn new(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
Self::new_with_engine_and_fs(engine, fs, settings)
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_mock(settings: Option<ExecutorSettings>) -> Self {
ExecutorContext {
engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
fs: Arc::new(FileManager::new()),
settings: settings.unwrap_or_default(),
context_type: ContextType::Mock,
}
}
#[cfg(target_arch = "wasm32")]
pub fn new_mock(engine: Arc<Box<dyn EngineManager>>, fs: Arc<FileManager>, settings: ExecutorSettings) -> Self {
ExecutorContext {
engine,
fs,
settings,
context_type: ContextType::Mock,
}
}
#[cfg(target_arch = "wasm32")]
pub fn new_mock_for_lsp(
fs_manager: crate::fs::wasm::FileSystemManager,
settings: ExecutorSettings,
) -> Result<Self, String> {
use crate::mock_engine;
let mock_engine = Arc::new(Box::new(
mock_engine::EngineConnection::new().map_err(|e| format!("Failed to create mock engine: {:?}", e))?,
) as Box<dyn EngineManager>);
let fs = Arc::new(FileManager::new(fs_manager));
Ok(ExecutorContext {
engine: mock_engine,
fs,
settings,
context_type: ContextType::Mock,
})
}
#[cfg(not(target_arch = "wasm32"))]
pub fn new_forwarded_mock(engine: Arc<Box<dyn EngineManager>>) -> Self {
ExecutorContext {
engine,
fs: Arc::new(FileManager::new()),
settings: Default::default(),
context_type: ContextType::MockCustomForwarded,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_with_client(
settings: ExecutorSettings,
token: Option<String>,
engine_addr: Option<String>,
) -> Result<Self> {
let client = crate::engine::new_zoo_client(token, engine_addr)?;
let ctx = Self::new(&client, settings).await?;
Ok(ctx)
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_with_default_client() -> Result<Self> {
let ctx = Self::new_with_client(Default::default(), None, None).await?;
Ok(ctx)
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn new_for_unit_test(engine_addr: Option<String>) -> Result<Self> {
let ctx = ExecutorContext::new_with_client(
ExecutorSettings {
highlight_edges: true,
enable_ssao: false,
show_grid: false,
replay: None,
project_directory: None,
current_file: None,
fixed_size_grid: false,
},
None,
engine_addr,
)
.await?;
Ok(ctx)
}
pub fn is_mock(&self) -> bool {
self.context_type == ContextType::Mock || self.context_type == ContextType::MockCustomForwarded
}
pub async fn no_engine_commands(&self) -> bool {
self.is_mock()
}
pub async fn send_clear_scene(
&self,
exec_state: &mut ExecState,
source_range: crate::execution::SourceRange,
) -> Result<(), KclError> {
exec_state.mod_local.artifacts.clear();
exec_state.global.root_module_artifacts.clear();
exec_state.global.artifacts.clear();
self.engine
.clear_scene(&mut exec_state.mod_local.id_generator, source_range)
.await?;
if self.settings.enable_ssao {
let cmd_id = exec_state.next_uuid();
exec_state
.batch_modeling_cmd(
ModelingCmdMeta::with_id(exec_state, self, source_range, cmd_id),
ModelingCmd::from(mcmd::SetOrderIndependentTransparency::builder().enabled(false).build()),
)
.await?;
}
Ok(())
}
pub async fn bust_cache_and_reset_scene(&self) -> Result<ExecOutcome, KclErrorWithOutputs> {
cache::bust_cache().await;
let outcome = self.run_with_caching(crate::Program::empty()).await?;
Ok(outcome)
}
async fn prepare_mem(&self, exec_state: &mut ExecState) -> Result<(), KclErrorWithOutputs> {
self.eval_prelude(exec_state, SourceRange::synthetic())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
exec_state.mut_stack().push_new_root_env(true);
Ok(())
}
fn restore_mock_memory(
exec_state: &mut ExecState,
mem: cache::SketchModeState,
_mock_config: &MockConfig,
) -> Result<(), KclErrorWithOutputs> {
*exec_state.mut_stack() = mem.stack;
exec_state.global.module_infos = mem.module_infos;
exec_state.global.path_to_source_id = mem.path_to_source_id;
exec_state.global.id_to_source = mem.id_to_source;
#[cfg(feature = "artifact-graph")]
{
let len = _mock_config
.sketch_block_id
.map(|sketch_block_id| sketch_block_id.0)
.unwrap_or(0);
if let Some(scene_objects) = mem.scene_objects.get(0..len) {
exec_state
.global
.root_module_artifacts
.restore_scene_objects(scene_objects);
} else {
let message = format!(
"Cached scene objects length {} is less than expected length from cached object ID generator {}",
mem.scene_objects.len(),
len
);
debug_assert!(false, "{message}");
return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
KclErrorDetails::new(message, vec![SourceRange::synthetic()]),
)));
}
}
Ok(())
}
pub async fn run_mock(
&self,
program: &crate::Program,
mock_config: &MockConfig,
) -> Result<ExecOutcome, KclErrorWithOutputs> {
assert!(
self.is_mock(),
"To use mock execution, instantiate via ExecutorContext::new_mock, not ::new"
);
let use_prev_memory = mock_config.use_prev_memory;
let mut exec_state = ExecState::new_mock(self, mock_config);
if use_prev_memory {
match cache::read_old_memory().await {
Some(mem) => Self::restore_mock_memory(&mut exec_state, mem, mock_config)?,
None => self.prepare_mem(&mut exec_state).await?,
}
} else {
self.prepare_mem(&mut exec_state).await?
};
exec_state.mut_stack().push_new_env_for_scope();
let result = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
let mut stack = exec_state.stack().clone();
let module_infos = exec_state.global.module_infos.clone();
let path_to_source_id = exec_state.global.path_to_source_id.clone();
let id_to_source = exec_state.global.id_to_source.clone();
#[cfg(feature = "artifact-graph")]
let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
#[cfg(not(feature = "artifact-graph"))]
let scene_objects = Default::default();
let outcome = exec_state.into_exec_outcome(result.0, self).await;
stack.squash_env(result.0);
let state = cache::SketchModeState {
stack,
module_infos,
path_to_source_id,
id_to_source,
scene_objects,
};
cache::write_old_memory(state).await;
Ok(outcome)
}
pub async fn run_with_caching(&self, program: crate::Program) -> Result<ExecOutcome, KclErrorWithOutputs> {
assert!(!self.is_mock());
let grid_scale = if self.settings.fixed_size_grid {
GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
} else {
GridScaleBehavior::ScaleWithZoom
};
let original_program = program.clone();
let (_program, exec_state, result) = match cache::read_old_ast().await {
Some(mut cached_state) => {
let old = CacheInformation {
ast: &cached_state.main.ast,
settings: &cached_state.settings,
};
let new = CacheInformation {
ast: &program.ast,
settings: &self.settings,
};
let (clear_scene, program, import_check_info) = match cache::get_changed_program(old, new).await {
CacheResult::ReExecute {
clear_scene,
reapply_settings,
program: changed_program,
} => {
if reapply_settings
&& self
.engine
.reapply_settings(
&self.settings,
Default::default(),
&mut cached_state.main.exec_state.id_generator,
grid_scale,
)
.await
.is_err()
{
(true, program, None)
} else {
(
clear_scene,
crate::Program {
ast: changed_program,
original_file_contents: program.original_file_contents,
},
None,
)
}
}
CacheResult::CheckImportsOnly {
reapply_settings,
ast: changed_program,
} => {
let mut reapply_failed = false;
if reapply_settings {
if self
.engine
.reapply_settings(
&self.settings,
Default::default(),
&mut cached_state.main.exec_state.id_generator,
grid_scale,
)
.await
.is_ok()
{
cache::write_old_ast(GlobalState::with_settings(
cached_state.clone(),
self.settings.clone(),
))
.await;
} else {
reapply_failed = true;
}
}
if reapply_failed {
(true, program, None)
} else {
let mut new_exec_state = ExecState::new(self);
let (new_universe, new_universe_map) =
self.get_universe(&program, &mut new_exec_state).await?;
let clear_scene = new_universe.values().any(|value| {
let id = value.1;
match (
cached_state.exec_state.get_source(id),
new_exec_state.global.get_source(id),
) {
(Some(s0), Some(s1)) => s0.source != s1.source,
_ => false,
}
});
if !clear_scene {
cache::write_old_memory(cached_state.mock_memory_state()).await;
return Ok(cached_state.into_exec_outcome(self).await);
}
(
true,
crate::Program {
ast: changed_program,
original_file_contents: program.original_file_contents,
},
Some((new_universe, new_universe_map, new_exec_state)),
)
}
}
CacheResult::NoAction(true) => {
if self
.engine
.reapply_settings(
&self.settings,
Default::default(),
&mut cached_state.main.exec_state.id_generator,
grid_scale,
)
.await
.is_ok()
{
cache::write_old_ast(GlobalState::with_settings(
cached_state.clone(),
self.settings.clone(),
))
.await;
cache::write_old_memory(cached_state.mock_memory_state()).await;
return Ok(cached_state.into_exec_outcome(self).await);
}
(true, program, None)
}
CacheResult::NoAction(false) => {
cache::write_old_memory(cached_state.mock_memory_state()).await;
return Ok(cached_state.into_exec_outcome(self).await);
}
};
let (exec_state, result) = match import_check_info {
Some((new_universe, new_universe_map, mut new_exec_state)) => {
self.send_clear_scene(&mut new_exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let result = self
.run_concurrent(
&program,
&mut new_exec_state,
Some((new_universe, new_universe_map)),
PreserveMem::Normal,
)
.await;
(new_exec_state, result)
}
None if clear_scene => {
let mut exec_state = cached_state.reconstitute_exec_state();
exec_state.reset(self);
self.send_clear_scene(&mut exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let result = self
.run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
.await;
(exec_state, result)
}
None => {
let mut exec_state = cached_state.reconstitute_exec_state();
exec_state.mut_stack().restore_env(cached_state.main.result_env);
let result = self
.run_concurrent(&program, &mut exec_state, None, PreserveMem::Always)
.await;
(exec_state, result)
}
};
(program, exec_state, result)
}
None => {
let mut exec_state = ExecState::new(self);
self.send_clear_scene(&mut exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let result = self
.run_concurrent(&program, &mut exec_state, None, PreserveMem::Normal)
.await;
(program, exec_state, result)
}
};
if result.is_err() {
cache::bust_cache().await;
}
let result = result?;
cache::write_old_ast(GlobalState::new(
exec_state.clone(),
self.settings.clone(),
original_program.ast,
result.0,
))
.await;
let outcome = exec_state.into_exec_outcome(result.0, self).await;
Ok(outcome)
}
pub async fn run(
&self,
program: &crate::Program,
exec_state: &mut ExecState,
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
self.run_concurrent(program, exec_state, None, PreserveMem::Normal)
.await
}
pub async fn run_concurrent(
&self,
program: &crate::Program,
exec_state: &mut ExecState,
universe_info: Option<(Universe, UniverseMap)>,
preserve_mem: PreserveMem,
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
(universe, universe_map)
} else {
self.get_universe(program, exec_state).await?
};
let default_planes = self.engine.get_default_planes().read().await.clone();
self.eval_prelude(exec_state, SourceRange::synthetic())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
for modules in import_graph::import_graph(&universe, self)
.map_err(|err| exec_state.error_with_outputs(err, None, default_planes.clone()))?
.into_iter()
{
#[cfg(not(target_arch = "wasm32"))]
let mut set = tokio::task::JoinSet::new();
#[allow(clippy::type_complexity)]
let (results_tx, mut results_rx): (
tokio::sync::mpsc::Sender<(ModuleId, ModulePath, Result<ModuleRepr, KclError>)>,
tokio::sync::mpsc::Receiver<_>,
) = tokio::sync::mpsc::channel(1);
for module in modules {
let Some((import_stmt, module_id, module_path, repr)) = universe.get(&module) else {
return Err(KclErrorWithOutputs::no_outputs(KclError::new_internal(
KclErrorDetails::new(format!("Module {module} not found in universe"), Default::default()),
)));
};
let module_id = *module_id;
let module_path = module_path.clone();
let source_range = SourceRange::from(import_stmt);
let module_exec_state = exec_state.clone();
self.add_import_module_ops(
exec_state,
&program.ast,
module_id,
&module_path,
source_range,
&universe_map,
);
let repr = repr.clone();
let exec_ctxt = self.clone();
let results_tx = results_tx.clone();
let exec_module = async |exec_ctxt: &ExecutorContext,
repr: &ModuleRepr,
module_id: ModuleId,
module_path: &ModulePath,
exec_state: &mut ExecState,
source_range: SourceRange|
-> Result<ModuleRepr, KclError> {
match repr {
ModuleRepr::Kcl(program, _) => {
let result = exec_ctxt
.exec_module_from_ast(
program,
module_id,
module_path,
exec_state,
source_range,
PreserveMem::Normal,
)
.await;
result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
}
ModuleRepr::Foreign(geom, _) => {
let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
.await
.map(|geom| Some(KclValue::ImportedGeometry(geom)));
result.map(|val| {
ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
})
}
ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
format!("Module {module_path} not found in universe"),
vec![source_range],
))),
}
};
#[cfg(target_arch = "wasm32")]
{
wasm_bindgen_futures::spawn_local(async move {
let mut exec_state = module_exec_state;
let exec_ctxt = exec_ctxt;
let result = exec_module(
&exec_ctxt,
&repr,
module_id,
&module_path,
&mut exec_state,
source_range,
)
.await;
results_tx
.send((module_id, module_path, result))
.await
.unwrap_or_default();
});
}
#[cfg(not(target_arch = "wasm32"))]
{
set.spawn(async move {
let mut exec_state = module_exec_state;
let exec_ctxt = exec_ctxt;
let result = exec_module(
&exec_ctxt,
&repr,
module_id,
&module_path,
&mut exec_state,
source_range,
)
.await;
results_tx
.send((module_id, module_path, result))
.await
.unwrap_or_default();
});
}
}
drop(results_tx);
while let Some((module_id, _, result)) = results_rx.recv().await {
match result {
Ok(new_repr) => {
let mut repr = exec_state.global.module_infos[&module_id].take_repr();
match &mut repr {
ModuleRepr::Kcl(_, cache) => {
let ModuleRepr::Kcl(_, session_data) = new_repr else {
unreachable!();
};
*cache = session_data;
}
ModuleRepr::Foreign(_, cache) => {
let ModuleRepr::Foreign(_, session_data) = new_repr else {
unreachable!();
};
*cache = session_data;
}
ModuleRepr::Dummy | ModuleRepr::Root => unreachable!(),
}
exec_state.global.module_infos[&module_id].restore_repr(repr);
}
Err(e) => {
return Err(exec_state.error_with_outputs(e, None, default_planes));
}
}
}
}
exec_state
.global
.root_module_artifacts
.extend(std::mem::take(&mut exec_state.mod_local.artifacts));
self.inner_run(program, exec_state, preserve_mem).await
}
async fn get_universe(
&self,
program: &crate::Program,
exec_state: &mut ExecState,
) -> Result<(Universe, UniverseMap), KclErrorWithOutputs> {
exec_state.add_root_module_contents(program);
let mut universe = std::collections::HashMap::new();
let default_planes = self.engine.get_default_planes().read().await.clone();
let root_imports = import_graph::import_universe(
self,
&ModulePath::Main,
&ModuleRepr::Kcl(program.ast.clone(), None),
&mut universe,
exec_state,
)
.await
.map_err(|err| exec_state.error_with_outputs(err, None, default_planes))?;
Ok((universe, root_imports))
}
#[cfg(not(feature = "artifact-graph"))]
fn add_import_module_ops(
&self,
_exec_state: &mut ExecState,
_program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
_module_id: ModuleId,
_module_path: &ModulePath,
_source_range: SourceRange,
_universe_map: &UniverseMap,
) {
}
#[cfg(feature = "artifact-graph")]
fn add_import_module_ops(
&self,
exec_state: &mut ExecState,
program: &crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
module_id: ModuleId,
module_path: &ModulePath,
source_range: SourceRange,
universe_map: &UniverseMap,
) {
match module_path {
ModulePath::Main => {
}
ModulePath::Local {
value,
original_import_path,
} => {
if universe_map.contains_key(value) {
use crate::NodePath;
let node_path = if source_range.is_top_level_module() {
let cached_body_items = exec_state.global.artifacts.cached_body_items();
NodePath::from_range(
&exec_state.build_program_lookup(program.clone()),
cached_body_items,
source_range,
)
.unwrap_or_default()
} else {
NodePath::placeholder()
};
let name = match original_import_path {
Some(value) => value.to_string_lossy(),
None => value.file_name().unwrap_or_default(),
};
exec_state.push_op(Operation::GroupBegin {
group: Group::ModuleInstance { name, module_id },
node_path,
source_range,
});
exec_state.push_op(Operation::GroupEnd);
}
}
ModulePath::Std { .. } => {
}
}
}
async fn inner_run(
&self,
program: &crate::Program,
exec_state: &mut ExecState,
preserve_mem: PreserveMem,
) -> Result<(EnvironmentRef, Option<ModelingSessionData>), KclErrorWithOutputs> {
let _stats = crate::log::LogPerfStats::new("Interpretation");
let grid_scale = if self.settings.fixed_size_grid {
GridScaleBehavior::Fixed(program.meta_settings().ok().flatten().map(|s| s.default_length_units))
} else {
GridScaleBehavior::ScaleWithZoom
};
self.engine
.reapply_settings(
&self.settings,
Default::default(),
exec_state.id_generator(),
grid_scale,
)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let default_planes = self.engine.get_default_planes().read().await.clone();
let result = self
.execute_and_build_graph(&program.ast, exec_state, preserve_mem)
.await;
crate::log::log(format!(
"Post interpretation KCL memory stats: {:#?}",
exec_state.stack().memory.stats
));
crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
async fn write_old_memory(ctx: &ExecutorContext, exec_state: &ExecState, env_ref: EnvironmentRef) {
if ctx.is_mock() {
return;
}
let mut stack = exec_state.stack().deep_clone();
stack.restore_env(env_ref);
let state = cache::SketchModeState {
stack,
module_infos: exec_state.global.module_infos.clone(),
path_to_source_id: exec_state.global.path_to_source_id.clone(),
id_to_source: exec_state.global.id_to_source.clone(),
#[cfg(feature = "artifact-graph")]
scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
#[cfg(not(feature = "artifact-graph"))]
scene_objects: Default::default(),
};
cache::write_old_memory(state).await;
}
let env_ref = match result {
Ok(env_ref) => env_ref,
Err((err, env_ref)) => {
if let Some(env_ref) = env_ref {
write_old_memory(self, exec_state, env_ref).await;
}
return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
}
};
write_old_memory(self, exec_state, env_ref).await;
let session_data = self.engine.get_session_data().await;
Ok((env_ref, session_data))
}
async fn execute_and_build_graph(
&self,
program: NodeRef<'_, crate::parsing::ast::types::Program>,
exec_state: &mut ExecState,
preserve_mem: PreserveMem,
) -> Result<EnvironmentRef, (KclError, Option<EnvironmentRef>)> {
#[cfg(feature = "artifact-graph")]
let start_op = exec_state.global.root_module_artifacts.operations.len();
self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
.await
.map_err(|e| (e, None))?;
let exec_result = self
.exec_module_body(
program,
exec_state,
preserve_mem,
ModuleId::default(),
&ModulePath::Main,
)
.await
.map(
|ModuleExecutionOutcome {
environment: env_ref,
artifacts: module_artifacts,
..
}| {
exec_state.global.root_module_artifacts.extend(module_artifacts);
env_ref
},
)
.map_err(|(err, env_ref, module_artifacts)| {
if let Some(module_artifacts) = module_artifacts {
exec_state.global.root_module_artifacts.extend(module_artifacts);
}
(err, env_ref)
});
#[cfg(feature = "artifact-graph")]
{
let programs = &exec_state.build_program_lookup(program.clone());
let cached_body_items = exec_state.global.artifacts.cached_body_items();
for op in exec_state
.global
.root_module_artifacts
.operations
.iter_mut()
.skip(start_op)
{
op.fill_node_paths(programs, cached_body_items);
}
for module in exec_state.global.module_infos.values_mut() {
if let ModuleRepr::Kcl(_, Some(outcome)) = &mut module.repr {
for op in &mut outcome.artifacts.operations {
op.fill_node_paths(programs, cached_body_items);
}
}
}
}
self.engine.ensure_async_commands_completed().await.map_err(|e| {
match &exec_result {
Ok(env_ref) => (e, Some(*env_ref)),
Err((exec_err, env_ref)) => (exec_err.clone(), *env_ref),
}
})?;
self.engine.clear_queues().await;
match exec_state.build_artifact_graph(&self.engine, program).await {
Ok(_) => exec_result,
Err(err) => exec_result.and_then(|env_ref| Err((err, Some(env_ref)))),
}
}
async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
if exec_state.stack().memory.requires_std() {
#[cfg(feature = "artifact-graph")]
let initial_ops = exec_state.mod_local.artifacts.operations.len();
let path = vec!["std".to_owned(), "prelude".to_owned()];
let resolved_path = ModulePath::from_std_import_path(&path)?;
let id = self
.open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
.await?;
let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
exec_state.mut_stack().memory.set_std(module_memory);
#[cfg(feature = "artifact-graph")]
exec_state.mod_local.artifacts.operations.truncate(initial_ops);
}
Ok(())
}
pub async fn prepare_snapshot(&self) -> std::result::Result<TakeSnapshot, ExecError> {
self.engine
.send_modeling_cmd(
uuid::Uuid::new_v4(),
crate::execution::SourceRange::default(),
&ModelingCmd::from(
mcmd::ZoomToFit::builder()
.object_ids(Default::default())
.animated(false)
.padding(0.1)
.build(),
),
)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let resp = self
.engine
.send_modeling_cmd(
uuid::Uuid::new_v4(),
crate::execution::SourceRange::default(),
&ModelingCmd::from(mcmd::TakeSnapshot::builder().format(ImageFormat::Png).build()),
)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::TakeSnapshot(contents),
} = resp
else {
return Err(ExecError::BadPng(format!(
"Instead of a TakeSnapshot response, the engine returned {resp:?}"
)));
};
Ok(contents)
}
pub async fn export(
&self,
format: kittycad_modeling_cmds::format::OutputFormat3d,
) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
let resp = self
.engine
.send_modeling_cmd(
uuid::Uuid::new_v4(),
crate::SourceRange::default(),
&kittycad_modeling_cmds::ModelingCmd::Export(
kittycad_modeling_cmds::Export::builder()
.entity_ids(vec![])
.format(format)
.build(),
),
)
.await?;
let kittycad_modeling_cmds::websocket::OkWebSocketResponseData::Export { files } = resp else {
return Err(KclError::new_internal(crate::errors::KclErrorDetails::new(
format!("Expected Export response, got {resp:?}",),
vec![SourceRange::default()],
)));
};
Ok(files)
}
pub async fn export_step(
&self,
deterministic_time: bool,
) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
let files = self
.export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
kittycad_modeling_cmds::format::step::export::Options::builder()
.coords(*kittycad_modeling_cmds::coord::KITTYCAD)
.maybe_created(if deterministic_time {
Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
KclError::new_internal(crate::errors::KclErrorDetails::new(
format!("Failed to parse date: {e}"),
vec![SourceRange::default()],
))
})?)
} else {
None
})
.build(),
))
.await?;
Ok(files)
}
pub async fn close(&self) {
self.engine.close().await;
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Ord, PartialOrd, Hash, ts_rs::TS)]
pub struct ArtifactId(Uuid);
impl ArtifactId {
pub fn new(uuid: Uuid) -> Self {
Self(uuid)
}
pub fn placeholder() -> Self {
Self(Uuid::nil())
}
}
impl From<Uuid> for ArtifactId {
fn from(uuid: Uuid) -> Self {
Self::new(uuid)
}
}
impl From<&Uuid> for ArtifactId {
fn from(uuid: &Uuid) -> Self {
Self::new(*uuid)
}
}
impl From<ArtifactId> for Uuid {
fn from(id: ArtifactId) -> Self {
id.0
}
}
impl From<&ArtifactId> for Uuid {
fn from(id: &ArtifactId) -> Self {
id.0
}
}
impl From<ModelingCmdId> for ArtifactId {
fn from(id: ModelingCmdId) -> Self {
Self::new(*id.as_ref())
}
}
impl From<&ModelingCmdId> for ArtifactId {
fn from(id: &ModelingCmdId) -> Self {
Self::new(*id.as_ref())
}
}
#[cfg(test)]
pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclError> {
parse_execute_with_project_dir(code, None).await
}
#[cfg(test)]
pub(crate) async fn parse_execute_with_project_dir(
code: &str,
project_directory: Option<TypedPath>,
) -> Result<ExecTestResults, KclError> {
let program = crate::Program::parse_no_errs(code)?;
let exec_ctxt = ExecutorContext {
engine: Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().map_err(
|err| {
KclError::new_internal(crate::errors::KclErrorDetails::new(
format!("Failed to create mock engine connection: {err}"),
vec![SourceRange::default()],
))
},
)?)),
fs: Arc::new(crate::fs::FileManager::new()),
settings: ExecutorSettings {
project_directory,
..Default::default()
},
context_type: ContextType::Mock,
};
let mut exec_state = ExecState::new(&exec_ctxt);
let result = exec_ctxt.run(&program, &mut exec_state).await?;
Ok(ExecTestResults {
program,
mem_env: result.0,
exec_ctxt,
exec_state,
})
}
#[cfg(test)]
#[derive(Debug)]
pub(crate) struct ExecTestResults {
program: crate::Program,
mem_env: EnvironmentRef,
exec_ctxt: ExecutorContext,
exec_state: ExecState,
}
#[cfg(feature = "artifact-graph")]
pub struct ProgramLookup {
programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
}
#[cfg(feature = "artifact-graph")]
impl ProgramLookup {
pub fn new(
current: crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>,
module_infos: state::ModuleInfoMap,
) -> Self {
let mut programs = IndexMap::with_capacity(module_infos.len());
for (id, info) in module_infos {
if let ModuleRepr::Kcl(program, _) = info.repr {
programs.insert(id, program);
}
}
programs.insert(ModuleId::default(), current);
Self { programs }
}
pub fn program_for_module(
&self,
module_id: ModuleId,
) -> Option<&crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>> {
self.programs.get(&module_id)
}
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
use super::*;
use crate::ModuleId;
use crate::errors::KclErrorDetails;
use crate::errors::Severity;
use crate::exec::NumericType;
use crate::execution::memory::Stack;
use crate::execution::types::RuntimeType;
#[track_caller]
fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_warn() {
let text = "@blah";
let result = parse_execute(text).await.unwrap();
let errs = result.exec_state.issues();
assert_eq!(errs.len(), 1);
assert_eq!(errs[0].severity, crate::errors::Severity::Warning);
assert!(
errs[0].message.contains("Unknown annotation"),
"unexpected warning message: {}",
errs[0].message
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_fn_definitions() {
let ast = r#"fn def(@x) {
return x
}
fn ghi(@x) {
return x
}
fn jkl(@x) {
return x
}
fn hmm(@x) {
return x
}
yo = 5 + 6
abc = 3
identifierGuy = 5
part001 = startSketchOn(XY)
|> startProfile(at = [-1.2, 4.83])
|> line(end = [2.8, 0])
|> angledLine(angle = 100 + 100, length = 3.01)
|> angledLine(angle = abc, length = 3.02)
|> angledLine(angle = def(yo), length = 3.03)
|> angledLine(angle = ghi(2), length = 3.04)
|> angledLine(angle = jkl(yo) + 2, length = 3.05)
|> close()
yo2 = hmm([identifierGuy + 5])"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn multiple_sketch_blocks_do_not_reuse_on_cache_name() {
let code = r#"
firstProfile = sketch(on = XY) {
edge1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
edge2 = line(start = [var 4mm, var 0mm], end = [var 4mm, var 3mm])
edge3 = line(start = [var 4mm, var 3mm], end = [var 0mm, var 3mm])
edge4 = line(start = [var 0mm, var 3mm], end = [var 0mm, var 0mm])
coincident([edge1.end, edge2.start])
coincident([edge2.end, edge3.start])
coincident([edge3.end, edge4.start])
coincident([edge4.end, edge1.start])
}
secondProfile = sketch(on = offsetPlane(XY, offset = 6mm)) {
edge5 = line(start = [var 1mm, var 1mm], end = [var 5mm, var 1mm])
edge6 = line(start = [var 5mm, var 1mm], end = [var 5mm, var 4mm])
edge7 = line(start = [var 5mm, var 4mm], end = [var 1mm, var 4mm])
edge8 = line(start = [var 1mm, var 4mm], end = [var 1mm, var 1mm])
coincident([edge5.end, edge6.start])
coincident([edge6.end, edge7.start])
coincident([edge7.end, edge8.start])
coincident([edge8.end, edge5.start])
}
firstSolid = extrude(region(point = [2mm, 1mm], sketch = firstProfile), length = 2mm)
secondSolid = extrude(region(point = [2mm, 2mm], sketch = secondProfile), length = 2mm)
"#;
let result = parse_execute(code).await.unwrap();
assert!(result.exec_state.issues().is_empty());
}
#[cfg(feature = "artifact-graph")]
#[tokio::test(flavor = "multi_thread")]
async fn sketch_block_artifact_preserves_standard_plane_name() {
let code = r#"
sketch001 = sketch(on = -YZ) {
line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 1mm])
}
"#;
let result = parse_execute(code).await.unwrap();
let sketch_blocks = result
.exec_state
.global
.artifacts
.graph
.values()
.filter_map(|artifact| match artifact {
Artifact::SketchBlock(block) => Some(block),
_ => None,
})
.collect::<Vec<_>>();
assert_eq!(sketch_blocks.len(), 1);
assert_eq!(sketch_blocks[0].standard_plane, Some(crate::engine::PlaneName::NegYz));
}
#[tokio::test(flavor = "multi_thread")]
async fn issue_10639_blend_example_with_two_sketch_blocks_executes() {
let code = r#"
sketch001 = sketch(on = YZ) {
line1 = line(start = [var 4.1mm, var -0.1mm], end = [var 5.5mm, var 0mm])
line2 = line(start = [var 5.5mm, var 0mm], end = [var 5.5mm, var 3mm])
line3 = line(start = [var 5.5mm, var 3mm], end = [var 3.9mm, var 2.8mm])
line4 = line(start = [var 4.1mm, var 3mm], end = [var 4.5mm, var -0.2mm])
coincident([line1.end, line2.start])
coincident([line2.end, line3.start])
coincident([line3.end, line4.start])
coincident([line4.end, line1.start])
}
sketch002 = sketch(on = -XZ) {
line5 = line(start = [var -5.3mm, var -0.1mm], end = [var -3.5mm, var -0.1mm])
line6 = line(start = [var -3.5mm, var -0.1mm], end = [var -3.5mm, var 3.1mm])
line7 = line(start = [var -3.5mm, var 4.5mm], end = [var -5.4mm, var 4.5mm])
line8 = line(start = [var -5.3mm, var 3.1mm], end = [var -5.3mm, var -0.1mm])
coincident([line5.end, line6.start])
coincident([line6.end, line7.start])
coincident([line7.end, line8.start])
coincident([line8.end, line5.start])
}
region001 = region(point = [-4.4mm, 2mm], sketch = sketch002)
extrude001 = extrude(region001, length = -2mm, bodyType = SURFACE)
region002 = region(point = [4.8mm, 1.5mm], sketch = sketch001)
extrude002 = extrude(region002, length = -2mm, bodyType = SURFACE)
myBlend = blend([extrude001.sketch.tags.line7, extrude002.sketch.tags.line3])
"#;
let result = parse_execute(code).await.unwrap();
assert!(result.exec_state.issues().is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn issue_10741_point_circle_coincident_executes() {
let code = r#"
sketch001 = sketch(on = YZ) {
circle1 = circle(start = [var -2.67mm, var 1.8mm], center = [var -1.53mm, var 0.78mm])
line1 = line(start = [var -1.05mm, var 2.22mm], end = [var -3.58mm, var -0.78mm])
coincident([line1.start, circle1])
}
"#;
let result = parse_execute(code).await.unwrap();
assert!(
result
.exec_state
.issues()
.iter()
.all(|issue| issue.severity != Severity::Error),
"unexpected execution issues: {:#?}",
result.exec_state.issues()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_pipe_substitutions_unary() {
let ast = r#"myVar = 3
part001 = startSketchOn(XY)
|> startProfile(at = [0, 0])
|> line(end = [3, 4], tag = $seg01)
|> line(end = [
min([segLen(seg01), myVar]),
-legLen(hypotenuse = segLen(seg01), leg = myVar)
])
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_pipe_substitutions() {
let ast = r#"myVar = 3
part001 = startSketchOn(XY)
|> startProfile(at = [0, 0])
|> line(end = [3, 4], tag = $seg01)
|> line(end = [
min([segLen(seg01), myVar]),
legLen(hypotenuse = segLen(seg01), leg = myVar)
])
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_inline_comment() {
let ast = r#"baseThick = 1
armAngle = 60
baseThickHalf = baseThick / 2
halfArmAngle = armAngle / 2
arrExpShouldNotBeIncluded = [1, 2, 3]
objExpShouldNotBeIncluded = { a = 1, b = 2, c = 3 }
part001 = startSketchOn(XY)
|> startProfile(at = [0, 0])
|> yLine(endAbsolute = 1)
|> xLine(length = 3.84) // selection-range-7ish-before-this
variableBelowShouldNotBeIncluded = 3
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_literal_in_pipe() {
let ast = r#"w = 20
l = 8
h = 10
fn thing() {
return -8
}
firstExtrude = startSketchOn(XY)
|> startProfile(at = [0,0])
|> line(end = [0, l])
|> line(end = [w, 0])
|> line(end = [0, thing()])
|> close()
|> extrude(length = h)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_unary_in_pipe() {
let ast = r#"w = 20
l = 8
h = 10
fn thing(@x) {
return -x
}
firstExtrude = startSketchOn(XY)
|> startProfile(at = [0,0])
|> line(end = [0, l])
|> line(end = [w, 0])
|> line(end = [0, thing(8)])
|> close()
|> extrude(length = h)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_array_in_pipe() {
let ast = r#"w = 20
l = 8
h = 10
fn thing(@x) {
return [0, -x]
}
firstExtrude = startSketchOn(XY)
|> startProfile(at = [0,0])
|> line(end = [0, l])
|> line(end = [w, 0])
|> line(end = thing(8))
|> close()
|> extrude(length = h)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_call_in_pipe() {
let ast = r#"w = 20
l = 8
h = 10
fn other_thing(@y) {
return -y
}
fn thing(@x) {
return other_thing(x)
}
firstExtrude = startSketchOn(XY)
|> startProfile(at = [0,0])
|> line(end = [0, l])
|> line(end = [w, 0])
|> line(end = [0, thing(8)])
|> close()
|> extrude(length = h)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_with_function_sketch() {
let ast = r#"fn box(h, l, w) {
myBox = startSketchOn(XY)
|> startProfile(at = [0,0])
|> line(end = [0, l])
|> line(end = [w, 0])
|> line(end = [0, -l])
|> close()
|> extrude(length = h)
return myBox
}
fnBox = box(h = 3, l = 6, w = 10)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_member_of_object_with_function_period() {
let ast = r#"fn box(@obj) {
myBox = startSketchOn(XY)
|> startProfile(at = obj.start)
|> line(end = [0, obj.l])
|> line(end = [obj.w, 0])
|> line(end = [0, -obj.l])
|> close()
|> extrude(length = obj.h)
return myBox
}
thisBox = box({start = [0,0], l = 6, w = 10, h = 3})
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
#[ignore] async fn test_object_member_starting_pipeline() {
let ast = r#"
fn test2() {
return {
thing: startSketchOn(XY)
|> startProfile(at = [0, 0])
|> line(end = [0, 1])
|> line(end = [1, 0])
|> line(end = [0, -1])
|> close()
}
}
x2 = test2()
x2.thing
|> extrude(length = 10)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
#[ignore] async fn test_execute_with_function_sketch_loop_objects() {
let ast = r#"fn box(obj) {
let myBox = startSketchOn(XY)
|> startProfile(at = obj.start)
|> line(end = [0, obj.l])
|> line(end = [obj.w, 0])
|> line(end = [0, -obj.l])
|> close()
|> extrude(length = obj.h)
return myBox
}
for var in [{start: [0,0], l: 6, w: 10, h: 3}, {start: [-10,-10], l: 3, w: 5, h: 1.5}] {
thisBox = box(var)
}"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
#[ignore] async fn test_execute_with_function_sketch_loop_array() {
let ast = r#"fn box(h, l, w, start) {
myBox = startSketchOn(XY)
|> startProfile(at = [0,0])
|> line(end = [0, l])
|> line(end = [w, 0])
|> line(end = [0, -l])
|> close()
|> extrude(length = h)
return myBox
}
for var in [[3, 6, 10, [0,0]], [1.5, 3, 5, [-10,-10]]] {
const thisBox = box(var[0], var[1], var[2], var[3])
}"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_get_member_of_array_with_function() {
let ast = r#"fn box(@arr) {
myBox =startSketchOn(XY)
|> startProfile(at = arr[0])
|> line(end = [0, arr[1]])
|> line(end = [arr[2], 0])
|> line(end = [0, -arr[1]])
|> close()
|> extrude(length = arr[3])
return myBox
}
thisBox = box([[0,0], 6, 10, 3])
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_function_cannot_access_future_definitions() {
let ast = r#"
fn returnX() {
// x shouldn't be defined yet.
return x
}
x = 5
answer = returnX()"#;
let result = parse_execute(ast).await;
let err = result.unwrap_err();
assert_eq!(err.message(), "`x` is not defined");
}
#[tokio::test(flavor = "multi_thread")]
async fn test_override_prelude() {
let text = "PI = 3.0";
let result = parse_execute(text).await.unwrap();
let issues = result.exec_state.issues();
assert!(issues.is_empty(), "issues={issues:#?}");
}
#[tokio::test(flavor = "multi_thread")]
async fn type_aliases() {
let text = r#"@settings(experimentalFeatures = allow)
type MyTy = [number; 2]
fn foo(@x: MyTy) {
return x[0]
}
foo([0, 1])
type Other = MyTy | Helix
"#;
let result = parse_execute(text).await.unwrap();
let issues = result.exec_state.issues();
assert!(issues.is_empty(), "issues={issues:#?}");
}
#[tokio::test(flavor = "multi_thread")]
async fn test_cannot_shebang_in_fn() {
let ast = r#"
fn foo() {
#!hello
return true
}
foo
"#;
let result = parse_execute(ast).await;
let err = result.unwrap_err();
assert_eq!(
err,
KclError::new_syntax(KclErrorDetails::new(
"Unexpected token: #".to_owned(),
vec![SourceRange::new(14, 15, ModuleId::default())],
)),
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_pattern_transform_function_cannot_access_future_definitions() {
let ast = r#"
fn transform(@replicaId) {
// x shouldn't be defined yet.
scale = x
return {
translate = [0, 0, replicaId * 10],
scale = [scale, 1, 0],
}
}
fn layer() {
return startSketchOn(XY)
|> circle( center= [0, 0], radius= 1, tag = $tag1)
|> extrude(length = 10)
}
x = 5
// The 10 layers are replicas of each other, with a transform applied to each.
shape = layer() |> patternTransform(instances = 10, transform = transform)
"#;
let result = parse_execute(ast).await;
let err = result.unwrap_err();
assert_eq!(err.message(), "`x` is not defined",);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_functions() {
let ast = r#"myVar = 2 + min([100, -1 + legLen(hypotenuse = 5, leg = 3)])"#;
let result = parse_execute(ast).await.unwrap();
assert_eq!(
5.0,
mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
.as_f64()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute() {
let ast = r#"myVar = 1 + 2 * (3 - 4) / -5 + 6"#;
let result = parse_execute(ast).await.unwrap();
assert_eq!(
7.4,
mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
.as_f64()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_start_negative() {
let ast = r#"myVar = -5 + 6"#;
let result = parse_execute(ast).await.unwrap();
assert_eq!(
1.0,
mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
.as_f64()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_pi() {
let ast = r#"myVar = PI * 2"#;
let result = parse_execute(ast).await.unwrap();
assert_eq!(
std::f64::consts::TAU,
mem_get_json(result.exec_state.stack(), result.mem_env, "myVar")
.as_f64()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_define_decimal_without_leading_zero() {
let ast = r#"thing = .4 + 7"#;
let result = parse_execute(ast).await.unwrap();
assert_eq!(
7.4,
mem_get_json(result.exec_state.stack(), result.mem_env, "thing")
.as_f64()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn pass_std_to_std() {
let ast = r#"sketch001 = startSketchOn(XY)
profile001 = circle(sketch001, center = [0, 0], radius = 2)
extrude001 = extrude(profile001, length = 5)
extrudes = patternLinear3d(
extrude001,
instances = 3,
distance = 5,
axis = [1, 1, 0],
)
clone001 = map(extrudes, f = clone)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_array_reduce_nested_array() {
let code = r#"
fn id(@el, accum) { return accum }
answer = reduce([], initial=[[[0,0]]], f=id)
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, "answer"),
KclValue::HomArray {
value: vec![KclValue::HomArray {
value: vec![KclValue::HomArray {
value: vec![
KclValue::Number {
value: 0.0,
ty: NumericType::default(),
meta: vec![SourceRange::new(69, 70, Default::default()).into()],
},
KclValue::Number {
value: 0.0,
ty: NumericType::default(),
meta: vec![SourceRange::new(71, 72, Default::default()).into()],
}
],
ty: RuntimeType::any(),
}],
ty: RuntimeType::any(),
}],
ty: RuntimeType::any(),
}
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_zero_param_fn() {
let ast = r#"sigmaAllow = 35000 // psi
leg1 = 5 // inches
leg2 = 8 // inches
fn thickness() { return 0.56 }
bracket = startSketchOn(XY)
|> startProfile(at = [0,0])
|> line(end = [0, leg1])
|> line(end = [leg2, 0])
|> line(end = [0, -thickness()])
|> line(end = [-leg2 + thickness(), 0])
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_unary_operator_not_succeeds() {
let ast = r#"
fn returnTrue() { return !false }
t = true
f = false
notTrue = !t
notFalse = !f
c = !!true
d = !returnTrue()
assertIs(!false, error = "expected to pass")
fn check(x) {
assertIs(!x, error = "expected argument to be false")
return true
}
check(x = false)
"#;
let result = parse_execute(ast).await.unwrap();
assert_eq!(
false,
mem_get_json(result.exec_state.stack(), result.mem_env, "notTrue")
.as_bool()
.unwrap()
);
assert_eq!(
true,
mem_get_json(result.exec_state.stack(), result.mem_env, "notFalse")
.as_bool()
.unwrap()
);
assert_eq!(
true,
mem_get_json(result.exec_state.stack(), result.mem_env, "c")
.as_bool()
.unwrap()
);
assert_eq!(
false,
mem_get_json(result.exec_state.stack(), result.mem_env, "d")
.as_bool()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_unary_operator_not_on_non_bool_fails() {
let code1 = r#"
// Yup, this is null.
myNull = 0 / 0
notNull = !myNull
"#;
assert_eq!(
parse_execute(code1).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: a number",
);
let code2 = "notZero = !0";
assert_eq!(
parse_execute(code2).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: a number",
);
let code3 = r#"
notEmptyString = !""
"#;
assert_eq!(
parse_execute(code3).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: a string",
);
let code4 = r#"
obj = { a = 1 }
notMember = !obj.a
"#;
assert_eq!(
parse_execute(code4).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: a number",
);
let code5 = "
a = []
notArray = !a";
assert_eq!(
parse_execute(code5).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: an empty array",
);
let code6 = "
x = {}
notObject = !x";
assert_eq!(
parse_execute(code6).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: an object",
);
let code7 = "
fn x() { return 1 }
notFunction = !x";
let fn_err = parse_execute(code7).await.unwrap_err();
assert!(
fn_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: "),
"Actual error: {fn_err:?}"
);
let code8 = "
myTagDeclarator = $myTag
notTagDeclarator = !myTagDeclarator";
let tag_declarator_err = parse_execute(code8).await.unwrap_err();
assert!(
tag_declarator_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
"Actual error: {tag_declarator_err:?}"
);
let code9 = "
myTagDeclarator = $myTag
notTagIdentifier = !myTag";
let tag_identifier_err = parse_execute(code9).await.unwrap_err();
assert!(
tag_identifier_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
"Actual error: {tag_identifier_err:?}"
);
let code10 = "notPipe = !(1 |> 2)";
assert_eq!(
parse_execute(code10).await.unwrap_err(),
KclError::new_syntax(KclErrorDetails::new(
"Unexpected token: !".to_owned(),
vec![SourceRange::new(10, 11, ModuleId::default())],
))
);
let code11 = "
fn identity(x) { return x }
notPipeSub = 1 |> identity(!%))";
assert_eq!(
parse_execute(code11).await.unwrap_err(),
KclError::new_syntax(KclErrorDetails::new(
"There was an unexpected `!`. Try removing it.".to_owned(),
vec![SourceRange::new(56, 57, ModuleId::default())],
))
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_start_sketch_on_invalid_kwargs() {
let current_dir = std::env::current_dir().unwrap();
let mut path = current_dir.join("tests/inputs/startSketchOn_0.kcl");
let mut code = std::fs::read_to_string(&path).unwrap();
assert_eq!(
parse_execute(&code).await.unwrap_err().message(),
"You cannot give both `face` and `normalToFace` params, you have to choose one or the other.".to_owned(),
);
path = current_dir.join("tests/inputs/startSketchOn_1.kcl");
code = std::fs::read_to_string(&path).unwrap();
assert_eq!(
parse_execute(&code).await.unwrap_err().message(),
"`alignAxis` is required if `normalToFace` is specified.".to_owned(),
);
path = current_dir.join("tests/inputs/startSketchOn_2.kcl");
code = std::fs::read_to_string(&path).unwrap();
assert_eq!(
parse_execute(&code).await.unwrap_err().message(),
"`normalToFace` is required if `alignAxis` is specified.".to_owned(),
);
path = current_dir.join("tests/inputs/startSketchOn_3.kcl");
code = std::fs::read_to_string(&path).unwrap();
assert_eq!(
parse_execute(&code).await.unwrap_err().message(),
"`normalToFace` is required if `alignAxis` is specified.".to_owned(),
);
path = current_dir.join("tests/inputs/startSketchOn_4.kcl");
code = std::fs::read_to_string(&path).unwrap();
assert_eq!(
parse_execute(&code).await.unwrap_err().message(),
"`normalToFace` is required if `normalOffset` is specified.".to_owned(),
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_negative_variable_in_binary_expression() {
let ast = r#"sigmaAllow = 35000 // psi
width = 1 // inch
p = 150 // lbs
distance = 6 // inches
FOS = 2
leg1 = 5 // inches
leg2 = 8 // inches
thickness_squared = distance * p * FOS * 6 / sigmaAllow
thickness = 0.56 // inches. App does not support square root function yet
bracket = startSketchOn(XY)
|> startProfile(at = [0,0])
|> line(end = [0, leg1])
|> line(end = [leg2, 0])
|> line(end = [0, -thickness])
|> line(end = [-leg2 + thickness, 0])
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_function_no_return() {
let ast = r#"fn test(@origin) {
origin
}
test([0, 0])
"#;
let result = parse_execute(ast).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("undefined"));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_max_stack_size_exceeded_error() {
let ast = r#"
fn forever(@n) {
return 1 + forever(n)
}
forever(1)
"#;
let result = parse_execute(ast).await;
let err = result.unwrap_err();
assert!(err.to_string().contains("stack size exceeded"), "actual: {:?}", err);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_doubly_nested_parens() {
let ast = r#"sigmaAllow = 35000 // psi
width = 4 // inch
p = 150 // Force on shelf - lbs
distance = 6 // inches
FOS = 2
leg1 = 5 // inches
leg2 = 8 // inches
thickness_squared = (distance * p * FOS * 6 / (sigmaAllow - width))
thickness = 0.32 // inches. App does not support square root function yet
bracket = startSketchOn(XY)
|> startProfile(at = [0,0])
|> line(end = [0, leg1])
|> line(end = [leg2, 0])
|> line(end = [0, -thickness])
|> line(end = [-1 * leg2 + thickness, 0])
|> line(end = [0, -1 * leg1 + thickness])
|> close()
|> extrude(length = width)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_nested_parens_one_less() {
let ast = r#" sigmaAllow = 35000 // psi
width = 4 // inch
p = 150 // Force on shelf - lbs
distance = 6 // inches
FOS = 2
leg1 = 5 // inches
leg2 = 8 // inches
thickness_squared = distance * p * FOS * 6 / (sigmaAllow - width)
thickness = 0.32 // inches. App does not support square root function yet
bracket = startSketchOn(XY)
|> startProfile(at = [0,0])
|> line(end = [0, leg1])
|> line(end = [leg2, 0])
|> line(end = [0, -thickness])
|> line(end = [-1 * leg2 + thickness, 0])
|> line(end = [0, -1 * leg1 + thickness])
|> close()
|> extrude(length = width)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_fn_as_operand() {
let ast = r#"fn f() { return 1 }
x = f()
y = x + 1
z = f() + 1
w = f() + f()
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_ids_stable_between_executions() {
let code = r#"sketch001 = startSketchOn(XZ)
|> startProfile(at = [61.74, 206.13])
|> xLine(length = 305.11, tag = $seg01)
|> yLine(length = -291.85)
|> xLine(length = -segLen(seg01))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> extrude(length = 40.14)
|> shell(
thickness = 3.14,
faces = [seg01]
)
"#;
let ctx = crate::test_server::new_context(true, None).await.unwrap();
let old_program = crate::Program::parse_no_errs(code).unwrap();
if let Err(err) = ctx.run_with_caching(old_program).await {
let report = err.into_miette_report_with_outputs(code).unwrap();
let report = miette::Report::new(report);
panic!("Error executing program: {report:?}");
}
let id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
let code = r#"sketch001 = startSketchOn(XZ)
|> startProfile(at = [62.74, 206.13])
|> xLine(length = 305.11, tag = $seg01)
|> yLine(length = -291.85)
|> xLine(length = -segLen(seg01))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> extrude(length = 40.14)
|> shell(
faces = [seg01],
thickness = 3.14,
)
"#;
let program = crate::Program::parse_no_errs(code).unwrap();
ctx.run_with_caching(program).await.unwrap();
let new_id_generator = cache::read_old_ast().await.unwrap().main.exec_state.id_generator;
assert_eq!(id_generator, new_id_generator);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_changing_a_setting_updates_the_cached_state() {
let code = r#"sketch001 = startSketchOn(XZ)
|> startProfile(at = [61.74, 206.13])
|> xLine(length = 305.11, tag = $seg01)
|> yLine(length = -291.85)
|> xLine(length = -segLen(seg01))
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|> close()
|> extrude(length = 40.14)
|> shell(
thickness = 3.14,
faces = [seg01]
)
"#;
let mut ctx = crate::test_server::new_context(true, None).await.unwrap();
let old_program = crate::Program::parse_no_errs(code).unwrap();
ctx.run_with_caching(old_program.clone()).await.unwrap();
let settings_state = cache::read_old_ast().await.unwrap().settings;
assert_eq!(settings_state, ctx.settings);
ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
ctx.run_with_caching(old_program.clone()).await.unwrap();
let settings_state = cache::read_old_ast().await.unwrap().settings;
assert_eq!(settings_state, ctx.settings);
ctx.settings.highlight_edges = !ctx.settings.highlight_edges;
ctx.run_with_caching(old_program).await.unwrap();
let settings_state = cache::read_old_ast().await.unwrap().settings;
assert_eq!(settings_state, ctx.settings);
ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn mock_after_not_mock() {
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let program = crate::Program::parse_no_errs("x = 2").unwrap();
let result = ctx.run_with_caching(program).await.unwrap();
assert_eq!(result.variables.get("x").unwrap().as_f64().unwrap(), 2.0);
let ctx2 = ExecutorContext::new_mock(None).await;
let program2 = crate::Program::parse_no_errs("z = x + 1").unwrap();
let result = ctx2.run_mock(&program2, &MockConfig::default()).await.unwrap();
assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
ctx.close().await;
ctx2.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn mock_then_add_extrude_then_mock_again() {
let code = "s = sketch(on = XY) {
line1 = line(start = [0.05, 0.05], end = [3.88, 0.81])
line2 = line(start = [3.88, 0.81], end = [0.92, 4.67])
coincident([line1.end, line2.start])
line3 = line(start = [0.92, 4.67], end = [0.05, 0.05])
coincident([line2.end, line3.start])
coincident([line1.start, line3.end])
}
";
let ctx = ExecutorContext::new_mock(None).await;
let program = crate::Program::parse_no_errs(code).unwrap();
let result = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
assert!(result.variables.contains_key("s"), "actual: {:?}", &result.variables);
let code2 = code.to_owned()
+ "
region001 = region(point = [1mm, 1mm], sketch = s)
extrude001 = extrude(region001, length = 1)
";
let program2 = crate::Program::parse_no_errs(&code2).unwrap();
let result = ctx.run_mock(&program2, &MockConfig::default()).await.unwrap();
assert!(
result.variables.contains_key("region001"),
"actual: {:?}",
&result.variables
);
ctx.close().await;
}
#[cfg(feature = "artifact-graph")]
#[tokio::test(flavor = "multi_thread")]
async fn mock_has_stable_ids() {
let ctx = ExecutorContext::new_mock(None).await;
let mock_config = MockConfig {
use_prev_memory: false,
..Default::default()
};
let code = "sk = startSketchOn(XY)
|> startProfile(at = [0, 0])";
let program = crate::Program::parse_no_errs(code).unwrap();
let result = ctx.run_mock(&program, &mock_config).await.unwrap();
let ids = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
assert!(!ids.is_empty(), "IDs should not be empty");
let ctx2 = ExecutorContext::new_mock(None).await;
let program2 = crate::Program::parse_no_errs(code).unwrap();
let result = ctx2.run_mock(&program2, &mock_config).await.unwrap();
let ids2 = result.artifact_graph.iter().map(|(k, _)| *k).collect::<Vec<_>>();
assert_eq!(ids, ids2, "Generated IDs should match");
ctx.close().await;
ctx2.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn mock_memory_restore_preserves_module_maps() {
clear_mem_cache().await;
let ctx = ExecutorContext::new_mock(None).await;
let cold_start = MockConfig {
use_prev_memory: false,
..Default::default()
};
ctx.run_mock(&crate::Program::empty(), &cold_start).await.unwrap();
let mem = cache::read_old_memory().await.unwrap();
assert!(
mem.path_to_source_id.len() > 3,
"expected prelude imports to populate multiple modules, got {:?}",
mem.path_to_source_id
);
let mut exec_state = ExecState::new_mock(&ctx, &MockConfig::default());
ExecutorContext::restore_mock_memory(&mut exec_state, mem.clone(), &MockConfig::default()).unwrap();
assert_eq!(exec_state.global.path_to_source_id, mem.path_to_source_id);
assert_eq!(exec_state.global.id_to_source, mem.id_to_source);
assert_eq!(exec_state.global.module_infos, mem.module_infos);
clear_mem_cache().await;
ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn run_with_caching_no_action_refreshes_mock_memory() {
cache::bust_cache().await;
clear_mem_cache().await;
let ctx = ExecutorContext::new_with_engine(
std::sync::Arc::new(Box::new(crate::engine::conn_mock::EngineConnection::new().unwrap())),
Default::default(),
);
let program = crate::Program::parse_no_errs(
r#"sketch001 = sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
}
"#,
)
.unwrap();
ctx.run_with_caching(program.clone()).await.unwrap();
let baseline_memory = cache::read_old_memory().await.unwrap();
assert!(
!baseline_memory.scene_objects.is_empty(),
"expected engine execution to persist full-scene mock memory"
);
cache::write_old_memory(cache::SketchModeState::new_for_tests()).await;
assert_eq!(cache::read_old_memory().await.unwrap().scene_objects.len(), 0);
ctx.run_with_caching(program).await.unwrap();
let refreshed_memory = cache::read_old_memory().await.unwrap();
assert_eq!(refreshed_memory.scene_objects, baseline_memory.scene_objects);
assert_eq!(refreshed_memory.path_to_source_id, baseline_memory.path_to_source_id);
assert_eq!(refreshed_memory.id_to_source, baseline_memory.id_to_source);
cache::bust_cache().await;
clear_mem_cache().await;
ctx.close().await;
}
#[cfg(feature = "artifact-graph")]
#[tokio::test(flavor = "multi_thread")]
async fn sim_sketch_mode_real_mock_real() {
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let code = r#"sketch001 = startSketchOn(XY)
profile001 = startProfile(sketch001, at = [0, 0])
|> line(end = [10, 0])
|> line(end = [0, 10])
|> line(end = [-10, 0])
|> line(end = [0, -10])
|> close()
"#;
let program = crate::Program::parse_no_errs(code).unwrap();
let result = ctx.run_with_caching(program).await.unwrap();
assert_eq!(result.operations.len(), 1);
let mock_ctx = ExecutorContext::new_mock(None).await;
let mock_program = crate::Program::parse_no_errs(code).unwrap();
let mock_result = mock_ctx.run_mock(&mock_program, &MockConfig::default()).await.unwrap();
assert_eq!(mock_result.operations.len(), 1);
let code2 = code.to_owned()
+ r#"
extrude001 = extrude(profile001, length = 10)
"#;
let program2 = crate::Program::parse_no_errs(&code2).unwrap();
let result = ctx.run_with_caching(program2).await.unwrap();
assert_eq!(result.operations.len(), 2);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn read_tag_version() {
let ast = r#"fn bar(@t) {
return startSketchOn(XY)
|> startProfile(at = [0,0])
|> angledLine(
angle = -60,
length = segLen(t),
)
|> line(end = [0, 0])
|> close()
}
sketch = startSketchOn(XY)
|> startProfile(at = [0,0])
|> line(end = [0, 10])
|> line(end = [10, 0], tag = $tag0)
|> line(endAbsolute = [0, 0])
fn foo() {
// tag0 tags an edge
return bar(tag0)
}
solid = sketch |> extrude(length = 10)
// tag0 tags a face
sketch2 = startSketchOn(solid, face = tag0)
|> startProfile(at = [0,0])
|> line(end = [0, 1])
|> line(end = [1, 0])
|> line(end = [0, 0])
foo() |> extrude(length = 1)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn experimental() {
let code = r#"
startSketchOn(XY)
|> startProfile(at = [0, 0], tag = $start)
|> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
"#;
let result = parse_execute(code).await.unwrap();
let issues = result.exec_state.issues();
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].severity, Severity::Error);
let msg = &issues[0].message;
assert!(msg.contains("experimental"), "found {msg}");
let code = r#"@settings(experimentalFeatures = allow)
startSketchOn(XY)
|> startProfile(at = [0, 0], tag = $start)
|> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
"#;
let result = parse_execute(code).await.unwrap();
let issues = result.exec_state.issues();
assert!(issues.is_empty(), "issues={issues:#?}");
let code = r#"@settings(experimentalFeatures = warn)
startSketchOn(XY)
|> startProfile(at = [0, 0], tag = $start)
|> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
"#;
let result = parse_execute(code).await.unwrap();
let issues = result.exec_state.issues();
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].severity, Severity::Warning);
let msg = &issues[0].message;
assert!(msg.contains("experimental"), "found {msg}");
let code = r#"@settings(experimentalFeatures = deny)
startSketchOn(XY)
|> startProfile(at = [0, 0], tag = $start)
|> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
"#;
let result = parse_execute(code).await.unwrap();
let issues = result.exec_state.issues();
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].severity, Severity::Error);
let msg = &issues[0].message;
assert!(msg.contains("experimental"), "found {msg}");
let code = r#"@settings(experimentalFeatures = foo)
startSketchOn(XY)
|> startProfile(at = [0, 0], tag = $start)
|> elliptic(center = [0, 0], angleStart = segAng(start), angleEnd = 160deg, majorRadius = 2, minorRadius = 3)
"#;
parse_execute(code).await.unwrap_err();
}
#[tokio::test(flavor = "multi_thread")]
async fn experimental_parameter() {
let code = r#"
fn inc(@x, @(experimental = true) amount? = 1) {
return x + amount
}
answer = inc(5, amount = 2)
"#;
let result = parse_execute(code).await.unwrap();
let issues = result.exec_state.issues();
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].severity, Severity::Error);
let msg = &issues[0].message;
assert!(msg.contains("experimental"), "found {msg}");
let code = r#"
fn inc(@x, @(experimental = true) amount? = 1) {
return x + amount
}
answer = inc(5)
"#;
let result = parse_execute(code).await.unwrap();
let issues = result.exec_state.issues();
assert!(issues.is_empty(), "issues={issues:#?}");
}
#[tokio::test(flavor = "multi_thread")]
async fn experimental_scalar_fixed_constraint() {
let code_left = r#"@settings(experimentalFeatures = warn)
sketch(on = XY) {
point1 = point(at = [var 0mm, var 0mm])
point1.at[0] == 1mm
}
"#;
let code_right = r#"@settings(experimentalFeatures = warn)
sketch(on = XY) {
point1 = point(at = [var 0mm, var 0mm])
1mm == point1.at[0]
}
"#;
for code in [code_left, code_right] {
let result = parse_execute(code).await.unwrap();
let issues = result.exec_state.issues();
let Some(error) = issues
.iter()
.find(|issue| issue.message.contains("scalar fixed constraint is experimental"))
else {
panic!("found {issues:#?}");
};
assert_eq!(error.severity, Severity::Warning);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_tangent_line_arc_executes_with_mock_engine() {
let code = std::fs::read_to_string("tests/tangent_line_arc/input.kcl").unwrap();
parse_execute(&code).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_tangent_arc_arc_math_only_executes_with_mock_engine() {
let code = std::fs::read_to_string("tests/tangent_arc_arc_math_only/input.kcl").unwrap();
parse_execute(&code).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_tangent_line_circle_executes_with_mock_engine() {
let code = std::fs::read_to_string("tests/tangent_line_circle/input.kcl").unwrap();
parse_execute(&code).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_tangent_circle_circle_native_executes_with_mock_engine() {
let code = std::fs::read_to_string("tests/tangent_circle_circle_native/input.kcl").unwrap();
parse_execute(&code).await.unwrap();
}
#[cfg(feature = "artifact-graph")]
async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
let program = crate::Program::parse_no_errs(kcl).unwrap();
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let mut exec_state = ExecState::new(&ctx);
let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
let outcome = exec_state.into_exec_outcome(env_ref, &ctx).await;
let report = outcome.sketch_constraint_report();
ctx.close().await;
report
}
#[cfg(feature = "artifact-graph")]
#[tokio::test(flavor = "multi_thread")]
async fn test_constraint_report_fully_constrained() {
let kcl = r#"
@settings(experimentalFeatures = allow)
sketch(on = YZ) {
line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
line1.start.at[0] == 2
line1.start.at[1] == 8
line1.end.at[0] == 5
line1.end.at[1] == 7
}
"#;
let report = run_constraint_report(kcl).await;
assert_eq!(report.fully_constrained.len(), 1);
assert_eq!(report.under_constrained.len(), 0);
assert_eq!(report.over_constrained.len(), 0);
assert_eq!(report.errors.len(), 0);
assert_eq!(report.fully_constrained[0].status, ConstraintKind::FullyConstrained);
}
#[cfg(feature = "artifact-graph")]
#[tokio::test(flavor = "multi_thread")]
async fn test_constraint_report_under_constrained() {
let kcl = r#"
sketch(on = YZ) {
line1 = line(start = [var 1.32mm, var -1.93mm], end = [var 6.08mm, var 2.51mm])
}
"#;
let report = run_constraint_report(kcl).await;
assert_eq!(report.fully_constrained.len(), 0);
assert_eq!(report.under_constrained.len(), 1);
assert_eq!(report.over_constrained.len(), 0);
assert_eq!(report.errors.len(), 0);
assert_eq!(report.under_constrained[0].status, ConstraintKind::UnderConstrained);
assert!(report.under_constrained[0].free_count > 0);
}
#[cfg(feature = "artifact-graph")]
#[tokio::test(flavor = "multi_thread")]
async fn test_constraint_report_over_constrained() {
let kcl = r#"
@settings(experimentalFeatures = allow)
sketch(on = YZ) {
line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
line1.start.at[0] == 2
line1.start.at[1] == 8
line1.end.at[0] == 5
line1.end.at[1] == 7
distance([line1.start, line1.end]) == 100mm
}
"#;
let report = run_constraint_report(kcl).await;
assert_eq!(report.over_constrained.len(), 1);
assert_eq!(report.errors.len(), 0);
assert_eq!(report.over_constrained[0].status, ConstraintKind::OverConstrained);
assert!(report.over_constrained[0].conflict_count > 0);
}
#[cfg(feature = "artifact-graph")]
#[tokio::test(flavor = "multi_thread")]
async fn test_constraint_report_multiple_sketches() {
let kcl = r#"
@settings(experimentalFeatures = allow)
s1 = sketch(on = YZ) {
line1 = line(start = [var 2mm, var 8mm], end = [var 5mm, var 7mm])
line1.start.at[0] == 2
line1.start.at[1] == 8
line1.end.at[0] == 5
line1.end.at[1] == 7
}
s2 = sketch(on = XZ) {
line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
}
"#;
let report = run_constraint_report(kcl).await;
assert_eq!(
report.fully_constrained.len()
+ report.under_constrained.len()
+ report.over_constrained.len()
+ report.errors.len(),
2,
"Expected 2 sketches total"
);
assert_eq!(report.fully_constrained.len(), 1);
assert_eq!(report.under_constrained.len(), 1);
}
}