use std::collections::BTreeMap;
use std::sync::Arc;
use anyhow::Result;
pub use artifact::ArtifactCommand;
pub(crate) use artifact::EntityCloneInfo;
pub(crate) use artifact::named_view_artifact;
pub(crate) use artifact::sketch_block_constraint_type;
use cache::GlobalState;
pub use cache::bust_cache;
pub use cache::clear_mem_cache;
pub use geometry::*;
pub use id_generator::IdGenerator;
pub(crate) use import::PreImportedGeometry;
use indexmap::IndexMap;
pub use kcl_api::Operation;
pub use kcl_api::artifact::Artifact;
pub use kcl_api::artifact::ArtifactGraph;
pub use kcl_api::artifact::CapSubType;
pub use kcl_api::artifact::CodeRef;
pub use kcl_api::artifact::GdtAnnotationArtifact;
pub use kcl_api::artifact::SketchBlock;
pub use kcl_api::artifact::SketchBlockConstraint;
#[allow(unused_imports)]
pub use kcl_api::artifact::SketchBlockConstraintType;
pub use kcl_api::artifact::StartSketchOnFace;
pub use kcl_api::artifact::StartSketchOnPlane;
use kcl_api::ast::node_path::NodePath;
pub use kcl_value::KclObjectFields;
pub use kcl_value::KclObjectKind;
pub use kcl_value::KclValue;
pub use kcl_value_view::EdgeCutViewExt;
pub use kcl_value_view::ExtrudeSurfaceViewExt;
pub use kcl_value_view::KclValueView;
pub use kcl_value_view::PathViewExt;
pub use kcl_value_view::SolidViewExt;
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;
#[cfg(test)]
pub(crate) use memory::MemoryBackendKind;
pub(crate) use modeling::ModelingCmdMeta;
pub use named_views::*;
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(crate) use solver_arc::SolverArc;
pub(crate) use state::ConstraintKey;
pub(crate) use state::ConstraintState;
pub(crate) use state::ConsumedRegionInfo;
pub(crate) use state::ConsumedRegionOperation;
pub(crate) use state::ConsumedSolidInfo;
pub(crate) use state::ConsumedSolidKey;
pub(crate) use state::ConsumedSolidOperation;
pub use state::DirectTagFilletMeta;
pub use state::DirectTagFilletTagEntry;
pub use state::EdgeRefactorMeta;
pub use state::EdgeRefactorStdlibFn;
pub use state::ExecState;
pub use state::KclVersion;
pub use state::LegacyAngleRefactorMeta;
pub use state::MetaSettings;
pub(crate) use state::ModuleArtifactState;
pub(crate) use state::PendingEdgeRefactorMeta;
pub(crate) use state::PendingLegacyAngleRefactorMeta;
pub use state::RefactorMetadata;
pub(crate) use state::TangencyMode;
use crate::CompilationIssue;
use crate::ExecError;
use crate::KclErrorWithOutputs;
use crate::NodePathExt;
use crate::SourceRange;
use crate::collections::AhashIndexSet;
use crate::engine::EngineBatchContext;
use crate::engine::GridScaleBehavior;
use crate::engine::engine_manager::EngineManager;
use crate::errors::KclError;
use crate::errors::KclErrorDetails;
use crate::execution::cache::CacheInformation;
use crate::execution::cache::CacheResult;
use crate::execution::cad_op::OperationExt;
use crate::execution::import_graph::Universe;
use crate::execution::import_graph::UniverseMap;
use crate::execution::typed_path::TypedPath;
use crate::front::Number;
use crate::front::Object;
use crate::front::ObjectId;
use crate::fs::FileManager;
use crate::fs::FileSystemHandle;
use crate::modules::ModuleExecutionOutcome;
use crate::modules::ModuleId;
use crate::modules::ModulePath;
use crate::modules::ModuleRepr;
use crate::modules::ModuleSource;
use crate::parsing::ast::types::Expr;
use crate::parsing::ast::types::ImportPath;
use crate::parsing::ast::types::NodeRef;
#[derive(Debug, Clone, Serialize, ts_rs::TS, PartialEq, Default)]
#[ts(export)]
pub struct OperationsByModule {
pub map: IndexMap<ModuleId, Vec<Operation>>,
}
#[derive(Clone, Serialize, ts_rs::TS)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct OperationCallbackArgs {
pub module_id: ModuleId,
pub operation: Operation,
pub index: usize,
}
pub trait ExecutionCallbacks: std::fmt::Debug + Send + Sync + 'static {
fn on_operation(&self, _args: OperationCallbackArgs) {}
}
impl OperationsByModule {
pub fn count(&self) -> usize {
self.map.values().map(Vec::len).sum()
}
pub fn is_empty(&self) -> bool {
self.map.values().all(Vec::is_empty)
}
pub fn get(&self, module_id: &ModuleId) -> Option<&Vec<Operation>> {
self.map.get(module_id)
}
pub fn values(&self) -> indexmap::map::Values<'_, ModuleId, Vec<Operation>> {
self.map.values()
}
pub fn insert(&mut self, module_id: ModuleId, operations: Vec<Operation>) {
self.map.insert(module_id, operations);
}
}
pub(crate) mod annotations;
mod artifact;
#[cfg(test)]
pub(crate) use artifact::mermaid_tests::ArtifactGraphMermaidExt;
pub(crate) mod cache;
mod cad_op;
pub(crate) mod exec_ast;
pub mod fn_call;
#[cfg(test)]
mod freedom_analysis_tests;
mod geometry;
#[cfg(test)]
mod hide_id_contract_kcl_test_pins;
mod id_generator;
mod import;
mod import_graph;
pub(crate) mod kcl_value;
pub(crate) mod kcl_value_view;
pub(crate) mod machine;
mod memory;
mod modeling;
mod named_views;
mod sketch_solve;
mod solver_arc;
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,
Return,
Exit,
}
impl ControlFlowKind {
pub fn is_some_return(&self) -> bool {
match self {
ControlFlowKind::Continue => false,
ControlFlowKind::Return => true,
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: Box<KclValue>,
pub control: ControlFlowKind,
}
impl KclValue {
pub(crate) fn continue_(self) -> KclValueControlFlow {
KclValueControlFlow {
value: Box::new(self),
control: ControlFlowKind::Continue,
}
}
pub(crate) fn return_(self) -> KclValueControlFlow {
KclValueControlFlow {
value: Box::new(self),
control: ControlFlowKind::Return,
}
}
pub(crate) fn exit(self) -> KclValueControlFlow {
KclValueControlFlow {
value: Box::new(self),
control: ControlFlowKind::Exit,
}
}
}
impl KclValueControlFlow {
pub fn is_some_return(&self) -> bool {
self.control.is_some_return()
}
pub(crate) fn is_return(&self) -> bool {
matches!(self.control, ControlFlowKind::Return)
}
pub(crate) fn is_exit(&self) -> bool {
matches!(self.control, ControlFlowKind::Exit)
}
pub(crate) fn source_ranges(&self) -> Vec<SourceRange> {
self.value.metadata().iter().map(|m| m.source_range).collect()
}
pub(crate) fn into_value(self) -> KclValue {
*self.value
}
}
#[must_use = "You should always handle the control flow value when it is returned"]
#[allow(clippy::large_enum_variant)]
#[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, KclValueView>,
#[cfg(test)]
#[serde(skip)]
#[ts(skip)]
pub(crate) test_program_memory: IndexMap<String, KclValue>,
pub operations: OperationsByModule,
pub artifact_graph: ArtifactGraph,
#[serde(skip)]
pub scene_objects: Vec<Object>,
#[serde(skip)]
pub source_range_to_object: BTreeMap<SourceRange, ObjectId>,
#[serde(skip)]
pub var_solutions: Vec<(SourceRange, Option<NodePath>, Number)>,
pub refactor_metadata: Vec<RefactorMetadata>,
pub issues: Vec<CompilationIssue>,
pub filenames: IndexMap<ModuleId, ModulePath>,
#[serde(skip)]
pub source_files: IndexMap<ModuleId, ModuleSource>,
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 instance_index: usize,
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>,
}
pub(crate) fn sketch_constraint_status_for_sketch(
scene_objects: &[Object],
sketch_obj: &Object,
) -> Option<SketchConstraintStatus> {
use crate::front::ObjectKind;
use crate::front::Segment;
let ObjectKind::Sketch(sketch) = &sketch_obj.kind else {
return None;
};
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 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
};
Some(SketchConstraintStatus {
name: sketch_obj.label.clone(),
instance_index: 0,
status,
free_count,
conflict_count,
total_count,
})
}
pub(crate) fn sketch_constraint_report_from_scene_objects(scene_objects: &[Object]) -> SketchConstraintReport {
let mut fully_constrained = Vec::new();
let mut under_constrained = Vec::new();
let mut over_constrained = Vec::new();
let mut errors = Vec::new();
let mut instance_counts = std::collections::HashMap::new();
for obj in scene_objects {
let Some(mut entry) = sketch_constraint_status_for_sketch(scene_objects, obj) else {
continue;
};
let count = instance_counts.entry(entry.name.clone()).or_insert(0);
entry.instance_index = *count;
*count += 1;
match entry.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> {
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)
}
pub fn errors(&self) -> impl Iterator<Item = &CompilationIssue> {
self.issues.iter().filter(|error| error.is_err())
}
pub fn sketch_constraint_report(&self) -> SketchConstraintReport {
sketch_constraint_report_from_scene_objects(&self.scene_objects)
}
pub fn render_sketch_png(
&self,
sketch_name: &str,
) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
self.render_sketch_png_instance(sketch_name, None)
}
pub fn render_sketch_png_instance(
&self,
sketch_name: &str,
instance_index: Option<usize>,
) -> std::result::Result<Vec<u8>, crate::tooling::sketch_visualizer::SketchVisualizationError> {
use crate::front::ObjectKind;
use crate::tooling::sketch_visualizer::SketchVisualizationError;
let sketches = self
.scene_objects
.iter()
.filter_map(|object| match &object.kind {
ObjectKind::Sketch(sketch) if object.label == sketch_name => Some(sketch),
_ => None,
})
.collect::<Vec<_>>();
let sketch = match (sketches.as_slice(), instance_index) {
([], _) => {
return Err(SketchVisualizationError::SketchNotFound {
name: sketch_name.to_owned(),
});
}
(_, Some(index)) => *sketches
.get(index)
.ok_or_else(|| SketchVisualizationError::InstanceNotFound {
name: sketch_name.to_owned(),
index,
count: sketches.len(),
})?,
([sketch], None) => *sketch,
(_, None) => {
return Err(SketchVisualizationError::AmbiguousSketchName {
name: sketch_name.to_owned(),
count: sketches.len(),
});
}
};
crate::tooling::sketch_visualizer::render_sketch_png(&self.scene_objects, sketch)
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct MockConfig {
pub use_prev_memory: bool,
pub sketch_block_id: Option<ObjectId>,
pub freedom_analysis: bool,
pub segment_ids_edited: AhashIndexSet<ObjectId>,
pub drag_anchors: Vec<SegmentDragAnchor>,
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, ts_rs::TS)]
#[ts(export, export_to = "FrontendApi.ts")]
#[serde(rename_all = "camelCase")]
pub struct SegmentDragAnchor {
pub segment_id: ObjectId,
pub target: crate::front::Point2d<Number>,
}
impl Default for MockConfig {
fn default() -> Self {
Self {
use_prev_memory: true,
sketch_block_id: None,
freedom_analysis: true,
segment_ids_edited: AhashIndexSet::default(),
drag_anchors: Vec::new(),
}
}
}
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())
}
pub(crate) fn is_body_created_tag(&self) -> bool {
self.get_cur_info().is_some_and(|info| {
matches!(&info.geometry, Geometry::Solid(_)) && info.path.is_none() && info.surface.is_some()
})
}
}
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(Clone)]
pub struct ExecutorContext {
pub engine: Arc<EngineManager>,
pub engine_batch: EngineBatchContext,
pub fs: FileSystemHandle,
pub settings: ExecutorSettings,
pub context_type: ContextType,
pub execution_callbacks: Option<Arc<dyn ExecutionCallbacks>>,
pub(crate) executor_kind: machine::ExecutorKind,
pub(crate) machine_call_depth_limit: usize,
}
impl std::fmt::Debug for ExecutorContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ExecutorContext")
.field("engine", &self.engine)
.field("engine_batch", &self.engine_batch)
.field("settings", &self.settings)
.field("context_type", &self.context_type)
.field("execution_callbacks", &self.execution_callbacks)
.field("executor_kind", &self.executor_kind)
.field("machine_call_depth_limit", &self.machine_call_depth_limit)
.finish()
}
}
#[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,
#[serde(default, skip_serializing_if = "is_false")]
pub skip_artifact_graph: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub heartbeats: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_backface_color: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pool: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub video_res_width: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub video_res_height: Option<u32>,
pub geometry_only: bool,
}
fn is_false(b: &bool) -> bool {
!*b
}
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,
skip_artifact_graph: false,
heartbeats: None,
default_backface_color: None,
pool: None,
video_res_width: None,
video_res_height: None,
geometry_only: false,
}
}
}
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 {
let modeling_settings = settings.modeling.unwrap_or_default();
Self {
highlight_edges: modeling_settings.highlight_edges.unwrap_or_default().into(),
enable_ssao: modeling_settings.enable_ssao.unwrap_or_default().into(),
show_grid: modeling_settings.show_scale_grid.unwrap_or_default(),
replay: None,
project_directory: None,
current_file: None,
fixed_size_grid: modeling_settings.fixed_size_grid.unwrap_or_default().0,
skip_artifact_graph: false,
heartbeats: None,
default_backface_color: modeling_settings.backface_color.map(|color| color.0),
pool: None,
video_res_width: None,
video_res_height: None,
geometry_only: false,
}
}
}
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.unwrap_or_default().into(),
enable_ssao: modeling.enable_ssao.unwrap_or_default().into(),
show_grid: modeling.show_scale_grid.unwrap_or_default(),
replay: None,
project_directory: None,
current_file: None,
fixed_size_grid: true,
skip_artifact_graph: false,
heartbeats: None,
default_backface_color: modeling.backface_color.map(|color| color.0),
pool: None,
video_res_width: None,
video_res_height: None,
geometry_only: false,
}
}
}
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,
skip_artifact_graph: false,
heartbeats: None,
default_backface_color: None,
pool: None,
video_res_width: None,
video_res_height: None,
geometry_only: false,
}
}
}
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<EngineManager>,
fs: FileSystemHandle,
settings: ExecutorSettings,
) -> Self {
ExecutorContext {
engine,
engine_batch: EngineBatchContext::default(),
fs,
settings,
context_type: ContextType::Live,
execution_callbacks: Default::default(),
executor_kind: machine::ExecutorKind::resolve(),
machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
}
}
fn clone_with_fresh_execution_batch(&self) -> Self {
Self {
engine: self.engine.clone(),
engine_batch: EngineBatchContext::new(),
fs: self.fs.clone(),
settings: self.settings.clone(),
context_type: self.context_type.clone(),
execution_callbacks: self.execution_callbacks.clone(),
executor_kind: self.executor_kind,
machine_call_depth_limit: self.machine_call_depth_limit,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn new_with_engine(engine: Arc<EngineManager>, settings: ExecutorSettings) -> Self {
Self::new_with_engine_and_fs(engine, crate::fs::new_file_system_handle(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: if settings.geometry_only {
Some("cpu".to_string())
} else {
settings.pool.clone()
},
geometry_only: Some(settings.geometry_only),
kcl_version: None,
pr,
unlocked_framerate: None,
webrtc: Some(false),
video_res_width: settings.video_res_width,
video_res_height: settings.video_res_height,
})
.await?;
let request_id = headers
.get("x-request-id")
.and_then(|id| id.to_str().ok())
.map(str::to_owned);
let engine_conn =
EngineManager::new_websocket_transport_with_request_id(ws, settings.heartbeats, request_id).await;
let engine = Arc::new(engine_conn);
Ok(Self::new_with_engine(engine, settings))
}
#[cfg(target_arch = "wasm32")]
pub fn new(engine: Arc<EngineManager>, fs: FileSystemHandle, 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(EngineManager::new_mock()),
engine_batch: EngineBatchContext::default(),
fs: crate::fs::new_file_system_handle(FileManager::new()),
settings: settings.unwrap_or_default(),
context_type: ContextType::Mock,
execution_callbacks: Default::default(),
executor_kind: machine::ExecutorKind::resolve(),
machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
}
}
#[cfg(target_arch = "wasm32")]
pub fn new_mock(engine: Arc<EngineManager>, fs: FileSystemHandle, settings: ExecutorSettings) -> Self {
ExecutorContext {
engine,
engine_batch: EngineBatchContext::default(),
fs,
settings,
context_type: ContextType::Mock,
execution_callbacks: Default::default(),
executor_kind: machine::ExecutorKind::resolve(),
machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
}
}
#[cfg(target_arch = "wasm32")]
pub fn new_mock_for_lsp(
fs_manager: crate::fs::wasm::FileSystemManager,
settings: ExecutorSettings,
) -> Result<Self, String> {
let fs = crate::fs::new_file_system_handle(FileManager::new(fs_manager));
Ok(ExecutorContext {
engine: Arc::new(EngineManager::new_mock()),
engine_batch: EngineBatchContext::default(),
fs,
settings,
context_type: ContextType::Mock,
execution_callbacks: Default::default(),
executor_kind: machine::ExecutorKind::resolve(),
machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
})
}
#[cfg(not(target_arch = "wasm32"))]
pub fn new_forwarded_mock(engine: Arc<EngineManager>) -> Self {
ExecutorContext {
engine,
engine_batch: EngineBatchContext::default(),
fs: crate::fs::new_file_system_handle(FileManager::new()),
settings: Default::default(),
context_type: ContextType::MockCustomForwarded,
execution_callbacks: Default::default(),
executor_kind: machine::ExecutorKind::resolve(),
machine_call_depth_limit: machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
}
}
#[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_geometry_only_with_default_client() -> Result<Self> {
Self::new_with_client(
ExecutorSettings {
geometry_only: true,
..Default::default()
},
None,
None,
)
.await
}
#[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,
skip_artifact_graph: false,
heartbeats: None,
default_backface_color: None,
pool: None,
video_res_width: None,
video_res_height: None,
geometry_only: 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(
&self.engine_batch,
&mut exec_state.mod_local.id_generator,
source_range,
self.settings.geometry_only,
)
.await?;
if !self.settings.geometry_only && 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)
.map_err(KclErrorWithOutputs::no_outputs)?;
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;
exec_state.mod_local.constraint_state = mem.constraint_state;
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> {
let (exec_state, main_ref) = self.run_mock_returning_state(program, mock_config).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();
let constraint_state = exec_state.mod_local.constraint_state.clone();
let scene_objects = exec_state.global.root_module_artifacts.scene_objects.clone();
let outcome = exec_state
.into_exec_outcome(main_ref, self)
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
stack.squash_env(main_ref).map_err(KclErrorWithOutputs::no_outputs)?;
let state = cache::SketchModeState {
stack,
module_infos,
path_to_source_id,
id_to_source,
constraint_state,
scene_objects,
};
cache::write_old_memory(state).await;
Ok(outcome)
}
async fn run_mock_returning_state(
&self,
program: &crate::Program,
mock_config: &MockConfig,
) -> Result<(ExecState, EnvironmentRef), 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()
.map_err(KclErrorWithOutputs::no_outputs)?;
let (main_ref, _) = self.inner_run(program, &mut exec_state, PreserveMem::Always).await?;
Ok((exec_state, main_ref))
}
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.engine_batch,
&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.engine_batch,
&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()
.map_err(KclErrorWithOutputs::no_outputs)?,
)
.await;
return cached_state
.into_exec_outcome(self)
.await
.map_err(KclErrorWithOutputs::no_outputs);
}
(
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.engine_batch,
&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()
.map_err(KclErrorWithOutputs::no_outputs)?,
)
.await;
return cached_state
.into_exec_outcome(self)
.await
.map_err(KclErrorWithOutputs::no_outputs);
}
(true, program, None)
}
CacheResult::NoAction(false) => {
cache::write_old_memory(
cached_state
.mock_memory_state()
.map_err(KclErrorWithOutputs::no_outputs)?,
)
.await;
return cached_state
.into_exec_outcome(self)
.await
.map_err(KclErrorWithOutputs::no_outputs);
}
};
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(self);
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(self);
exec_state
.mut_stack()
.restore_env(cached_state.main.result_env)
.map_err(KclErrorWithOutputs::no_outputs)?;
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
.map_err(KclErrorWithOutputs::no_outputs)?;
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> {
exec_state.set_entry_point_kcl_version(program);
let (universe, universe_map) = if let Some((universe, universe_map)) = universe_info {
(universe, universe_map)
} else {
self.get_universe(program, exec_state).await?
};
let mut sorted_imports: Vec<_> = universe_map.iter().collect();
sorted_imports.sort_by_key(|(_, import_stmt)| SourceRange::from(*import_stmt));
for (_path, import_stmt) in sorted_imports {
let filename = match &import_stmt.path {
ImportPath::Kcl { filename } => filename.to_string(),
ImportPath::Foreign { path } => path.to_string(),
ImportPath::Std { .. } => continue,
};
if let Some((_, module_id, module_path, _)) = universe.get(&filename)
&& let ModulePath::Local { value, .. } = module_path
{
let name = import_stmt
.module_name()
.unwrap_or_else(|| value.file_name().unwrap_or_default());
let source_range = SourceRange::from(import_stmt);
exec_state.push_op(crate::execution::cad_op::Operation::ModuleInstance {
name,
module_id: *module_id,
glob: matches!(
import_stmt.selector,
crate::parsing::ast::types::ImportSelector::Glob(_)
),
node_path: crate::NodePath::placeholder(),
source_range,
});
}
}
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();
let repr = repr.clone();
let exec_ctxt = self.clone_with_fresh_execution_batch();
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, _) => {
exec_state.mod_local.artifacts = Default::default();
let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
.await
.map(|geom| Some(KclValue::ImportedGeometry(geom)))
.map_err(|err| err.add_import_location(&module_path.import_name(), source_range));
let module_artifacts = std::mem::take(&mut exec_state.mod_local.artifacts);
result.map(|val| ModuleRepr::Foreign(geom.clone(), Some((val, module_artifacts))))
}
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) => {
let e = import_graph::add_import_backtrace(e, module_id, &universe);
return Err(exec_state.error_with_outputs(e, None, default_planes));
}
}
}
}
exec_state.mod_local.artifacts.operations.clear();
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
.map_err(|mut error| {
let source_ranges = error.error.source_ranges();
if !source_ranges.is_empty()
&& !source_ranges.iter().any(|range| range.module_id().is_top_level())
&& let Some(outermost) = source_ranges.last()
{
error.error =
import_graph::add_import_backtrace_from(error.error.clone(), outermost.module_id(), &universe);
}
error
})
}
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))
}
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");
exec_state.set_entry_point_kcl_version(program);
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.engine_batch,
&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,
) -> Result<(), KclError> {
if ctx.is_mock() {
return Ok(());
}
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(),
constraint_state: exec_state.mod_local.constraint_state.clone(),
scene_objects: exec_state.global.root_module_artifacts.scene_objects.clone(),
};
cache::write_old_memory(state).await;
Ok(())
}
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
.map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
}
return Err(exec_state.error_with_outputs(err, env_ref, default_planes));
}
};
write_old_memory(self, exec_state, env_ref)
.await
.map_err(|err| exec_state.error_with_outputs(err, Some(env_ref), default_planes.clone()))?;
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>)> {
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)
});
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(&self.engine_batch)
.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(&self.engine_batch).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() {
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)?;
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(
&self.engine_batch,
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(
&self.engine_batch,
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(
&self.engine_batch,
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;
}
}
pub use kcl_api::ArtifactId;
pub fn cmd_id_ref_to_artifact_id(id: &ModelingCmdId) -> ArtifactId {
ArtifactId::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> {
parse_execute_with_executor_kind(code, project_directory, machine::ExecutorKind::resolve()).await
}
#[cfg(test)]
pub(crate) fn new_mock_executor_context(
project_directory: Option<TypedPath>,
executor_kind: machine::ExecutorKind,
) -> ExecutorContext {
ExecutorContext {
engine: Arc::new(EngineManager::new_mock()),
engine_batch: EngineBatchContext::default(),
fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
settings: ExecutorSettings {
project_directory,
..Default::default()
},
context_type: ContextType::Mock,
execution_callbacks: Default::default(),
executor_kind,
machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
}
}
#[cfg(test)]
pub(crate) async fn parse_execute_with_executor_kind(
code: &str,
project_directory: Option<TypedPath>,
executor_kind: machine::ExecutorKind,
) -> Result<ExecTestResults, KclError> {
let program = crate::Program::parse_no_errs(code)?;
let exec_ctxt = new_mock_executor_context(project_directory, executor_kind);
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(test)]
impl ExecTestResults {
pub(crate) fn root_module_artifact_commands(&self) -> &[ArtifactCommand] {
&self.exec_state.global.root_module_artifacts.commands
}
pub(crate) fn issues(&self) -> &[CompilationIssue] {
self.exec_state.issues()
}
#[track_caller]
pub(crate) fn variable(&self, name: &str) -> KclValue {
self.exec_state
.stack()
.memory
.get_from_unchecked(name, self.mem_env)
.unwrap()
}
}
pub struct ProgramLookup {
programs: IndexMap<ModuleId, crate::parsing::ast::types::Node<crate::parsing::ast::types::Program>>,
}
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 kcl_api::NumericType;
use pretty_assertions::assert_eq;
use super::*;
use crate::ModuleId;
use crate::errors::KclErrorDetails;
use crate::errors::Severity;
use crate::execution::kcl_value::TypeDef;
use crate::execution::memory::Stack;
use crate::execution::types::RuntimeType;
macro_rules! kcl_input {
($file:literal) => {
include_str!(concat!("../../e2e/executor/inputs/", $file, ".kcl"))
};
}
#[test]
fn clone_with_fresh_execution_batch_keeps_executor_selection() {
let mut ctx = new_mock_executor_context(None, machine::ExecutorKind::Machine);
ctx.machine_call_depth_limit = 123;
let cloned = ctx.clone_with_fresh_execution_batch();
assert_eq!(cloned.executor_kind, machine::ExecutorKind::Machine);
assert_eq!(cloned.machine_call_depth_limit, 123);
}
#[tokio::test(flavor = "multi_thread")]
async fn concurrent_foreign_import_preserves_artifact_command() {
let tmpdir = tempfile::TempDir::with_prefix("zma_foreign_import_artifact").unwrap();
tokio::fs::write(tmpdir.path().join("cube.obj"), "o cube\n")
.await
.unwrap();
let program = crate::Program::parse_no_errs("import \"cube.obj\" as cube\n\nmodel = cube\n").unwrap();
let ctx = new_mock_executor_context(
Some(crate::TypedPath(tmpdir.path().into())),
machine::ExecutorKind::resolve(),
);
let mut exec_state = ExecState::new(&ctx);
let (main_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
let outcome = exec_state
.into_exec_outcome(main_ref, &ctx)
.await
.expect("foreign import execution should produce an outcome");
ctx.close().await;
let KclValueView::ImportedGeometry(imported) = &outcome.variables["model"] else {
panic!("model should be imported geometry");
};
let artifact_id = ArtifactId::new(imported.id);
let Some(Artifact::ImportedGeometry(artifact)) = outcome.artifact_graph.get(&artifact_id) else {
panic!("foreign import should produce an imported geometry artifact");
};
assert_eq!(artifact.id, artifact_id);
assert!(!artifact.code_ref.node_path.is_empty());
}
#[tokio::test(flavor = "multi_thread")]
async fn nested_import_preserves_inner_error_and_backtrace() {
let project_dir = crate::TypedPath::new("/zma-kcl-import-error");
let main_path = project_dir.join("main.kcl");
let assembly_path = project_dir.join("assembly.kcl");
let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
let files = [
(
project_dir.join("broken.kcl").to_string(),
b"export brokenValue = missingName + 1\n".to_vec(),
),
(
assembly_path.to_string(),
b"import brokenValue from \"broken.kcl\"\n\nexport assemblyValue = brokenValue\n".to_vec(),
),
]
.into_iter()
.collect();
let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
let settings = ExecutorSettings {
project_directory: Some(project_dir),
current_file: Some(main_path.clone()),
..Default::default()
};
let program = crate::Program::parse_no_errs(main_code).unwrap();
let assert_error = |error: &KclErrorWithOutputs| {
let KclError::UndefinedValue { details, name } = &error.error else {
panic!("expected UndefinedValue, got {:#?}", error.error);
};
assert_eq!(name.as_deref(), Some("missingName"));
assert_eq!(details.message, "`missingName` is not defined");
assert_eq!(
error
.error
.backtrace()
.iter()
.map(|frame| frame.fn_name.as_deref())
.collect::<Vec<_>>(),
[Some("import broken.kcl"), Some("import assembly.kcl"), None]
);
assert_eq!(
error
.error
.backtrace()
.iter()
.map(|frame| frame.kind)
.collect::<Vec<_>>(),
[
kcl_error::BacktraceItemKind::Import,
kcl_error::BacktraceItemKind::Import,
kcl_error::BacktraceItemKind::Call
]
);
let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
assert!(report.filename.ends_with("broken.kcl"));
assert_eq!(
report
.related
.iter()
.map(|related| related.filename.as_str())
.collect::<Vec<_>>(),
[assembly_path.to_string(), main_path.to_string()]
);
let rendered = format!("{:?}", miette::Report::new(report));
assert!(rendered.contains("broken.kcl"));
assert!(rendered.contains("assembly.kcl"));
assert!(rendered.contains("main.kcl"));
assert!(rendered.contains("export brokenValue = missingName + 1"));
assert!(!rendered.contains("Failed to read contents"));
};
let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
mock_ctx.fs = fs.clone();
let mock_error = mock_ctx
.run_mock(
&program,
&MockConfig {
use_prev_memory: false,
..Default::default()
},
)
.await
.unwrap_err();
mock_ctx.close().await;
assert_error(&mock_error);
let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
concurrent_ctx.fs = fs;
let mut exec_state = ExecState::new(&concurrent_ctx);
let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
concurrent_ctx.close().await;
assert_error(&concurrent_error);
}
#[tokio::test(flavor = "multi_thread")]
async fn function_error_across_import_keeps_backtrace_innermost_first() {
let project_dir = crate::TypedPath::new("/zma-kcl-import-fn-error");
let main_path = project_dir.join("main.kcl");
let main_code = "import assemblyValue from \"assembly.kcl\"\n\nassemblyValue\n";
let files = [
(
project_dir.join("helper.kcl").to_string(),
b"export fn inner() { return missingName }\nexport fn outer() { return inner() }\n".to_vec(),
),
(
project_dir.join("assembly.kcl").to_string(),
b"import outer from \"helper.kcl\"\n\nexport assemblyValue = outer()\n".to_vec(),
),
]
.into_iter()
.collect();
let fs = crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files));
let settings = ExecutorSettings {
project_directory: Some(project_dir.clone()),
current_file: Some(main_path),
..Default::default()
};
let program = crate::Program::parse_no_errs(main_code).unwrap();
let assert_error = |error: &KclErrorWithOutputs| {
assert!(
matches!(&error.error, KclError::UndefinedValue { .. }),
"expected UndefinedValue, got {:#?}",
error.error
);
assert_eq!(
error
.error
.backtrace()
.iter()
.map(|frame| frame.fn_name.as_deref())
.collect::<Vec<_>>(),
[Some("inner"), Some("outer"), Some("import assembly.kcl"), None]
);
assert_eq!(
error
.error
.backtrace()
.iter()
.map(|frame| frame.kind)
.collect::<Vec<_>>(),
[
kcl_error::BacktraceItemKind::Call,
kcl_error::BacktraceItemKind::Call,
kcl_error::BacktraceItemKind::Import,
kcl_error::BacktraceItemKind::Call
]
);
let report = error.clone().into_miette_report_with_outputs(main_code).unwrap();
assert!(report.filename.ends_with("helper.kcl"));
assert_eq!(
report
.related
.iter()
.map(|related| related.filename.as_str())
.collect::<Vec<_>>(),
[
project_dir.join("assembly.kcl").to_string(),
project_dir.join("main.kcl").to_string()
]
);
let rendered = format!("{:?}", miette::Report::new(report));
assert!(rendered.contains("return missingName"));
assert!(!rendered.contains("Failed to read contents"));
};
let mut mock_ctx = ExecutorContext::new_mock(Some(settings.clone())).await;
mock_ctx.fs = fs.clone();
let mock_error = mock_ctx
.run_mock(
&program,
&MockConfig {
use_prev_memory: false,
..Default::default()
},
)
.await
.unwrap_err();
mock_ctx.close().await;
assert_error(&mock_error);
let mut concurrent_ctx = ExecutorContext::new_mock(Some(settings)).await;
concurrent_ctx.fs = fs;
let mut exec_state = ExecState::new(&concurrent_ctx);
let concurrent_error = concurrent_ctx.run(&program, &mut exec_state).await.unwrap_err();
concurrent_ctx.close().await;
assert_error(&concurrent_error);
}
#[track_caller]
fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
memory.memory.get_from_unchecked(name, env).unwrap()
}
async fn execute_variables_with_backend(
code: &str,
backend: memory::MemoryBackendKind,
) -> IndexMap<String, KclValueView> {
execute_outcome_with_backend(code, backend).await.variables
}
async fn execute_outcome_with_backend(code: &str, backend: memory::MemoryBackendKind) -> ExecOutcome {
let program = crate::Program::parse_no_errs(code).unwrap();
let ctx = ExecutorContext::new_mock(None).await;
let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
let outcome = exec_state
.into_exec_outcome(env_ref, &ctx)
.await
.expect("test execution outcome should collect variables");
ctx.close().await;
outcome
}
async fn execute_error_variables_with_backend(
code: &str,
backend: memory::MemoryBackendKind,
) -> IndexMap<String, KclValueView> {
let program = crate::Program::parse_no_errs(code).unwrap();
let ctx = ExecutorContext::new_mock(None).await;
let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
let error = ctx.run(&program, &mut exec_state).await.unwrap_err();
ctx.close().await;
error.variables
}
async fn execute_project_variables_with_backend(
main_code: &str,
files: &[(&str, &str)],
backend: memory::MemoryBackendKind,
) -> IndexMap<String, KclValueView> {
let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_memory_backend_project").unwrap();
for (name, contents) in files {
tokio::fs::write(tmpdir.path().join(name), contents).await.unwrap();
}
let program = crate::Program::parse_no_errs(main_code).unwrap();
let ctx = ExecutorContext {
engine: Arc::new(EngineManager::new_mock()),
engine_batch: EngineBatchContext::default(),
fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
settings: ExecutorSettings {
project_directory: Some(crate::TypedPath(tmpdir.path().into())),
..Default::default()
},
context_type: ContextType::Mock,
execution_callbacks: Default::default(),
executor_kind: machine::ExecutorKind::resolve(),
machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
};
let mut exec_state = ExecState::new_with_memory_backend(&ctx, backend);
let (env_ref, _) = ctx.run(&program, &mut exec_state).await.unwrap();
let outcome = exec_state
.into_exec_outcome(env_ref, &ctx)
.await
.expect("test execution outcome should collect variables");
ctx.close().await;
outcome.variables
}
async fn run_with_caching_variables_with_backend(
code: &str,
backend: memory::MemoryBackendKind,
) -> IndexMap<String, KclValueView> {
let _backend = memory::MemoryBackendKind::override_for_test(backend);
cache::bust_cache().await;
clear_mem_cache().await;
let ctx = ExecutorContext::new_with_engine(Arc::new(EngineManager::new_mock()), Default::default());
let program = crate::Program::parse_no_errs(code).unwrap();
ctx.run_with_caching(program.clone()).await.unwrap();
let cached = ctx.run_with_caching(program).await.unwrap();
cache::bust_cache().await;
clear_mem_cache().await;
ctx.close().await;
cached.variables
}
async fn run_mock_variables_with_backend(
code: &str,
backend: memory::MemoryBackendKind,
) -> IndexMap<String, KclValueView> {
let _backend = memory::MemoryBackendKind::override_for_test(backend);
clear_mem_cache().await;
let ctx = ExecutorContext::new_mock(None).await;
let first = crate::Program::parse_no_errs("x = 2").unwrap();
ctx.run_mock(
&first,
&MockConfig {
use_prev_memory: false,
..Default::default()
},
)
.await
.unwrap();
let program = crate::Program::parse_no_errs(code).unwrap();
let outcome = ctx.run_mock(&program, &MockConfig::default()).await.unwrap();
clear_mem_cache().await;
ctx.close().await;
outcome.variables
}
fn sorted_variable_keys(variables: &IndexMap<String, KclValueView>) -> Vec<String> {
let mut keys = variables.keys().cloned().collect::<Vec<_>>();
keys.sort();
keys
}
async fn collect_backend_results<T, Fut>(
mut run: impl FnMut(memory::MemoryBackendKind) -> Fut,
) -> Vec<(memory::MemoryBackendKind, T)>
where
Fut: std::future::Future<Output = T>,
{
let all = memory::MemoryBackendKind::all();
let mut results = Vec::with_capacity(all.len());
for &kind in all {
results.push((kind, run(kind).await));
}
results
}
fn assert_backend_results_match<T>(results: &[(memory::MemoryBackendKind, T)])
where
T: std::fmt::Debug + PartialEq,
{
let (first, rest) = results.split_first().expect("expected at least one memory backend");
let (first_kind, first_result) = first;
for (kind, result) in rest {
assert_eq!(
result, first_result,
"memory kind {kind:?} doesn't match {first_kind:?}"
);
}
}
fn assert_backend_variable_results_match_expected_keys(
results: &[(memory::MemoryBackendKind, IndexMap<String, KclValueView>)],
expected_keys: &[&str],
) {
let (first_kind, first_variables) = results.first().expect("expected at least one memory backend");
let expected_keys = expected_keys.iter().map(|key| (*key).to_owned()).collect::<Vec<_>>();
assert_eq!(
sorted_variable_keys(first_variables),
expected_keys,
"memory kind {first_kind:?} doesn't match expected variables"
);
assert_backend_results_match(results);
}
fn assert_number_variable(variables: &IndexMap<String, KclValueView>, key: &str, expected: f64) {
let value = variables.get(key).unwrap_or_else(|| panic!("missing variable `{key}`"));
let KclValueView::Number { value, .. } = value else {
panic!("expected `{key}` to be a number, got {value:?}");
};
assert_eq!(*value, expected, "{key}: {value:?}");
}
#[tokio::test(flavor = "multi_thread")]
async fn exec_outcome_variables_match_between_memory_backends() {
let code = "x = 2\ny = x + 1\narr = [x, y]";
let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
assert_backend_variable_results_match_expected_keys(&results, &["arr", "x", "y"]);
}
#[tokio::test(flavor = "multi_thread")]
async fn error_output_variables_match_between_memory_backends() {
let code = "x = 2\ny = missing + 1";
let results = collect_backend_results(|kind| execute_error_variables_with_backend(code, kind)).await;
assert_backend_variable_results_match_expected_keys(&results, &["x"]);
}
#[tokio::test(flavor = "multi_thread")]
async fn cached_execution_variables_match_between_memory_backends() {
let code = "x = 2\ny = x + 1";
let results = collect_backend_results(|kind| run_with_caching_variables_with_backend(code, kind)).await;
assert_backend_variable_results_match_expected_keys(&results, &["x", "y"]);
}
#[tokio::test(flavor = "multi_thread")]
async fn mock_execution_variables_match_between_memory_backends() {
let code = "y = x + 1";
let results = collect_backend_results(|kind| run_mock_variables_with_backend(code, kind)).await;
assert_backend_variable_results_match_expected_keys(&results, &["y"]);
}
#[tokio::test(flavor = "multi_thread")]
async fn module_imports_and_exported_closures_match_between_memory_backends() {
let module_code = r#"
export base = 40
export fn addBase(n) {
return n + base
}
"#;
let main_code = r#"
import base, addBase from 'math.kcl'
import 'math.kcl'
named = addBase(n = 2)
qualified = math::addBase(n = 1)
direct = math::base
"#;
let files = [("math.kcl", module_code)];
let results =
collect_backend_results(|kind| execute_project_variables_with_backend(main_code, &files, kind)).await;
let (_, first_variables) = results.first().expect("expected at least one memory backend");
assert_number_variable(first_variables, "named", 42.0);
assert_number_variable(first_variables, "qualified", 41.0);
assert_number_variable(first_variables, "direct", 40.0);
assert_backend_results_match(&results);
}
#[tokio::test(flavor = "multi_thread")]
async fn sketch_block_variables_match_between_memory_backends() {
let code = r#"
sketch001 = sketch(on = XY) {
line1 = line(start = [0, 0], end = [1, 0])
line2 = line(start = [1, 0], end = [0, 1])
}
lineCount = 2
"#;
let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
let (_, first_variables) = results.first().expect("expected at least one memory backend");
assert!(first_variables.contains_key("sketch001"), "actual: {first_variables:?}");
assert_number_variable(first_variables, "lineCount", 2.0);
assert_backend_results_match(&results);
}
#[tokio::test(flavor = "multi_thread")]
async fn tag_call_stack_lookup_matches_between_memory_backends() {
let code = r#"
sketch001 = startSketchOn(XY)
|> startProfile(at = [0, 0])
|> xLine(length = 10, tag = $seg01)
segLength = segLen(seg01)
"#;
let results = collect_backend_results(|kind| execute_variables_with_backend(code, kind)).await;
let (_, first_variables) = results.first().expect("expected at least one memory backend");
assert_number_variable(first_variables, "segLength", 10.0);
assert_backend_results_match(&results);
}
#[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());
}
#[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_string_uppercase() {
let composed = "\u{e9}";
let uppercase_composed = "\u{c9}";
let decomposed = "e\u{301}";
let uppercase_decomposed = "E\u{301}";
let code = format!(
r#"
ascii = string::uppercase("Kcl")
unicode_expansion = string::uppercase("Straße")
uncased = string::uppercase("東京")
empty = string::uppercase("")
composed = string::uppercase("{composed}")
decomposed = string::uppercase("{decomposed}")
piped = "ready" |> string::uppercase()
"#
);
let result = parse_execute(&code).await.unwrap();
for (name, expected) in [
("ascii", "KCL"),
("unicode_expansion", "STRASSE"),
("uncased", "東京"),
("empty", ""),
("composed", uppercase_composed),
("decomposed", uppercase_decomposed),
("piped", "READY"),
] {
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, name)
.as_str()
.unwrap(),
expected,
"{name}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_string_lowercase() {
let composed = "\u{c9}";
let lowercase_composed = "\u{e9}";
let decomposed = "E\u{301}";
let lowercase_decomposed = "e\u{301}";
let expanded = "i\u{307}";
let code = format!(
r#"
ascii = string::lowercase("KCL")
final_sigma = string::lowercase("ΟΣ")
medial_sigma = string::lowercase("ΟΣΑ")
unicode_expansion = string::lowercase("İ")
uncased = string::lowercase("東京")
empty = string::lowercase("")
composed = string::lowercase("{composed}")
decomposed = string::lowercase("{decomposed}")
piped = "READY" |> string::lowercase()
"#
);
let result = parse_execute(&code).await.unwrap();
for (name, expected) in [
("ascii", "kcl"),
("final_sigma", "ος"),
("medial_sigma", "οσα"),
("unicode_expansion", expanded),
("uncased", "東京"),
("empty", ""),
("composed", lowercase_composed),
("decomposed", lowercase_decomposed),
("piped", "ready"),
] {
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, name)
.as_str()
.unwrap(),
expected,
"{name}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_string_is_equal() {
let composed = "\u{e9}";
let decomposed = "e\u{301}";
let code = format!(
r#"
exact_same = string::isEqual("KCL", to = "KCL")
exact_different_case = string::isEqual("KCL", to = "kcl")
explicit_case_sensitive = string::isEqual("KCL", to = "kcl", caseInsensitive = false)
case_insensitive_ascii = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
case_fold_expansion = string::isEqual("Straße", to = "STRASSE", caseInsensitive = true)
case_fold_expansion_reversed = string::isEqual("STRASSE", to = "Straße", caseInsensitive = true)
case_fold_sigma = string::isEqual("ος", to = "οσ", caseInsensitive = true)
case_fold_non_turkic = string::isEqual("I", to = "i", caseInsensitive = true)
case_fold_not_turkic = string::isEqual("I", to = "ı", caseInsensitive = true)
empty_same = string::isEqual("", to = "")
empty_different = string::isEqual("", to = "KCL")
exact_without_normalization = string::isEqual("{composed}", to = "{decomposed}")
case_fold_without_normalization = string::isEqual("{composed}", to = "{decomposed}", caseInsensitive = true)
piped = "ready" |> string::isEqual(to = "READY", caseInsensitive = true)
"#
);
let result = parse_execute(&code).await.unwrap();
for (name, expected) in [
("exact_same", true),
("exact_different_case", false),
("explicit_case_sensitive", false),
("case_insensitive_ascii", true),
("case_fold_expansion", true),
("case_fold_expansion_reversed", true),
("case_fold_sigma", true),
("case_fold_non_turkic", true),
("case_fold_not_turkic", false),
("empty_same", true),
("empty_different", false),
("exact_without_normalization", false),
("case_fold_without_normalization", false),
("piped", true),
] {
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, name)
.as_bool()
.unwrap(),
expected,
"{name}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_string_is_equal_inside_sketch_block_is_predicate() {
let code = r#"
@settings(experimentalFeatures = allow)
sketch(on = XY) {
stringsAreEqual = string::isEqual("KCL", to = "kcl", caseInsensitive = true)
}
"#;
parse_execute(code).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_string_trim() {
let ascii_whitespace = " \t\n";
let tab = "\t";
let non_breaking_space = "\u{a0}";
let em_space = "\u{2003}";
let ideographic_space = "\u{3000}";
let zero_width_space = "\u{200b}";
let decomposed = "e\u{301}";
let code = format!(
r#"
ascii = string::trim("{ascii_whitespace}KCL{ascii_whitespace}")
internal = string::trim(" KCL{tab}strings ")
unicode = string::trim("{non_breaking_space}{em_space}KCL{ideographic_space}")
all_whitespace = string::trim("{ascii_whitespace}{non_breaking_space}")
empty = string::trim("")
unchanged = string::trim("KCL")
without_normalization = string::trim(" {decomposed} ")
non_whitespace = string::trim("{zero_width_space}KCL{zero_width_space}")
piped = " ready " |> string::trim()
"#
);
let result = parse_execute(&code).await.unwrap();
let non_whitespace = format!("{zero_width_space}KCL{zero_width_space}");
for (name, expected) in [
("ascii", "KCL"),
("internal", "KCL\tstrings"),
("unicode", "KCL"),
("all_whitespace", ""),
("empty", ""),
("unchanged", "KCL"),
("without_normalization", decomposed),
("non_whitespace", non_whitespace.as_str()),
("piped", "ready"),
] {
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, name)
.as_str()
.unwrap(),
expected,
"{name}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_string_trim_start() {
let ascii_whitespace = " \t\n";
let tab = "\t";
let non_breaking_space = "\u{a0}";
let em_space = "\u{2003}";
let ideographic_space = "\u{3000}";
let zero_width_space = "\u{200b}";
let decomposed = "e\u{301}";
let code = format!(
r#"
ascii = string::trimStart("{ascii_whitespace}KCL{ascii_whitespace}")
internal = string::trimStart(" KCL{tab}strings")
unicode = string::trimStart("{non_breaking_space}{em_space}KCL{ideographic_space}")
all_whitespace = string::trimStart("{ascii_whitespace}{non_breaking_space}")
empty = string::trimStart("")
unchanged = string::trimStart("KCL")
without_normalization = string::trimStart(" {decomposed}")
non_whitespace_prefix = string::trimStart("{zero_width_space}{ascii_whitespace}KCL")
piped = " ready " |> string::trimStart()
"#
);
let result = parse_execute(&code).await.unwrap();
let ascii = format!("KCL{ascii_whitespace}");
let unicode = format!("KCL{ideographic_space}");
let non_whitespace_prefix = format!("{zero_width_space}{ascii_whitespace}KCL");
for (name, expected) in [
("ascii", ascii.as_str()),
("internal", "KCL\tstrings"),
("unicode", unicode.as_str()),
("all_whitespace", ""),
("empty", ""),
("unchanged", "KCL"),
("without_normalization", decomposed),
("non_whitespace_prefix", non_whitespace_prefix.as_str()),
("piped", "ready "),
] {
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, name)
.as_str()
.unwrap(),
expected,
"{name}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_string_trim_end() {
let ascii_whitespace = " \t\n";
let tab = "\t";
let non_breaking_space = "\u{a0}";
let em_space = "\u{2003}";
let ideographic_space = "\u{3000}";
let zero_width_space = "\u{200b}";
let decomposed = "e\u{301}";
let code = format!(
r#"
ascii = string::trimEnd("{ascii_whitespace}KCL{ascii_whitespace}")
internal = string::trimEnd("KCL{tab}strings ")
unicode = string::trimEnd("{non_breaking_space}KCL{em_space}{ideographic_space}")
all_whitespace = string::trimEnd("{ascii_whitespace}{non_breaking_space}")
empty = string::trimEnd("")
unchanged = string::trimEnd("KCL")
without_normalization = string::trimEnd("{decomposed} ")
non_whitespace_suffix = string::trimEnd("KCL{ascii_whitespace}{zero_width_space}")
piped = " ready " |> string::trimEnd()
"#
);
let result = parse_execute(&code).await.unwrap();
let ascii = format!("{ascii_whitespace}KCL");
let unicode = format!("{non_breaking_space}KCL");
let non_whitespace_suffix = format!("KCL{ascii_whitespace}{zero_width_space}");
for (name, expected) in [
("ascii", ascii.as_str()),
("internal", "KCL\tstrings"),
("unicode", unicode.as_str()),
("all_whitespace", ""),
("empty", ""),
("unchanged", "KCL"),
("without_normalization", decomposed),
("non_whitespace_suffix", non_whitespace_suffix.as_str()),
("piped", " ready"),
] {
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, name)
.as_str()
.unwrap(),
expected,
"{name}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_string_to_string() {
for (name, expr, expected) in [
("unitless integer", "12", "12"),
("unitless fractional", "1.5", "1.5"),
("no digits dropped", "0.1 + 0.2", "0.30000000000000004"),
("unitless negative", "-7", "-7"),
("unitless zero", "0", "0"),
("negative zero", "-0", "0"),
("count", "3_", "3_"),
("millimeters", "12mm", "12mm"),
("centimeters", "12cm", "12cm"),
("meters", "12m", "12m"),
("inches", "1.5in", "1.5in"),
("feet", "2ft", "2ft"),
("yards", "3yd", "3yd"),
("degrees", "90deg", "90deg"),
("radians", "1.5rad", "1.5rad"),
("length arithmetic", "2mm + 10mm", "12mm"),
("units the type system loses", "2mm * 10mm", "20"),
("unitless arithmetic", "1 + 2", "3"),
] {
let code = format!("actual = string::toString({expr})");
let result = parse_execute(&code).await.unwrap();
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
.as_str()
.unwrap(),
expected,
"case: {name}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_string_to_string_ignores_the_files_default_unit() {
let code = "@settings(defaultLengthUnit = inch)\nactual = string::toString(12)";
let result = parse_execute(code).await.unwrap();
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
.as_str()
.unwrap(),
"12"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_string_to_string_rejects_a_non_number() {
let error = parse_execute(r#"actual = string::toString("already text")"#)
.await
.unwrap_err();
assert_eq!(
error.message(),
"The input argument of `string::toString` requires a value with type `number`, but found a value with type `string`."
);
assert!(
matches!(error, KclError::Argument { .. }),
"expected an Argument error, found {error:?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_string_to_string_accepts_a_piped_argument() {
let result = parse_execute("actual = 12mm |> string::toString()").await.unwrap();
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
.as_str()
.unwrap(),
"12mm"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_string_to_string_echoes_how_the_literal_was_written() {
for literal in [
"12",
"1.5",
"0.30000000000000004",
"3_",
"2.5_",
"-4_",
"12mm",
"-5mm",
"1.5in",
"90deg",
"1.5rad",
] {
let code = format!("actual = string::toString({literal})");
let result = parse_execute(&code).await.unwrap();
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
.as_str()
.unwrap(),
literal,
"literal: {literal}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_string_to_string_spells_out_non_finite_numbers() {
for (name, expr, expected) in [
("positive infinity", "1 / 0", "Infinity"),
("negative infinity", "-1 / 0", "-Infinity"),
("nan", "0 / 0", "NaN"),
("infinity from a length", "1mm / 0", "Infinity"),
("nan from a length", "0mm / 0", "NaN"),
("infinity from an angle", "1deg / 0", "Infinity"),
] {
let code = format!("actual = string::toString({expr})");
let result = parse_execute(&code).await.unwrap();
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, "actual")
.as_str()
.unwrap(),
expected,
"case: {name}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_string_equality_operators() {
let composed = "\u{e9}";
let decomposed = "e\u{301}";
let code = format!(
r#"
equal_same_ascii = "KCL" == "KCL"
equal_different_case = "KCL" == "kcl"
not_equal_same_ascii = "KCL" != "KCL"
not_equal_different_case = "KCL" != "kcl"
equal_same_unicode = "{composed}" == "{composed}"
not_equal_same_unicode = "{composed}" != "{composed}"
equal_without_normalization = "{composed}" == "{decomposed}"
not_equal_without_normalization = "{composed}" != "{decomposed}"
"#
);
let result = parse_execute(&code).await.unwrap();
for (name, expected) in [
("equal_same_ascii", true),
("equal_different_case", false),
("not_equal_same_ascii", false),
("not_equal_different_case", true),
("equal_same_unicode", true),
("not_equal_same_unicode", false),
("equal_without_normalization", false),
("not_equal_without_normalization", true),
] {
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, name)
.as_bool()
.unwrap(),
expected,
"{name}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn test_string_equality_inside_sketch_block_fails_like_number_equality() {
let string_code = r#"
@settings(experimentalFeatures = allow)
sketch(on = XY) {
stringsAreEqual = "KCL" == "KCL"
}
"#;
let number_code = r#"
@settings(experimentalFeatures = allow)
sketch(on = XY) {
numbersAreEqual = 1 == 1
}
"#;
assert_eq!(
parse_execute(string_code).await.unwrap_err().message(),
"Cannot create an equivalence constraint between values of these types: a string and a string"
);
assert_eq!(
parse_execute(number_code).await.unwrap_err().message(),
"Cannot create an equivalence constraint between values of these types: a number and a number"
);
}
#[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();
let msg = err.to_string();
assert!(
msg.contains("stack size exceeded") || msg.contains("Call depth limit"),
"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, true).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, true).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_geometry_only_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_number_variable(&result.variables, "x", 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_number_variable(&result.variables, "z", 3.0);
ctx.close().await;
ctx2.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn mock_execution_succeeds_after_split() {
let code = kcl_input!("repro_mock_extrude");
let ctx = ExecutorContext::new_mock(None).await;
let program = crate::Program::parse_no_errs(code).unwrap();
let _result = match ctx.run_mock(&program, &MockConfig::default()).await {
Ok(res) => res,
Err(e) => panic!("{}", e.error),
};
}
#[tokio::test(flavor = "multi_thread")]
async fn mock_execution_rejects_oob_on_frontend_array() {
let code = r#"
values = [10, 20]
third = values[2]
"#;
let ctx = ExecutorContext::new_mock(None).await;
let program = crate::Program::parse_no_errs(code).unwrap();
let err = ctx.run_mock(&program, &MockConfig::default()).await.unwrap_err();
ctx.close().await;
assert!(
err.error.message().contains("array doesn't have any item at index 2"),
"{err:?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn mock_execution_pattern_circular_number() {
let code = kcl_input!("repro_mock_pattern_circular");
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();
let copies = result
.variables
.get("copies")
.expect("no variable called 'copies' found");
let value = match copies {
KclValueView::Solid { .. } => {
panic!("One solid?");
}
KclValueView::HomArray { value } => value,
other => panic!("{other:#?}"),
};
let actual_instances = value.len();
let expected_instances = 10; assert_eq!(actual_instances, expected_instances);
}
#[tokio::test(flavor = "multi_thread")]
async fn mock_execution_subtract() {
let code = kcl_input!("repro_mock_subtract");
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;
ctx.close().await;
let result = match result {
Ok(x) => x,
Err(e) => {
let error = e.error;
panic!("{error}");
}
};
let subtracted_parts = result
.variables
.get("subtractedParts")
.expect("no variable called 'subtracted_parts' found");
let subtracted_parts = match subtracted_parts {
KclValueView::Solid { .. } => {
panic!("One solid?");
}
KclValueView::HomArray { value } => value,
other => panic!("{other:#?}"),
};
let expected_number_of_parts = 2;
let actual_number_of_parts = subtracted_parts.len();
assert_eq!(actual_number_of_parts, expected_number_of_parts);
}
#[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;
}
#[tokio::test(flavor = "multi_thread")]
async fn face_parent_solid_stays_compact_for_repeated_sketch_on_face() {
let code = format!(
r#"{}
face7 = faceOf(solid6, face = r6.tags.line1)
r7 = squareRegion(onSurface = face7)
solid7 = extrude(r7, length = width)
"#,
include_str!("../../tests/endless_impeller/input.kcl")
);
let result = parse_execute(&code).await.unwrap();
let solid7 = mem_get_json(result.exec_state.stack(), result.mem_env, "solid7");
assert!(matches!(solid7, KclValue::Solid { .. }), "actual: {solid7:?}");
let face7 = match mem_get_json(result.exec_state.stack(), result.mem_env, "face7") {
KclValue::Face { value } => value,
value => panic!("expected face7 to be a Face, got {value:?}"),
};
assert!(face7.parent_solid.creator_sketch_id.is_some());
}
#[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 mut 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
);
mem.constraint_state.insert(
crate::front::ObjectId(1),
indexmap::indexmap! {
crate::execution::ConstraintKey::LineCircle([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) =>
crate::execution::ConstraintState::Tangency(crate::execution::TangencyMode::LineCircle(ezpz::LineSide::Left))
},
);
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);
assert_eq!(exec_state.mod_local.constraint_state, mem.constraint_state);
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(Arc::new(EngineManager::new_mock()), 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;
}
#[tokio::test(flavor = "multi_thread")]
async fn sim_sketch_mode_real_mock_real() {
let ctx = ExecutorContext::new_geometry_only_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.get(&ModuleId::default()).unwrap().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.get(&ModuleId::default()).unwrap().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.get(&ModuleId::default()).unwrap().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 default_angle_unit_warns_in_legacy_kcl() {
for version in ["", "kclVersion = 1.0, ", "kclVersion = 2.0, "] {
for unit in ["deg", "rad"] {
let code = format!("@settings({version}defaultAngleUnit = {unit})\nx = 1\n");
let result = parse_execute(&code).await.unwrap();
let issues = result.issues();
assert_eq!(issues.len(), 1, "code={code}");
assert_eq!(issues[0].severity, Severity::Warning, "code={code}");
assert_eq!(
issues[0].message,
"The `defaultAngleUnit` setting is deprecated; use explicit units for angles"
);
assert_eq!(variable_f64(&result, "x"), 1.0);
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn default_angle_unit_errors_in_kcl_v3() {
for settings in [
"@settings(kclVersion = \"3.0-preview\", defaultAngleUnit = deg)",
"@settings(defaultAngleUnit = rad, kclVersion = \"3.0-preview\")",
"@settings(defaultAngleUnit = deg)\n@settings(kclVersion = \"3.0-preview\")",
"@settings(kclVersion = \"3.0-preview\")\n@settings(defaultAngleUnit = rad)",
] {
let code = format!("{settings}\nx = 1\n");
let Err(error) = parse_execute(&code).await else {
panic!("defaultAngleUnit must fail in KCL 3.0: {code}");
};
assert_eq!(
error.message(),
"The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles",
"code={code}"
);
let ranges = error.source_ranges();
assert_eq!(ranges.len(), 1);
assert!(code[ranges[0].start()..ranges[0].end()].contains("defaultAngleUnit"));
}
}
#[tokio::test(flavor = "multi_thread")]
async fn default_angle_unit_error_cannot_be_suppressed() {
for version in ["1.0", "2.0", "\"3.0-preview\""] {
let code = format!(
"@warnings(allow = angleUnits)\n@settings(kclVersion = {version}, defaultAngleUnit = deg)\nx = 1\n"
);
let result = parse_execute(&code).await;
if version == "\"3.0-preview\"" {
assert_eq!(
result.unwrap_err().message(),
"The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles"
);
} else {
assert!(result.unwrap().issues().is_empty(), "code={code}");
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn default_angle_unit_in_import_uses_effective_kcl_version() {
let dep = "@settings(defaultAngleUnit = deg)\nexport x = 1\n";
for version in ["1.0", "2.0", "\"3.0-preview\""] {
let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
if version == "\"3.0-preview\"" {
assert_eq!(
result.unwrap_err().message(),
"The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles"
);
} else {
assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0);
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn diagnostics_attribute_in_import_uses_effective_kcl_version() {
let dep = "@diagnostics(allow = bogus)\nexport x = 1\n";
for version in ["1.0", "2.0", "\"3.0-preview\""] {
let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
if version == "\"3.0-preview\"" {
let error = result.unwrap_err();
let message = error.message();
assert!(
message.starts_with("Unexpected diagnostic value: `bogus`; accepted values: "),
"main={main}, message={message}"
);
} else {
assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0, "main={main}");
}
}
let dep = "@warnings(allow = bogus)\nexport x = 1\n";
for version in ["1.0", "2.0", "\"3.0-preview\""] {
let main = format!("@settings(kclVersion = {version})\nimport x from \"dep.kcl\"\n");
let result = execute_with_modules(&main, &[("dep.kcl", dep)]).await;
if version == "\"3.0-preview\"" {
assert_eq!(variable_f64(&result.unwrap(), "x"), 1.0, "main={main}");
} else {
let error = result.unwrap_err();
let message = error.message();
assert!(
message.starts_with("Unexpected warning value: `bogus`; accepted values: "),
"main={main}, message={message}"
);
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn entry_point_kcl_version_records_declared_version() {
for (code, expected) in [
("x = 1\n", None),
("@settings(defaultLengthUnit = in)\nx = 1\n", None),
("@settings(kclVersion = 1.0)\nx = 1\n", Some(KclVersion::V1)),
("@settings(kclVersion = 2.0)\nx = 1\n", Some(KclVersion::V2)),
(
"@settings(kclVersion = \"3.0-preview\")\nx = 1\n",
Some(KclVersion::V3Preview),
),
] {
let result = parse_execute(code).await.unwrap();
assert_eq!(
result.exec_state.global.entry_point_kcl_version, expected,
"code={code}"
);
assert_eq!(
result.exec_state.entry_point_version_is_v3_or_higher(),
expected == Some(KclVersion::V3Preview),
"code={code}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_version_lookup_prefers_entry_point_over_module_local() {
let mut exec_state = parse_execute("x = 1\n").await.unwrap().exec_state;
exec_state.global.entry_point_kcl_version = None;
exec_state.mod_local.settings.kcl_version = KclVersion::V2;
assert_eq!(exec_state.kcl_version(), KclVersion::V2);
assert_eq!(exec_state.legacy_caller_kcl_version(), KclVersion::V2);
exec_state.global.entry_point_kcl_version = Some(KclVersion::V1);
assert_eq!(exec_state.kcl_version(), KclVersion::V2);
exec_state.global.entry_point_kcl_version = Some(KclVersion::V3Preview);
assert_eq!(exec_state.kcl_version(), KclVersion::V3Preview);
assert_eq!(exec_state.legacy_caller_kcl_version(), KclVersion::V2);
}
#[tokio::test(flavor = "multi_thread")]
async fn mock_execution_records_entry_point_kcl_version() {
use futures::FutureExt;
clear_mem_cache().await;
let ctx = ExecutorContext::new_mock(None).await;
let fresh_memory = MockConfig {
use_prev_memory: false,
..Default::default()
};
let prev_memory = MockConfig::default();
let v3_program = crate::Program::parse_no_errs("@settings(kclVersion = \"3.0-preview\")\nx = 1\n").unwrap();
let v2_program = crate::Program::parse_no_errs("@settings(kclVersion = 2.0)\nx = 1\n").unwrap();
let test_result = std::panic::AssertUnwindSafe(async {
let (exec_state, _) = ctx.run_mock_returning_state(&v3_program, &fresh_memory).await.unwrap();
assert_eq!(
exec_state.global.entry_point_kcl_version,
Some(KclVersion::V3Preview),
"mock execution should record a 3.0-preview entry point"
);
assert!(exec_state.entry_point_version_is_v3_or_higher());
ctx.run_mock(&v3_program, &fresh_memory).await.unwrap();
let (exec_state, _) = ctx.run_mock_returning_state(&v2_program, &prev_memory).await.unwrap();
assert_eq!(exec_state.global.entry_point_kcl_version, Some(KclVersion::V2));
assert!(!exec_state.entry_point_version_is_v3_or_higher());
ctx.run_mock(&v2_program, &fresh_memory).await.unwrap();
let (exec_state, _) = ctx.run_mock_returning_state(&v3_program, &prev_memory).await.unwrap();
assert_eq!(exec_state.global.entry_point_kcl_version, Some(KclVersion::V3Preview));
})
.catch_unwind()
.await;
clear_mem_cache().await;
ctx.close().await;
if let Err(panic) = test_result {
std::panic::resume_unwind(panic);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn mock_execution_applies_v3_semantics() {
use futures::FutureExt;
clear_mem_cache().await;
let ctx = ExecutorContext::new_mock(None).await;
let fresh_memory = MockConfig {
use_prev_memory: false,
..Default::default()
};
let program = crate::Program::parse_no_errs(
r#"@settings(kclVersion = "3.0-preview")
fn f() {
return 1
assert(1, isEqualTo = 2, error = "code after return ran")
}
x = f()
outer = 1
y = if true {
outer = 2
outer + 10
} else {
0
}
"#,
)
.unwrap();
let test_result = std::panic::AssertUnwindSafe(async {
let (exec_state, env) = ctx.run_mock_returning_state(&program, &fresh_memory).await.unwrap();
let var = |name: &str| mem_get_json(exec_state.stack(), env, name).as_f64().unwrap();
assert_eq!(var("x"), 1.0, "early return produces the function's value");
assert_eq!(var("y"), 12.0, "the branch sees its own shadowing binding");
assert_eq!(var("outer"), 1.0, "the outer binding is unchanged after the if");
})
.catch_unwind()
.await;
clear_mem_cache().await;
ctx.close().await;
if let Err(panic) = test_result {
std::panic::resume_unwind(panic);
}
}
fn commands_everywhere(result: &ExecTestResults) -> impl Iterator<Item = &kittycad_modeling_cmds::ModelingCmd> {
let module_commands = result
.exec_state
.global
.module_infos
.values()
.filter_map(|info| match &info.repr {
ModuleRepr::Kcl(_, Some(outcome)) => Some(outcome.artifacts.commands.iter()),
_ => None,
})
.flatten();
result
.root_module_artifact_commands()
.iter()
.chain(module_commands)
.map(|artifact_command| &artifact_command.command)
}
fn emitted_fillet_versions_everywhere(
result: &ExecTestResults,
) -> Vec<kittycad_modeling_cmds::shared::EdgeCutVersion> {
commands_everywhere(result)
.filter_map(|command| match command {
kittycad_modeling_cmds::ModelingCmd::Solid3dCutEdges(command) => Some(command.version),
_ => None,
})
.collect()
}
fn emitted_region_versions_everywhere(
result: &ExecTestResults,
) -> Vec<kittycad_modeling_cmds::shared::RegionVersion> {
commands_everywhere(result)
.filter_map(|command| match command {
kittycad_modeling_cmds::ModelingCmd::CreateRegion(command) => Some(command.version.clone()),
_ => None,
})
.collect()
}
const FILLET_AT_MODULE_TOP_LEVEL: &str = r#"
profile = startSketchOn(XY)
|> startProfile(at = [0, 0])
|> line(end = [10, 0], tag = $edge)
|> line(end = [0, 10])
|> line(end = [-10, 0])
|> close()
solid = extrude(profile, length = 10)
fillet(solid, tags = [edge], radius = 1)
"#;
const FILLET_IN_EXPORTED_FN: &str = r#"
export fn filletedBox() {
profile = startSketchOn(XY)
|> startProfile(at = [0, 0])
|> line(end = [10, 0], tag = $edge)
|> line(end = [0, 10])
|> line(end = [-10, 0])
|> close()
solid = extrude(profile, length = 10)
return fillet(solid, tags = [edge], radius = 1)
}
"#;
#[tokio::test(flavor = "multi_thread")]
async fn entry_point_v3_pins_kcl_version_for_imported_modules() {
use kittycad_modeling_cmds::shared::EdgeCutVersion;
let dep = FILLET_AT_MODULE_TOP_LEVEL;
let main = r#"@settings(kclVersion = "3.0-preview")
import "dep.kcl" as dep
"#;
let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
assert_eq!(emitted_fillet_versions_everywhere(&result), vec![EdgeCutVersion::V2]);
let dep = FILLET_IN_EXPORTED_FN;
let main = r#"@settings(kclVersion = "3.0-preview")
import filletedBox from "dep.kcl"
box = filletedBox()
"#;
let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
assert_eq!(emitted_fillet_versions_everywhere(&result), vec![EdgeCutVersion::V2]);
}
const REGION_AT_MODULE_TOP_LEVEL: &str = r#"
profile = sketch(on = XY) {
outline = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
}
disc = region(segments = [profile.outline])
"#;
const REGION_IN_EXPORTED_FN: &str = r#"
export fn disc() {
profile = sketch(on = XY) {
outline = circle(start = [var 5mm, var 0mm], center = [var 0mm, var 0mm])
}
return region(segments = [profile.outline])
}
"#;
#[tokio::test(flavor = "multi_thread")]
async fn legacy_kcl_version_quirk_applies_without_v3_entry_point() {
use kittycad_modeling_cmds::shared::RegionVersion;
let dep = format!("@settings(kclVersion = 1.0)\n{REGION_AT_MODULE_TOP_LEVEL}");
let main = r#"@settings(kclVersion = 2.0)
import "dep.kcl" as dep
"#;
let result = execute_with_modules(main, &[("dep.kcl", &dep)]).await.unwrap();
assert_eq!(emitted_region_versions_everywhere(&result), vec![RegionVersion::V0]);
let dep = format!("@settings(kclVersion = 1.0)\n{REGION_IN_EXPORTED_FN}");
let main = r#"@settings(kclVersion = 2.0)
import disc from "dep.kcl"
face = disc()
"#;
let result = execute_with_modules(main, &[("dep.kcl", &dep)]).await.unwrap();
assert_eq!(emitted_region_versions_everywhere(&result), vec![RegionVersion::V1]);
}
fn versioned_modules_context(modules: &[(&str, &str)]) -> ExecutorContext {
let project_dir = crate::TypedPath::new("/zma-kcl-version-mismatch");
let files = modules
.iter()
.map(|(name, source)| (project_dir.join(name).to_string(), source.as_bytes().to_vec()))
.collect();
ExecutorContext {
engine: Arc::new(EngineManager::new_mock()),
engine_batch: EngineBatchContext::default(),
fs: crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files)),
settings: ExecutorSettings {
current_file: Some(project_dir.join("main.kcl")),
project_directory: Some(project_dir),
..Default::default()
},
context_type: ContextType::Mock,
execution_callbacks: Default::default(),
executor_kind: machine::ExecutorKind::resolve(),
machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
}
}
async fn run_versioned_modules(main: &str, modules: &[(&str, &str)]) -> Result<(), KclError> {
let ctx = versioned_modules_context(modules);
let program = crate::Program::parse_no_errs(main).unwrap();
let mut exec_state = ExecState::new(&ctx);
let result = ctx.run(&program, &mut exec_state).await;
ctx.close().await;
result.map(|_| ()).map_err(|err| err.error)
}
async fn run_versioned_modules_mock(main: &str, modules: &[(&str, &str)]) -> Result<(), KclError> {
let ctx = versioned_modules_context(modules);
let program = crate::Program::parse_no_errs(main).unwrap();
let mock_config = MockConfig {
use_prev_memory: false,
..Default::default()
};
let result = ctx.run_mock_returning_state(&program, &mock_config).await;
ctx.close().await;
result.map(|_| ()).map_err(|err| err.error)
}
const V3_MAIN_IMPORTING_DEP: &str =
"@settings(kclVersion = \"3.0-preview\")\nimport width from \"dep.kcl\"\nx = width\n";
fn dep_declaring(version: &str) -> String {
format!("@settings(kclVersion = {version})\nexport width = 10\n")
}
#[track_caller]
fn assert_kcl_version_mismatch(error: &KclError, expected_dep_version: &str) {
assert!(matches!(error, KclError::Semantic { .. }), "{error:#?}");
assert_eq!(
error.message(),
format!(
"Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` declares kclVersion 3.0-preview, but the imported file `/zma-kcl-version-mismatch/dep.kcl` declares kclVersion {expected_dep_version}. Update the kclVersion setting in one of these files to match the other."
)
);
}
#[tokio::test(flavor = "multi_thread")]
async fn imported_module_kcl_version_must_match_v3_entry_point() {
for dep_version in ["2.0", "1.0"] {
let dep = dep_declaring(dep_version);
let error = run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
.await
.expect_err("mismatched kclVersion should be rejected");
assert_kcl_version_mismatch(&error, dep_version);
let ranges = error.source_ranges();
assert_eq!(ranges.len(), 2, "{ranges:#?}");
assert!(!ranges[0].module_id().is_top_level());
let declaration = format!("kclVersion = {dep_version}");
let start = dep.find(&declaration).unwrap();
assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + declaration.len()));
assert!(ranges[1].module_id().is_top_level());
let import_stmt = "import width from \"dep.kcl\"";
let start = V3_MAIN_IMPORTING_DEP.find(import_stmt).unwrap();
assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
assert_eq!(
error
.backtrace()
.iter()
.map(|frame| frame.fn_name.as_deref())
.collect::<Vec<_>>(),
[Some("import dep.kcl"), None]
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn imported_module_without_kcl_version_is_allowed_under_v3_entry_point() {
for dep in [
"export width = 10\n",
"@settings(defaultLengthUnit = in)\nexport width = 10\n",
] {
run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", dep)])
.await
.unwrap_or_else(|err| panic!("dep={dep:?}: {err:#?}"));
}
}
#[tokio::test(flavor = "multi_thread")]
async fn imported_module_matching_v3_kcl_version_is_allowed() {
for dep_version in ["\"3.0-preview\"", "\"3-preview\"", "\"3.0.0-preview\""] {
let dep = dep_declaring(dep_version);
run_versioned_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
.await
.unwrap_or_else(|err| panic!("dep={dep_version}: {err:#?}"));
}
}
#[tokio::test(flavor = "multi_thread")]
async fn pre_v3_kcl_versions_may_be_mixed_without_v3_entry_point() {
for main_header in ["", "@settings(kclVersion = 1.0)\n", "@settings(kclVersion = 2.0)\n"] {
for dep_version in ["1.0", "2.0"] {
let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
let dep = dep_declaring(dep_version);
run_versioned_modules(&main, &[("dep.kcl", &dep)])
.await
.unwrap_or_else(|err| panic!("main={main_header:?} dep={dep_version}: {err:#?}"));
}
}
}
const V3_DEP_UNDER_UNDECLARED_ENTRY_POINT: &str = "Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` does not declare a kclVersion, but the imported file `/zma-kcl-version-mismatch/dep.kcl` declares kclVersion 3.0-preview. Declare the same kclVersion in the entry point, or update the setting in the imported file.";
#[tokio::test(flavor = "multi_thread")]
async fn v3_import_requires_v3_entry_point() {
for (main_header, entry_point_declares, fix) in [
(
"",
"does not declare a kclVersion",
"Declare the same kclVersion in the entry point, or update the setting in the imported file.",
),
(
"@settings(kclVersion = 1.0)\n",
"declares kclVersion 1.0",
"Update the kclVersion setting in one of these files to match the other.",
),
(
"@settings(kclVersion = 2.0)\n",
"declares kclVersion 2.0",
"Update the kclVersion setting in one of these files to match the other.",
),
] {
let main = format!("{main_header}import width from \"dep.kcl\"\nx = width\n");
let dep = dep_declaring("\"3.0-preview\"");
let error = run_versioned_modules(&main, &[("dep.kcl", &dep)])
.await
.expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
assert!(matches!(error, KclError::Semantic { .. }), "{error:#?}");
assert_eq!(
error.message(),
format!(
"Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` {entry_point_declares}, but the imported file `/zma-kcl-version-mismatch/dep.kcl` declares kclVersion 3.0-preview. {fix}"
),
"main={main_header:?}"
);
let ranges = error.source_ranges();
assert_eq!(ranges.len(), 2, "{ranges:#?}");
assert!(!ranges[0].module_id().is_top_level());
let declaration = "kclVersion = \"3.0-preview\"";
let start = dep.find(declaration).unwrap();
assert_eq!((ranges[0].start(), ranges[0].end()), (start, start + declaration.len()));
assert!(ranges[1].module_id().is_top_level());
let import_stmt = "import width from \"dep.kcl\"";
let start = main.find(import_stmt).unwrap();
assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
assert_eq!(
error
.backtrace()
.iter()
.map(|frame| frame.fn_name.as_deref())
.collect::<Vec<_>>(),
[Some("import dep.kcl"), None]
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn v3_import_spellings_are_all_rejected_without_v3_entry_point() {
let main = "import width from \"dep.kcl\"\nx = width\n";
for dep_version in ["\"3-preview\"", "\"3.0.0-preview\""] {
let dep = dep_declaring(dep_version);
let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
.await
.expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
assert_eq!(
error.message(),
V3_DEP_UNDER_UNDECLARED_ENTRY_POINT,
"dep={dep_version}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn unreferenced_v3_whole_module_import_is_checked_in_mock_execution() {
let main = "import \"dep.kcl\" as dep\nx = 1\n";
let dep = dep_declaring("\"3.0-preview\"");
let error = run_versioned_modules_mock(main, &[("dep.kcl", &dep)])
.await
.expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected in mock execution");
assert_eq!(error.message(), V3_DEP_UNDER_UNDECLARED_ENTRY_POINT);
let ranges = error.source_ranges();
assert_eq!(ranges.len(), 2, "{ranges:#?}");
assert!(!ranges[0].module_id().is_top_level());
assert!(ranges[1].module_id().is_top_level());
let import_stmt = "import \"dep.kcl\" as dep";
let start = main.find(import_stmt).unwrap();
assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
.await
.expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected in engine execution");
assert_eq!(error.message(), V3_DEP_UNDER_UNDECLARED_ENTRY_POINT);
}
#[tokio::test(flavor = "multi_thread")]
async fn transitive_v3_import_requires_v3_entry_point() {
let main = "import doubled from \"a.kcl\"\nx = doubled\n";
let a = "import width from \"b.kcl\"\nexport doubled = width * 2\n";
let b = dep_declaring("\"3.0-preview\"");
let error = run_versioned_modules(main, &[("a.kcl", a), ("b.kcl", &b)])
.await
.expect_err("a transitive KCL 3.0 import without a KCL 3.0 entry point should be rejected");
assert_eq!(
error.message(),
"Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` does not declare a kclVersion, but the imported file `/zma-kcl-version-mismatch/b.kcl` declares kclVersion 3.0-preview. Declare the same kclVersion in the entry point, or update the setting in the imported file."
);
assert_eq!(
error
.backtrace()
.iter()
.map(|frame| frame.fn_name.as_deref())
.collect::<Vec<_>>(),
[Some("import b.kcl"), Some("import a.kcl"), None]
);
}
#[tokio::test(flavor = "multi_thread")]
async fn v3_import_without_v3_entry_point_or_entry_point_path() {
let main = "import width from \"dep.kcl\"\nx = width\n";
let dep = dep_declaring("\"3.0-preview\"");
let error = execute_with_modules(main, &[("dep.kcl", &dep)])
.await
.expect_err("a KCL 3.0 import without a KCL 3.0 entry point should be rejected");
let message = error.message();
assert!(
message.starts_with(
"Mixing KCL versions in a single program is not allowed. The entry point does not declare a kclVersion, but the imported file `"
),
"{message}"
);
assert!(
message.ends_with(
"dep.kcl` declares kclVersion 3.0-preview. Declare the same kclVersion in the entry point, or update the setting in the imported file."
),
"{message}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn transitive_import_kcl_version_mismatch_names_entry_point_and_mismatched_file() {
let main = "@settings(kclVersion = \"3.0-preview\")\nimport doubled from \"a.kcl\"\nx = doubled\n";
let a = "import width from \"b.kcl\"\nexport doubled = width * 2\n";
let b = dep_declaring("2.0");
let error = run_versioned_modules(main, &[("a.kcl", a), ("b.kcl", &b)])
.await
.expect_err("mismatched kclVersion in a transitive import should be rejected");
assert_eq!(
error.message(),
"Mixing KCL versions in a single program is not allowed. The entry point `/zma-kcl-version-mismatch/main.kcl` declares kclVersion 3.0-preview, but the imported file `/zma-kcl-version-mismatch/b.kcl` declares kclVersion 2.0. Update the kclVersion setting in one of these files to match the other."
);
assert_eq!(
error
.backtrace()
.iter()
.map(|frame| frame.fn_name.as_deref())
.collect::<Vec<_>>(),
[Some("import b.kcl"), Some("import a.kcl"), None]
);
let ranges = error.source_ranges();
assert_eq!(ranges.len(), 3, "{ranges:#?}");
assert!(!ranges[0].module_id().is_top_level());
assert!(!ranges[1].module_id().is_top_level());
assert!(ranges[2].module_id().is_top_level());
}
#[tokio::test(flavor = "multi_thread")]
async fn unreferenced_whole_module_import_kcl_version_is_checked_in_mock_execution() {
let main = "@settings(kclVersion = \"3.0-preview\")\nimport \"dep.kcl\" as dep\nx = 1\n";
let dep = dep_declaring("2.0");
let error = run_versioned_modules_mock(main, &[("dep.kcl", &dep)])
.await
.expect_err("mismatched kclVersion should be rejected in mock execution");
assert_kcl_version_mismatch(&error, "2.0");
let ranges = error.source_ranges();
assert_eq!(ranges.len(), 2, "{ranges:#?}");
assert!(!ranges[0].module_id().is_top_level());
assert!(ranges[1].module_id().is_top_level());
let import_stmt = "import \"dep.kcl\" as dep";
let start = main.find(import_stmt).unwrap();
assert_eq!((ranges[1].start(), ranges[1].end()), (start, start + import_stmt.len()));
let error = run_versioned_modules(main, &[("dep.kcl", &dep)])
.await
.expect_err("mismatched kclVersion should be rejected in engine execution");
assert_kcl_version_mismatch(&error, "2.0");
run_versioned_modules_mock(main, &[("dep.kcl", "export width = 10\n")])
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn std_modules_are_exempt_from_kcl_version_matching() {
let main = "@settings(kclVersion = \"3.0-preview\", experimentalFeatures = allow)\nimport QUARTER_TURN from \"std::turns\"\nx = QUARTER_TURN\n";
run_versioned_modules(main, &[]).await.unwrap();
run_versioned_modules_mock(main, &[]).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_version_mismatch_without_entry_point_path() {
let dep = dep_declaring("2.0");
let error = execute_with_modules(V3_MAIN_IMPORTING_DEP, &[("dep.kcl", &dep)])
.await
.expect_err("mismatched kclVersion should be rejected");
let message = error.message();
assert!(
message.starts_with(
"Mixing KCL versions in a single program is not allowed. The entry point declares kclVersion 3.0-preview, but the imported file `"
),
"{message}"
);
assert!(
message.ends_with(
"dep.kcl` declares kclVersion 2.0. Update the kclVersion setting in one of these files to match the other."
),
"{message}"
);
}
#[track_caller]
fn variable_f64(result: &ExecTestResults, name: &str) -> f64 {
mem_get_json(result.exec_state.stack(), result.mem_env, name)
.as_f64()
.unwrap()
}
#[tokio::test(flavor = "multi_thread")]
async fn return_terminates_function_early_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn f() {
return 1
assert(1, isEqualTo = 2, error = "code after return ran")
}
x = f()
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 1.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn second_return_is_unreachable_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn f() {
return 1
return 2
}
x = f()
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 1.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn return_inside_if_arm_returns_from_function_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn f(@b) {
dummy = if b {
return 1
0
} else {
0
}
return 2
}
x = f(true)
y = f(false)
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 1.0);
assert_eq!(variable_f64(&result, "y"), 2.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn return_inside_nested_if_returns_from_function_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn f(@a, b) {
dummy = if a {
inner = if b {
return 10
0
} else {
1
}
inner + 1
} else {
2
}
return dummy * 100
}
x = f(true, b = true)
y = f(true, b = false)
z = f(false, b = false)
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 10.0);
assert_eq!(variable_f64(&result, "y"), 200.0);
assert_eq!(variable_f64(&result, "z"), 200.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn return_inside_closure_returns_only_from_closure_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn outer() {
inner = fn() {
return 5
assert(1, isEqualTo = 2, error = "code after inner return ran")
}
v = inner()
return v + 1
}
x = outer()
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 6.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn return_type_coercion_applies_to_early_return_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn f(): number(mm) {
return 1
assert(1, isEqualTo = 2, error = "code after return ran")
}
x = f()
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 1.0);
let code = r#"@settings(kclVersion = "3.0-preview")
fn f(): number(mm) {
return "nope"
}
x = f()
"#;
let err = parse_execute(code).await.expect_err("coercion failure should error");
assert!(err.message().contains("type"), "unexpected message: {}", err.message());
}
#[tokio::test(flavor = "multi_thread")]
async fn return_at_top_level_errors() {
for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
let code = format!("{header}return 1\n");
assert_eq!(
parse_execute(&code).await.expect_err("should error").message(),
"Cannot return from outside a function."
);
}
let code = r#"@settings(kclVersion = "3.0-preview")
x = if true {
return 1
0
} else {
0
}
"#;
assert_eq!(
parse_execute(code).await.expect_err("should error").message(),
"Cannot return from outside a function."
);
}
#[tokio::test(flavor = "multi_thread")]
async fn exit_inside_function_still_exits_program_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn f() {
exit()
return 1
}
x = f()
assert(1, isEqualTo = 2, error = "code after exit ran")
"#;
parse_execute(code).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn return_inside_sketch_block_terminates_function_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
fn f() {
sketch(on = XY) {
l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
return 42
}
return 0
}
x = f()
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 42.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn return_inside_sketch_block_ignored_without_v3() {
let code = r#"@settings(experimentalFeatures = allow)
fn f() {
sketch(on = XY) {
l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
return 42
}
return 0
}
x = f()
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 0.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn code_after_return_still_runs_without_v3() {
let code = r#"fn f() {
return 1
assert(1, isEqualTo = 2, error = "ran past return")
}
x = f()
"#;
let err = parse_execute(code).await.expect_err("should error");
assert!(
err.message().contains("ran past return"),
"unexpected message: {}",
err.message()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn multiple_returns_error_without_v3() {
let code = r#"fn f() {
return 1
return 2
}
x = f()
"#;
assert_eq!(
parse_execute(code).await.expect_err("should error").message(),
"Multiple returns from a single function."
);
}
#[tokio::test(flavor = "multi_thread")]
async fn if_arm_return_plus_function_return_errors_without_v3() {
let code = r#"fn f() {
dummy = if true {
return 1
0
} else {
0
}
return 2
}
x = f()
"#;
assert_eq!(
parse_execute(code).await.expect_err("should error").message(),
"Multiple returns from a single function."
);
}
#[tokio::test(flavor = "multi_thread")]
async fn top_level_if_arm_return_ignored_without_v3() {
let code = r#"x = if true {
return 1
0
} else {
0
}
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 0.0);
assert_eq!(variable_f64(&result, memory::RETURN_NAME), 1.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn return_semantics_gated_on_entry_point_not_module() {
let dep = r#"@settings(kclVersion = "3.0-preview")
export fn f() {
return 1
assert(1, isEqualTo = 2, error = "ran past return")
}
"#;
let main = r#"@settings(kclVersion = 2.0)
import f from "dep.kcl"
x = f()
"#;
let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
assert!(
err.message()
.starts_with("Mixing KCL versions in a single program is not allowed."),
"unexpected message: {}",
err.message()
);
let dep = r#"export fn f() {
return 1
assert(1, isEqualTo = 2, error = "ran past return")
}
"#;
let main = r#"@settings(kclVersion = "3.0-preview")
import f from "dep.kcl"
x = f()
"#;
let result = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 1.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn return_inside_map_and_reduce_callbacks_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
doubled = map([1, 2, 3], f = fn(@i) {
return i * 2
assert(1, isEqualTo = 2, error = "code after return ran in the map callback")
})
assert(doubled[0], isEqualTo = 2, error = "map result 0")
assert(doubled[1], isEqualTo = 4, error = "map result 1")
assert(doubled[2], isEqualTo = 6, error = "map result 2")
total = reduce([1, 2, 3], initial = 0, f = fn(@i, accum) {
return accum + i
assert(1, isEqualTo = 2, error = "code after return ran in the reduce callback")
})
assert(total, isEqualTo = 6, error = "reduce total")
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "total"), 6.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn early_returns_do_not_leak_machine_call_depth() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn one() {
return 1
assert(1, isEqualTo = 2, error = "code after return ran")
}
total = reduce([1..100], initial = 0, f = fn(@i, accum) {
return accum + one()
})
assert(total, isEqualTo = 100, error = "each call returns 1")
"#;
let result = parse_execute(code).await.unwrap();
let high_water = result.exec_state.global.machine_depth_high_water;
assert!(high_water < 10, "high water: {high_water}");
}
#[tokio::test(flavor = "multi_thread")]
async fn top_level_if_arm_return_in_imported_module_errors_in_v3() {
let dep = r#"x = if true {
return 1
0
} else {
0
}
export y = x
"#;
let main = r#"@settings(kclVersion = "3.0-preview")
import y from "dep.kcl"
z = y
"#;
let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
assert!(
err.message().contains("Cannot return from outside a function."),
"unexpected message: {}",
err.message()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn return_of_exit_still_exits_program_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn f() {
return exit()
}
x = f()
assert(1, isEqualTo = 2, error = "code after exit ran")
"#;
parse_execute(code).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn if_arm_bindings_do_not_leak_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
x = if true {
y = 1
y
} else {
0
}
z = y
"#;
let err = parse_execute(code).await.expect_err("should error");
assert!(
err.message().contains("`y` is not defined"),
"unexpected message: {}",
err.message()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn if_arm_bindings_leak_without_v3() {
for header in ["", "@settings(kclVersion = 2.0)\n"] {
let code = format!(
r#"{header}x = if true {{
y = 1
y
}} else {{
0
}}
z = y
"#
);
let result = parse_execute(&code).await.unwrap();
assert_eq!(variable_f64(&result, "z"), 1.0);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn if_arm_shadowing_allowed_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
y = 1
x = if true {
y = 2
y + 10
} else {
0
}
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 12.0);
assert_eq!(variable_f64(&result, "y"), 1.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn if_arm_shadowing_still_errors_without_v3() {
for header in ["", "@settings(kclVersion = 2.0)\n"] {
let code = format!(
r#"{header}y = 1
x = if true {{
y = 2
y
}} else {{
0
}}
"#
);
let err = parse_execute(&code).await.expect_err("should error");
assert!(
err.message().contains("Cannot redefine `y`"),
"unexpected message: {}",
err.message()
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn if_arm_closure_escape_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
n = 1
f = if true {
m = 41
g = fn() {
return m + n
}
g
} else {
g = fn() {
return 0
}
g
}
x = f()
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 42.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn recursive_if_arm_closure_keeps_enclosing_function_frame_alive_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn makeCounter() {
outer = 40
selected = if true {
inner = 2
fn count(@n) {
return if n == 0 {
outer + inner
} else {
count(n - 1) + 1
}
}
count
} else {
fn fallback(@n) {
return n
}
fallback
}
return selected
}
counter = makeCounter()
x = counter(3)
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 45.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn return_inside_scoped_if_arm_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn f(@b) {
local = if b {
w = 1
return w + 9
0
} else {
0
}
return local
}
x = f(true)
y = f(false)
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 10.0);
assert_eq!(variable_f64(&result, "y"), 0.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn else_if_and_nested_if_scoping_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
x = if false {
0
} else if true {
a = 1
b = if true {
c = 2
a + c
} else {
0
}
a + b
} else {
0
}
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 4.0);
let code = r#"@settings(kclVersion = "3.0-preview")
x = if true {
b = if true {
c = 2
c
} else {
0
}
b + c
} else {
0
}
"#;
let err = parse_execute(code).await.expect_err("should error");
assert!(
err.message().contains("`c` is not defined"),
"unexpected message: {}",
err.message()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn else_if_and_final_else_arms_are_isolated_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
x = if false {
0
} else if true {
y = 1
y
} else {
0
}
z = y
"#;
let err = parse_execute(code).await.expect_err("should error");
assert!(
err.message().contains("`y` is not defined"),
"unexpected message: {}",
err.message()
);
let code = r#"@settings(kclVersion = "3.0-preview")
x = if false {
0
} else if false {
0
} else {
y = 1
y
}
z = y
"#;
let err = parse_execute(code).await.expect_err("should error");
assert!(
err.message().contains("`y` is not defined"),
"unexpected message: {}",
err.message()
);
let code = r#"@settings(kclVersion = "3.0-preview")
outer = 1
x = if false {
0
} else if true {
outer = 2
outer + 10
} else {
0
}
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 12.0);
assert_eq!(variable_f64(&result, "outer"), 1.0);
let code = r#"@settings(kclVersion = "3.0-preview")
outer = 1
x = if false {
0
} else if false {
0
} else {
outer = 2
outer + 10
}
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "x"), 12.0);
assert_eq!(variable_f64(&result, "outer"), 1.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn else_if_and_final_else_arm_bindings_leak_without_v3() {
for header in ["", "@settings(kclVersion = 2.0)\n"] {
let code = format!(
r#"{header}x = if false {{
0
}} else if true {{
y = 1
y
}} else {{
0
}}
z = y
"#
);
let result = parse_execute(&code).await.unwrap();
assert_eq!(variable_f64(&result, "z"), 1.0, "code={code}");
let code = format!(
r#"{header}x = if false {{
0
}} else if false {{
0
}} else {{
y = 1
y
}}
z = y
"#
);
let result = parse_execute(&code).await.unwrap();
assert_eq!(variable_f64(&result, "z"), 1.0, "code={code}");
let code = format!(
r#"{header}outer = 1
x = if false {{
0
}} else if true {{
outer = 2
outer
}} else {{
0
}}
"#
);
let err = parse_execute(&code).await.expect_err("should error");
assert!(
err.message().contains("Cannot redefine `outer`"),
"unexpected message: {}",
err.message()
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn error_inside_if_arm_unwinds_balanced_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn f() {
dummy = if true {
assert(1, isEqualTo = 2, error = "boom")
0
} else {
0
}
return dummy
}
x = f()
"#;
let err = parse_execute(code).await.expect_err("should error");
assert!(err.message().contains("boom"), "unexpected message: {}", err.message());
}
#[tokio::test(flavor = "multi_thread")]
async fn exit_inside_scoped_if_arm_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn f() {
dummy = if true {
exit()
0
} else {
0
}
return dummy
}
x = f()
assert(1, isEqualTo = 2, error = "code after exit ran")
"#;
parse_execute(code).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn if_arm_scoping_gated_on_entry_point_not_module() {
let dep = r#"@settings(kclVersion = "3.0-preview")
ignored = if true {
leaked = 1
leaked
} else {
0
}
export leakCheck = leaked
"#;
let main = r#"@settings(kclVersion = 2.0)
import leakCheck from "dep.kcl"
x = leakCheck
"#;
let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
assert!(
err.message()
.starts_with("Mixing KCL versions in a single program is not allowed."),
"unexpected message: {}",
err.message()
);
let dep = r#"ignored = if true {
arm = 1
arm
} else {
0
}
export fn leakCheck() {
return arm
}
"#;
let main = r#"@settings(kclVersion = "3.0-preview")
import leakCheck from "dep.kcl"
x = leakCheck()
"#;
let err = execute_with_modules(main, &[("dep.kcl", dep)]).await.unwrap_err();
assert!(
err.message().contains("`arm` is not defined"),
"unexpected message: {}",
err.message()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn unwind_through_sketch_block_inside_scoped_if_arm_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
fn f() {
dummy = if true {
s = sketch(on = XY) {
l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
q = notDefinedAnywhere
}
0
} else {
0
}
return dummy
}
x = f()
"#;
let err = parse_execute(code).await.unwrap_err();
assert!(
err.message().contains("`notDefinedAnywhere` is not defined"),
"unexpected message: {}",
err.message()
);
let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
fn f() {
dummy = if true {
s = sketch(on = XY) {
l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
e = exit()
}
0
} else {
0
}
return dummy
}
x = f()
assert(1, isEqualTo = 2, error = "code after exit ran")
"#;
parse_execute(code).await.unwrap();
let code = r#"@settings(kclVersion = "3.0-preview", experimentalFeatures = allow)
fn g() {
dummy = if true {
s = sketch(on = XY) {
l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
return 42
}
0
} else {
0
}
return 0
}
y = g()
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "y"), 42.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn tag_declared_inside_if_arm_is_arm_local_in_v3() {
let arm_body = r#"p = if true {
profile = startSketchOn(XY)
|> startProfile(at = [0, 0])
|> line(end = [10, 0], tag = $edge)
|> line(end = [0, 10])
|> line(end = [-10, 0])
|> close()
inArmLen = segLen(edge)
assert(inArmLen, isEqualTo = 10, error = "tag is usable within its arm")
profile
} else {
startSketchOn(XY)
|> startProfile(at = [0, 0])
|> line(end = [5, 0])
|> line(end = [0, 5])
|> line(end = [-5, 0])
|> close()
}
len = segLen(edge)
"#;
let code = format!("@settings(kclVersion = \"3.0-preview\")\n{arm_body}");
let err = parse_execute(&code).await.unwrap_err();
assert!(
err.message().contains("`edge` is not defined"),
"unexpected message: {}",
err.message()
);
let result = parse_execute(arm_body).await.unwrap();
assert_eq!(variable_f64(&result, "len"), 10.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn if_arm_scopes_do_not_retain_function_frames_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn pick(@i) {
r = if i > 50 {
a = i * 2
a
} else {
b = i + 1
b
}
return r
}
results = map([1..100], f = fn(@i) { return pick(i) })
assert(results[0], isEqualTo = 2, error = "pick(1) = 2")
assert(results[99], isEqualTo = 200, error = "pick(100) = 200")
"#;
let result = parse_execute(code).await.unwrap();
let retained = result.exec_state.stack().memory.envs_with_bindings();
assert!(retained < 20, "retained environments: {retained}");
}
#[tokio::test(flavor = "multi_thread")]
async fn if_arm_scoping_inside_pipe_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
cond = true
result = 5
|> if cond {
a = 20
a
} else {
0
}
|> max([%, 1])
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "result"), 20.0);
let code = r#"@settings(kclVersion = "3.0-preview")
cond = true
result = 5
|> if cond {
a = 20
a
} else {
0
}
leaked = a
"#;
let err = parse_execute(code).await.unwrap_err();
assert!(
err.message().contains("`a` is not defined"),
"unexpected message: {}",
err.message()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn member_expression_evaluates_object_before_property_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
x = a[b]
"#;
let err = parse_execute(code).await.expect_err("should error");
assert_eq!(err.message(), "`a` is not defined");
}
#[tokio::test(flavor = "multi_thread")]
async fn member_expression_evaluates_property_before_object_without_v3() {
let code = r#"@settings(kclVersion = 2.0)
x = a[b]
"#;
let err = parse_execute(code).await.expect_err("should error");
assert_eq!(err.message(), "`b` is not defined");
}
#[tokio::test(flavor = "multi_thread")]
async fn member_expression_undefined_object_with_static_property_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
x = a.b
"#;
let err = parse_execute(code).await.expect_err("should error");
assert_eq!(err.message(), "`a` is not defined");
}
#[tokio::test(flavor = "multi_thread")]
async fn member_expression_values_in_v3() {
let code = r#"@settings(kclVersion = "3.0-preview")
fn xs() {
return [10, 20, 30]
}
fn one() {
return 1
}
obj = { inner = { xs = xs() } }
objs = [obj, obj]
a = obj.inner.xs[one()]
b = xs()[one() + 1]
c = objs[0].inner.xs[0]
"#;
let result = parse_execute(code).await.unwrap();
assert_eq!(variable_f64(&result, "a"), 20.0);
assert_eq!(variable_f64(&result, "b"), 30.0);
assert_eq!(variable_f64(&result, "c"), 10.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn exit_inside_member_expression_in_v3() {
for code in [
r#"@settings(kclVersion = "3.0-preview")
x = exit()[0]
assert(1, isEqualTo = 2, error = "code after exit ran")
"#,
r#"@settings(kclVersion = "3.0-preview")
arr = [1]
x = arr[exit()]
assert(1, isEqualTo = 2, error = "code after exit ran")
"#,
] {
parse_execute(code).await.unwrap();
}
}
#[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();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_shadowed_get_opposite_edge_binding_does_not_panic() {
let code = r#"startX = 2
baseSketch = sketch(on = XY) {
yoyo = line(start = [startX, 0], end = [7, 6])
line2 = line(start = [7, 6], end = [7, 12])
hi = line(start = [7, 12], end = [startX, 0])
}
baseRegion = region(point = [5.5, 6], sketch = baseSketch)
myExtrude = extrude(
baseRegion,
length = 5,
tagEnd = $endCap,
tagStart = $startCap,
)
yodawg = getCommonEdge(faces = [
baseRegion.tags.hi,
baseRegion.tags.yoyo
])
cutSketch = sketch(on = YZ) {
myDisambigutator = line(start = [-3.29, 4.75], end = [2.03, 2.44])
myDisambigutator2 = line(start = [2.03, 2.44], end = [-3.49, 0.31])
line3 = line(start = [-3.49, 0.31], end = [-3.29, 4.75])
}
cutRegion = region(point = [-1.5833333333, 2.5], sketch = cutSketch)
extrude001 = extrude(cutRegion, length = 5)
solid001 = subtract(myExtrude, tools = extrude001)
yoyo = getOppositeEdge(baseRegion.tags.hi)
fillet(solid001, radius = 0.1, tags = yoyo)
"#;
parse_execute(code).await.unwrap();
}
async fn run_constraint_report(kcl: &str) -> SketchConstraintReport {
let program = crate::Program::parse_no_errs(kcl).unwrap();
let ctx = ExecutorContext::new_geometry_only_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
.expect("constraint report test outcome should collect variables");
let report = outcome.sketch_constraint_report();
ctx.close().await;
report
}
#[tokio::test(flavor = "multi_thread")]
async fn warn_when_sketch_is_over_constrained() {
let code = r#"
sketch001 = sketch(on = XY) {
line1 = line(start = [var -10.64mm, var 26.44mm], end = [var 13.05mm, var 5.52mm])
fixed([line1.start, ORIGIN])
fixed([line1.start, [20, 20]])
}
"#;
let result = parse_execute(code).await.unwrap();
let issues = result.exec_state.issues();
let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
panic!("expected over-constrained warning; found {issues:#?}");
};
assert_eq!(warning.severity, Severity::Warning);
}
#[tokio::test(flavor = "multi_thread")]
async fn over_constrained_warning_identifies_signed_vertical_distance_direction() {
let code = r#"
sketch001 = sketch(on = XY) {
line1 = line(start = [var 0mm, var 10mm], end = [var 0mm, var 0mm])
fixed([line1.start, [0mm, 10mm]])
fixed([line1.end, ORIGIN])
verticalDistance([line1.start, line1.end]) == 10mm
}
"#;
let result = parse_execute(code).await.unwrap();
let issues = result.exec_state.issues();
let Some(warning) = issues.iter().find(|issue| issue.message.contains("over-constrained")) else {
panic!("expected over-constrained warning; found {issues:#?}");
};
assert!(
warning.message.contains(
"Unsatisfied signed verticalDistance constraint: a positive right-hand side requires the second point to be above the first"
),
"expected signed-direction diagnostic; found {warning:#?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn no_warning_when_sketch_is_not_over_constrained() {
let code = r#"
sketch001 = sketch(on = XY) {
line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
}
"#;
let result = parse_execute(code).await.unwrap();
let issues = result.exec_state.issues();
assert!(
!issues.iter().any(|issue| issue.message.contains("over-constrained")),
"did not expect over-constrained warning; found {issues:#?}"
);
}
#[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);
}
#[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);
}
#[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);
}
#[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);
}
#[tokio::test(flavor = "multi_thread")]
async fn test_constraint_report_reports_sketch_names() {
let kcl = r#"
@settings(experimentalFeatures = allow)
fixedSketch = 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
}
looseSketch = sketch(on = XZ) {
line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
}
conflictSketch = sketch(on = XY) {
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.errors.len(), 0);
assert_eq!(report.fully_constrained.len(), 1);
assert_eq!(report.under_constrained.len(), 1);
assert_eq!(report.over_constrained.len(), 1);
assert_eq!(report.fully_constrained[0].name, "fixedSketch");
assert_eq!(report.under_constrained[0].name, "looseSketch");
assert_eq!(report.over_constrained[0].name, "conflictSketch");
}
#[tokio::test(flavor = "multi_thread")]
async fn test_constraint_report_name_empty_without_declaration() {
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.under_constrained.len(), 1);
assert_eq!(report.under_constrained[0].name, "");
}
#[tokio::test(flavor = "multi_thread")]
async fn test_constraint_report_names_repeat_across_calls() {
let kcl = r#"
fn makeSketch() {
inner = sketch(on = XY) {
line1 = line(start = [var 1mm, var 2mm], end = [var 3mm, var 4mm])
}
return inner
}
first = makeSketch()
second = makeSketch()
"#;
let report = run_constraint_report(kcl).await;
assert_eq!(report.under_constrained.len(), 2);
assert_eq!(report.under_constrained[0].name, "inner");
assert_eq!(report.under_constrained[1].name, "inner");
}
#[tokio::test(flavor = "multi_thread")]
async fn test_enum_declaration_is_experimental() {
let code = "type Color { | Red }";
assert_eq!(
parse_execute(code).await.unwrap_err().message(),
"Use of enum declarations is experimental and may change or be removed."
);
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_declaration_registers_type() {
let code = r#"@settings(experimentalFeatures = allow)
type Color { | Red | Green }
"#;
parse_execute(code).await.unwrap();
let code = r#"@settings(experimentalFeatures = allow)
export type Color { | Red | Green }
"#;
parse_execute(code).await.unwrap();
let code = r#"@settings(experimentalFeatures = allow)
type Empty { | }
"#;
parse_execute(code).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_declaration_rejects_nested_scope() {
let allow = "@settings(experimentalFeatures = allow)\n";
for (case, code) in [
(
"function body",
format!("{allow}fn palette() {{\n type Color {{ | Red }}\n return 0\n}}\npalette()\n"),
),
(
"sketch block",
format!(
"{allow}sketch(on = XY) {{\n type Color {{ | Red }}\n l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
),
),
(
"if arm",
format!("{allow}x = if true {{\n type Color {{ | Red }}\n 0\n}} else {{\n 0\n}}\n"),
),
] {
assert_eq!(
parse_execute(&code).await.unwrap_err().message(),
"Enum declarations are only supported at the top-level of a file. Move `type Color` to the top-level.",
"case: {case}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_alone_is_restricted_to_top_level() {
let allow = "@settings(experimentalFeatures = allow)\n";
for (case, code) in [
(
"function body",
format!("{allow}fn f() {{\n type Temperature = number(_)\n return 0\n}}\nx = f()\n"),
),
(
"sketch block",
format!(
"{allow}sketch(on = XY) {{\n type Temperature = number(_)\n l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}}\n"
),
),
] {
parse_execute(&code)
.await
.unwrap_or_else(|err| panic!("a type alias should be allowed in a {case}: {}", err.message()));
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_declaration_rejects_duplicate() {
let code = r#"@settings(experimentalFeatures = allow)
type Color { | Red | Green | Red }
"#;
assert_eq!(
parse_execute(code).await.unwrap_err().message(),
"Duplicate variant `Red` in enum `Color`."
);
}
async fn execute_with_modules(main: &str, modules: &[(&str, &str)]) -> Result<ExecTestResults, KclError> {
let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_enum_clash").unwrap();
for (name, source) in modules {
tokio::fs::write(tmpdir.path().join(name), source).await.unwrap();
}
parse_execute_with_project_dir(main, Some(crate::TypedPath(tmpdir.path().into()))).await
}
async fn issues_with_empty_module(main: &str) -> Vec<crate::errors::CompilationIssue> {
use futures::FutureExt;
let project_dir = crate::TypedPath::new("/zma-kcl-member-ranges");
let files = [(project_dir.join("m.kcl").to_string(), Vec::new())]
.into_iter()
.collect();
let program = crate::Program::parse_no_errs(main).unwrap();
let ctx = ExecutorContext {
engine: Arc::new(EngineManager::new_mock()),
engine_batch: EngineBatchContext::default(),
fs: crate::fs::new_file_system_handle(crate::InMemoryFiles::new(files)),
settings: ExecutorSettings {
project_directory: Some(project_dir),
..Default::default()
},
context_type: ContextType::Mock,
execution_callbacks: Default::default(),
executor_kind: machine::ExecutorKind::resolve(),
machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
};
let mut exec_state = ExecState::new(&ctx);
let run_result = std::panic::AssertUnwindSafe(ctx.run(&program, &mut exec_state))
.catch_unwind()
.await;
ctx.close().await;
if let Err(panic) = run_result {
std::panic::resume_unwind(panic);
}
exec_state.issues().to_vec()
}
#[tokio::test(flavor = "multi_thread")]
async fn member_object_diagnostics_use_object_range() {
for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
let main = format!("{header}import \"m.kcl\" as m\nx = m.field\n");
let issues = issues_with_empty_module(&main).await;
let warning = issues
.iter()
.find(|issue| issue.message.contains("no return value"))
.expect("missing-return warning should be recorded");
let object_start = main.rfind("m.field").unwrap();
assert_eq!(
(warning.source_range.start(), warning.source_range.end()),
(object_start, object_start + 1),
"warning should point at the object's span (header={header:?})"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn member_property_diagnostics_use_property_range() {
for header in ["", "@settings(kclVersion = \"3.0-preview\")\n"] {
let main = format!("{header}import \"m.kcl\" as m\narr = [1]\nx = arr[m]\n");
let issues = issues_with_empty_module(&main).await;
let warning = issues
.iter()
.find(|issue| issue.message.contains("no return value"))
.expect("missing-return warning should be recorded");
let prop_start = main.rfind("[m]").unwrap() + 1;
assert_eq!(
(warning.source_range.start(), warning.source_range.end()),
(prop_start, prop_start + 1),
"warning should point at the property's span (header={header:?})"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn backtrace_reports_fully_qualified_fn_names() {
let main = "import \"m.kcl\" as m\nx = m::f()\n";
let modules = [("m.kcl", "export fn f() {\n return undefinedVariable\n}\n")];
let err = execute_with_modules(main, &modules).await.unwrap_err();
let fn_names: Vec<_> = err.backtrace().into_iter().filter_map(|item| item.fn_name).collect();
assert_eq!(fn_names, vec!["m::f".to_owned()]);
}
#[tokio::test(flavor = "multi_thread")]
async fn whole_module_name_executes_as_operand() {
let main = r#"import "m.kcl" as m
sum = m + m
neg = -m
"#;
let result = execute_with_modules(main, &[("m.kcl", "42\n")]).await.unwrap();
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, "sum").as_f64(),
Some(84.0)
);
assert_eq!(
mem_get_json(result.exec_state.stack(), result.mem_env, "neg").as_f64(),
Some(-42.0)
);
}
#[tokio::test(flavor = "multi_thread")]
async fn whole_module_without_return_as_operand_errors() {
let main = "import \"m.kcl\" as m
x = m + 1
";
let err = execute_with_modules(main, &[("m.kcl", "")]).await.unwrap_err();
assert!(
err.message().contains("Expected a number, but found none"),
"expected the operand to be the module's missing-return KclNone, got: {}",
err.message()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_rejects_name_clash_with_module() {
let plain_module = ("Color.kcl", "export x = 1\n");
let enum_module = (
"enums.kcl",
"@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
);
for (case, main, modules) in [
(
"module then enum",
"@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\ntype Color { | Red }\n",
vec![plain_module],
),
(
"enum then module",
"@settings(experimentalFeatures = allow)\ntype Color { | Red }\nimport \"Color.kcl\"\n",
vec![plain_module],
),
(
"named import of an enum",
"@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport Color from 'enums.kcl'\n",
vec![plain_module, enum_module],
),
(
"glob import of an enum",
"@settings(experimentalFeatures = allow)\nimport \"Color.kcl\"\nimport * from 'enums.kcl'\n",
vec![plain_module, enum_module],
),
(
"module then enum alias",
"@settings(experimentalFeatures = allow)\ntype Base { | Red }\nimport \"Color.kcl\"\ntype Color = Base\n",
vec![plain_module],
),
(
"enum alias then module",
"@settings(experimentalFeatures = allow)\ntype Base { | Red }\ntype Color = Base\nimport \"Color.kcl\"\n",
vec![plain_module],
),
] {
let err = execute_with_modules(main, &modules).await.unwrap_err();
assert_eq!(
err.message(),
"An enum and a module cannot share the name `Color` in the same scope, because `Color::x` would be ambiguous. Rename one of them.",
"case: {case}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_alias_can_shadow_module_from_outer_scope() {
let main = r#"@settings(experimentalFeatures = allow)
type Color { | Red }
import "Shade.kcl"
fn pick(): Color {
type Shade = Color
return Shade::Red
}
result = pick()
"#;
let result = execute_with_modules(main, &[("Shade.kcl", "export value = 1\n")])
.await
.unwrap();
let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "result") else {
panic!("`result` should hold an enum value");
};
assert_eq!(value.qualified_name(), "Color::Red");
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_constructs_variant() {
let allow = "@settings(experimentalFeatures = allow)\n";
let colors = (
"colors.kcl",
"@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
);
for (case, main, modules) in [
(
"declared locally",
format!("{allow}type Color {{ | Red | Green }}\nx = Color::Red\n"),
vec![],
),
(
"reached through a module path",
format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
vec![colors],
),
(
"imported by name",
format!("{allow}import Color from 'colors.kcl'\nx = Color::Red\n"),
vec![colors],
),
(
"imported under an alias",
format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade::Red\n"),
vec![colors],
),
] {
let result = execute_with_modules(&main, &modules)
.await
.unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
panic!("case: {case}: `x` should hold an enum value");
};
assert_eq!(value.qualified_name(), "Color::Red", "case: {case}");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_aliases_preserve_the_original_declaration() {
let code = r#"@settings(experimentalFeatures = allow)
type Color { | Red | Green }
type C = Color
type D = C
original = Color::Red
directAlias = C::Red
chainedAlias = D::Red
directEqualsOriginal = directAlias == original
chainEqualsOriginal = chainedAlias == original
fn passThroughAlias(@color: C): Color {
return color
}
passed = passThroughAlias(D::Green)
"#;
let result = parse_execute(code).await.unwrap();
let memory = result.exec_state.stack();
let KclValue::Type {
value: TypeDef::Enum(original_def),
..
} = mem_get_json(memory, result.mem_env, &format!("{}Color", memory::TYPE_PREFIX))
else {
panic!("`Color` should hold an enum definition");
};
for alias in ["C", "D"] {
let KclValue::Type {
value: TypeDef::Enum(alias_def),
..
} = mem_get_json(memory, result.mem_env, &format!("{}{alias}", memory::TYPE_PREFIX))
else {
panic!("`{alias}` should hold an enum definition");
};
assert!(Arc::ptr_eq(&original_def, &alias_def), "alias: {alias}");
}
for name in ["directEqualsOriginal", "chainEqualsOriginal"] {
let KclValue::Bool { value, .. } = mem_get_json(memory, result.mem_env, name) else {
panic!("`{name}` should hold a boolean");
};
assert!(value, "comparison: {name}");
}
let KclValue::Enum { value: passed } = mem_get_json(memory, result.mem_env, "passed") else {
panic!("`passed` should hold an enum value");
};
assert_eq!(passed.enum_id(), original_def.id());
assert_eq!(passed.variant(), "Green");
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_aliases_survive_qualified_imports_and_reexports() {
let main = r#"@settings(experimentalFeatures = allow)
import "colors.kcl"
import "aliases.kcl"
import "tones.kcl"
import Paint as Finish from "aliases.kcl"
import * from "aliases.kcl"
original = colors::Color::Red
qualifiedAlias = aliases::Paint::Red
namedImportAlias = Finish::Green
globImportAlias = Paint::Red
namedTargetAlias = tones::Tint::Green
aliasesEqualOriginal = qualifiedAlias == original
fn throughAlias(@color: Finish): colors::Color {
return color
}
fn throughOriginal(@color: colors::Color): Finish {
return color
}
fromAlias = throughAlias(Finish::Green)
originalIntoAlias = throughAlias(colors::Color::Green)
fromOriginal = throughOriginal(colors::Color::Red)
"#;
let modules = [
(
"colors.kcl",
"@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
),
(
"palette.kcl",
"@settings(experimentalFeatures = allow)\nimport \"colors.kcl\" as swatches\nexport type Shade = swatches::Color\n",
),
(
"aliases.kcl",
"@settings(experimentalFeatures = allow)\nimport \"palette.kcl\"\nexport type Paint = palette::Shade\n",
),
(
"tones.kcl",
"@settings(experimentalFeatures = allow)\nimport Color from \"colors.kcl\"\nexport type Tint = Color\n",
),
];
let result = execute_with_modules(main, &modules).await.unwrap();
let memory = result.exec_state.stack();
let KclValue::Enum { value: original } = mem_get_json(memory, result.mem_env, "original") else {
panic!("`original` should hold an enum value");
};
let original_id = original.enum_id();
for (name, variant) in [
("qualifiedAlias", "Red"),
("namedImportAlias", "Green"),
("globImportAlias", "Red"),
("namedTargetAlias", "Green"),
("fromAlias", "Green"),
("originalIntoAlias", "Green"),
("fromOriginal", "Red"),
] {
let KclValue::Enum { value } = mem_get_json(memory, result.mem_env, name) else {
panic!("`{name}` should hold an enum value");
};
assert_eq!(value.enum_id(), original_id, "value: {name}");
assert_eq!(value.variant(), variant, "value: {name}");
}
let KclValue::Bool { value, .. } = mem_get_json(memory, result.mem_env, "aliasesEqualOriginal") else {
panic!("`aliasesEqualOriginal` should hold a boolean");
};
assert!(value);
let KclValue::Type {
value: TypeDef::Enum(finish_def),
..
} = mem_get_json(memory, result.mem_env, &format!("{}Finish", memory::TYPE_PREFIX))
else {
panic!("`Finish` should hold an enum definition");
};
assert_eq!(finish_def.id(), original_id);
}
#[tokio::test(flavor = "multi_thread")]
async fn signature_types_resolve_in_declaring_module() {
let colors = (
"colors.kcl",
"@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n return c\n}\n",
);
let main =
"@settings(experimentalFeatures = allow)\nimport \"colors.kcl\"\nr = colors::paint(colors::Color::Red)\n";
let result = execute_with_modules(main, &[colors]).await.unwrap();
let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
panic!("`r` should hold an enum value");
};
assert_eq!(value.qualified_name(), "Color::Red");
}
#[tokio::test(flavor = "multi_thread")]
async fn qualified_type_paths_resolve_in_aliases_and_ascriptions() {
let main = r#"@settings(experimentalFeatures = allow)
type ViewOrientation = view::Orientation
front = view::Orientation::Front: view::Orientation
"#;
parse_execute(main).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn unknown_qualified_type_reports_the_written_name() {
let main = "fn f(@value: missing::Orientation) {}\n";
let err = parse_execute(main).await.unwrap_err();
assert_eq!(err.message(), "Unknown type: missing::Orientation");
}
#[tokio::test(flavor = "multi_thread")]
async fn signature_types_resolve_under_import_alias() {
let colors = (
"colors.kcl",
"@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n\nexport fn paint(@c: Color) {\n return c\n}\n",
);
let main = "@settings(experimentalFeatures = allow)\nimport \"colors.kcl\" as painter\nr = painter::paint(painter::Color::Red)\n";
let result = execute_with_modules(main, &[colors]).await.unwrap();
let KclValue::Enum { value } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
panic!("`r` should hold an enum value");
};
assert_eq!(value.qualified_name(), "Color::Red");
}
#[tokio::test(flavor = "multi_thread")]
async fn signature_types_ignore_caller_scope() {
let broken = (
"broken.kcl",
"@settings(experimentalFeatures = allow)\nexport fn f(@x: Missing) {\n return x\n}\n",
);
let main = "@settings(experimentalFeatures = allow)\ntype Missing = string\nimport \"broken.kcl\"\nr = broken::f(\"hi\")\n";
let err = execute_with_modules(main, &[broken]).await.unwrap_err();
assert!(
err.message().contains("Unknown type: Missing"),
"message: {}",
err.message()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn signature_types_reject_forward_reference() {
let main = "@settings(experimentalFeatures = allow)\nfn f(@x: Later) {\n return x\n}\ntype Later = string\n";
let err = parse_execute(main).await.unwrap_err();
assert!(
err.message().contains("Unknown type: Later"),
"message: {}",
err.message()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn signature_types_resolve_in_enclosing_scope() {
let main = "@settings(experimentalFeatures = allow)\ntype Width = string\nfn makeMeasure() {\n type Width = number(mm)\n return fn(@w: Width) { return w }\n}\nmeasure = makeMeasure()\nr = measure(42)\n";
let result = parse_execute(main).await.unwrap();
let KclValue::Number { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "r") else {
panic!("`r` should hold a number");
};
assert_eq!(value, 42.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn signature_number_types_ignore_module_default_units() {
let units_in = (
"units_in.kcl",
"@settings(defaultLengthUnit = in)\nexport fn passThrough(@x: number(Length)) {\n return x\n}\n",
);
let main = "import \"units_in.kcl\"\na = units_in::passThrough(42)\nb = units_in::passThrough(42mm)\nc = units_in::passThrough(42in)\n";
let result = execute_with_modules(main, &[units_in]).await.unwrap();
for (name, expected_ty) in [
(
"a",
kcl_api::NumericType::Default {
len: kcl_api::UnitLength::Millimeters,
angle: kcl_api::UnitAngle::Degrees,
},
),
(
"b",
kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Millimeters)),
),
(
"c",
kcl_api::NumericType::Known(kcl_api::UnitType::Length(kcl_api::UnitLength::Inches)),
),
] {
let KclValue::Number { value, ty, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name)
else {
panic!("`{name}` should hold a number");
};
assert_eq!(value, 42.0, "`{name}` should keep its magnitude");
assert_eq!(ty, expected_ty, "`{name}` should keep the caller-side unit context");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn signature_types_use_declaring_scope_when_both_scopes_define_the_name() {
let m1 = (
"m1.kcl",
"@settings(experimentalFeatures = allow)\ntype A = string\n\nexport fn test(@a: A) {\n return a\n}\n",
);
let main =
"@settings(experimentalFeatures = allow)\nimport * from \"m1.kcl\"\ntype A = number(mm)\nx = test(2mm)\n";
let err = execute_with_modules(main, &[m1]).await.unwrap_err();
assert_eq!(
err.message(),
"The input argument of `test` requires a value with type `A`, but found a number (mm) (with type `number(mm)`)."
);
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_rejects_bad_variant_paths() {
let allow = "@settings(experimentalFeatures = allow)\n";
for (case, main, modules, message) in [
(
"unknown variant",
format!("{allow}type Color {{ | Red | Green }}\nx = Color::Blue\n"),
vec![],
"`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
),
(
"unknown variant through an alias",
format!("{allow}type Color {{ | Red | Green }}\ntype Paint = Color\nx = Paint::Blue\n"),
vec![],
"`Blue` is not a variant of enum `Color`. Its variants are: Red, Green.",
),
(
"enum with no variants",
format!("{allow}type Empty {{ | }}\nx = Empty::Red\n"),
vec![],
"`Red` is not a variant of enum `Empty`. Enum `Empty` has no variants.",
),
(
"path continues past the enum",
format!("{allow}type Color {{ | Red }}\nx = Color::Red::more\n"),
vec![],
"`Color` is an enum, so only a variant name can follow it. There is nothing to reach through `Color::Red`.",
),
(
"variant name is case sensitive",
format!("{allow}type Color {{ | Red }}\nx = Color::red\n"),
vec![],
"`red` is not a variant of enum `Color`. Its variants are: Red.",
),
(
"enum not exported from its module",
format!("{allow}import \"colors.kcl\"\nx = colors::Color::Red\n"),
vec![(
"colors.kcl",
"@settings(experimentalFeatures = allow)\ntype Color { | Red }\n",
)],
"Item Color not found in module's exported items",
),
(
"a non-enum type alias cannot head a path",
format!("{allow}type T = number(_)\nx = T::foo\n"),
vec![],
"`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
),
(
"a chained non-enum type alias cannot head a path",
format!("{allow}type T = number(_)\ntype U = T\nx = U::foo\n"),
vec![],
"`U` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
),
(
"a qualified non-enum type alias cannot head a path",
format!("{allow}import \"types.kcl\"\nx = types::T::foo\n"),
vec![(
"types.kcl",
"@settings(experimentalFeatures = allow)\nexport type T = number(_)\n",
)],
"`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
),
(
"an alias containing an enum is not an enum alias",
format!("{allow}type Color {{ | Red }}\ntype T = Color | string\nx = T::Red\n"),
vec![],
"`T` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
),
(
"a value cannot head a path",
"Color = 5\nx = Color::Red\n".to_owned(),
vec![],
"`Color` is not defined",
),
] {
let err = execute_with_modules(&main, &modules).await.unwrap_err();
assert_eq!(err.message(), message, "case: {case}");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_compares_by_variant() {
let code = r#"@settings(experimentalFeatures = allow)
type Color { | Red | Green }
sameEq = Color::Red == Color::Red
sameNeq = Color::Red != Color::Red
otherEq = Color::Red == Color::Green
otherNeq = Color::Red != Color::Green
"#;
let result = parse_execute(code).await.unwrap();
for (name, expected) in [
("sameEq", true),
("sameNeq", false),
("otherEq", false),
("otherNeq", true),
] {
let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
panic!("`{name}` should hold a bool");
};
assert_eq!(value, expected, "variable: {name}");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_usable_inside_sketch_block() {
let code = r#"@settings(experimentalFeatures = allow)
type Color { | Red | Green }
sketch(on = XY) {
c = Color::Red
assertIs(Color::Red != Color::Green)
assertIs(!(Color::Red != Color::Red))
l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])
}
"#;
parse_execute(code)
.await
.unwrap_or_else(|err| panic!("enum use inside a sketch block should work: {}", err.message()));
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_eq_reserved_inside_sketch_block() {
let allow = "@settings(experimentalFeatures = allow)\n";
let tail = " l1 = line(start = [var 0mm, var 0mm], end = [var 10mm, var 0mm])\n}\n";
for (case, declaration, comparison, types) in [
(
"enums",
"type Color { | Red | Green }\n",
"Color::Red == Color::Green",
"a value of enum `Color` and a value of enum `Color`",
),
("strings", "", "\"a\" == \"b\"", "a string and a string"),
("numbers", "", "1 == 2", "a number and a number"),
] {
let code = format!("{allow}{declaration}sketch(on = XY) {{\n x = {comparison}\n{tail}");
assert_eq!(
parse_execute(&code).await.unwrap_err().message(),
format!("Cannot create an equivalence constraint between values of these types: {types}"),
"case: {case}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_same_file_imported_twice_is_one_type() {
let main = r#"@settings(experimentalFeatures = allow)
import Color as A from 'colors.kcl'
import Color as B from 'colors.kcl'
x = A::Red == B::Red
y = A::Red == B::Green
"#;
let result = execute_with_modules(
main,
&[(
"colors.kcl",
"@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
)],
)
.await
.unwrap();
for (name, expected) in [("x", true), ("y", false)] {
let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, name) else {
panic!("`{name}` should hold a bool");
};
assert_eq!(value, expected, "variable: {name}");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_rejects_comparison_across_types() {
let allow = "@settings(experimentalFeatures = allow)\n";
let color = (
"a.kcl",
"@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
);
let other_color = (
"b.kcl",
"@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
);
for (case, main, modules, message) in [
(
"two enums declared separately",
format!("{allow}type Color {{ | Red }}\ntype Shade {{ | Red }}\nx = Color::Red == Shade::Red\n"),
vec![],
"Cannot compare enum `Color` with enum `Shade`. They are different types.",
),
(
"two enums sharing a name",
format!(
"{allow}import Color as A from 'a.kcl'\nimport Color as B from 'b.kcl'\nx = A::Red == B::Red\n"
),
vec![color, other_color],
"Cannot compare two different enums that are both named `Color`. They come from separate declarations.",
),
(
"an enum and a number",
format!("{allow}type Color {{ | Red }}\nx = Color::Red == 5\n"),
vec![],
"Cannot compare enum `Color::Red` with a number.",
),
(
"a number and an enum, in that order",
format!("{allow}type Color {{ | Red }}\nx = 5 == Color::Red\n"),
vec![],
"Cannot compare enum `Color::Red` with a number.",
),
(
"an enum and a string",
format!("{allow}type Color {{ | Red }}\nx = Color::Red == \"Red\"\n"),
vec![],
"Cannot compare enum `Color::Red` with a string.",
),
] {
let err = execute_with_modules(&main, &modules).await.unwrap_err();
assert_eq!(err.message(), message, "case: {case}");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_rejects_bare_type_name_as_value() {
let allow = "@settings(experimentalFeatures = allow)\n";
let colors = (
"colors.kcl",
"@settings(experimentalFeatures = allow)\nexport type Color { | Red | Green }\n",
);
for (case, main, modules, message) in [
(
"enum suggests a variant",
format!("{allow}type Color {{ | Red | Green }}\nx = Color\n"),
vec![],
"`Color` is a type, not a value. Use one of its variants, such as `Color::Red`.",
),
(
"suggestion uses the import alias",
format!("{allow}import Color as Shade from 'colors.kcl'\nx = Shade\n"),
vec![colors],
"`Shade` is a type, not a value. Use one of its variants, such as `Shade::Red`.",
),
(
"suggestion uses the type alias",
format!("{allow}type Color {{ | Red | Green }}\ntype Paint = Color\nx = Paint\n"),
vec![],
"`Paint` is a type, not a value. Use one of its variants, such as `Paint::Red`.",
),
(
"enum with no variants suggests nothing",
format!("{allow}type Empty {{ | }}\nx = Empty\n"),
vec![],
"`Empty` is a type, not a value.",
),
(
"a type alias reports the same way",
format!("{allow}type T = number(_)\nx = T\n"),
vec![],
"`T` is a type, not a value.",
),
(
"an unknown name is still undefined",
"x = Nope\n".to_owned(),
vec![],
"`Nope` is not defined",
),
] {
let err = execute_with_modules(&main, &modules).await.unwrap_err();
assert_eq!(err.message(), message, "case: {case}");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_use_gated_by_consuming_module() {
let colors = (
"colors.kcl",
"@settings(experimentalFeatures = allow)\nexport type Color { | Red }\n",
);
let aliases = (
"aliases.kcl",
"@settings(experimentalFeatures = allow)\nimport \"colors.kcl\"\nexport type Shade = colors::Color\n",
);
for (case, main, modules) in [
(
"original binding",
"import \"colors.kcl\"\nx = colors::Color::Red\n",
vec![colors],
),
(
"re-exported alias",
"import \"aliases.kcl\"\nx = aliases::Shade::Red\n",
vec![colors, aliases],
),
] {
let result = execute_with_modules(main, &modules)
.await
.unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
let issues = &result.exec_state.global.issues;
assert_eq!(issues.len(), 1, "case: {case}: issues: {issues:?}");
assert_eq!(
issues[0].message, "Use of the enum `Color` is experimental and may change or be removed.",
"case: {case}"
);
assert_eq!(issues[0].severity, Severity::Error, "case: {case}");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_use_not_gated_when_consumer_allows_it() {
let code = r#"@settings(experimentalFeatures = allow)
type Color { | Red }
x = Color::Red
"#;
let result = parse_execute(code).await.unwrap();
assert!(
result.exec_state.global.issues.is_empty(),
"issues: {:?}",
result.exec_state.global.issues
);
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_allows_name_sharing_outside_modules() {
for (case, main, modules) in [
(
"an alias may share a name with a module",
"@settings(experimentalFeatures = allow)\ntype Temperature = number(_)\nimport \"Temperature.kcl\"\nx = Temperature::x\n",
vec![("Temperature.kcl", "export x = 1\n")],
),
(
"a value may share a name with an enum",
"@settings(experimentalFeatures = allow)\ntype Color { | Red }\nColor = 5\n",
vec![],
),
] {
if let Err(err) = execute_with_modules(main, &modules).await {
panic!("case: {case}: {}", err.message());
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_declaration_rejects_redefinition() {
let code = r#"@settings(experimentalFeatures = allow)
type Color { | Red }
type Color { | Green }
"#;
assert_eq!(
parse_execute(code).await.unwrap_err().message(),
"Redefinition of type Color."
);
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_projects_to_string() {
let header = r#"
@settings(experimentalFeatures = allow)
type Color { | Red | Green }
type Label = string
"#;
for (case, body, expected) in [
("a variant", "x = Color::Red: string", "Red"),
("another variant of the same enum", "x = Color::Green: string", "Green"),
("an alias of the target type", "x = Color::Red: Label", "Red"),
(
"an element of a projected array",
r#"
pair = [Color::Red, Color::Green]: [string]
x = pair[1]
"#,
"Green",
),
(
"an element of a nested projected array",
r#"
grid = [[Color::Green]]: [[string]]
x = grid[0][0]
"#,
"Green",
),
(
"a one-element array against a bare string",
"x = [Color::Red]: string",
"Red",
),
] {
let result = parse_execute(&format!("{header}{body}\n"))
.await
.unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
let KclValue::String { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
panic!("case: {case}: `x` should hold a string");
};
assert_eq!(value, expected, "case: {case}");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_ascription_keeps_the_enum() {
let header = r#"
@settings(experimentalFeatures = allow)
type Color { | Red | Green }
type Paint = Color
"#;
for (case, expression, expected) in [
("its own type", "(Color::Red: Color) == Color::Red", true),
("an alias of its own type", "(Color::Red: Paint) == Color::Red", true),
(
"the ascription does not change which variant it is",
"(Color::Red: Color) == Color::Green",
false,
),
] {
let result = parse_execute(&format!("{header}x = {expression}\n"))
.await
.unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x") else {
panic!("case: {case}: `x` should hold a bool");
};
assert_eq!(value, expected, "case: {case}");
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_projection_is_not_implicit() {
let header = r#"
@settings(experimentalFeatures = allow)
type Color { | Red | Green }
"#;
let found = "but found a value of enum `Color` (with type `Color`).";
for (case, body, expected) in [
(
"unlabeled argument",
r#"
fn label(@text: string) { return text }
x = label(Color::Red)
"#,
format!("The input argument of `label` requires a value with type `string`, {found}"),
),
(
"labeled argument",
r#"
fn label(text: string) { return text }
x = label(text = Color::Red)
"#,
format!("text requires a value with type `string`, {found}"),
),
(
"return",
r#"
fn label(): string { return Color::Red }
x = label()
"#,
format!("This function requires its result to be a value with type `string`, {found}"),
),
(
"inside an array at an argument boundary",
r#"
fn labels(@text: [string]) { return text }
x = labels([Color::Red])
"#,
"The input argument of `labels` requires an array of strings (`[string]`), but found an array of `Color` with 1 value (with type `[any; 1]`).".to_owned(),
),
] {
assert_eq!(
parse_execute(&format!("{header}{body}\n")).await.unwrap_err().message(),
expected,
"case: {case}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_ascription_rejections() {
let header = r#"
@settings(experimentalFeatures = allow)
type Color { | Red }
type Shade { | Red }
"#;
let no_number = "Cannot project enum `Color` to a number. An enum projects to `string`; projecting to a number is not supported yet.";
for (case, expression, expected) in [
("a number target", "Color::Red: number(_)", no_number.to_owned()),
(
"a number target reached through an array, so the reason survives the walk",
"[Color::Red]: [number(_)]",
no_number.to_owned(),
),
(
"a boolean target, which is not a projection at all",
"Color::Red: bool",
"could not coerce a value of enum `Color` (with type `Color`) to type `bool`".to_owned(),
),
(
"another enum whose variants happen to match",
"Color::Red: Shade",
"could not coerce a value of enum `Color` (with type `Color`) to type `Shade`".to_owned(),
),
] {
assert_eq!(
parse_execute(&format!("{header}x = {expression}\n"))
.await
.unwrap_err()
.message(),
expected,
"case: {case}"
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn enum_flows_through_declared_types() {
let header = r#"
@settings(experimentalFeatures = allow)
type Color { | Red | Green }
type Paint = Color
type Shade { | Red }
"#;
for (case, body, expected) in [
(
"an alias parameter accepts the original enum",
r#"
fn paint(@c: Paint) { return c }
x = paint(Color::Red) == Paint::Red
"#,
None,
),
(
"an unlabeled parameter",
r#"
fn paint(@c: Color) { return c }
x = paint(Color::Red) == Color::Red
"#,
None,
),
(
"a labeled parameter",
r#"
fn paint(c: Color) { return c }
x = paint(c = Color::Green) == Color::Green
"#,
None,
),
(
"a declared return type",
r#"
fn pick(): Color { return Color::Red }
x = pick() == Color::Red
"#,
None,
),
(
"an array parameter",
r#"
fn firstOf(@cs: [Color]) { return cs[0] }
x = firstOf([Color::Red, Color::Green]) == Color::Red
"#,
None,
),
(
"an object field",
r#"
fn take(@o: { c: Color }) { return o.c }
x = take({ c = Color::Green }) == Color::Green
"#,
None,
),
(
"a union that names the enum",
r#"
fn either(@v: Color | string) { return v }
x = either(Color::Red) == Color::Red
"#,
None,
),
(
"the same union given the other member",
r#"
fn either(@v: Color | string) { return v }
x = either("plain") == "plain"
"#,
None,
),
(
"an alias parameter rejects a different enum",
r#"
fn paint(@c: Paint) { return c }
x = paint(Shade::Red) == Shade::Red
"#,
Some(
"The input argument of `paint` requires a value with type `Paint`, but found a value of enum `Shade` (with type `Shade`).",
),
),
(
"another declaration at the same boundary",
r#"
fn paint(@c: Color) { return c }
x = paint(Shade::Red) == Shade::Red
"#,
Some(
"The input argument of `paint` requires a value with type `Color`, but found a value of enum `Shade` (with type `Shade`).",
),
),
] {
let code = format!("{header}{body}\n");
match expected {
None => {
let result = parse_execute(&code)
.await
.unwrap_or_else(|err| panic!("case: {case}: {}", err.message()));
let KclValue::Bool { value, .. } = mem_get_json(result.exec_state.stack(), result.mem_env, "x")
else {
panic!("case: {case}: `x` should hold a bool");
};
assert!(value, "case: {case}: the value did not survive the boundary");
}
Some(message) => assert_eq!(
parse_execute(&code).await.unwrap_err().message(),
message,
"case: {case}"
),
}
}
}
}