Merge remote-tracking branch 'origin' into paultag/import

This commit is contained in:
Paul Tagliamonte
2025-03-18 09:17:51 -04:00
715 changed files with 457142 additions and 534601 deletions

View File

@ -1153,7 +1153,7 @@ fn find_examples(text: &str, filename: &str) -> Vec<(String, String)> {
async fn run_example(text: &str) -> Result<()> {
let program = crate::Program::parse_no_errs(text)?;
let ctx = ExecutorContext::new_with_default_client(crate::UnitLength::Mm).await?;
let mut exec_state = crate::execution::ExecState::new(&ctx.settings);
let mut exec_state = crate::execution::ExecState::new(&ctx);
ctx.run(&program, &mut exec_state).await?;
Ok(())
}

View File

@ -128,9 +128,9 @@ impl StdLibFnArg {
""
};
if self.type_ == "Sketch"
|| self.type_ == "SketchSet"
|| self.type_ == "[Sketch]"
|| self.type_ == "Solid"
|| self.type_ == "SolidSet"
|| self.type_ == "[Solid]"
|| self.type_ == "SketchSurface"
|| self.type_ == "SketchOrSurface"
|| self.type_ == "SolidOrImportedGeometry"

View File

@ -18,7 +18,7 @@ use tokio::sync::{mpsc, oneshot, RwLock};
use tokio_tungstenite::tungstenite::Message as WsMsg;
use uuid::Uuid;
use super::ExecutionKind;
use super::{EngineStats, ExecutionKind};
use crate::{
engine::EngineManager,
errors::{KclError, KclErrorDetails},
@ -52,6 +52,7 @@ pub struct EngineConnection {
session_data: Arc<RwLock<Option<ModelingSessionData>>>,
execution_kind: Arc<RwLock<ExecutionKind>>,
stats: EngineStats,
}
pub struct TcpRead {
@ -344,6 +345,7 @@ impl EngineConnection {
default_planes: Default::default(),
session_data,
execution_kind: Default::default(),
stats: Default::default(),
})
}
}
@ -378,22 +380,12 @@ impl EngineManager for EngineConnection {
original
}
async fn default_planes(
&self,
id_generator: &mut IdGenerator,
source_range: SourceRange,
) -> Result<DefaultPlanes, KclError> {
{
let opt = self.default_planes.read().await.as_ref().cloned();
if let Some(planes) = opt {
return Ok(planes);
}
} // drop the read lock
fn stats(&self) -> &EngineStats {
&self.stats
}
let new_planes = self.new_default_planes(id_generator, source_range).await?;
*self.default_planes.write().await = Some(new_planes.clone());
Ok(new_planes)
fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>> {
self.default_planes.clone()
}
async fn clear_scene_post_hook(

View File

@ -16,7 +16,7 @@ use kittycad_modeling_cmds::{self as kcmc};
use tokio::sync::RwLock;
use uuid::Uuid;
use super::ExecutionKind;
use super::{EngineStats, ExecutionKind};
use crate::{
errors::KclError,
exec::DefaultPlanes,
@ -30,6 +30,9 @@ pub struct EngineConnection {
batch_end: Arc<RwLock<IndexMap<uuid::Uuid, (WebSocketRequest, SourceRange)>>>,
artifact_commands: Arc<RwLock<Vec<ArtifactCommand>>>,
execution_kind: Arc<RwLock<ExecutionKind>>,
/// The default planes for the scene.
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
stats: EngineStats,
}
impl EngineConnection {
@ -39,6 +42,8 @@ impl EngineConnection {
batch_end: Arc::new(RwLock::new(IndexMap::new())),
artifact_commands: Arc::new(RwLock::new(Vec::new())),
execution_kind: Default::default(),
default_planes: Default::default(),
stats: Default::default(),
})
}
}
@ -57,6 +62,10 @@ impl crate::engine::EngineManager for EngineConnection {
Arc::new(RwLock::new(IndexMap::new()))
}
fn stats(&self) -> &EngineStats {
&self.stats
}
fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>> {
self.artifact_commands.clone()
}
@ -73,12 +82,8 @@ impl crate::engine::EngineManager for EngineConnection {
original
}
async fn default_planes(
&self,
_id_generator: &mut IdGenerator,
_source_range: SourceRange,
) -> Result<DefaultPlanes, KclError> {
Ok(DefaultPlanes::default())
fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>> {
self.default_planes.clone()
}
async fn clear_scene_post_hook(

View File

@ -11,7 +11,7 @@ use uuid::Uuid;
use wasm_bindgen::prelude::*;
use crate::{
engine::ExecutionKind,
engine::{EngineStats, ExecutionKind},
errors::{KclError, KclErrorDetails},
execution::{ArtifactCommand, DefaultPlanes, IdGenerator},
SourceRange,
@ -31,12 +31,6 @@ extern "C" {
idToRangeStr: String,
) -> Result<js_sys::Promise, js_sys::Error>;
#[wasm_bindgen(method, js_name = wasmGetDefaultPlanes, catch)]
fn get_default_planes(this: &EngineCommandManager) -> Result<js_sys::Promise, js_sys::Error>;
#[wasm_bindgen(method, js_name = clearDefaultPlanes, catch)]
fn clear_default_planes(this: &EngineCommandManager) -> Result<(), js_sys::Error>;
#[wasm_bindgen(method, js_name = startNewSession, catch)]
fn start_new_session(this: &EngineCommandManager) -> Result<js_sys::Promise, js_sys::Error>;
}
@ -49,6 +43,9 @@ pub struct EngineConnection {
responses: Arc<RwLock<IndexMap<Uuid, WebSocketResponse>>>,
artifact_commands: Arc<RwLock<Vec<ArtifactCommand>>>,
execution_kind: Arc<RwLock<ExecutionKind>>,
/// The default planes for the scene.
default_planes: Arc<RwLock<Option<DefaultPlanes>>>,
stats: EngineStats,
}
// Safety: WebAssembly will only ever run in a single-threaded context.
@ -65,6 +62,8 @@ impl EngineConnection {
responses: Arc::new(RwLock::new(IndexMap::new())),
artifact_commands: Arc::new(RwLock::new(Vec::new())),
execution_kind: Default::default(),
default_planes: Default::default(),
stats: Default::default(),
})
}
@ -144,6 +143,10 @@ impl crate::engine::EngineManager for EngineConnection {
self.responses.clone()
}
fn stats(&self) -> &EngineStats {
&self.stats
}
fn artifact_commands(&self) -> Arc<RwLock<Vec<ArtifactCommand>>> {
self.artifact_commands.clone()
}
@ -160,59 +163,18 @@ impl crate::engine::EngineManager for EngineConnection {
original
}
async fn default_planes(
&self,
_id_generator: &mut IdGenerator,
source_range: SourceRange,
) -> Result<DefaultPlanes, KclError> {
// Get the default planes.
let promise = self.manager.get_default_planes().map_err(|e| {
KclError::Engine(KclErrorDetails {
message: e.to_string().into(),
source_ranges: vec![source_range],
})
})?;
let value = crate::wasm::JsFuture::from(promise).await.map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to wait for promise from get default planes: {:?}", e),
source_ranges: vec![source_range],
})
})?;
// Parse the value as a string.
let s = value.as_string().ok_or_else(|| {
KclError::Engine(KclErrorDetails {
message: format!(
"Failed to get string from response from get default planes: `{:?}`",
value
),
source_ranges: vec![source_range],
})
})?;
// Deserialize the response.
let default_planes: DefaultPlanes = serde_json::from_str(&s).map_err(|e| {
KclError::Engine(KclErrorDetails {
message: format!("Failed to deserialize default planes: {:?}", e),
source_ranges: vec![source_range],
})
})?;
Ok(default_planes)
fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>> {
self.default_planes.clone()
}
async fn clear_scene_post_hook(
&self,
_id_generator: &mut IdGenerator,
id_generator: &mut IdGenerator,
source_range: SourceRange,
) -> Result<(), KclError> {
self.manager.clear_default_planes().map_err(|e| {
KclError::Engine(KclErrorDetails {
message: e.to_string().into(),
source_ranges: vec![source_range],
})
})?;
// Remake the default planes, since they would have been removed after the scene was cleared.
let new_planes = self.new_default_planes(id_generator, source_range).await?;
*self.default_planes.write().await = Some(new_planes);
// Start a new session.
let promise = self.manager.start_new_session().map_err(|e| {

View File

@ -8,7 +8,13 @@ pub mod conn_mock;
#[cfg(feature = "engine")]
pub mod conn_wasm;
use std::{collections::HashMap, sync::Arc};
use std::{
collections::HashMap,
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use indexmap::IndexMap;
use kcmc::{
@ -58,6 +64,21 @@ impl ExecutionKind {
}
}
#[derive(Default, Debug)]
pub struct EngineStats {
pub commands_batched: AtomicUsize,
pub batches_sent: AtomicUsize,
}
impl Clone for EngineStats {
fn clone(&self) -> Self {
Self {
commands_batched: AtomicUsize::new(self.commands_batched.load(Ordering::Relaxed)),
batches_sent: AtomicUsize::new(self.batches_sent.load(Ordering::Relaxed)),
}
}
}
#[async_trait::async_trait]
pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
/// Get the batch of commands to be sent to the engine.
@ -95,11 +116,28 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
async fn replace_execution_kind(&self, execution_kind: ExecutionKind) -> ExecutionKind;
/// Get the default planes.
fn get_default_planes(&self) -> Arc<RwLock<Option<DefaultPlanes>>>;
fn stats(&self) -> &EngineStats;
/// Get the default planes, creating them if they don't exist.
async fn default_planes(
&self,
id_generator: &mut IdGenerator,
_source_range: SourceRange,
) -> Result<DefaultPlanes, crate::errors::KclError>;
source_range: SourceRange,
) -> Result<DefaultPlanes, KclError> {
{
let opt = self.get_default_planes().read().await.as_ref().cloned();
if let Some(planes) = opt {
return Ok(planes);
}
} // drop the read lock
let new_planes = self.new_default_planes(id_generator, source_range).await?;
*self.get_default_planes().write().await = Some(new_planes.clone());
Ok(new_planes)
}
/// Helpers to be called after clearing a scene.
/// (These really only apply to wasm for now).
@ -239,6 +277,34 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// Add cmd to the batch.
self.batch().write().await.push((req, source_range));
self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
Ok(())
}
// Add a vector of modeling commands to the batch but don't fire it right away.
// This allows you to force them all to be added together in the same order.
// When we are running things in parallel this prevents race conditions that might come
// if specific commands are run before others.
async fn batch_modeling_cmds(
&self,
source_range: SourceRange,
cmds: &[ModelingCmdReq],
) -> Result<(), crate::errors::KclError> {
// In isolated mode, we don't send the command to the engine.
if self.execution_kind().await.is_isolated() {
return Ok(());
}
// Add cmds to the batch.
let mut extended_cmds = Vec::with_capacity(cmds.len());
for cmd in cmds {
extended_cmds.push((WebSocketRequest::ModelingCmdReq(cmd.clone()), source_range));
}
self.stats()
.commands_batched
.fetch_add(extended_cmds.len(), Ordering::Relaxed);
self.batch().write().await.extend(extended_cmds);
Ok(())
}
@ -264,6 +330,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
// Add cmd to the batch end.
self.batch_end().write().await.insert(id, (req, source_range));
self.stats().commands_batched.fetch_add(1, Ordering::Relaxed);
Ok(())
}
@ -366,6 +433,7 @@ pub trait EngineManager: std::fmt::Debug + Send + Sync + 'static {
if batch_end {
self.batch_end().write().await.clear();
}
self.stats().batches_sent.fetch_add(1, Ordering::Relaxed);
// We pop off the responses to cleanup our mappings.
match final_req {

View File

@ -4,7 +4,7 @@ use thiserror::Error;
use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity};
use crate::{
execution::{ArtifactCommand, ArtifactGraph, Operation},
execution::{ArtifactCommand, ArtifactGraph, DefaultPlanes, Operation},
lsp::IntoDiagnostic,
modules::{ModulePath, ModuleSource},
source_range::SourceRange,
@ -131,6 +131,7 @@ pub struct KclErrorWithOutputs {
pub artifact_graph: ArtifactGraph,
pub filenames: IndexMap<ModuleId, ModulePath>,
pub source_files: IndexMap<ModuleId, ModuleSource>,
pub default_planes: Option<DefaultPlanes>,
}
impl KclErrorWithOutputs {
@ -141,6 +142,7 @@ impl KclErrorWithOutputs {
artifact_graph: ArtifactGraph,
filenames: IndexMap<ModuleId, ModulePath>,
source_files: IndexMap<ModuleId, ModuleSource>,
default_planes: Option<DefaultPlanes>,
) -> Self {
Self {
error,
@ -149,6 +151,7 @@ impl KclErrorWithOutputs {
artifact_graph,
filenames,
source_files,
default_planes,
}
}
pub fn no_outputs(error: KclError) -> Self {
@ -159,6 +162,7 @@ impl KclErrorWithOutputs {
artifact_graph: Default::default(),
filenames: Default::default(),
source_files: Default::default(),
default_planes: Default::default(),
}
}
pub fn into_miette_report_with_outputs(self, code: &str) -> anyhow::Result<ReportWithOutputs> {
@ -178,13 +182,8 @@ impl KclErrorWithOutputs {
path: self
.filenames
.get(&first_source_range.module_id())
.ok_or_else(|| {
anyhow::anyhow!(
"Could not find filename for module id: {:?}",
first_source_range.module_id()
)
})?
.clone(),
.cloned()
.unwrap_or(ModulePath::Main),
});
let filename = source.path.to_string();
let kcl_source = source.source.to_string();
@ -192,11 +191,10 @@ impl KclErrorWithOutputs {
let mut related = Vec::new();
for source_range in source_ranges {
let module_id = source_range.module_id();
let source = self
.source_files
.get(&module_id)
.cloned()
.ok_or_else(|| anyhow::anyhow!("Could not find source file for module id: {:?}", module_id))?;
let source = self.source_files.get(&module_id).cloned().unwrap_or(ModuleSource {
source: code.to_string(),
path: self.filenames.get(&module_id).cloned().unwrap_or(ModulePath::Main),
});
let error = self.error.override_source_ranges(vec![source_range]);
let report = Report {
error,

View File

@ -6,7 +6,7 @@ use kittycad_modeling_cmds::{
ok_response::OkModelingCmdResponse,
shared::ExtrusionFaceCapType,
websocket::{BatchResponse, OkWebSocketResponseData, WebSocketResponse},
EnableSketchMode, ModelingCmd, SketchModeDisable,
EnableSketchMode, ModelingCmd,
};
use schemars::JsonSchema;
use serde::{ser::SerializeSeq, Deserialize, Serialize};
@ -498,13 +498,23 @@ pub(super) fn build_artifact_graph(
) -> Result<ArtifactGraph, KclError> {
let mut map = IndexMap::new();
let mut path_to_plane_id_map = FnvHashMap::default();
let mut current_plane_id = None;
for artifact_command in artifact_commands {
if let ModelingCmd::EnableSketchMode(EnableSketchMode { entity_id, .. }) = artifact_command.command {
current_plane_id = Some(entity_id);
}
if let ModelingCmd::SketchModeDisable(SketchModeDisable { .. }) = artifact_command.command {
// If we get a start path command, we need to set the plane ID to the
// current plane ID.
// THIS IS THE ONLY THING WE CAN ASSUME IS ALWAYS SEQUENTIAL SINCE ITS PART OF THE
// SAME ATOMIC COMMANDS BATCHING.
if let ModelingCmd::StartPath(_) = artifact_command.command {
if let Some(plane_id) = current_plane_id {
path_to_plane_id_map.insert(artifact_command.cmd_id, plane_id);
}
}
if let ModelingCmd::SketchModeDisable(_) = artifact_command.command {
current_plane_id = None;
}
@ -513,7 +523,7 @@ pub(super) fn build_artifact_graph(
&map,
artifact_command,
&flattened_responses,
current_plane_id,
&path_to_plane_id_map,
ast,
exec_artifacts,
)?;
@ -609,7 +619,7 @@ fn artifacts_to_update(
artifacts: &IndexMap<ArtifactId, Artifact>,
artifact_command: &ArtifactCommand,
responses: &FnvHashMap<Uuid, OkModelingCmdResponse>,
current_plane_id: Option<Uuid>,
path_to_plane_id_map: &FnvHashMap<Uuid, Uuid>,
_ast: &Node<Program>,
exec_artifacts: &IndexMap<ArtifactId, Artifact>,
) -> Result<Vec<Artifact>, KclError> {
@ -643,20 +653,12 @@ fn artifacts_to_update(
code_ref: CodeRef { range, path_to_node },
})]);
}
ModelingCmd::EnableSketchMode(_) => {
let current_plane_id = current_plane_id.ok_or_else(|| {
KclError::Internal(KclErrorDetails {
message: format!(
"Expected a current plane ID when processing EnableSketchMode command, but we have none: {id:?}"
),
source_ranges: vec![range],
})
})?;
let existing_plane = artifacts.get(&ArtifactId::new(current_plane_id));
ModelingCmd::EnableSketchMode(EnableSketchMode { entity_id, .. }) => {
let existing_plane = artifacts.get(&ArtifactId::new(*entity_id));
match existing_plane {
Some(Artifact::Wall(wall)) => {
return Ok(vec![Artifact::Wall(Wall {
id: current_plane_id.into(),
id: entity_id.into(),
seg_id: wall.seg_id,
edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(),
sweep_id: wall.sweep_id,
@ -666,7 +668,7 @@ fn artifacts_to_update(
}
Some(Artifact::Cap(cap)) => {
return Ok(vec![Artifact::Cap(Cap {
id: current_plane_id.into(),
id: entity_id.into(),
sub_type: cap.sub_type,
edge_cut_edge_ids: cap.edge_cut_edge_ids.clone(),
sweep_id: cap.sweep_id,
@ -680,7 +682,7 @@ fn artifacts_to_update(
_ => Vec::new(),
};
return Ok(vec![Artifact::Plane(Plane {
id: current_plane_id.into(),
id: entity_id.into(),
path_ids,
code_ref: CodeRef { range, path_to_node },
})]);
@ -689,7 +691,7 @@ fn artifacts_to_update(
}
ModelingCmd::StartPath(_) => {
let mut return_arr = Vec::new();
let current_plane_id = current_plane_id.ok_or_else(|| {
let current_plane_id = path_to_plane_id_map.get(&artifact_command.cmd_id).ok_or_else(|| {
KclError::Internal(KclErrorDetails {
message: format!(
"Expected a current plane ID when processing StartPath command, but we have none: {id:?}"
@ -699,24 +701,24 @@ fn artifacts_to_update(
})?;
return_arr.push(Artifact::Path(Path {
id,
plane_id: current_plane_id.into(),
plane_id: (*current_plane_id).into(),
seg_ids: Vec::new(),
sweep_id: None,
solid2d_id: None,
code_ref: CodeRef { range, path_to_node },
}));
let plane = artifacts.get(&ArtifactId::new(current_plane_id));
let plane = artifacts.get(&ArtifactId::new(*current_plane_id));
if let Some(Artifact::Plane(plane)) = plane {
let code_ref = plane.code_ref.clone();
return_arr.push(Artifact::Plane(Plane {
id: current_plane_id.into(),
id: (*current_plane_id).into(),
path_ids: vec![id],
code_ref,
}));
}
if let Some(Artifact::Wall(wall)) = plane {
return_arr.push(Artifact::Wall(Wall {
id: current_plane_id.into(),
id: (*current_plane_id).into(),
seg_id: wall.seg_id,
edge_cut_edge_ids: wall.edge_cut_edge_ids.clone(),
sweep_id: wall.sweep_id,
@ -726,7 +728,7 @@ fn artifacts_to_update(
}
if let Some(Artifact::Cap(cap)) = plane {
return_arr.push(Artifact::Cap(Cap {
id: current_plane_id.into(),
id: (*current_plane_id).into(),
sub_type: cap.sub_type,
edge_cut_edge_ids: cap.edge_cut_edge_ids.clone(),
sweep_id: cap.sweep_id,

View File

@ -180,15 +180,9 @@ pub enum OpKclValue {
Sketch {
value: Box<OpSketch>,
},
Sketches {
value: Vec<OpSketch>,
},
Solid {
value: Box<OpSolid>,
},
Solids {
value: Vec<OpSolid>,
},
Helix {
value: Box<OpHelix>,
},
@ -234,7 +228,7 @@ impl From<&KclValue> for OpKclValue {
ty: ty.clone(),
},
KclValue::String { value, .. } => Self::String { value: value.clone() },
KclValue::MixedArray { value, .. } => {
KclValue::MixedArray { value, .. } | KclValue::HomArray { value, .. } => {
let value = value.iter().map(Self::from).collect();
Self::Array { value }
}
@ -244,7 +238,7 @@ impl From<&KclValue> for OpKclValue {
}
KclValue::TagIdentifier(tag_identifier) => Self::TagIdentifier {
value: tag_identifier.value.clone(),
artifact_id: tag_identifier.info.as_ref().map(|info| ArtifactId::new(info.id)),
artifact_id: tag_identifier.get_cur_info().map(|info| ArtifactId::new(info.id)),
},
KclValue::TagDeclarator(node) => Self::TagDeclarator {
name: node.name.clone(),
@ -260,29 +254,11 @@ impl From<&KclValue> for OpKclValue {
artifact_id: value.artifact_id,
}),
},
KclValue::Sketches { value } => {
let value = value
.iter()
.map(|sketch| OpSketch {
artifact_id: sketch.artifact_id,
})
.collect();
Self::Sketches { value }
}
KclValue::Solid { value } => Self::Solid {
value: Box::new(OpSolid {
artifact_id: value.artifact_id,
}),
},
KclValue::Solids { value } => {
let value = value
.iter()
.map(|solid| OpSolid {
artifact_id: solid.artifact_id,
})
.collect();
Self::Solids { value }
}
KclValue::Helix { value } => Self::Helix {
value: Box::new(OpHelix {
artifact_id: value.artifact_id,
@ -295,7 +271,6 @@ impl From<&KclValue> for OpKclValue {
KclValue::Module { .. } => Self::Module {},
KclValue::KclNone { .. } => Self::KclNone {},
KclValue::Type { .. } => Self::Type {},
KclValue::Tombstone { .. } => unreachable!("Tombstone OpKclValue"),
}
}
}

View File

@ -8,11 +8,11 @@ use crate::{
execution::{
annotations,
cad_op::{OpArg, OpKclValue, Operation},
kcl_value::{FunctionSource, NumericType, PrimitiveType, RuntimeType},
kcl_value::{FunctionSource, NumericType, RuntimeType},
memory,
state::ModuleState,
BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, Plane, PlaneType, Point3d,
TagEngineInfo, TagIdentifier,
BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, PlaneType, TagEngineInfo,
TagIdentifier,
},
modules::{ModuleId, ModulePath, ModuleRepr},
parsing::ast::types::{
@ -23,7 +23,7 @@ use crate::{
},
source_range::SourceRange,
std::{
args::{Arg, FromKclValue, KwArgs},
args::{Arg, KwArgs},
FunctionKind,
},
CompilationError,
@ -55,10 +55,9 @@ impl ExecutorContext {
for annotation in annotations {
if annotation.name() == Some(annotations::SETTINGS) {
if matches!(body_type, BodyType::Root) {
let old_units = exec_state.length_unit();
exec_state.mod_local.settings.update_from_annotation(annotation)?;
let new_units = exec_state.length_unit();
if !self.engine.execution_kind().await.is_isolated() && old_units != new_units {
if !self.engine.execution_kind().await.is_isolated() {
self.engine
.set_units(new_units.into(), annotation.as_source_range())
.await?;
@ -94,6 +93,7 @@ impl ExecutorContext {
exec_state: &mut ExecState,
exec_kind: ExecutionKind,
preserve_mem: bool,
module_id: ModuleId,
path: &ModulePath,
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>), KclError> {
crate::log::log(format!("enter module {path} {}", exec_state.stack()));
@ -101,7 +101,12 @@ impl ExecutorContext {
let old_units = exec_state.length_unit();
let original_execution = self.engine.replace_execution_kind(exec_kind).await;
let mut local_state = ModuleState::new(&self.settings, path.std_path(), exec_state.stack().memory.clone());
let mut local_state = ModuleState::new(
&self.settings,
path.std_path(),
exec_state.stack().memory.clone(),
Some(module_id),
);
if !preserve_mem {
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
}
@ -504,7 +509,7 @@ impl ExecutorContext {
ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
ModuleRepr::Kcl(_, Some((env_ref, items))) => Ok((*env_ref, items.clone())),
ModuleRepr::Kcl(program, cache) => self
.exec_module_from_ast(program, &path, exec_state, exec_kind, source_range)
.exec_module_from_ast(program, module_id, &path, exec_state, exec_kind, source_range)
.await
.map(|(_, er, items)| {
*cache = Some((er, items.clone()));
@ -535,7 +540,7 @@ impl ExecutorContext {
let result = match &repr {
ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
ModuleRepr::Kcl(program, _) => self
.exec_module_from_ast(program, &path, exec_state, exec_kind, source_range)
.exec_module_from_ast(program, module_id, &path, exec_state, exec_kind, source_range)
.await
.map(|(val, _, _)| val),
ModuleRepr::Foreign(geom) => super::import::send_to_engine(geom.clone(), self)
@ -551,13 +556,16 @@ impl ExecutorContext {
async fn exec_module_from_ast(
&self,
program: &Node<Program>,
module_id: ModuleId,
path: &ModulePath,
exec_state: &mut ExecState,
exec_kind: ExecutionKind,
source_range: SourceRange,
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>), KclError> {
exec_state.global.mod_loader.enter_module(path);
let result = self.exec_module_body(program, exec_state, exec_kind, false, path).await;
let result = self
.exec_module_body(program, exec_state, exec_kind, false, module_id, path)
.await;
exec_state.global.mod_loader.leave_module(path);
result.map_err(|err| {
@ -690,11 +698,11 @@ impl ExecutorContext {
let result = self
.execute_expr(&expr.expr, exec_state, metadata, &[], statement_kind)
.await?;
coerce(result, &expr.ty, exec_state).map_err(|value| {
coerce(&result, &expr.ty, exec_state).ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!(
"could not coerce {} value to type {}",
value.human_friendly_type(),
result.human_friendly_type(),
expr.ty
),
source_ranges: vec![expr.into()],
@ -706,72 +714,14 @@ impl ExecutorContext {
}
}
fn coerce(value: KclValue, ty: &Node<Type>, exec_state: &mut ExecState) -> Result<KclValue, KclValue> {
let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, (&value).into())
fn coerce(value: &KclValue, ty: &Node<Type>, exec_state: &mut ExecState) -> Option<KclValue> {
let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into())
.map_err(|e| {
exec_state.err(e);
value.clone()
})?
.ok_or_else(|| value.clone())?;
if value.has_type(&ty) {
return Ok(value);
}
})
.ok()??;
// TODO coerce numeric types
if let KclValue::Object { value, meta } = value {
return match ty {
RuntimeType::Primitive(PrimitiveType::Plane) => {
let origin = value
.get("origin")
.and_then(Point3d::from_kcl_val)
.ok_or_else(|| KclValue::Object {
value: value.clone(),
meta: meta.clone(),
})?;
let x_axis = value
.get("xAxis")
.and_then(Point3d::from_kcl_val)
.ok_or_else(|| KclValue::Object {
value: value.clone(),
meta: meta.clone(),
})?;
let y_axis = value
.get("yAxis")
.and_then(Point3d::from_kcl_val)
.ok_or_else(|| KclValue::Object {
value: value.clone(),
meta: meta.clone(),
})?;
let z_axis = value
.get("zAxis")
.and_then(Point3d::from_kcl_val)
.ok_or_else(|| KclValue::Object {
value: value.clone(),
meta: meta.clone(),
})?;
let id = exec_state.global.id_generator.next_uuid();
let plane = Plane {
id,
artifact_id: id.into(),
origin,
x_axis,
y_axis,
z_axis,
value: PlaneType::Uninit,
// TODO use length unit from origin
units: exec_state.length_unit(),
meta,
};
Ok(KclValue::Plane { value: Box::new(plane) })
}
_ => Err(KclValue::Object { value, meta }),
};
}
Err(value)
value.coerce(&ty, exec_state)
}
impl BinaryPart {
@ -797,33 +747,7 @@ impl BinaryPart {
}
impl Node<MemberExpression> {
pub fn get_result_array(&self, exec_state: &mut ExecState, index: usize) -> Result<KclValue, KclError> {
let array = match &self.object {
MemberObject::MemberExpression(member_expr) => member_expr.get_result(exec_state)?,
MemberObject::Identifier(identifier) => {
let value = exec_state.stack().get(&identifier.name, identifier.into())?;
value.clone()
}
};
let KclValue::MixedArray { value: array, meta: _ } = array else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("MemberExpression array is not an array: {:?}", array),
source_ranges: vec![self.clone().into()],
}));
};
if let Some(value) = array.get(index) {
Ok(value.to_owned())
} else {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("index {} not found in array", index),
source_ranges: vec![self.clone().into()],
}))
}
}
pub fn get_result(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
fn get_result(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
let property = Property::try_from(self.computed, self.property.clone(), exec_state, self.into())?;
let object = match &self.object {
// TODO: Don't use recursion here, use a loop.
@ -1424,11 +1348,22 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
// TODO: This could probably be done in a better way, but as of now this was my only idea
// and it works.
match result {
KclValue::Sketch { value: ref mut sketch } => {
for (_, tag) in sketch.tags.iter() {
exec_state
.mut_stack()
.insert_or_update(tag.value.clone(), KclValue::TagIdentifier(Box::new(tag.clone())));
KclValue::Sketch { value } => {
for (name, tag) in value.tags.iter() {
if exec_state.stack().cur_frame_contains(name) {
exec_state.mut_stack().update(name, |v, _| {
v.as_mut_tag().unwrap().merge_info(tag);
});
} else {
exec_state
.mut_stack()
.add(
name.to_owned(),
KclValue::TagIdentifier(Box::new(tag.clone())),
SourceRange::default(),
)
.unwrap();
}
}
}
KclValue::Solid { ref mut value } => {
@ -1437,7 +1372,7 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
// Get the past tag and update it.
let tag_id = if let Some(t) = value.sketch.tags.get(&tag.name) {
let mut t = t.clone();
let Some(ref info) = t.info else {
let Some(info) = t.get_cur_info() else {
return Err(KclError::Internal(KclErrorDetails {
message: format!("Tag {} does not have path info", tag.name),
source_ranges: vec![tag.into()],
@ -1447,59 +1382,70 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
let mut info = info.clone();
info.surface = Some(v.clone());
info.sketch = value.id;
t.info = Some(info);
t.info.push((exec_state.stack().current_epoch(), info));
t
} else {
// It's probably a fillet or a chamfer.
// Initialize it.
TagIdentifier {
value: tag.name.clone(),
info: Some(TagEngineInfo {
id: v.get_id(),
surface: Some(v.clone()),
path: None,
sketch: value.id,
}),
info: vec![(
exec_state.stack().current_epoch(),
TagEngineInfo {
id: v.get_id(),
surface: Some(v.clone()),
path: None,
sketch: value.id,
},
)],
meta: vec![Metadata {
source_range: tag.clone().into(),
}],
}
};
exec_state
.mut_stack()
.insert_or_update(tag.name.clone(), KclValue::TagIdentifier(Box::new(tag_id.clone())));
// update the sketch tags.
value.sketch.tags.insert(tag.name.clone(), tag_id);
value.sketch.merge_tags(Some(&tag_id).into_iter());
if exec_state.stack().cur_frame_contains(&tag.name) {
exec_state.mut_stack().update(&tag.name, |v, _| {
v.as_mut_tag().unwrap().merge_info(&tag_id);
});
} else {
exec_state
.mut_stack()
.add(
tag.name.clone(),
KclValue::TagIdentifier(Box::new(tag_id)),
SourceRange::default(),
)
.unwrap();
}
}
}
// Find the stale sketch in memory and update it.
if !value.sketch.tags.is_empty() {
let updates: Vec<_> = exec_state
let sketches_to_update: Vec<_> = exec_state
.stack()
.find_all_in_current_env(|v| match v {
.find_keys_in_current_env(|v| match v {
KclValue::Sketch { value: sk } => sk.artifact_id == value.sketch.artifact_id,
_ => false,
})
.map(|(k, v)| {
let mut sketch = v.as_sketch().unwrap().clone();
for (tag_name, tag_id) in value.sketch.tags.iter() {
sketch.tags.insert(tag_name.clone(), tag_id.clone());
}
(
k.clone(),
KclValue::Sketch {
value: Box::new(sketch),
},
)
})
.cloned()
.collect();
updates
.into_iter()
.for_each(|(k, v)| exec_state.mut_stack().insert_or_update(k, v))
for k in sketches_to_update {
exec_state.mut_stack().update(&k, |v, _| {
let sketch = v.as_mut_sketch().unwrap();
sketch.merge_tags(value.sketch.tags.values());
});
}
}
}
KclValue::MixedArray { value, .. } | KclValue::HomArray { value, .. } => {
for v in value {
update_memory_for_tags_of_geometry(v, exec_state)?;
}
}
_ => {}
@ -1511,7 +1457,7 @@ impl Node<TagDeclarator> {
pub async fn execute(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
let memory_item = KclValue::TagIdentifier(Box::new(TagIdentifier {
value: self.name.clone(),
info: None,
info: Vec::new(),
meta: vec![Metadata {
source_range: self.into(),
}],
@ -2026,8 +1972,8 @@ mod test {
use std::sync::Arc;
use tokio::{sync::RwLock, task::JoinSet};
#[test]
fn test_assign_args_to_params() {
#[tokio::test(flavor = "multi_thread")]
async fn test_assign_args_to_params() {
// Set up a little framework for this test.
fn mem(number: usize) -> KclValue {
KclValue::Number {
@ -2138,7 +2084,16 @@ mod test {
digest: None,
});
let args = args.into_iter().map(Arg::synthetic).collect();
let mut exec_state = ExecState::new(&Default::default());
let exec_ctxt = ExecutorContext {
engine: Arc::new(Box::new(
crate::engine::conn_mock::EngineConnection::new().await.unwrap(),
)),
fs: Arc::new(crate::fs::FileManager::new()),
stdlib: Arc::new(RwLock::new(crate::std::StdLib::new())),
settings: Default::default(),
context_type: ContextType::Mock,
};
let mut exec_state = ExecState::new(&exec_ctxt);
exec_state.mod_local.stack = Stack::new_for_tests();
let actual = assign_args_to_params(func_expr, args, &mut exec_state).map(|_| exec_state.mod_local.stack);
assert_eq!(
@ -2278,7 +2233,7 @@ import 'a.kcl'
settings: Default::default(),
context_type: ContextType::Mock,
};
let mut exec_state = ExecState::new(&exec_ctxt.settings);
let mut exec_state = ExecState::new(&exec_ctxt);
eprintln!("{:?}", exec_ctxt);

View File

@ -3,15 +3,14 @@ use std::ops::{Add, AddAssign, Mul};
use anyhow::Result;
use indexmap::IndexMap;
use kittycad_modeling_cmds as kcmc;
use kittycad_modeling_cmds::length_unit::LengthUnit;
use kittycad_modeling_cmds::{each_cmd as mcmd, length_unit::LengthUnit, websocket::ModelingCmdReq, ModelingCmd};
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::ArtifactId;
use crate::{
errors::KclError,
execution::{ExecState, Metadata, TagEngineInfo, TagIdentifier, UnitLen},
execution::{ArtifactId, ExecState, Metadata, TagEngineInfo, TagIdentifier, UnitLen},
parsing::ast::types::{Node, NodeRef, TagDeclarator, TagNode},
std::sketch::PlaneData,
};
@ -24,8 +23,8 @@ type Point3D = kcmc::shared::Point3d<f64>;
#[ts(export)]
#[serde(tag = "type")]
pub enum Geometry {
Sketch(Box<Sketch>),
Solid(Box<Solid>),
Sketch(Sketch),
Solid(Solid),
}
impl Geometry {
@ -53,8 +52,8 @@ impl Geometry {
#[serde(tag = "type")]
#[allow(clippy::vec_box)]
pub enum Geometries {
Sketches(Vec<Box<Sketch>>),
Solids(Vec<Box<Solid>>),
Sketches(Vec<Sketch>),
Solids(Vec<Solid>),
}
impl From<Geometry> for Geometries {
@ -66,150 +65,6 @@ impl From<Geometry> for Geometries {
}
}
/// A sketch or a group of sketches.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
#[allow(clippy::vec_box)]
pub enum SketchSet {
Sketch(Box<Sketch>),
Sketches(Vec<Box<Sketch>>),
}
impl SketchSet {
pub fn meta(&self) -> Vec<Metadata> {
match self {
SketchSet::Sketch(sg) => sg.meta.clone(),
SketchSet::Sketches(sg) => sg.iter().flat_map(|sg| sg.meta.clone()).collect(),
}
}
}
impl From<SketchSet> for Vec<Sketch> {
fn from(value: SketchSet) -> Self {
match value {
SketchSet::Sketch(sg) => vec![*sg],
SketchSet::Sketches(sgs) => sgs.into_iter().map(|sg| *sg).collect(),
}
}
}
impl From<Sketch> for SketchSet {
fn from(sg: Sketch) -> Self {
SketchSet::Sketch(Box::new(sg))
}
}
impl From<Box<Sketch>> for SketchSet {
fn from(sg: Box<Sketch>) -> Self {
SketchSet::Sketch(sg)
}
}
impl From<Vec<Sketch>> for SketchSet {
fn from(sg: Vec<Sketch>) -> Self {
if sg.len() == 1 {
SketchSet::Sketch(Box::new(sg[0].clone()))
} else {
SketchSet::Sketches(sg.into_iter().map(Box::new).collect())
}
}
}
impl From<Vec<Box<Sketch>>> for SketchSet {
fn from(sg: Vec<Box<Sketch>>) -> Self {
if sg.len() == 1 {
SketchSet::Sketch(sg[0].clone())
} else {
SketchSet::Sketches(sg)
}
}
}
impl From<SketchSet> for Vec<Box<Sketch>> {
fn from(sg: SketchSet) -> Self {
match sg {
SketchSet::Sketch(sg) => vec![sg],
SketchSet::Sketches(sgs) => sgs,
}
}
}
impl From<&Sketch> for Vec<Box<Sketch>> {
fn from(sg: &Sketch) -> Self {
vec![Box::new(sg.clone())]
}
}
impl From<Box<Sketch>> for Vec<Box<Sketch>> {
fn from(sg: Box<Sketch>) -> Self {
vec![sg]
}
}
/// A solid or a group of solids.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
#[allow(clippy::vec_box)]
pub enum SolidSet {
Solid(Box<Solid>),
Solids(Vec<Box<Solid>>),
}
impl From<Solid> for SolidSet {
fn from(eg: Solid) -> Self {
SolidSet::Solid(Box::new(eg))
}
}
impl From<Box<Solid>> for SolidSet {
fn from(eg: Box<Solid>) -> Self {
SolidSet::Solid(eg)
}
}
impl From<Vec<Solid>> for SolidSet {
fn from(eg: Vec<Solid>) -> Self {
if eg.len() == 1 {
SolidSet::Solid(Box::new(eg[0].clone()))
} else {
SolidSet::Solids(eg.into_iter().map(Box::new).collect())
}
}
}
impl From<Vec<Box<Solid>>> for SolidSet {
fn from(eg: Vec<Box<Solid>>) -> Self {
if eg.len() == 1 {
SolidSet::Solid(eg[0].clone())
} else {
SolidSet::Solids(eg)
}
}
}
impl From<SolidSet> for Vec<Box<Solid>> {
fn from(eg: SolidSet) -> Self {
match eg {
SolidSet::Solid(eg) => vec![eg],
SolidSet::Solids(egs) => egs,
}
}
}
impl From<&Solid> for Vec<Box<Solid>> {
fn from(eg: &Solid) -> Self {
vec![Box::new(eg.clone())]
}
}
impl From<Box<Solid>> for Vec<Box<Solid>> {
fn from(eg: Box<Solid>) -> Self {
vec![eg]
}
}
/// Data for an imported geometry.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -219,7 +74,7 @@ pub struct ImportedGeometry {
pub id: uuid::Uuid,
/// The original file paths.
pub value: Vec<String>,
#[serde(rename = "__meta")]
#[serde(skip)]
pub meta: Vec<Metadata>,
}
@ -227,25 +82,40 @@ pub struct ImportedGeometry {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
#[allow(clippy::vec_box)]
pub enum SolidOrImportedGeometry {
Solid(Box<Solid>),
ImportedGeometry(Box<ImportedGeometry>),
}
impl SolidOrImportedGeometry {
pub fn id(&self) -> uuid::Uuid {
match self {
SolidOrImportedGeometry::Solid(s) => s.id,
SolidOrImportedGeometry::ImportedGeometry(s) => s.id,
}
}
SolidSet(Vec<Solid>),
}
impl From<SolidOrImportedGeometry> for crate::execution::KclValue {
fn from(value: SolidOrImportedGeometry) -> Self {
match value {
SolidOrImportedGeometry::Solid(s) => crate::execution::KclValue::Solid { value: s },
SolidOrImportedGeometry::ImportedGeometry(s) => crate::execution::KclValue::ImportedGeometry(*s),
SolidOrImportedGeometry::SolidSet(mut s) => {
if s.len() == 1 {
crate::execution::KclValue::Solid {
value: Box::new(s.pop().unwrap()),
}
} else {
crate::execution::KclValue::HomArray {
value: s
.into_iter()
.map(|s| crate::execution::KclValue::Solid { value: Box::new(s) })
.collect(),
ty: crate::execution::PrimitiveType::Solid,
}
}
}
}
}
}
impl SolidOrImportedGeometry {
pub(crate) fn ids(&self) -> Vec<uuid::Uuid> {
match self {
SolidOrImportedGeometry::ImportedGeometry(s) => vec![s.id],
SolidOrImportedGeometry::SolidSet(s) => s.iter().map(|s| s.id).collect(),
}
}
}
@ -266,7 +136,7 @@ pub struct Helix {
/// Is the helix rotation counter clockwise?
pub ccw: bool,
pub units: UnitLen,
#[serde(rename = "__meta")]
#[serde(skip)]
pub meta: Vec<Metadata>,
}
@ -289,7 +159,7 @@ pub struct Plane {
/// The z-axis (normal).
pub z_axis: Point3d,
pub units: UnitLen,
#[serde(rename = "__meta")]
#[serde(skip)]
pub meta: Vec<Metadata>,
}
@ -367,7 +237,7 @@ impl Plane {
}
pub(crate) fn from_plane_data(value: PlaneData, exec_state: &mut ExecState) -> Self {
let id = exec_state.global.id_generator.next_uuid();
let id = exec_state.next_uuid();
match value {
PlaneData::XY => Plane {
id,
@ -440,17 +310,20 @@ impl Plane {
x_axis,
y_axis,
z_axis,
} => Plane {
id,
artifact_id: id.into(),
origin,
x_axis,
y_axis,
z_axis,
value: PlaneType::Custom,
units: exec_state.length_unit(),
meta: vec![],
},
} => {
let id = exec_state.next_uuid();
Plane {
id,
artifact_id: id.into(),
origin,
x_axis,
y_axis,
z_axis,
value: PlaneType::Custom,
units: exec_state.length_unit(),
meta: vec![],
}
}
}
}
@ -480,7 +353,7 @@ pub struct Face {
/// The solid the face is on.
pub solid: Box<Solid>,
pub units: UnitLen,
#[serde(rename = "__meta")]
#[serde(skip)]
pub meta: Vec<Metadata>,
}
@ -528,10 +401,45 @@ pub struct Sketch {
pub original_id: uuid::Uuid,
pub units: UnitLen,
/// Metadata.
#[serde(rename = "__meta")]
#[serde(skip)]
pub meta: Vec<Metadata>,
}
impl Sketch {
// Tell the engine to enter sketch mode on the sketch.
// Run a specific command, then exit sketch mode.
pub(crate) fn build_sketch_mode_cmds(
&self,
exec_state: &mut ExecState,
inner_cmd: ModelingCmdReq,
) -> Vec<ModelingCmdReq> {
vec![
// Before we extrude, we need to enable the sketch mode.
// We do this here in case extrude is called out of order.
ModelingCmdReq {
cmd: ModelingCmd::from(mcmd::EnableSketchMode {
animated: false,
ortho: false,
entity_id: self.on.id(),
adjust_camera: false,
planar_normal: if let SketchSurface::Plane(plane) = &self.on {
// We pass in the normal for the plane here.
Some(plane.z_axis.into())
} else {
None
},
}),
cmd_id: exec_state.next_uuid().into(),
},
inner_cmd,
ModelingCmdReq {
cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
cmd_id: exec_state.next_uuid().into(),
},
]
}
}
/// A sketch type.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
@ -598,19 +506,35 @@ impl GetTangentialInfoFromPathsResult {
}
impl Sketch {
pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path) {
pub(crate) fn add_tag(&mut self, tag: NodeRef<'_, TagDeclarator>, current_path: &Path, exec_state: &ExecState) {
let mut tag_identifier: TagIdentifier = tag.into();
let base = current_path.get_base();
tag_identifier.info = Some(TagEngineInfo {
id: base.geo_meta.id,
sketch: self.id,
path: Some(current_path.clone()),
surface: None,
});
tag_identifier.info.push((
exec_state.stack().current_epoch(),
TagEngineInfo {
id: base.geo_meta.id,
sketch: self.id,
path: Some(current_path.clone()),
surface: None,
},
));
self.tags.insert(tag.name.to_string(), tag_identifier);
}
pub(crate) fn merge_tags<'a>(&mut self, tags: impl Iterator<Item = &'a TagIdentifier>) {
for t in tags {
match self.tags.get_mut(&t.value) {
Some(id) => {
id.merge_info(t);
}
None => {
self.tags.insert(t.value.clone(), t.clone());
}
}
}
}
/// Get the path most recently sketched.
pub(crate) fn latest_path(&self) -> Option<&Path> {
self.paths.last()
@ -659,7 +583,7 @@ pub struct Solid {
pub edge_cuts: Vec<EdgeCut>,
pub units: UnitLen,
/// Metadata.
#[serde(rename = "__meta")]
#[serde(skip)]
pub meta: Vec<Metadata>,
}
@ -908,6 +832,19 @@ pub enum Path {
#[ts(type = "[number, number]")]
p3: [f64; 2],
},
ArcThreePoint {
#[serde(flatten)]
base: BasePath,
/// Point 1 of the arc (base on the end of previous segment)
#[ts(type = "[number, number]")]
p1: [f64; 2],
/// Point 2 of the arc (interior kwarg)
#[ts(type = "[number, number]")]
p2: [f64; 2],
/// Point 3 of the arc (end kwarg)
#[ts(type = "[number, number]")]
p3: [f64; 2],
},
/// A path that is horizontal.
Horizontal {
#[serde(flatten)]
@ -968,6 +905,7 @@ impl From<&Path> for PathType {
Path::AngledLineTo { .. } => Self::AngledLineTo,
Path::Base { .. } => Self::Base,
Path::Arc { .. } => Self::Arc,
Path::ArcThreePoint { .. } => Self::Arc,
}
}
}
@ -984,6 +922,7 @@ impl Path {
Path::Circle { base, .. } => base.geo_meta.id,
Path::CircleThreePoint { base, .. } => base.geo_meta.id,
Path::Arc { base, .. } => base.geo_meta.id,
Path::ArcThreePoint { base, .. } => base.geo_meta.id,
}
}
@ -998,6 +937,7 @@ impl Path {
Path::Circle { base, .. } => base.tag.clone(),
Path::CircleThreePoint { base, .. } => base.tag.clone(),
Path::Arc { base, .. } => base.tag.clone(),
Path::ArcThreePoint { base, .. } => base.tag.clone(),
}
}
@ -1012,6 +952,7 @@ impl Path {
Path::Circle { base, .. } => base,
Path::CircleThreePoint { base, .. } => base,
Path::Arc { base, .. } => base,
Path::ArcThreePoint { base, .. } => base,
}
}
@ -1061,6 +1002,10 @@ impl Path {
// TODO: Call engine utils to figure this out.
linear_distance(self.get_from(), self.get_to())
}
Self::ArcThreePoint { .. } => {
// TODO: Call engine utils to figure this out.
linear_distance(self.get_from(), self.get_to())
}
}
}
@ -1075,6 +1020,7 @@ impl Path {
Path::Circle { base, .. } => Some(base),
Path::CircleThreePoint { base, .. } => Some(base),
Path::Arc { base, .. } => Some(base),
Path::ArcThreePoint { base, .. } => Some(base),
}
}
@ -1086,6 +1032,17 @@ impl Path {
center: *center,
ccw: *ccw,
},
Path::ArcThreePoint { p1, p2, p3, .. } => {
let circle_center =
crate::std::utils::calculate_circle_from_3_points([(*p1).into(), (*p2).into(), (*p3).into()]);
let radius = linear_distance(&[circle_center.center.x, circle_center.center.y], p1);
let center_point = [circle_center.center.x, circle_center.center.y];
GetTangentialInfoFromPathsResult::Circle {
center: center_point,
ccw: true,
radius,
}
}
Path::Circle {
center, ccw, radius, ..
} => GetTangentialInfoFromPathsResult::Circle {

View File

@ -0,0 +1,83 @@
//! A generator for ArtifactIds that can be stable across executions.
use crate::execution::ModuleId;
const NAMESPACE_KCL: uuid::Uuid = uuid::uuid!("efcd6508-4ce6-4a09-8317-e6a6994a3cd7");
/// A generator for ArtifactIds that can be stable across executions.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct IdGenerator {
module_id: Option<ModuleId>,
next_id: u64,
}
impl IdGenerator {
pub fn new(module_id: Option<ModuleId>) -> Self {
Self { module_id, next_id: 0 }
}
pub fn next_uuid(&mut self) -> uuid::Uuid {
let next_id = self.next_id;
let next = format!(
"{} {}",
self.module_id.map(|id| id.to_string()).unwrap_or("none".to_string()),
next_id
);
let next_uuid = uuid::Uuid::new_v5(&NAMESPACE_KCL, next.as_bytes());
self.next_id += 1;
next_uuid
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_id_generator() {
let mut generator = IdGenerator::new(Some(ModuleId::default()));
let uuid1 = generator.next_uuid();
let uuid2 = generator.next_uuid();
assert_ne!(uuid1, uuid2);
}
#[test]
// Test that the same generator produces the same UUIDs.
fn test_id_generator_stable() {
let mut generator = IdGenerator::new(Some(ModuleId::default()));
let uuid1 = generator.next_uuid();
let uuid2 = generator.next_uuid();
let mut generator = IdGenerator::new(Some(ModuleId::default()));
let uuid3 = generator.next_uuid();
let uuid4 = generator.next_uuid();
assert_eq!(uuid1, uuid3);
assert_eq!(uuid2, uuid4);
}
#[test]
// Generate 20 uuids and make sure all are unique.
fn test_id_generator_unique() {
let mut generator = IdGenerator::new(Some(ModuleId::default()));
let mut uuids = Vec::new();
for _ in 0..20 {
uuids.push(generator.next_uuid());
}
for i in 0..uuids.len() {
for j in i + 1..uuids.len() {
assert_ne!(uuids[i], uuids[j]);
}
}
}
}

View File

@ -6,13 +6,12 @@ use serde::{Deserialize, Serialize};
use super::{
memory::{self, EnvironmentRef},
MetaSettings,
MetaSettings, Point3d,
};
use crate::{
errors::KclErrorDetails,
execution::{
ExecState, ExecutorContext, Face, Helix, ImportedGeometry, Metadata, Plane, Sketch, SketchSet, Solid, SolidSet,
TagIdentifier,
ExecState, ExecutorContext, Face, Helix, ImportedGeometry, Metadata, Plane, Sketch, Solid, TagIdentifier,
},
parsing::{
ast::types::{
@ -21,7 +20,10 @@ use crate::{
},
token::NumericSuffix,
},
std::{args::Arg, StdFnProps},
std::{
args::{Arg, FromKclValue},
StdFnProps,
},
CompilationError, KclError, ModuleId, SourceRange,
};
@ -34,33 +36,40 @@ pub type KclObjectFields = HashMap<String, KclValue>;
pub enum KclValue {
Uuid {
value: ::uuid::Uuid,
#[serde(rename = "__meta")]
#[serde(skip)]
meta: Vec<Metadata>,
},
Bool {
value: bool,
#[serde(rename = "__meta")]
#[serde(skip)]
meta: Vec<Metadata>,
},
Number {
value: f64,
ty: NumericType,
#[serde(rename = "__meta")]
#[serde(skip)]
meta: Vec<Metadata>,
},
String {
value: String,
#[serde(rename = "__meta")]
#[serde(skip)]
meta: Vec<Metadata>,
},
MixedArray {
value: Vec<KclValue>,
#[serde(rename = "__meta")]
#[serde(skip)]
meta: Vec<Metadata>,
},
// An array where all values have a shared type (not necessarily the same principal type).
HomArray {
value: Vec<KclValue>,
// The type of values, not the array type.
#[serde(skip)]
ty: PrimitiveType,
},
Object {
value: KclObjectFields,
#[serde(rename = "__meta")]
#[serde(skip)]
meta: Vec<Metadata>,
},
TagIdentifier(Box<TagIdentifier>),
@ -74,15 +83,9 @@ pub enum KclValue {
Sketch {
value: Box<Sketch>,
},
Sketches {
value: Vec<Box<Sketch>>,
},
Solid {
value: Box<Solid>,
},
Solids {
value: Vec<Box<Solid>>,
},
Helix {
value: Box<Helix>,
},
@ -91,30 +94,24 @@ pub enum KclValue {
Function {
#[serde(skip)]
value: FunctionSource,
#[serde(rename = "__meta")]
#[serde(skip)]
meta: Vec<Metadata>,
},
Module {
value: ModuleId,
#[serde(rename = "__meta")]
#[serde(skip)]
meta: Vec<Metadata>,
},
#[ts(skip)]
Type {
#[serde(skip)]
value: Option<(PrimitiveType, StdFnProps)>,
#[serde(rename = "__meta")]
#[serde(skip)]
meta: Vec<Metadata>,
},
KclNone {
value: KclNone,
#[serde(rename = "__meta")]
meta: Vec<Metadata>,
},
// Only used for memory management. Should never be visible outside of the memory module.
Tombstone {
value: (),
#[serde(rename = "__meta")]
#[serde(skip)]
meta: Vec<Metadata>,
},
}
@ -145,48 +142,46 @@ impl JsonSchema for FunctionSource {
}
}
impl From<SketchSet> for KclValue {
fn from(sg: SketchSet) -> Self {
match sg {
SketchSet::Sketch(value) => KclValue::Sketch { value },
SketchSet::Sketches(value) => KclValue::Sketches { value },
}
}
}
impl From<Vec<Box<Sketch>>> for KclValue {
fn from(sg: Vec<Box<Sketch>>) -> Self {
KclValue::Sketches { value: sg }
}
}
impl From<SolidSet> for KclValue {
fn from(eg: SolidSet) -> Self {
match eg {
SolidSet::Solid(eg) => KclValue::Solid { value: eg },
SolidSet::Solids(egs) => KclValue::Solids { value: egs },
}
}
}
impl From<Vec<Box<Solid>>> for KclValue {
fn from(eg: Vec<Box<Solid>>) -> Self {
impl From<Vec<Sketch>> for KclValue {
fn from(mut eg: Vec<Sketch>) -> Self {
if eg.len() == 1 {
KclValue::Solid { value: eg[0].clone() }
KclValue::Sketch {
value: Box::new(eg.pop().unwrap()),
}
} else {
KclValue::Solids { value: eg }
KclValue::HomArray {
value: eg
.into_iter()
.map(|s| KclValue::Sketch { value: Box::new(s) })
.collect(),
ty: crate::execution::PrimitiveType::Sketch,
}
}
}
}
impl From<Vec<Solid>> for KclValue {
fn from(mut eg: Vec<Solid>) -> Self {
if eg.len() == 1 {
KclValue::Solid {
value: Box::new(eg.pop().unwrap()),
}
} else {
KclValue::HomArray {
value: eg.into_iter().map(|s| KclValue::Solid { value: Box::new(s) }).collect(),
ty: crate::execution::PrimitiveType::Solid,
}
}
}
}
impl From<KclValue> for Vec<SourceRange> {
fn from(item: KclValue) -> Self {
match item {
KclValue::TagDeclarator(t) => vec![SourceRange::new(t.start, t.end, t.module_id)],
KclValue::TagIdentifier(t) => to_vec_sr(&t.meta),
KclValue::Solid { value } => to_vec_sr(&value.meta),
KclValue::Solids { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Sketch { value } => to_vec_sr(&value.meta),
KclValue::Sketches { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Helix { value } => to_vec_sr(&value.meta),
KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta),
KclValue::Function { meta, .. } => to_vec_sr(&meta),
@ -196,12 +191,12 @@ impl From<KclValue> for Vec<SourceRange> {
KclValue::Number { meta, .. } => to_vec_sr(&meta),
KclValue::String { meta, .. } => to_vec_sr(&meta),
KclValue::MixedArray { meta, .. } => to_vec_sr(&meta),
KclValue::HomArray { value, .. } => value.iter().flat_map(Into::<Vec<SourceRange>>::into).collect(),
KclValue::Object { meta, .. } => to_vec_sr(&meta),
KclValue::Module { meta, .. } => to_vec_sr(&meta),
KclValue::Uuid { meta, .. } => to_vec_sr(&meta),
KclValue::Type { meta, .. } => to_vec_sr(&meta),
KclValue::KclNone { meta, .. } => to_vec_sr(&meta),
KclValue::Tombstone { .. } => unreachable!("Tombstone SourceRange"),
}
}
}
@ -216,9 +211,7 @@ impl From<&KclValue> for Vec<SourceRange> {
KclValue::TagDeclarator(t) => vec![SourceRange::new(t.start, t.end, t.module_id)],
KclValue::TagIdentifier(t) => to_vec_sr(&t.meta),
KclValue::Solid { value } => to_vec_sr(&value.meta),
KclValue::Solids { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Sketch { value } => to_vec_sr(&value.meta),
KclValue::Sketches { value } => value.iter().flat_map(|eg| to_vec_sr(&eg.meta)).collect(),
KclValue::Helix { value } => to_vec_sr(&value.meta),
KclValue::ImportedGeometry(i) => to_vec_sr(&i.meta),
KclValue::Function { meta, .. } => to_vec_sr(meta),
@ -229,11 +222,11 @@ impl From<&KclValue> for Vec<SourceRange> {
KclValue::String { meta, .. } => to_vec_sr(meta),
KclValue::Uuid { meta, .. } => to_vec_sr(meta),
KclValue::MixedArray { meta, .. } => to_vec_sr(meta),
KclValue::HomArray { value, .. } => value.iter().flat_map(Into::<Vec<SourceRange>>::into).collect(),
KclValue::Object { meta, .. } => to_vec_sr(meta),
KclValue::Module { meta, .. } => to_vec_sr(meta),
KclValue::KclNone { meta, .. } => to_vec_sr(meta),
KclValue::Type { meta, .. } => to_vec_sr(meta),
KclValue::Tombstone { .. } => unreachable!("Tombstone &SourceRange"),
}
}
}
@ -253,22 +246,20 @@ impl KclValue {
KclValue::Number { meta, .. } => meta.clone(),
KclValue::String { value: _, meta } => meta.clone(),
KclValue::MixedArray { value: _, meta } => meta.clone(),
KclValue::HomArray { value, .. } => value.iter().flat_map(|v| v.metadata()).collect(),
KclValue::Object { value: _, meta } => meta.clone(),
KclValue::TagIdentifier(x) => x.meta.clone(),
KclValue::TagDeclarator(x) => vec![x.metadata()],
KclValue::Plane { value } => value.meta.clone(),
KclValue::Face { value } => value.meta.clone(),
KclValue::Sketch { value } => value.meta.clone(),
KclValue::Sketches { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::Solid { value } => value.meta.clone(),
KclValue::Solids { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::Helix { value } => value.meta.clone(),
KclValue::ImportedGeometry(x) => x.meta.clone(),
KclValue::Function { meta, .. } => meta.clone(),
KclValue::Module { meta, .. } => meta.clone(),
KclValue::KclNone { meta, .. } => meta.clone(),
KclValue::Type { meta, .. } => meta.clone(),
KclValue::Tombstone { .. } => unreachable!("Tombstone Metadata"),
}
}
@ -285,29 +276,6 @@ impl KclValue {
Some(ast.as_source_range())
}
pub(crate) fn get_solid_set(&self) -> Result<SolidSet> {
match self {
KclValue::Solid { value } => Ok(SolidSet::Solid(value.clone())),
KclValue::Solids { value } => Ok(SolidSet::Solids(value.clone())),
KclValue::MixedArray { value, .. } => {
let solids: Vec<_> = value
.iter()
.enumerate()
.map(|(i, v)| {
v.as_solid().map(|v| v.to_owned()).map(Box::new).ok_or_else(|| {
anyhow::anyhow!(
"expected this array to only contain solids, but element {i} was actually {}",
v.human_friendly_type()
)
})
})
.collect::<Result<_, _>>()?;
Ok(SolidSet::Solids(solids))
}
_ => anyhow::bail!("Not a solid or solids: {:?}", self),
}
}
#[allow(unused)]
pub(crate) fn none() -> Self {
Self::KclNone {
@ -324,9 +292,7 @@ impl KclValue {
KclValue::TagDeclarator(_) => "TagDeclarator",
KclValue::TagIdentifier(_) => "TagIdentifier",
KclValue::Solid { .. } => "Solid",
KclValue::Solids { .. } => "Solids",
KclValue::Sketch { .. } => "Sketch",
KclValue::Sketches { .. } => "Sketches",
KclValue::Helix { .. } => "Helix",
KclValue::ImportedGeometry(_) => "ImportedGeometry",
KclValue::Function { .. } => "Function",
@ -336,11 +302,11 @@ impl KclValue {
KclValue::Number { .. } => "number",
KclValue::String { .. } => "string (text)",
KclValue::MixedArray { .. } => "array (list)",
KclValue::HomArray { .. } => "array (list)",
KclValue::Object { .. } => "object",
KclValue::Module { .. } => "module",
KclValue::Type { .. } => "type",
KclValue::KclNone { .. } => "None",
KclValue::Tombstone { .. } => "TOMBSTONE",
}
}
@ -367,16 +333,14 @@ impl KclValue {
}
}
pub(crate) fn map_env_ref(&self, env_map: &HashMap<EnvironmentRef, EnvironmentRef>) -> Self {
pub(crate) fn map_env_ref(&self, old_env: usize, new_env: usize) -> Self {
let mut result = self.clone();
if let KclValue::Function {
value: FunctionSource::User { ref mut memory, .. },
..
} = result
{
if let Some(new) = env_map.get(memory) {
*memory = *new;
}
memory.replace_env(old_env, new_env);
}
result
}
@ -501,6 +465,21 @@ impl KclValue {
}
}
pub fn as_mut_sketch(&mut self) -> Option<&mut Sketch> {
if let KclValue::Sketch { value } = self {
Some(value)
} else {
None
}
}
pub fn as_mut_tag(&mut self) -> Option<&mut TagIdentifier> {
if let KclValue::TagIdentifier(value) = self {
Some(value)
} else {
None
}
}
pub fn as_f64(&self) -> Option<f64> {
if let KclValue::Number { value, .. } = &self {
Some(*value)
@ -563,17 +542,6 @@ impl KclValue {
}
}
/// Get an optional tag from a memory item.
pub fn get_tag_declarator_opt(&self) -> Result<Option<TagNode>, KclError> {
match self {
KclValue::TagDeclarator(t) => Ok(Some((**t).clone())),
_ => Err(KclError::Semantic(KclErrorDetails {
message: format!("Not a tag declarator: {:?}", self),
source_ranges: self.clone().into(),
})),
}
}
/// If this KCL value is a bool, retrieve it.
pub fn get_bool(&self) -> Result<bool, KclError> {
let Self::Bool { value: b, .. } = self else {
@ -594,6 +562,215 @@ impl KclValue {
self_ty.subtype(ty)
}
/// Coerce `self` to a new value which has `ty` as it's closest supertype.
///
/// If the result is Some, then:
/// - result.principal_type().unwrap().subtype(ty)
///
/// If self.principal_type() == ty then result == self
pub fn coerce(&self, ty: &RuntimeType, exec_state: &mut ExecState) -> Option<KclValue> {
match ty {
RuntimeType::Primitive(ty) => self.coerce_to_primitive_type(ty, exec_state),
RuntimeType::Array(ty, len) => self.coerce_to_array_type(ty, *len, exec_state),
RuntimeType::Tuple(tys) => self.coerce_to_tuple_type(tys, exec_state),
RuntimeType::Union(tys) => self.coerce_to_union_type(tys, exec_state),
RuntimeType::Object(tys) => self.coerce_to_object_type(tys, exec_state),
}
}
fn coerce_to_primitive_type(&self, ty: &PrimitiveType, exec_state: &mut ExecState) -> Option<KclValue> {
let value = match self {
KclValue::MixedArray { value, .. } | KclValue::HomArray { value, .. } if value.len() == 1 => &value[0],
_ => self,
};
match ty {
// TODO numeric type coercions
PrimitiveType::Number(_ty) => match value {
KclValue::Number { .. } => Some(value.clone()),
_ => None,
},
PrimitiveType::String => match value {
KclValue::String { .. } => Some(value.clone()),
_ => None,
},
PrimitiveType::Boolean => match value {
KclValue::Bool { .. } => Some(value.clone()),
_ => None,
},
PrimitiveType::Sketch => match value {
KclValue::Sketch { .. } => Some(value.clone()),
_ => None,
},
PrimitiveType::Solid => match value {
KclValue::Solid { .. } => Some(value.clone()),
_ => None,
},
PrimitiveType::Plane => match value {
KclValue::Plane { .. } => Some(value.clone()),
KclValue::Object { value, meta } => {
let origin = value.get("origin").and_then(Point3d::from_kcl_val)?;
let x_axis = value.get("xAxis").and_then(Point3d::from_kcl_val)?;
let y_axis = value.get("yAxis").and_then(Point3d::from_kcl_val)?;
let z_axis = value.get("zAxis").and_then(Point3d::from_kcl_val)?;
let id = exec_state.mod_local.id_generator.next_uuid();
let plane = Plane {
id,
artifact_id: id.into(),
origin,
x_axis,
y_axis,
z_axis,
value: super::PlaneType::Uninit,
// TODO use length unit from origin
units: exec_state.length_unit(),
meta: meta.clone(),
};
Some(KclValue::Plane { value: Box::new(plane) })
}
_ => None,
},
PrimitiveType::ImportedGeometry => match value {
KclValue::ImportedGeometry { .. } => Some(value.clone()),
_ => None,
},
}
}
fn coerce_to_array_type(&self, ty: &PrimitiveType, len: ArrayLen, exec_state: &mut ExecState) -> Option<KclValue> {
match self {
KclValue::HomArray { value, ty: aty } => {
// TODO could check types of values individually
if aty != ty {
return None;
}
let value = match len {
ArrayLen::None => value.clone(),
ArrayLen::NonEmpty => {
if value.is_empty() {
return None;
}
value.clone()
}
ArrayLen::Known(n) => {
if n != value.len() {
return None;
}
value[..n].to_vec()
}
};
Some(KclValue::HomArray { value, ty: ty.clone() })
}
KclValue::MixedArray { value, .. } => {
let value = match len {
ArrayLen::None => value.clone(),
ArrayLen::NonEmpty => {
if value.is_empty() {
return None;
}
value.clone()
}
ArrayLen::Known(n) => {
if n != value.len() {
return None;
}
value[..n].to_vec()
}
};
let rt = RuntimeType::Primitive(ty.clone());
let value = value
.iter()
.map(|v| v.coerce(&rt, exec_state))
.collect::<Option<Vec<_>>>()?;
Some(KclValue::HomArray { value, ty: ty.clone() })
}
KclValue::KclNone { .. } if len.satisfied(0) => Some(KclValue::HomArray {
value: Vec::new(),
ty: ty.clone(),
}),
value if len.satisfied(1) => {
if value.has_type(&RuntimeType::Primitive(ty.clone())) {
Some(KclValue::HomArray {
value: vec![value.clone()],
ty: ty.clone(),
})
} else {
None
}
}
_ => None,
}
}
fn coerce_to_tuple_type(&self, tys: &[PrimitiveType], exec_state: &mut ExecState) -> Option<KclValue> {
match self {
KclValue::MixedArray { value, .. } | KclValue::HomArray { value, .. } => {
if value.len() < tys.len() {
return None;
}
let mut result = Vec::new();
for (i, t) in tys.iter().enumerate() {
result.push(value[i].coerce_to_primitive_type(t, exec_state)?);
}
Some(KclValue::MixedArray {
value: result,
meta: Vec::new(),
})
}
KclValue::KclNone { meta, .. } if tys.is_empty() => Some(KclValue::MixedArray {
value: Vec::new(),
meta: meta.clone(),
}),
value if tys.len() == 1 => {
if value.has_type(&RuntimeType::Primitive(tys[0].clone())) {
Some(KclValue::MixedArray {
value: vec![value.clone()],
meta: Vec::new(),
})
} else {
None
}
}
_ => None,
}
}
fn coerce_to_union_type(&self, tys: &[RuntimeType], exec_state: &mut ExecState) -> Option<KclValue> {
for t in tys {
if let Some(v) = self.coerce(t, exec_state) {
return Some(v);
}
}
None
}
fn coerce_to_object_type(&self, tys: &[(String, RuntimeType)], _exec_state: &mut ExecState) -> Option<KclValue> {
match self {
KclValue::Object { value, .. } => {
for (s, t) in tys {
// TODO coerce fields
if !value.get(s)?.has_type(t) {
return None;
}
}
// TODO remove non-required fields
Some(self.clone())
}
_ => None,
}
}
pub fn principal_type(&self) -> Option<RuntimeType> {
match self {
KclValue::Bool { .. } => Some(RuntimeType::Primitive(PrimitiveType::Boolean)),
@ -608,26 +785,24 @@ impl KclValue {
}
KclValue::Plane { .. } => Some(RuntimeType::Primitive(PrimitiveType::Plane)),
KclValue::Sketch { .. } => Some(RuntimeType::Primitive(PrimitiveType::Sketch)),
KclValue::Sketches { .. } => Some(RuntimeType::Array(PrimitiveType::Sketch)),
KclValue::Solid { .. } => Some(RuntimeType::Primitive(PrimitiveType::Solid)),
KclValue::Solids { .. } => Some(RuntimeType::Array(PrimitiveType::Solid)),
KclValue::ImportedGeometry(..) => Some(RuntimeType::Primitive(PrimitiveType::ImportedGeometry)),
KclValue::MixedArray { value, .. } => Some(RuntimeType::Tuple(
value
.iter()
.map(|v| v.principal_type().and_then(RuntimeType::primitive))
.collect::<Option<Vec<_>>>()?,
)),
KclValue::HomArray { ty, value, .. } => Some(RuntimeType::Array(ty.clone(), ArrayLen::Known(value.len()))),
KclValue::Face { .. } => None,
KclValue::Helix { .. }
| KclValue::ImportedGeometry(..)
| KclValue::Function { .. }
| KclValue::Module { .. }
| KclValue::TagIdentifier(_)
| KclValue::TagDeclarator(_)
| KclValue::KclNone { .. }
| KclValue::Type { .. }
| KclValue::Uuid { .. }
| KclValue::Tombstone { .. } => None,
| KclValue::Uuid { .. } => None,
}
}
@ -729,20 +904,18 @@ impl KclValue {
KclValue::TagIdentifier(tag) => Some(format!("${}", tag.value)),
// TODO better Array and Object stringification
KclValue::MixedArray { .. } => Some("[...]".to_owned()),
KclValue::HomArray { .. } => Some("[...]".to_owned()),
KclValue::Object { .. } => Some("{ ... }".to_owned()),
KclValue::Module { .. }
| KclValue::Solid { .. }
| KclValue::Solids { .. }
| KclValue::Sketch { .. }
| KclValue::Sketches { .. }
| KclValue::Helix { .. }
| KclValue::ImportedGeometry(_)
| KclValue::Function { .. }
| KclValue::Plane { .. }
| KclValue::Face { .. }
| KclValue::KclNone { .. }
| KclValue::Type { .. }
| KclValue::Tombstone { .. } => None,
| KclValue::Type { .. } => None,
}
}
}
@ -750,7 +923,8 @@ impl KclValue {
#[derive(Debug, Clone, PartialEq)]
pub enum RuntimeType {
Primitive(PrimitiveType),
Array(PrimitiveType),
Array(PrimitiveType, ArrayLen),
Union(Vec<RuntimeType>),
Tuple(Vec<PrimitiveType>),
Object(Vec<(String, RuntimeType)>),
}
@ -765,7 +939,9 @@ impl RuntimeType {
Type::Primitive(pt) => {
PrimitiveType::from_parsed(pt, exec_state, source_range)?.map(RuntimeType::Primitive)
}
Type::Array(pt) => PrimitiveType::from_parsed(pt, exec_state, source_range)?.map(RuntimeType::Array),
Type::Array(pt) => {
PrimitiveType::from_parsed(pt, exec_state, source_range)?.map(|t| RuntimeType::Array(t, ArrayLen::None))
}
Type::Object { properties } => properties
.into_iter()
.map(|p| {
@ -781,15 +957,37 @@ impl RuntimeType {
})
}
pub fn human_friendly_type(&self) -> String {
match self {
RuntimeType::Primitive(ty) => ty.to_string(),
RuntimeType::Array(ty, ArrayLen::None) => format!("an array of {}", ty.display_multiple()),
RuntimeType::Array(ty, ArrayLen::NonEmpty) => format!("one or more {}", ty.display_multiple()),
RuntimeType::Array(ty, ArrayLen::Known(n)) => format!("an array of {n} {}", ty.display_multiple()),
RuntimeType::Union(tys) => tys
.iter()
.map(Self::human_friendly_type)
.collect::<Vec<_>>()
.join(" or "),
RuntimeType::Tuple(tys) => format!(
"an array with values of types ({})",
tys.iter().map(PrimitiveType::to_string).collect::<Vec<_>>().join(", ")
),
RuntimeType::Object(_) => format!("an object with fields {}", self),
}
}
// Subtype with no coercion, including refining numeric types.
fn subtype(&self, sup: &RuntimeType) -> bool {
use RuntimeType::*;
match (self, sup) {
(Primitive(t1), Primitive(t2)) => t1 == t2,
// TODO arrays could be covariant
(Primitive(t1), Primitive(t2)) | (Array(t1), Array(t2)) => t1 == t2,
(Array(t1, l1), Array(t2, l2)) => t1 == t2 && l1.subtype(*l2),
(Tuple(t1), Tuple(t2)) => t1 == t2,
(Tuple(t1), Array(t2)) => t1.iter().all(|t| t == t2),
(Tuple(t1), Array(t2, l2)) => (l2.satisfied(t1.len())) && t1.iter().all(|t| t == t2),
(Union(ts1), Union(ts2)) => ts1.iter().all(|t| ts2.contains(t)),
(t1, Union(ts2)) => ts2.contains(t1),
// TODO record subtyping - subtype can be larger, fields can be covariant.
(Object(t1), Object(t2)) => t1 == t2,
_ => false,
@ -808,12 +1006,21 @@ impl fmt::Display for RuntimeType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RuntimeType::Primitive(t) => t.fmt(f),
RuntimeType::Array(t) => write!(f, "[{t}]"),
RuntimeType::Array(t, l) => match l {
ArrayLen::None => write!(f, "[{t}]"),
ArrayLen::NonEmpty => write!(f, "[{t}; 1+]"),
ArrayLen::Known(n) => write!(f, "[{t}; {n}]"),
},
RuntimeType::Tuple(ts) => write!(
f,
"[{}]",
ts.iter().map(|t| t.to_string()).collect::<Vec<_>>().join(", ")
),
RuntimeType::Union(ts) => write!(
f,
"{}",
ts.iter().map(|t| t.to_string()).collect::<Vec<_>>().join(" | ")
),
RuntimeType::Object(items) => write!(
f,
"{{ {} }}",
@ -827,6 +1034,34 @@ impl fmt::Display for RuntimeType {
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ArrayLen {
None,
NonEmpty,
Known(usize),
}
impl ArrayLen {
pub fn subtype(self, other: ArrayLen) -> bool {
match (self, other) {
(_, ArrayLen::None) => true,
(ArrayLen::NonEmpty, ArrayLen::NonEmpty) => true,
(ArrayLen::Known(size), ArrayLen::NonEmpty) if size > 0 => true,
(ArrayLen::Known(s1), ArrayLen::Known(s2)) if s1 == s2 => true,
_ => false,
}
}
/// True if the length constraint is satisfied by the supplied length.
fn satisfied(self, len: usize) -> bool {
match self {
ArrayLen::None => true,
ArrayLen::NonEmpty => len > 0,
ArrayLen::Known(s) => len == s,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum PrimitiveType {
Number(NumericType),
@ -835,6 +1070,7 @@ pub enum PrimitiveType {
Sketch,
Solid,
Plane,
ImportedGeometry,
}
impl PrimitiveType {
@ -866,6 +1102,19 @@ impl PrimitiveType {
_ => None,
})
}
fn display_multiple(&self) -> String {
match self {
PrimitiveType::Number(NumericType::Known(unit)) => format!("numbers({unit})"),
PrimitiveType::Number(_) => "numbers".to_owned(),
PrimitiveType::String => "strings".to_owned(),
PrimitiveType::Boolean => "bools".to_owned(),
PrimitiveType::Sketch => "Sketches".to_owned(),
PrimitiveType::Solid => "Solids".to_owned(),
PrimitiveType::Plane => "Planes".to_owned(),
PrimitiveType::ImportedGeometry => "imported geometries".to_owned(),
}
}
}
impl fmt::Display for PrimitiveType {
@ -878,6 +1127,7 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Sketch => write!(f, "Sketch"),
PrimitiveType::Solid => write!(f, "Solid"),
PrimitiveType::Plane => write!(f, "Plane"),
PrimitiveType::ImportedGeometry => write!(f, "imported geometry"),
}
}
}

View File

@ -6,18 +6,18 @@
//! one per execution. It has no explicit support for caching between executions.
//!
//! Memory is mostly immutable (since KCL does not support mutation or reassignment). However, tags
//! may change as code is executed and that mutates memory. Therefore,
//! may change as code is executed and that mutates memory. Therefore to some extent,
//! ProgramMemory supports mutability and does not rely on KCL's (mostly) immutable nature.
//!
//! ProgramMemory is observably monotonic, i.e., it only grows and even when we pop a stack frame,
//! the frame is retained unless we can prove it is unreferenced. We remove some values which we
//! know cannot be referenced, but we should in the future do better garbage collection (of values
//! and envs).
//! and envs).
//!
//! ## Concepts
//!
//! There are three main moving parts for ProgramMemory: environments, snapshots, and stacks. I'll
//! cover environments (and the call stack) first as if snapshots didn't exist, then describe snapshots.
//! There are three main moving parts for ProgramMemory: environments, epochs, and stacks. I'll
//! cover environments (and the call stack) first as if epochs didn't exist, then describe epochs.
//!
//! An environment is a set of bindings (i.e., a map from names to values). Environments handle
//! both scoping and context switching. A new lexical scope means a new environment. Nesting of scopes
@ -81,12 +81,25 @@
//! temporally) the definition of `c`. (Note that although KCL does not permit mutation, objects
//! can change due to the way tags are implemented).
//!
//! To make this work, when we save a reference to an enclosing scope we take a snapshot of memory at
//! that point and save a reference to that snapshot. When we call a function, the parent of the new
//! callee env is that snapshot, not the current version of the enclosing scope.
//! To make this work, we have the concept of an epoch. An epoch is a simple, global, monotonic counter
//! which is incremented at any significant moment in execution (we use the term snapshot). When a
//! value is saved in memory we also save the epoch at which it was stored.
//!
//! Entering an inline scope (e.g., the body of an `if` statement) means pushing an env whose parent
//! is the current env. We don't need to snapshot in this case.
//! When we save a reference to an enclosing scope we take a snapshot and save that epoch as part of
//! the reference. When we call a function, we use the epoch when it was defined to look up variables,
//! ignoring any variables which have a creation time later than the saved epoch.
//!
//! Because the callee could create new variables (with a creation time of the current epoch) which
//! the callee should be able to read, we can't simply check the epoch with the callees (and we'd need
//! to maintain a stack of callee epochs for further calls, etc.). Instead a stack frame consists of
//! a reference to an environment and an epoch at which reads should take place. When we call a function
//! this creates a new env using the current epoch, and it's parent env (which is the enclosing scope
//! of the function declaration) includes the epoch at which the function was declared.
//!
//! So far, this handles variables created after a function is declared, but does not handle mutation.
//! Mutation must be handled internally in values, see for example `TagIdentifier`. It is suggested
//! that objects rely on epochs for this. Since epochs are linked to the stack frame, only objects in
//! the current stack frame should be mutated.
//!
//! ### Std
//!
@ -107,53 +120,17 @@
//! Pushing and popping stack frames is straightforward. Most get/set/update operations don't touch
//! the call stack other than the current env (updating tags on function return is the exception).
//!
//! Snapshots are maintained within an environment and are always specific to an environment. Snapshots
//! must also have a parent reference (since they are logically a snapshot of all memory). This parent
//! refers to a snapshot within the parent env. When a snapshot is created, we must create a snapshot
//! object for each parent env. When using a snapshot we must check the parent snapshot whenever
//! we check the parent env (and not the current version of the parent env).
//!
//! An environment will have many snapshots, they are kept in time order, and do not reference each
//! other. (The parent of a snapshot is always in another env).
//!
//! A snapshot is created empty (we don't copy memory) and we use a copy-on-write design: when a
//! value in an environment is modified, we copy the old version into the most recent snapshot (note
//! that we never overwrite a value in the snapshot, if a value is modified multiple times, we want
//! to keep the original version, not an intermediate one). Likewise, if we insert a new variable,
//! we put a tombstone value in the snapshot.
//!
//! When we read from the current version of an environment, we simply read from the bindings in the
//! env and ignore the snapshots. When we read from a snapshot, we first check the specific snapshot
//! for the key, then check any newer snapshots, then finally check the env bindings.
//!
//! A minor optimisation is that when creating a snapshot, if the previous one is empty, then
//! we can reuse that rather than creating a new one. Since we only create a snapshot when a function
//! is declared and the function decl is immediately saved into the new snapshot, the empty snapshot
//! optimisation only happens with parent snapshots (though if the env tree is deep this means we
//! can save a lot of snapshots).
//!
//! ## Invariants
//!
//! There's obviously a bunch of invariants in this design, some are kinda obvious, some are limited
//! in scope and are documented inline, here are some others:
//!
//! - The current env and all envs in the call stack are 'just envs', never a snapshot (we could
//! use just a ref to an env, rather than to a snapshot but this is pretty inconvenient, so just
//! know that the snapshot ref is always to the current version). Only the parent envs or saved refs
//! can be refs to snapshots.
//! - We only ever write into the current env, never into any parent envs (though we can read from
//! both).
//! - Therefore, there is no concept of writing into a snapshot, only reading from one.
//! - The env ref saved with a function decl is always to a snapshot, never to the current version.
//! - If there are no snapshots in an environment and it is no longer in the call stack, then there
//! are no references from function decls to the env (if it is the parent of an env with extant refs
//! then there would be snapshots in the child env and that implies there must be a snapshot in the
//! parent to be the parent of that snapshot).
//! - We only ever write (or mutate) at the most recent epoch, never at an older one.
//! - The env ref saved with a function decl is always to an historic epoch, never to the current one.
//! - Since KCL does not have submodules and decls are not visible outside of a nested scope, all
//! references to variables in other modules must be in the root scope of a module.
//! - Therefore, an active env must either be on the call stack, have snapshots, or be a root env. This
//! is however a conservative approximation since snapshots may exist even if there are no live
//! references to an env.
//!
//! ## Concurrency and thread-safety
//!
@ -227,7 +204,6 @@
use std::{
cell::UnsafeCell,
collections::HashMap,
fmt,
pin::Pin,
sync::{
@ -267,6 +243,7 @@ pub(crate) struct ProgramMemory {
/// Statistics about the memory, should not be used for anything other than meta-info.
pub(crate) stats: MemoryStats,
next_stack_id: AtomicUsize,
epoch: AtomicUsize,
write_lock: AtomicBool,
}
@ -307,7 +284,7 @@ impl fmt::Display for Stack {
.call_stack
.iter()
.chain(Some(&self.current_env))
.map(|e| format!("EnvRef({}, {})", e.0, e.1 .0))
.map(|e| format!("EnvRef({}, {})", e.0, e.1))
.collect();
write!(f, "Stack {}\nstack frames:\n{}", self.id, stack.join("\n"))
}
@ -322,6 +299,7 @@ impl ProgramMemory {
std: None,
stats: MemoryStats::default(),
next_stack_id: AtomicUsize::new(1),
epoch: AtomicUsize::new(1),
write_lock: AtomicBool::new(false),
})
}
@ -340,10 +318,12 @@ impl ProgramMemory {
std: self.std,
stats: MemoryStats::default(),
next_stack_id: AtomicUsize::new(self.next_stack_id.load(Ordering::Relaxed)),
epoch: AtomicUsize::new(self.epoch.load(Ordering::Relaxed)),
write_lock: AtomicBool::new(false),
})
}
/// Create a new stack object referencing this `ProgramMemory`.
pub fn new_stack(self: Arc<Self>) -> Stack {
let id = self.next_stack_id.fetch_add(1, Ordering::Relaxed);
assert!(id > 0);
@ -367,7 +347,7 @@ impl ProgramMemory {
self.std.is_none()
}
/// Get a value from a specific snapshot of the memory.
/// Get a value from a specific environment of the memory at a specific point in time.
pub fn get_from(
&self,
var: &str,
@ -438,7 +418,7 @@ impl ProgramMemory {
let new_env = Environment::new(parent, is_root_env, owner);
self.with_envs(|envs| {
let result = EnvironmentRef(envs.len(), SnapshotRef::none());
let result = EnvironmentRef(envs.len(), usize::MAX);
// Note this might reallocate, which would hold the `with_envs` spin lock for way too long
// so somehow we should make sure we don't do that (though honestly the chance of that
// happening while another thread is waiting for the lock is pretty small).
@ -490,23 +470,12 @@ impl ProgramMemory {
})
}
#[cfg(test)]
fn update_with_env(&self, key: &str, value: KclValue, env: usize, owner: usize) {
self.stats.mutation_count.fetch_add(1, Ordering::Relaxed);
self.get_env(env).insert_or_update(key.to_owned(), value, owner);
}
/// Get a value from memory without checking for ownership of the env.
///
/// This is not safe to use in general and should only be used if you have unique access to
/// the `self` which is generally only true during testing.
#[cfg(test)]
pub fn get_from_unchecked(
&self,
var: &str,
mut env_ref: EnvironmentRef,
source_range: SourceRange,
) -> Result<&KclValue, KclError> {
pub fn get_from_unchecked(&self, var: &str, mut env_ref: EnvironmentRef) -> Result<&KclValue, KclError> {
loop {
let env = self.get_env(env_ref.index());
env_ref = match env.get_unchecked(var, env_ref.1) {
@ -518,7 +487,7 @@ impl ProgramMemory {
Err(KclError::UndefinedValue(KclErrorDetails {
message: format!("memory item key `{}` is not defined", var),
source_ranges: vec![source_range],
source_ranges: vec![],
}))
}
}
@ -544,6 +513,11 @@ impl Stack {
stack
}
/// Get the current (globally most recent) epoch.
pub fn current_epoch(&self) -> usize {
self.memory.epoch.load(Ordering::Relaxed)
}
/// Push a new (standard KCL) stack frame on to the call stack.
///
/// `parent` is the environment where the function being called is declared (not the caller's
@ -577,7 +551,7 @@ impl Stack {
// Rust functions shouldn't try to set or access anything in their environment, so don't
// waste time and space on a new env. Using usize::MAX means we'll get an overflow if we
// try to access anything rather than a silent error.
self.current_env = EnvironmentRef(usize::MAX, SnapshotRef::none());
self.current_env = EnvironmentRef(usize::MAX, 0);
}
/// Push a new stack frame on to the call stack with no connection to a parent environment.
@ -596,7 +570,6 @@ impl Stack {
/// SAFETY: the env must not be being used by another `Stack` since we'll move the env from
/// read-only to owned.
pub fn restore_env(&mut self, env: EnvironmentRef) {
assert!(env.1.is_none());
self.call_stack.push(self.current_env);
self.memory.get_env(env.index()).restore_owner(self.id);
self.current_env = env;
@ -642,25 +615,28 @@ impl Stack {
}
let mut old_env = self.memory.take_env(old);
if old_env.is_empty() {
return;
}
// Map of any old env refs to the current env.
let snapshot_map: HashMap<_, _> = old_env
.snapshot_parents()
.map(|(s, p)| (EnvironmentRef(old.0, s), (EnvironmentRef(self.current_env.0, p))))
.collect();
// Make a new scope so we override variables properly.
self.push_new_env_for_scope();
// Move the variables in the popped env into the current env.
let env = self.memory.get_env(self.current_env.index());
for (k, v) in old_env.as_mut().take_bindings() {
env.insert_or_update(k.clone(), v.map_env_ref(&snapshot_map), self.id);
for (k, (e, v)) in old_env.as_mut().take_bindings() {
env.insert(k, e, v.map_env_ref(old.0, self.current_env.0), self.id);
}
}
/// Snapshot the current state of the memory.
pub fn snapshot(&mut self) -> EnvironmentRef {
self.memory.stats.snapshot_count.fetch_add(1, Ordering::Relaxed);
let snapshot = env::snapshot(&self.memory, self.current_env, self.id);
EnvironmentRef(self.current_env.0, snapshot)
self.memory.stats.epoch_count.fetch_add(1, Ordering::Relaxed);
let env = self.memory.get_env(self.current_env.index());
env.mark_as_refed();
let prev_epoch = self.memory.epoch.fetch_add(1, Ordering::Relaxed);
EnvironmentRef(self.current_env.0, prev_epoch)
}
/// Add a value to the program memory (in the current scope). The value must not already exist.
@ -675,16 +651,21 @@ impl Stack {
self.memory.stats.mutation_count.fetch_add(1, Ordering::Relaxed);
env.insert(key, value, self.id);
env.insert(key, self.memory.epoch.load(Ordering::Relaxed), value, self.id);
Ok(())
}
pub fn insert_or_update(&mut self, key: String, value: KclValue) {
/// Update a variable in memory. `key` must exist in memory. If it doesn't, this function will panic
/// in debug builds and do nothing in release builds.
pub fn update(&mut self, key: &str, f: impl Fn(&mut KclValue, usize)) {
self.memory.stats.mutation_count.fetch_add(1, Ordering::Relaxed);
self.memory
.get_env(self.current_env.index())
.insert_or_update(key, value, self.id);
self.memory.get_env(self.current_env.index()).update(
key,
f,
self.memory.epoch.load(Ordering::Relaxed),
self.id,
);
}
/// Get a value from the program memory.
@ -693,38 +674,41 @@ impl Stack {
self.memory.get_from(var, self.current_env, source_range, self.id)
}
/// Whether the current frame of the stack contains a variable with the given name.
pub fn cur_frame_contains(&self, var: &str) -> bool {
let env = self.memory.get_env(self.current_env.index());
env.contains_key(var)
}
/// Get a key from the first KCL (i.e., non-Rust) stack frame on the call stack.
pub fn get_from_call_stack(&self, key: &str, source_range: SourceRange) -> Result<&KclValue, KclError> {
pub fn get_from_call_stack(&self, key: &str, source_range: SourceRange) -> Result<(usize, &KclValue), KclError> {
if !self.current_env.skip_env() {
return self.get(key, source_range);
return Ok((self.current_env.1, self.get(key, source_range)?));
}
for env in self.call_stack.iter().rev() {
if !env.skip_env() {
return self.memory.get_from(key, *env, source_range, self.id);
return Ok((env.1, self.memory.get_from(key, *env, source_range, self.id)?));
}
}
unreachable!("It can't be Rust frames all the way down");
}
/// Iterate over all key/value pairs in the current environment which satisfy the provided
/// predicate.
pub fn find_all_in_current_env<'a>(
/// Iterate over all keys in the current environment which satisfy the provided predicate.
pub fn find_keys_in_current_env<'a>(
&'a self,
pred: impl Fn(&KclValue) -> bool + 'a,
) -> impl Iterator<Item = (&'a String, &'a KclValue)> {
self.memory.find_all_in_env(self.current_env, pred, self.id)
) -> impl Iterator<Item = &'a String> {
self.memory
.find_all_in_env(self.current_env, pred, self.id)
.map(|(k, _)| k)
}
/// Iterate over all key/value pairs in the specified environment which satisfy the provided
/// predicate. `env` must either be read-only or owned by `self`.
pub fn find_all_in_env<'a>(
&'a self,
env: EnvironmentRef,
pred: impl Fn(&KclValue) -> bool + 'a,
) -> impl Iterator<Item = (&'a String, &'a KclValue)> {
self.memory.find_all_in_env(env, pred, self.id)
pub fn find_all_in_env(&self, env: EnvironmentRef) -> impl Iterator<Item = (&String, &KclValue)> {
self.memory.find_all_in_env(env, |_| true, self.id)
}
/// Walk all values accessible from any environment in the call stack.
@ -781,7 +765,7 @@ impl<'a> Iterator for CallStackIterator<'a> {
return next;
}
if let Some(env_ref) = self.stack.memory.get_env(self.cur_env.index()).parent(self.cur_env.1) {
if let Some(env_ref) = self.stack.memory.get_env(self.cur_env.index()).parent() {
self.cur_env = env_ref;
self.init_iter();
} else {
@ -816,23 +800,32 @@ impl<'a> Iterator for CallStackIterator<'a> {
#[cfg(test)]
impl PartialEq for Stack {
fn eq(&self, other: &Self) -> bool {
let vars: Vec<_> = self.find_all_in_current_env(|_| true).collect();
let vars_other: Vec<_> = other.find_all_in_current_env(|_| true).collect();
vars == vars_other
let vars: Vec<_> = self.find_keys_in_current_env(|_| true).collect();
let vars_other: Vec<_> = other.find_keys_in_current_env(|_| true).collect();
if vars != vars_other {
return false;
}
vars.iter()
.all(|k| self.get(k, SourceRange::default()).unwrap() == other.get(k, SourceRange::default()).unwrap())
}
}
/// An index pointing to an environment at a point in time (either a snapshot or the current version, see the module docs).
/// An index pointing to an environment at a point in time.
///
/// The first field indexes an environment, the second field is an epoch. An epoch of 0 is indicates
/// a dummy, error, or placeholder env ref, an epoch of `usize::MAX` represents the current most
/// recent epoch.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Hash, Eq, ts_rs::TS, JsonSchema)]
pub struct EnvironmentRef(usize, SnapshotRef);
pub struct EnvironmentRef(usize, usize);
impl EnvironmentRef {
fn dummy() -> Self {
Self(usize::MAX, SnapshotRef(usize::MAX))
Self(usize::MAX, 0)
}
fn is_regular(&self) -> bool {
self.0 < usize::MAX && self.1 .0 < usize::MAX
self.0 < usize::MAX && self.1 > 0
}
fn index(&self) -> usize {
@ -842,33 +835,11 @@ impl EnvironmentRef {
fn skip_env(&self) -> bool {
self.0 == usize::MAX
}
}
/// An index pointing to a snapshot within a specific (unspecified) environment.
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Hash, Eq, ts_rs::TS, JsonSchema)]
struct SnapshotRef(usize);
impl SnapshotRef {
/// Represents no snapshot, use the current version of the environment.
fn none() -> Self {
Self(0)
}
/// `self` represents a snapshot.
fn is_some(self) -> bool {
self.0 > 0
}
/// `self` represents the current version.
fn is_none(self) -> bool {
self.0 == 0
}
// Precondition: self.is_some()
fn index(&self) -> usize {
// Note that `0` is a distinguished value meaning 'no snapshot', so the reference value
// is one greater than the index into the list of snapshots.
self.0 - 1
pub fn replace_env(&mut self, old: usize, new: usize) {
if self.0 == old {
self.0 = new;
}
}
}
@ -877,8 +848,8 @@ impl SnapshotRef {
pub(crate) struct MemoryStats {
// Total number of environments created.
env_count: AtomicUsize,
// Total number of snapshots created.
snapshot_count: AtomicUsize,
// Total number of epochs.
epoch_count: AtomicUsize,
// Total number of values inserted or updated.
mutation_count: AtomicUsize,
// The number of envs we delete when popped from the call stack.
@ -900,12 +871,10 @@ mod env {
#[derive(Debug)]
pub(super) struct Environment {
bindings: UnsafeCell<IndexMap<String, KclValue>>,
// invariant: self.parent.is_none() => forall s in self.snapshots: s.parent_snapshot.is_none()
snapshots: UnsafeCell<Vec<Snapshot>>,
bindings: UnsafeCell<IndexMap<String, (usize, KclValue)>>,
// An outer scope, if one exists.
parent: Option<EnvironmentRef>,
is_root_env: bool,
might_be_refed: AtomicBool,
// The id of the `Stack` if this `Environment` is on a call stack. If this is >0 then it may
// only be read or written by that `Stack`; if 0 then the env is read-only.
owner: AtomicUsize,
@ -918,9 +887,8 @@ mod env {
assert!(self.owner.load(Ordering::Acquire) == 0);
Self {
bindings: UnsafeCell::new(self.get_bindings().clone()),
snapshots: UnsafeCell::new(self.iter_snapshots().cloned().collect()),
parent: self.parent,
is_root_env: self.is_root_env,
might_be_refed: AtomicBool::new(self.might_be_refed.load(Ordering::Acquire)),
owner: AtomicUsize::new(0),
_unpin: PhantomPinned,
}
@ -931,45 +899,19 @@ mod env {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let parent = self
.parent
.map(|e| format!("EnvRef({}, {})", e.0, e.1 .0))
.map(|e| format!("EnvRef({}, {})", e.0, e.1))
.unwrap_or("_".to_owned());
let data: Vec<String> = self
.get_bindings()
.iter()
.map(|(k, v)| format!("{k}: {}", v.human_friendly_type()))
.map(|(k, v)| format!("{k}: {}@{}", v.1.human_friendly_type(), v.0))
.collect();
let snapshots: Vec<String> = self.iter_snapshots().map(|s| s.to_string()).collect();
write!(
f,
"Env {{\n parent: {parent},\n owner: {},\n is root: {},\n bindings:\n {},\n snapshots:\n {}\n}}",
"Env {{\n parent: {parent},\n owner: {},\n ref'ed?: {},\n bindings:\n {}\n}}",
self.owner.load(Ordering::Relaxed),
self.is_root_env,
self.might_be_refed.load(Ordering::Relaxed),
data.join("\n "),
snapshots.join("\n ")
)
}
}
#[derive(Debug, Clone, PartialEq)]
struct Snapshot {
/// The version of the owning environment's parent environment corresponding to this snapshot.
parent_snapshot: Option<SnapshotRef>,
/// CoW'ed data from the environment.
data: IndexMap<String, KclValue>,
}
impl fmt::Display for Snapshot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let parent = self.parent_snapshot.map(|s| s.0.to_string()).unwrap_or("_".to_owned());
let data: Vec<String> = self
.data
.iter()
.map(|(k, v)| format!("{k}: {}", v.human_friendly_type()))
.collect();
write!(
f,
"Snapshot {{\n parent: {parent},\n data: {},\n }}",
data.join("\n ")
)
}
}
@ -977,80 +919,47 @@ mod env {
impl Environment {
/// Create a new environment, parent points to it's surrounding lexical scope or the std
/// env if it's a root scope.
pub(super) fn new(parent: Option<EnvironmentRef>, is_root_env: bool, owner: usize) -> Self {
pub(super) fn new(parent: Option<EnvironmentRef>, might_be_refed: bool, owner: usize) -> Self {
assert!(parent.map(|p| p.is_regular()).unwrap_or(true));
Self {
bindings: UnsafeCell::new(IndexMap::new()),
snapshots: UnsafeCell::new(Vec::new()),
parent,
is_root_env,
might_be_refed: AtomicBool::new(might_be_refed),
owner: AtomicUsize::new(owner),
_unpin: PhantomPinned,
}
}
// Mark this env as read-only (see module docs).
/// Mark this env as read-only (see module docs).
pub(super) fn read_only(&self) {
self.owner.store(0, Ordering::Release);
}
// Mark this env as owned (see module docs).
/// Mark this env as owned (see module docs).
pub(super) fn restore_owner(&self, owner: usize) {
self.owner.store(owner, Ordering::Release);
}
// SAFETY: either the owner of the env is on the Rust stack or the env is read-only.
fn snapshots_len(&self) -> usize {
unsafe { self.snapshots.get().as_ref().unwrap().len() }
/// Mark this environment as possibly having external references.
pub(super) fn mark_as_refed(&self) {
self.might_be_refed.store(true, Ordering::Release);
}
// SAFETY: either the owner of the env is on the Rust stack or the env is read-only.
fn get_shapshot(&self, index: usize) -> &Snapshot {
unsafe { &self.snapshots.get().as_ref().unwrap()[index] }
}
// SAFETY: either the owner of the env is on the Rust stack or the env is read-only.
fn iter_snapshots(&self) -> impl Iterator<Item = &Snapshot> {
unsafe { self.snapshots.get().as_ref().unwrap().iter() }
}
fn cur_snapshot(&self, owner: usize) -> Option<&mut Snapshot> {
assert!(owner > 0 && self.owner.load(Ordering::Acquire) == owner);
unsafe { self.snapshots.get().as_mut().unwrap().last_mut() }
}
// SAFETY: either the owner of the env is on the Rust stack or the env is read-only.
fn get_bindings(&self) -> &IndexMap<String, KclValue> {
fn get_bindings(&self) -> &IndexMap<String, (usize, KclValue)> {
unsafe { self.bindings.get().as_ref().unwrap() }
}
// SAFETY do not call this function while a previous mutable reference is live
#[allow(clippy::mut_from_ref)]
fn get_mut_bindings(&self, owner: usize) -> &mut IndexMap<String, KclValue> {
fn get_mut_bindings(&self, owner: usize) -> &mut IndexMap<String, (usize, KclValue)> {
assert!(owner > 0 && self.owner.load(Ordering::Acquire) == owner);
unsafe { self.bindings.get().as_mut().unwrap() }
}
// True if the env is empty and not a root env.
// True if the env is empty and has no external references.
pub(super) fn is_empty(&self) -> bool {
self.snapshots_len() == 0 && self.get_bindings().is_empty() && !self.is_root_env
}
fn push_snapshot(&self, parent: Option<SnapshotRef>, owner: usize) -> SnapshotRef {
let env_owner = self.owner.load(Ordering::Acquire);
// The env is read-only, no need to snapshot.
if env_owner == 0 {
return SnapshotRef::none();
}
assert!(
owner > 0 && env_owner == owner,
"mutating owner: {owner}, env: {self}({env_owner})"
);
unsafe {
let snapshots = self.snapshots.get().as_mut().unwrap();
snapshots.push(Snapshot::new(parent));
SnapshotRef(snapshots.len())
}
self.get_bindings().is_empty() && !self.might_be_refed.load(Ordering::Acquire)
}
/// Possibly compress this environment by deleting the memory.
@ -1062,116 +971,61 @@ mod env {
/// See module docs for more details.
pub(super) fn compact(&self, owner: usize) {
// Don't compress if there might be a closure or import referencing us.
if self.snapshots_len() != 0 || self.is_root_env {
if self.might_be_refed.load(Ordering::Acquire) {
return;
}
*self.get_mut_bindings(owner) = IndexMap::new();
}
pub(super) fn get(
&self,
key: &str,
snapshot: SnapshotRef,
owner: usize,
) -> Result<&KclValue, Option<EnvironmentRef>> {
pub(super) fn get(&self, key: &str, epoch: usize, owner: usize) -> Result<&KclValue, Option<EnvironmentRef>> {
let env_owner = self.owner.load(Ordering::Acquire);
assert!(env_owner == 0 || env_owner == owner);
self.get_unchecked(key, snapshot)
self.get_unchecked(key, epoch)
}
/// Get a value from memory without checking the env's ownership invariant. Prefer to use `get`.
pub(super) fn get_unchecked(
&self,
key: &str,
snapshot: SnapshotRef,
) -> Result<&KclValue, Option<EnvironmentRef>> {
if snapshot.is_some() {
for i in snapshot.index()..self.snapshots_len() {
match self.get_shapshot(i).data.get(key) {
Some(KclValue::Tombstone { .. }) => return Err(self.parent(snapshot)),
Some(v) => return Ok(v),
None => {}
}
}
}
pub(super) fn get_unchecked(&self, key: &str, epoch: usize) -> Result<&KclValue, Option<EnvironmentRef>> {
self.get_bindings()
.get(key)
.and_then(|v| match v {
KclValue::Tombstone { .. } => None,
_ => Some(v),
})
.ok_or(self.parent(snapshot))
.and_then(|(e, v)| if *e <= epoch { Some(v) } else { None })
.ok_or(self.parent)
}
/// Find the `EnvironmentRef` of the parent of this environment corresponding to the specified snapshot.
pub(super) fn parent(&self, snapshot: SnapshotRef) -> Option<EnvironmentRef> {
if snapshot.is_none() {
return self.parent;
}
pub(super) fn update(&self, key: &str, f: impl Fn(&mut KclValue, usize), epoch: usize, owner: usize) {
let Some((_, value)) = self.get_mut_bindings(owner).get_mut(key) else {
debug_assert!(false, "Missing memory entry for {key}");
return;
};
match self.get_shapshot(snapshot.index()).parent_snapshot {
Some(sr) => Some(EnvironmentRef(self.parent.unwrap().0, sr)),
None => self.parent,
}
f(value, epoch);
}
/// Iterate over all values in the environment at the specified snapshot.
pub(super) fn values<'a>(&'a self, snapshot: SnapshotRef) -> Box<dyn Iterator<Item = &'a KclValue> + 'a> {
if snapshot.is_none() {
return Box::new(self.get_bindings().values());
}
pub(super) fn parent(&self) -> Option<EnvironmentRef> {
self.parent
}
/// Iterate over all values in the environment at the specified epoch.
pub(super) fn values<'a>(&'a self, epoch: usize) -> Box<dyn Iterator<Item = &'a KclValue> + 'a> {
Box::new(
self.get_bindings()
.iter()
.filter_map(move |(k, v)| {
(!self.snapshot_contains_key(k, snapshot) && !matches!(v, KclValue::Tombstone { .. }))
.then_some(v)
})
.chain(
self.iter_snapshots()
.flat_map(|s| s.data.values().filter(|v| !matches!(v, KclValue::Tombstone { .. }))),
),
.values()
.filter_map(move |(e, v)| (*e <= epoch).then_some(v)),
)
}
/// Pure insert, panics if `key` is already in this environment.
///
/// Precondition: !self.contains_key(key)
pub(super) fn insert(&self, key: String, value: KclValue, owner: usize) {
pub(super) fn insert(&self, key: String, epoch: usize, value: KclValue, owner: usize) {
debug_assert!(!self.get_bindings().contains_key(&key));
if let Some(s) = self.cur_snapshot(owner) {
s.data.insert(key.clone(), tombstone());
}
self.get_mut_bindings(owner).insert(key, value);
}
pub(super) fn insert_or_update(&self, key: String, value: KclValue, owner: usize) {
if let Some(s) = self.cur_snapshot(owner) {
if !s.data.contains_key(&key) {
let old_value = self.get_bindings().get(&key).cloned().unwrap_or_else(tombstone);
s.data.insert(key.clone(), old_value);
}
}
self.get_mut_bindings(owner).insert(key, value);
}
/// Was the key contained in this environment at the specified point in time.
fn snapshot_contains_key(&self, key: &str, snapshot: SnapshotRef) -> bool {
for i in snapshot.index()..self.snapshots_len() {
if self.get_shapshot(i).data.contains_key(key) {
return true;
}
}
false
self.get_mut_bindings(owner).insert(key, (epoch, value));
}
/// Is the key currently contained in this environment.
pub(super) fn contains_key(&self, key: &str) -> bool {
!matches!(self.get_bindings().get(key), Some(KclValue::Tombstone { .. }) | None)
self.get_bindings().contains_key(key)
}
/// Iterate over all key/value pairs currently in this environment where the value satisfies
@ -1186,61 +1040,14 @@ mod env {
self.get_bindings()
.iter()
.filter(move |(_, v)| f(v) && !matches!(v, KclValue::Tombstone { .. }))
.filter_map(move |(k, (_, v))| f(v).then_some((k, v)))
}
/// Take all bindings from the environment.
pub(super) fn take_bindings(self: Pin<&mut Self>) -> impl Iterator<Item = (String, KclValue)> {
pub(super) fn take_bindings(self: Pin<&mut Self>) -> impl Iterator<Item = (String, (usize, KclValue))> {
// SAFETY: caller must have unique access since self is mut. We're not moving or invalidating `self`.
let bindings = std::mem::take(unsafe { self.bindings.get().as_mut().unwrap() });
bindings
.into_iter()
.filter(move |(_, v)| !matches!(v, KclValue::Tombstone { .. }))
}
/// Returns an iterator over any snapshots in this environment, returning the ref to the
/// snapshot and its parent.
pub(super) fn snapshot_parents(&self) -> impl Iterator<Item = (SnapshotRef, SnapshotRef)> + '_ {
self.iter_snapshots()
.enumerate()
.map(|(i, s)| (SnapshotRef(i + 1), s.parent_snapshot.unwrap()))
}
}
impl Snapshot {
fn new(parent_snapshot: Option<SnapshotRef>) -> Self {
Snapshot {
parent_snapshot,
data: IndexMap::new(),
}
}
}
/// Build a new snapshot of the specified environment at the current moment.
///
/// This is non-trival since we have to build the tree of parent snapshots.
pub(super) fn snapshot(mem: &ProgramMemory, env_ref: EnvironmentRef, owner: usize) -> SnapshotRef {
let env = mem.get_env(env_ref.index());
let parent_snapshot = env.parent.map(|p| snapshot(mem, p, owner));
let env = mem.get_env(env_ref.index());
if env.snapshots_len() == 0 {
return env.push_snapshot(parent_snapshot, owner);
}
let prev_snapshot = env.cur_snapshot(owner).unwrap();
if prev_snapshot.data.is_empty() && prev_snapshot.parent_snapshot == parent_snapshot {
// If the prev snapshot is empty, reuse it.
return SnapshotRef(env.snapshots_len());
}
env.push_snapshot(parent_snapshot, owner)
}
fn tombstone() -> KclValue {
KclValue::Tombstone {
value: (),
meta: Vec::new(),
bindings.into_iter()
}
}
}
@ -1270,16 +1077,9 @@ mod test {
}
}
fn expect_small_number(value: &KclValue) -> Option<i64> {
match value {
KclValue::Number { value, .. } if value > &0.0 && value < &10.0 => Some(*value as i64),
_ => None,
}
}
#[track_caller]
fn assert_get_from(mem: &Stack, key: &str, n: i64, snapshot: EnvironmentRef) {
match mem.memory.get_from_unchecked(key, snapshot, sr()).unwrap() {
match mem.memory.get_from_unchecked(key, snapshot).unwrap() {
KclValue::Number { value, .. } => assert_eq!(*value as i64, n),
_ => unreachable!(),
}
@ -1318,7 +1118,7 @@ mod test {
assert_get(mem, "a", 1);
mem.add("b".to_owned(), val(3), sr()).unwrap();
assert_get(mem, "b", 3);
mem.memory.get_from_unchecked("b", sn, sr()).unwrap_err();
mem.memory.get_from_unchecked("b", sn).unwrap_err();
}
#[test]
@ -1337,11 +1137,11 @@ mod test {
assert_get(mem, "b", 3);
assert_get(mem, "c", 6);
assert_get_from(mem, "a", 1, sn1);
mem.memory.get_from_unchecked("b", sn1, sr()).unwrap_err();
mem.memory.get_from_unchecked("c", sn1, sr()).unwrap_err();
mem.memory.get_from_unchecked("b", sn1).unwrap_err();
mem.memory.get_from_unchecked("c", sn1).unwrap_err();
assert_get_from(mem, "a", 1, sn2);
assert_get_from(mem, "b", 3, sn2);
mem.memory.get_from_unchecked("c", sn2, sr()).unwrap_err();
mem.memory.get_from_unchecked("c", sn2).unwrap_err();
}
#[test]
@ -1481,7 +1281,7 @@ mod test {
mem.pop_env();
// old snapshot still untouched
mem.memory.get_from_unchecked("b", sn, sr()).unwrap_err();
mem.memory.get_from_unchecked("b", sn).unwrap_err();
}
#[test]
@ -1503,62 +1303,22 @@ mod test {
mem.pop_env();
// old snapshots still untouched
mem.memory.get_from_unchecked("b", sn1, sr()).unwrap_err();
mem.memory.get_from_unchecked("b", sn1).unwrap_err();
assert_get_from(mem, "b", 3, sn2);
mem.memory.get_from_unchecked("c", sn2, sr()).unwrap_err();
mem.memory.get_from_unchecked("c", sn2).unwrap_err();
assert_get_from(mem, "b", 4, sn3);
mem.memory.get_from_unchecked("c", sn3, sr()).unwrap_err();
}
#[test]
fn snap_env_two_updates() {
let mem = &mut Stack::new_for_tests();
mem.add("a".to_owned(), val(1), sr()).unwrap();
let sn1 = mem.snapshot();
mem.add("b".to_owned(), val(3), sr()).unwrap();
let sn2 = mem.snapshot();
let callee_env = mem.current_env.0;
mem.push_new_env_for_call(sn2);
let sn3 = mem.snapshot();
mem.add("b".to_owned(), val(4), sr()).unwrap();
let sn4 = mem.snapshot();
mem.insert_or_update("b".to_owned(), val(6));
mem.memory.update_with_env("b", val(7), callee_env, mem.id);
assert_get(mem, "b", 6);
assert_get_from(mem, "b", 3, sn3);
assert_get_from(mem, "b", 4, sn4);
let vals: Vec<_> = mem.walk_call_stack().filter_map(expect_small_number).collect();
let expected = [6, 1, 3, 1, 7];
assert_eq!(vals, expected);
let popped = mem.pop_env();
assert_get(mem, "b", 7);
mem.memory.get_from_unchecked("b", sn1, sr()).unwrap_err();
assert_get_from(mem, "b", 3, sn2);
let vals: Vec<_> = mem.walk_call_stack().filter_map(expect_small_number).collect();
let expected = [1, 7];
assert_eq!(vals, expected);
let popped_env = mem.memory.get_env(popped.index());
let sp: Vec<_> = popped_env.snapshot_parents().collect();
assert_eq!(
sp,
vec![(SnapshotRef(1), SnapshotRef(2)), (SnapshotRef(2), SnapshotRef(2))]
);
mem.memory.get_from_unchecked("c", sn3).unwrap_err();
}
#[test]
fn squash_env() {
let mem = &mut Stack::new_for_tests();
mem.add("a".to_owned(), val(1), sr()).unwrap();
mem.add("b".to_owned(), val(3), sr()).unwrap();
let sn1 = mem.snapshot();
mem.push_new_env_for_call(sn1);
mem.add("b".to_owned(), val(2), sr()).unwrap();
let sn2 = mem.snapshot();
mem.add(
"f".to_owned(),
@ -1581,11 +1341,10 @@ mod test {
KclValue::Function {
value: FunctionSource::User { memory, .. },
..
} if memory == &sn1 => {}
v => panic!("{v:#?}"),
} if memory.0 == mem.current_env.0 => {}
v => panic!("{v:#?}, expected {sn1:?}"),
}
assert_eq!(mem.memory.envs().len(), 1);
assert_eq!(mem.current_env, EnvironmentRef(0, SnapshotRef(0)));
assert_eq!(mem.memory.envs().len(), 2);
}
#[test]

View File

@ -10,6 +10,7 @@ use cache::OldAstState;
pub use cache::{bust_cache, clear_mem_cache};
pub use cad_op::Operation;
pub use geometry::*;
pub use id_generator::IdGenerator;
pub(crate) use import::{
import_foreign, send_to_engine as send_import_to_engine, PreImportedGeometry, ZOO_COORD_SYSTEM,
};
@ -25,7 +26,7 @@ use kittycad_modeling_cmds as kcmc;
pub use memory::EnvironmentRef;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub use state::{ExecState, IdGenerator, MetaSettings};
pub use state::{ExecState, MetaSettings};
use tokio::sync::RwLock;
use crate::{
@ -50,6 +51,7 @@ pub(crate) mod cache;
mod cad_op;
mod exec_ast;
mod geometry;
mod id_generator;
mod import;
pub(crate) mod kcl_value;
mod memory;
@ -73,6 +75,8 @@ pub struct ExecOutcome {
pub errors: Vec<CompilationError>,
/// File Names in module Id array index order
pub filenames: IndexMap<ModuleId, ModulePath>,
/// The default planes.
pub default_planes: Option<DefaultPlanes>,
}
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
@ -92,11 +96,46 @@ pub struct DefaultPlanes {
#[serde(tag = "type", rename_all = "camelCase")]
pub struct TagIdentifier {
pub value: String,
pub info: Option<TagEngineInfo>,
#[serde(rename = "__meta")]
// Multi-version representation of info about the tag. Kept ordered. The usize is the epoch at which the info
// was written. Note that there might be multiple versions of tag info from the same epoch, the version with
// the higher index will be the most recent.
#[serde(skip)]
pub info: Vec<(usize, TagEngineInfo)>,
#[serde(skip)]
pub meta: Vec<Metadata>,
}
impl TagIdentifier {
/// Get the tag info for this tag at a specified epoch.
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
}
/// Get the most recent tag info for this tag.
pub fn get_cur_info(&self) -> Option<&TagEngineInfo> {
self.info.last().map(|i| &i.1)
}
/// Add info from a different instance of this tag.
pub fn merge_info(&mut self, other: &TagIdentifier) {
assert_eq!(&self.value, &other.value);
'new_info: for (oe, ot) in &other.info {
for (e, _) in &self.info {
if e > oe {
continue 'new_info;
}
}
self.info.push((*oe, ot.clone()));
}
}
}
impl Eq for TagIdentifier {}
impl std::fmt::Display for TagIdentifier {
@ -111,7 +150,7 @@ impl std::str::FromStr for TagIdentifier {
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self {
value: s.to_string(),
info: None,
info: Vec::new(),
meta: Default::default(),
})
}
@ -500,7 +539,7 @@ impl ExecutorContext {
source_range: crate::execution::SourceRange,
) -> Result<(), KclError> {
self.engine
.clear_scene(&mut exec_state.global.id_generator, source_range)
.clear_scene(&mut exec_state.mod_local.id_generator, source_range)
.await
}
@ -519,7 +558,7 @@ impl ExecutorContext {
) -> Result<ExecOutcome, KclErrorWithOutputs> {
assert!(self.is_mock());
let mut exec_state = ExecState::new(&self.settings);
let mut exec_state = ExecState::new(self);
if use_prev_memory {
match cache::read_old_memory().await {
Some(mem) => *exec_state.mut_stack() = mem,
@ -540,7 +579,7 @@ impl ExecutorContext {
// memory, not to the exec_state which is not cached for mock execution.
let mut mem = exec_state.stack().clone();
let outcome = exec_state.to_mock_wasm_outcome(result.0);
let outcome = exec_state.to_mock_wasm_outcome(result.0).await;
mem.squash_env(result.0);
cache::write_old_memory(mem).await;
@ -608,13 +647,13 @@ impl ExecutorContext {
})
.await;
let outcome = old_state.to_wasm_outcome(result_env);
let outcome = old_state.to_wasm_outcome(result_env).await;
return Ok(outcome);
}
(true, program)
}
CacheResult::NoAction(false) => {
let outcome = old_state.to_wasm_outcome(result_env);
let outcome = old_state.to_wasm_outcome(result_env).await;
return Ok(outcome);
}
};
@ -622,7 +661,7 @@ impl ExecutorContext {
let (exec_state, preserve_mem) = if clear_scene {
// Pop the execution state, since we are starting fresh.
let mut exec_state = old_state;
exec_state.reset(&self.settings);
exec_state.reset(self);
// We don't do this in mock mode since there is no engine connection
// anyways and from the TS side we override memory and don't want to clear it.
@ -639,7 +678,7 @@ impl ExecutorContext {
(program, exec_state, preserve_mem)
} else {
let mut exec_state = ExecState::new(&self.settings);
let mut exec_state = ExecState::new(self);
self.send_clear_scene(&mut exec_state, Default::default())
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
@ -664,7 +703,7 @@ impl ExecutorContext {
})
.await;
let outcome = exec_state.to_wasm_outcome(result.0);
let outcome = exec_state.to_wasm_outcome(result.0).await;
Ok(outcome)
}
@ -700,6 +739,7 @@ impl ExecutorContext {
.await
.map_err(KclErrorWithOutputs::no_outputs)?;
let default_planes = self.engine.get_default_planes().read().await.clone();
let env_ref = self
.execute_and_build_graph(&program.ast, exec_state, preserve_mem)
.await
@ -718,6 +758,7 @@ impl ExecutorContext {
exec_state.global.artifact_graph.clone(),
module_id_to_module_path,
exec_state.global.id_to_source.clone(),
default_planes,
)
})?;
@ -725,6 +766,7 @@ impl ExecutorContext {
"Post interpretation KCL memory stats: {:#?}",
exec_state.stack().memory.stats
));
crate::log::log(format!("Engine stats: {:?}", self.engine.stats()));
if !self.is_mock() {
let mut mem = exec_state.stack().deep_clone();
@ -755,6 +797,7 @@ impl ExecutorContext {
exec_state,
ExecutionKind::Normal,
preserve_mem,
ModuleId::default(),
&ModulePath::Main,
)
.await;
@ -890,38 +933,24 @@ impl ExecutorContext {
&self,
deterministic_time: bool,
) -> Result<Vec<kittycad_modeling_cmds::websocket::RawFile>, KclError> {
let mut files = self
let files = self
.export(kittycad_modeling_cmds::format::OutputFormat3d::Step(
kittycad_modeling_cmds::format::step::export::Options {
coords: *kittycad_modeling_cmds::coord::KITTYCAD,
created: None,
created: if deterministic_time {
Some("2021-01-01T00:00:00Z".parse().map_err(|e| {
KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to parse date: {}", e),
source_ranges: vec![SourceRange::default()],
})
})?)
} else {
None
},
},
))
.await?;
if deterministic_time {
for kittycad_modeling_cmds::websocket::RawFile { contents, .. } in &mut files {
use std::fmt::Write;
let utf8 = std::str::from_utf8(contents).unwrap();
let mut postprocessed = String::new();
for line in utf8.lines() {
if line.starts_with("FILE_NAME") {
let name = "test.step";
let time = "2021-01-01T00:00:00Z";
let author = "Test";
let org = "Zoo";
let version = "zoo.dev beta";
let system = "zoo.dev";
let authorization = "Test";
writeln!(&mut postprocessed, "FILE_NAME('{name}', '{time}', ('{author}'), ('{org}'), '{version}', '{system}', '{authorization}');").unwrap();
} else {
writeln!(&mut postprocessed, "{line}").unwrap();
}
}
*contents = postprocessed.into_bytes();
}
}
Ok(files)
}
@ -948,7 +977,7 @@ pub(crate) async fn parse_execute(code: &str) -> Result<ExecTestResults, KclErro
settings: Default::default(),
context_type: ContextType::Mock,
};
let mut exec_state = ExecState::new(&exec_ctxt.settings);
let mut exec_state = ExecState::new(&exec_ctxt);
let result = exec_ctxt.run(&program, &mut exec_state).await?;
Ok(ExecTestResults {
@ -978,11 +1007,7 @@ mod tests {
/// Convenience function to get a JSON value from memory and unwrap.
#[track_caller]
fn mem_get_json(memory: &Stack, env: EnvironmentRef, name: &str) -> KclValue {
memory
.memory
.get_from_unchecked(name, env, SourceRange::default())
.unwrap()
.to_owned()
memory.memory.get_from_unchecked(name, env).unwrap().to_owned()
}
#[tokio::test(flavor = "multi_thread")]
@ -1864,15 +1889,6 @@ let w = f() + f()
parse_execute(ast).await.unwrap();
}
#[test]
fn test_serialize_memory_item() {
let mem = KclValue::Solids {
value: Default::default(),
};
let json = serde_json::to_string(&mem).unwrap();
assert_eq!(json, r#"{"type":"Solids","value":[]}"#);
}
#[tokio::test(flavor = "multi_thread")]
async fn kcl_test_ids_stable_between_executions() {
let code = r#"sketch001 = startSketchOn(XZ)
@ -1895,10 +1911,14 @@ let w = f() + f()
let old_program = crate::Program::parse_no_errs(code).unwrap();
// Execute the program.
ctx.run_with_caching(old_program).await.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);
}
// Get the id_generator from the first execution.
let id_generator = cache::read_old_ast().await.unwrap().exec_state.global.id_generator;
let id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator;
let code = r#"sketch001 = startSketchOn(XZ)
|> startProfileAt([62.74, 206.13], %)
@ -1919,7 +1939,7 @@ let w = f() + f()
// Execute the program.
ctx.run_with_caching(program).await.unwrap();
let new_id_generator = cache::read_old_ast().await.unwrap().exec_state.global.id_generator;
let new_id_generator = cache::read_old_ast().await.unwrap().exec_state.mod_local.id_generator;
assert_eq!(id_generator, new_id_generator);
}
@ -1948,7 +1968,6 @@ let w = f() + f()
// Execute the program.
ctx.run_with_caching(old_program.clone()).await.unwrap();
// Get the id_generator from the first execution.
let settings_state = cache::read_old_ast().await.unwrap().settings;
// Ensure the settings are as expected.
@ -1960,7 +1979,6 @@ let w = f() + f()
// Execute the program.
ctx.run_with_caching(old_program.clone()).await.unwrap();
// Get the id_generator from the first execution.
let settings_state = cache::read_old_ast().await.unwrap().settings;
// Ensure the settings are as expected.
@ -1972,7 +1990,6 @@ let w = f() + f()
// Execute the program.
ctx.run_with_caching(old_program).await.unwrap();
// Get the id_generator from the first execution.
let settings_state = cache::read_old_ast().await.unwrap().settings;
// Ensure the settings are as expected.
@ -1991,4 +2008,41 @@ let w = f() + f()
let result = ctx2.run_mock(program2, true).await.unwrap();
assert_eq!(result.variables.get("z").unwrap().as_f64().unwrap(), 3.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn read_tag_version() {
let ast = r#"fn bar(t) {
return startSketchOn(XY)
|> startProfileAt([0,0], %)
|> angledLine({
angle = -60,
length = segLen(t),
}, %)
|> line(end = [0, 0])
|> close()
}
sketch = startSketchOn(XY)
|> startProfileAt([0,0], %)
|> line(end = [0, 10])
|> line(end = [10, 0], tag = $tag0)
|> line(end = [0, 0])
fn foo() {
// tag0 tags an edge
return bar(tag0)
}
solid = sketch |> extrude(length = 10)
// tag0 tags a face
sketch2 = startSketchOn(solid, tag0)
|> startProfileAt([0,0], %)
|> line(end = [0, 1])
|> line(end = [1, 0])
|> line(end = [0, 0])
foo() |> extrude(length = 1)
"#;
parse_execute(ast).await.unwrap();
}
}

View File

@ -10,7 +10,9 @@ use uuid::Uuid;
use crate::{
errors::{KclError, KclErrorDetails, Severity},
execution::{
annotations, kcl_value,
annotations,
id_generator::IdGenerator,
kcl_value,
memory::{ProgramMemory, Stack},
Artifact, ArtifactCommand, ArtifactGraph, ArtifactId, EnvironmentRef, ExecOutcome, ExecutorSettings, KclValue,
Operation, UnitAngle, UnitLen,
@ -26,12 +28,11 @@ use crate::{
pub struct ExecState {
pub(super) global: GlobalState,
pub(super) mod_local: ModuleState,
pub(super) exec_context: Option<super::ExecutorContext>,
}
#[derive(Debug, Clone)]
pub(super) struct GlobalState {
/// The stable artifact ID generator.
pub id_generator: IdGenerator,
/// Map from source file absolute path to module ID.
pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
/// Map from module ID to source file.
@ -62,6 +63,8 @@ pub(super) struct GlobalState {
#[derive(Debug, Clone)]
pub(super) struct ModuleState {
/// The id generator for this module.
pub id_generator: IdGenerator,
pub stack: Stack,
/// The current value of the pipe operator returned from the previous
/// expression. If we're not currently in a pipeline, this will be None.
@ -73,25 +76,21 @@ pub(super) struct ModuleState {
}
impl ExecState {
pub fn new(exec_settings: &ExecutorSettings) -> Self {
pub fn new(exec_context: &super::ExecutorContext) -> Self {
ExecState {
global: GlobalState::new(exec_settings),
mod_local: ModuleState::new(exec_settings, None, ProgramMemory::new()),
global: GlobalState::new(&exec_context.settings),
mod_local: ModuleState::new(&exec_context.settings, None, ProgramMemory::new(), Default::default()),
exec_context: Some(exec_context.clone()),
}
}
pub(super) fn reset(&mut self, exec_settings: &ExecutorSettings) {
let mut id_generator = self.global.id_generator.clone();
// We do not pop the ids, since we want to keep the same id generator.
// This is for the front end to keep track of the ids.
id_generator.next_id = 0;
let mut global = GlobalState::new(exec_settings);
global.id_generator = id_generator;
pub(super) fn reset(&mut self, exec_context: &super::ExecutorContext) {
let global = GlobalState::new(&exec_context.settings);
*self = ExecState {
global,
mod_local: ModuleState::new(exec_settings, None, ProgramMemory::new()),
mod_local: ModuleState::new(&exec_context.settings, None, ProgramMemory::new(), Default::default()),
exec_context: Some(exec_context.clone()),
};
}
@ -113,13 +112,13 @@ impl ExecState {
/// Convert to execution outcome when running in WebAssembly. We want to
/// reduce the amount of data that crosses the WASM boundary as much as
/// possible.
pub fn to_wasm_outcome(self, main_ref: EnvironmentRef) -> ExecOutcome {
pub async fn to_wasm_outcome(self, main_ref: EnvironmentRef) -> ExecOutcome {
// Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState.
ExecOutcome {
variables: self
.stack()
.find_all_in_env(main_ref, |_| true)
.find_all_in_env(main_ref)
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
operations: self.global.operations,
@ -132,16 +131,21 @@ impl ExecState {
.iter()
.map(|(k, v)| ((*v), k.clone()))
.collect(),
default_planes: if let Some(ctx) = &self.exec_context {
ctx.engine.get_default_planes().read().await.clone()
} else {
None
},
}
}
pub fn to_mock_wasm_outcome(self, main_ref: EnvironmentRef) -> ExecOutcome {
pub async fn to_mock_wasm_outcome(self, main_ref: EnvironmentRef) -> ExecOutcome {
// Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState.
ExecOutcome {
variables: self
.stack()
.find_all_in_env(main_ref, |_| true)
.find_all_in_env(main_ref)
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
operations: Default::default(),
@ -149,6 +153,11 @@ impl ExecState {
artifact_graph: Default::default(),
errors: self.global.errors,
filenames: Default::default(),
default_planes: if let Some(ctx) = &self.exec_context {
ctx.engine.get_default_planes().read().await.clone()
} else {
None
},
}
}
@ -160,8 +169,12 @@ impl ExecState {
&mut self.mod_local.stack
}
pub(crate) fn next_uuid(&mut self) -> Uuid {
self.global.id_generator.next_uuid()
pub fn next_uuid(&mut self) -> Uuid {
self.mod_local.id_generator.next_uuid()
}
pub fn id_generator(&mut self) -> &mut IdGenerator {
&mut self.mod_local.id_generator
}
pub(crate) fn add_artifact(&mut self, artifact: Artifact) {
@ -245,7 +258,6 @@ impl ExecState {
impl GlobalState {
fn new(settings: &ExecutorSettings) -> Self {
let mut global = GlobalState {
id_generator: Default::default(),
path_to_source_id: Default::default(),
module_infos: Default::default(),
artifacts: Default::default(),
@ -278,8 +290,14 @@ impl GlobalState {
}
impl ModuleState {
pub(super) fn new(exec_settings: &ExecutorSettings, std_path: Option<String>, memory: Arc<ProgramMemory>) -> Self {
pub(super) fn new(
exec_settings: &ExecutorSettings,
std_path: Option<String>,
memory: Arc<ProgramMemory>,
module_id: Option<ModuleId>,
) -> Self {
ModuleState {
id_generator: IdGenerator::new(module_id),
stack: memory.new_stack(),
pipe_value: Default::default(),
module_exports: Default::default(),
@ -336,29 +354,3 @@ impl MetaSettings {
Ok(())
}
}
/// A generator for ArtifactIds that can be stable across executions.
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct IdGenerator {
pub(super) next_id: usize,
ids: Vec<uuid::Uuid>,
}
impl IdGenerator {
pub fn new() -> Self {
Self::default()
}
pub fn next_uuid(&mut self) -> uuid::Uuid {
if let Some(id) = self.ids.get(self.next_id) {
self.next_id += 1;
*id
} else {
let id = uuid::Uuid::new_v4();
self.ids.push(id);
self.next_id += 1;
id
}
}
}

View File

@ -8,11 +8,11 @@
#[allow(unused_macros)]
macro_rules! println {
($($rest:tt)*) => {
#[cfg(feature = "disable-println")]
#[cfg(all(feature = "disable-println", not(test)))]
{
let _ = format!($($rest)*);
}
#[cfg(not(feature = "disable-println"))]
#[cfg(any(not(feature = "disable-println"), test))]
std::println!($($rest)*)
}
}
@ -20,11 +20,11 @@ macro_rules! println {
#[allow(unused_macros)]
macro_rules! eprintln {
($($rest:tt)*) => {
#[cfg(feature = "disable-println")]
#[cfg(all(feature = "disable-println", not(test)))]
{
let _ = format!($($rest)*);
}
#[cfg(not(feature = "disable-println"))]
#[cfg(any(not(feature = "disable-println"), test))]
std::eprintln!($($rest)*)
}
}
@ -32,11 +32,11 @@ macro_rules! eprintln {
#[allow(unused_macros)]
macro_rules! print {
($($rest:tt)*) => {
#[cfg(feature = "disable-println")]
#[cfg(all(feature = "disable-println", not(test)))]
{
let _ = format!($($rest)*);
}
#[cfg(not(feature = "disable-println"))]
#[cfg(any(not(feature = "disable-println"), test))]
std::print!($($rest)*)
}
}
@ -44,11 +44,11 @@ macro_rules! print {
#[allow(unused_macros)]
macro_rules! eprint {
($($rest:tt)*) => {
#[cfg(feature = "disable-println")]
#[cfg(all(feature = "disable-println", not(test)))]
{
let _ = format!($($rest)*);
}
#[cfg(not(feature = "disable-println"))]
#[cfg(any(not(feature = "disable-println"), test))]
std::eprint!($($rest)*)
}
}
@ -81,7 +81,7 @@ pub mod walk;
mod wasm;
pub use coredump::CoreDump;
pub use engine::{EngineManager, ExecutionKind};
pub use engine::{EngineManager, EngineStats, ExecutionKind};
pub use errors::{
CompilationError, ConnectionError, ExecError, KclError, KclErrorWithOutputs, Report, ReportWithOutputs,
};
@ -96,6 +96,8 @@ pub use modules::ModuleId;
pub use parsing::ast::{modify::modify_ast_for_sketch, types::FormatOptions};
pub use settings::types::{project::ProjectConfiguration, Configuration, UnitLength};
pub use source_range::SourceRange;
#[cfg(not(target_arch = "wasm32"))]
pub use unparser::recast_dir;
// Rather than make executor public and make lots of it pub(crate), just re-export into a new module.
// Ideally we wouldn't export these things at all, they should only be used for testing.
@ -108,10 +110,14 @@ pub mod wasm_engine {
pub use crate::{
coredump::wasm::{CoreDumpManager, CoreDumper},
engine::conn_wasm::{EngineCommandManager, EngineConnection},
fs::wasm::FileSystemManager,
fs::wasm::{FileManager, FileSystemManager},
};
}
pub mod mock_engine {
pub use crate::engine::conn_mock::EngineConnection;
}
#[cfg(not(target_arch = "wasm32"))]
pub mod native_engine {
pub use crate::engine::conn::EngineConnection;

View File

@ -36,7 +36,7 @@ macro_rules! logln {
}
pub(crate) use logln;
#[cfg(all(not(feature = "disable-println"), not(target_arch = "wasm32")))]
#[cfg(any(test, all(not(feature = "disable-println"), not(target_arch = "wasm32"))))]
#[inline]
fn log_inner(msg: String) {
eprintln!("{msg}");
@ -48,7 +48,7 @@ fn log_inner(msg: String) {
web_sys::console::log_1(&msg.into());
}
#[cfg(feature = "disable-println")]
#[cfg(all(feature = "disable-println", not(test)))]
#[inline]
fn log_inner(_msg: String) {}

View File

@ -1170,7 +1170,7 @@ impl LanguageServer for Backend {
Hover::Variable { name, ty: None, range } => Ok(with_cached_var(&name, |value| {
let mut text: String = format!("```\n{}", name);
if let Some(ty) = value.principal_type() {
text.push_str(&format!(": {}", ty));
text.push_str(&format!(": {}", ty.human_friendly_type()));
}
if let Some(v) = value.value_str() {
text.push_str(&format!(" = {}", v));

View File

@ -11,7 +11,13 @@ use kcl_lib::{ExecState, ExecutorContext, ExecutorSettings, Program};
async fn main() {
let mut args = env::args();
args.next();
let filename = args.next().unwrap_or_else(|| "main.kcl".to_owned());
let mut filename = args.next().unwrap_or_else(|| "main.kcl".to_owned());
if !filename.ends_with(".kcl") {
if !filename.ends_with('/') && !filename.ends_with('\\') {
filename += "/";
}
filename += "main.kcl";
}
let mut f = File::open(&filename).unwrap();
let mut text = String::new();
@ -36,6 +42,6 @@ async fn main() {
)
.await
.unwrap();
let mut exec_state = ExecState::new(&ctx.settings);
let mut exec_state = ExecState::new(&ctx);
ctx.run(&program, &mut exec_state).await.unwrap();
}

View File

@ -33,6 +33,12 @@ impl ModuleId {
}
}
impl std::fmt::Display for ModuleId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ModuleLoader {
/// The stack of import statements for detecting circular module imports.

View File

@ -2149,7 +2149,7 @@ impl From<&Node<TagDeclarator>> for TagIdentifier {
fn from(tag: &Node<TagDeclarator>) -> Self {
TagIdentifier {
value: tag.name.clone(),
info: None,
info: Vec::new(),
meta: vec![Metadata {
source_range: tag.into(),
}],
@ -2937,7 +2937,7 @@ impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Type::Primitive(primitive_type) => primitive_type.fmt(f),
Type::Array(primitive_type) => write!(f, "{primitive_type}[]"),
Type::Array(primitive_type) => write!(f, "[{primitive_type}]"),
Type::Object { properties } => {
write!(f, "{{")?;
let mut first = true;
@ -3509,7 +3509,7 @@ const cylinder = startSketchOn('-XZ')
#[tokio::test(flavor = "multi_thread")]
async fn test_parse_type_args_array_on_functions() {
let some_program_string = r#"fn thing = (arg0: number[], arg1: string[], tag?: string) => {
let some_program_string = r#"fn thing = (arg0: [number], arg1: [string], tag?: string) => {
return arg0
}"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
@ -3540,7 +3540,7 @@ const cylinder = startSketchOn('-XZ')
#[tokio::test(flavor = "multi_thread")]
async fn test_parse_type_args_object_on_functions() {
let some_program_string = r#"fn thing = (arg0: number[], arg1: {thing: number, things: string[], more?: string}, tag?: string) => {
let some_program_string = r#"fn thing = (arg0: [number], arg1: {thing: number, things: [string], more?: string}, tag?: string) => {
return arg0
}"#;
let module_id = ModuleId::default();
@ -3594,7 +3594,7 @@ const cylinder = startSketchOn('-XZ')
56,
module_id,
),
type_: Some(Node::new(Type::Array(PrimitiveType::String), 58, 64, module_id)),
type_: Some(Node::new(Type::Array(PrimitiveType::String), 59, 65, module_id)),
default_value: None,
labeled: true,
digest: None
@ -3625,7 +3625,7 @@ const cylinder = startSketchOn('-XZ')
#[tokio::test(flavor = "multi_thread")]
async fn test_parse_return_type_on_functions() {
let some_program_string = r#"fn thing(): {thing: number, things: string[], more?: string} {
let some_program_string = r#"fn thing(): {thing: number, things: [string], more?: string} {
return 1
}"#;
let module_id = ModuleId::default();
@ -3675,7 +3675,7 @@ const cylinder = startSketchOn('-XZ')
34,
module_id,
),
type_: Some(Node::new(Type::Array(PrimitiveType::String), 36, 42, module_id)),
type_: Some(Node::new(Type::Array(PrimitiveType::String), 37, 43, module_id)),
default_value: None,
labeled: true,
digest: None

View File

@ -4,17 +4,17 @@
use std::{cell::RefCell, collections::BTreeMap};
use winnow::{
combinator::{alt, delimited, opt, peek, preceded, repeat, separated, separated_pair, terminated},
combinator::{alt, delimited, opt, peek, preceded, repeat, repeat_till, separated, separated_pair, terminated},
dispatch,
error::{ErrMode, StrContext, StrContextValue},
prelude::*,
stream::Stream,
token::{any, one_of, take_till},
token::{any, none_of, one_of, take_till},
};
use super::{
ast::types::{Ascription, ImportPath, LabelledExpression},
token::NumericSuffix,
token::{NumericSuffix, RESERVED_WORDS},
};
use crate::{
docs::StdLibFn,
@ -746,21 +746,58 @@ pub(crate) fn array_elem_by_elem(i: &mut TokenSlice) -> PResult<Node<ArrayExpres
)
.context(expected("array contents, a list of elements (like [1, 2, 3])"))
.parse_next(i)?;
ignore_trailing_comma(i);
ignore_whitespace(i);
let end = close_bracket(i)
.map_err(|e| {
if let Some(mut err) = e.clone().into_inner() {
err.cause = Some(CompilationError::fatal(
open.as_source_range(),
"Array is missing a closing bracket(`]`)",
));
ErrMode::Cut(err)
} else {
// ErrMode::Incomplete, not sure if it's actually possible to end up with this here
e
}
})?
.end;
let maybe_end = close_bracket(i).map_err(|e| {
if let Some(mut err) = e.clone().into_inner() {
let start_range = open.as_source_range();
let end_range = i.as_source_range();
err.cause = Some(CompilationError::fatal(
SourceRange::from([start_range.start(), end_range.start(), end_range.module_id().as_usize()]),
"Encountered an unexpected character(s) before finding a closing bracket(`]`) for the array",
));
ErrMode::Cut(err)
} else {
// ErrMode::Incomplete, not sure if it's actually possible to end up with this here
e
}
});
if maybe_end.is_err() {
// if there is a closing bracket at some point, but it wasn't the next token, it's likely that they forgot a comma between some
// of the elements
let maybe_closing_bracket: PResult<((), Token)> = peek(repeat_till(
0..,
none_of(|token: Token| {
// bail out early if we encounter something that is for sure not allowed in an
// array, otherwise we could seek to find a closing bracket until the end of the
// file
RESERVED_WORDS
.keys()
.chain([",,", "{", "}", "["].iter())
.any(|word| *word == token.value)
})
.void(),
one_of(|term: Token| term.value == "]"),
))
.parse_next(i);
let has_closing_bracket = maybe_closing_bracket.is_ok();
if has_closing_bracket {
let start_range = i.as_source_range();
// safe to unwrap here because we checked it was Ok above
let end_range = maybe_closing_bracket.unwrap().1.as_source_range();
let e = ContextError {
context: vec![],
cause: Some(CompilationError::fatal(
SourceRange::from([start_range.start(), end_range.end(), end_range.module_id().as_usize()]),
"Unexpected character encountered. You might be missing a comma in between elements.",
)),
};
return Err(ErrMode::Cut(e));
}
}
let end = maybe_end?.end;
// Sort the array's elements (i.e. expression nodes) from the noncode nodes.
let (elements, non_code_nodes): (Vec<_>, BTreeMap<usize, _>) = elements.into_iter().enumerate().fold(
@ -819,7 +856,7 @@ fn array_end_start(i: &mut TokenSlice) -> PResult<Node<ArrayRangeExpression>> {
}
fn object_property_same_key_and_val(i: &mut TokenSlice) -> PResult<Node<ObjectProperty>> {
let key = nameable_identifier.context(expected("the property's key (the name or identifier of the property), e.g. in 'height: 4', 'height' is the property key")).parse_next(i)?;
let key = nameable_identifier.context(expected("the property's key (the name or identifier of the property), e.g. in 'height = 4', 'height' is the property key")).parse_next(i)?;
ignore_whitespace(i);
Ok(Node {
start: key.start,
@ -846,7 +883,7 @@ fn object_property(i: &mut TokenSlice) -> PResult<Node<ObjectProperty>> {
ignore_whitespace(i);
let expr = match expression
.context(expected(
"the value which you're setting the property to, e.g. in 'height: 4', the value is 4",
"the value which you're setting the property to, e.g. in 'height = 4', the value is 4",
))
.parse_next(i)
{
@ -892,7 +929,7 @@ fn property_separator(i: &mut TokenSlice) -> PResult<()> {
alt((
// Normally you need a comma.
comma_sep,
// But, if the array is ending, no need for a comma.
// But, if the object is ending, no need for a comma.
peek(preceded(opt(whitespace), close_brace)).void(),
))
.parse_next(i)
@ -926,10 +963,62 @@ pub(crate) fn object(i: &mut TokenSlice) -> PResult<Node<ObjectExpression>> {
)),
)
.context(expected(
"a comma-separated list of key-value pairs, e.g. 'height: 4, width: 3'",
"a comma-separated list of key-value pairs, e.g. 'height = 4, width = 3'",
))
.parse_next(i)?;
ignore_trailing_comma(i);
ignore_whitespace(i);
let maybe_end = close_brace(i).map_err(|e| {
if let Some(mut err) = e.clone().into_inner() {
let start_range = open.as_source_range();
let end_range = i.as_source_range();
err.cause = Some(CompilationError::fatal(
SourceRange::from([start_range.start(), end_range.start(), end_range.module_id().as_usize()]),
"Encountered an unexpected character(s) before finding a closing brace(`}`) for the object",
));
ErrMode::Cut(err)
} else {
// ErrMode::Incomplete, not sure if it's actually possible to end up with this here
e
}
});
if maybe_end.is_err() {
// if there is a closing brace at some point, but it wasn't the next token, it's likely that they forgot a comma between some
// of the properties
let maybe_closing_brace: PResult<((), Token)> = peek(repeat_till(
0..,
none_of(|token: Token| {
// bail out early if we encounter something that is for sure not allowed in an
// object, otherwise we could seek to find a closing brace until the end of the
// file
RESERVED_WORDS
.keys()
.chain([",,", "[", "]", "{"].iter())
.any(|word| *word == token.value)
})
.void(),
one_of(|c: Token| c.value == "}"),
))
.parse_next(i);
let has_closing_brace = maybe_closing_brace.is_ok();
if has_closing_brace {
let start_range = i.as_source_range();
// okay to unwrap here because we checked it was Ok above
let end_range = maybe_closing_brace.unwrap().1.as_source_range();
let e = ContextError {
context: vec![],
cause: Some(CompilationError::fatal(
SourceRange::from([start_range.start(), end_range.end(), end_range.module_id().as_usize()]),
"Unexpected character encountered. You might be missing a comma in between properties.",
)),
};
return Err(ErrMode::Cut(e));
}
}
let end = maybe_end?.end;
// Sort the object's properties from the noncode nodes.
let (properties, non_code_nodes): (Vec<_>, BTreeMap<usize, _>) = properties.into_iter().enumerate().fold(
(Vec::new(), BTreeMap::new()),
@ -945,9 +1034,7 @@ pub(crate) fn object(i: &mut TokenSlice) -> PResult<Node<ObjectExpression>> {
(properties, non_code_nodes)
},
);
ignore_trailing_comma(i);
ignore_whitespace(i);
let end = close_brace(i)?.end;
let non_code_meta = NonCodeMeta {
non_code_nodes,
..Default::default()
@ -2564,7 +2651,7 @@ fn argument_type(i: &mut TokenSlice) -> PResult<Node<Type>> {
))
}),
// Array types
(primitive_type, open_bracket, close_bracket).map(|(t, _, _)| Ok(t.map(Type::Array))),
(open_bracket, primitive_type, close_bracket).map(|(_, t, _)| Ok(t.map(Type::Array))),
// Primitive types
primitive_type.map(|t| Ok(t.map(Type::Primitive))),
))
@ -3869,7 +3956,7 @@ mySk1 = startSketchOn(XY)
assert_eq!(
src_expected,
src_actual,
"expected error would highlight {} but it actually highlighted {}",
"expected error would highlight `{}` but it actually highlighted `{}`",
&p[src_expected[0]..src_expected[1]],
&p[src_actual[0]..src_actual[1]],
);
@ -4060,7 +4147,11 @@ z(-[["#,
#[test]
fn test_parse_weird_lots_of_fancy_brackets() {
assert_err(r#"zz({{{{{{{{)iegAng{{{{{{{##"#, "Unexpected token: (", [2, 3]);
assert_err(
r#"zz({{{{{{{{)iegAng{{{{{{{##"#,
"Encountered an unexpected character(s) before finding a closing brace(`}`) for the object",
[3, 4],
);
}
#[test]
@ -4601,10 +4692,123 @@ let myBox = box([0,0], -3, -16, -10)
}
#[test]
fn test_parse_missing_closing_bracket() {
fn test_parse_array_missing_closing_bracket() {
let some_program_string = r#"
sketch001 = startSketchOn('XZ') |> startProfileAt([90.45, 119.09, %)"#;
assert_err(some_program_string, "Array is missing a closing bracket(`]`)", [51, 52]);
assert_err(
some_program_string,
"Encountered an unexpected character(s) before finding a closing bracket(`]`) for the array",
[51, 67],
);
}
#[test]
fn test_parse_array_missing_comma() {
let some_program_string = r#"
sketch001 = startSketchOn('XZ') |> startProfileAt([90.45 119.09], %)"#;
assert_err(
some_program_string,
"Unexpected character encountered. You might be missing a comma in between elements.",
[52, 65],
);
}
#[test]
fn test_parse_array_reserved_word_early_exit() {
// since there is an early exit if encountering a reserved word, the error should be about
// that and not the missing comma
let some_program_string = r#"
sketch001 = startSketchOn('XZ') |> startProfileAt([90.45 $struct], %)"#;
assert_err(
some_program_string,
"Encountered an unexpected character(s) before finding a closing bracket(`]`) for the array",
[51, 52],
);
}
#[test]
fn test_parse_array_random_brace() {
let some_program_string = r#"
sketch001 = startSketchOn('XZ') |> startProfileAt([}], %)"#;
assert_err(
some_program_string,
"Encountered an unexpected character(s) before finding a closing bracket(`]`) for the array",
[51, 52],
);
}
#[test]
fn test_parse_object_missing_closing_brace() {
let some_program_string = r#"{
foo = bar,"#;
assert_err(
some_program_string,
"Encountered an unexpected character(s) before finding a closing brace(`}`) for the object",
[0, 23],
);
}
#[test]
fn test_parse_object_reserved_word_early_exit() {
// since there is an early exit if encountering a reserved word, the error should be about
// that and not the missing comma
let some_program_string = r#"{bar = foo struct = man}"#;
assert_err(
some_program_string,
"Encountered an unexpected character(s) before finding a closing brace(`}`) for the object",
[0, 1],
);
}
#[test]
fn test_parse_object_missing_comma() {
let some_program_string = r#"{
foo = bar,
bar = foo
bat = man
}"#;
assert_err(
some_program_string,
"Unexpected character encountered. You might be missing a comma in between properties.",
[37, 78],
);
}
#[test]
fn test_parse_object_missing_comma_one_line() {
let some_program_string = r#"{bar = foo bat = man}"#;
assert_err(
some_program_string,
"Unexpected character encountered. You might be missing a comma in between properties.",
[1, 21],
);
}
#[test]
fn test_parse_object_random_bracket() {
let some_program_string = r#"{]}"#;
assert_err(
some_program_string,
"Encountered an unexpected character(s) before finding a closing brace(`}`) for the object",
[0, 1],
);
}
#[test]
fn test_parse_object_shorthand_missing_comma() {
let some_program_string = r#"
bar = 1
{
foo = bar,
bar
bat = man
}"#;
assert_err(
some_program_string,
"Unexpected character encountered. You might be missing a comma in between properties.",
[54, 89],
);
}
#[test]

View File

@ -24,7 +24,6 @@ use crate::{
mod tokeniser;
#[cfg(test)]
pub(crate) use tokeniser::RESERVED_WORDS;
// Note the ordering, it's important that `m` comes after `mm` and `cm`.
@ -162,7 +161,9 @@ impl IntoIterator for TokenStream {
#[derive(Debug, Clone)]
pub(crate) struct TokenSlice<'a> {
stream: &'a TokenStream,
/// Current position of the leading Token in the stream
start: usize,
/// The number of total Tokens in the stream
end: usize,
}
@ -190,6 +191,21 @@ impl<'a> TokenSlice<'a> {
stream: self.stream,
}
}
pub fn as_source_range(&self) -> SourceRange {
let stream_len = self.stream.tokens.len();
let first_token = if stream_len == self.start {
&self.stream.tokens[self.start - 1]
} else {
self.token(0)
};
let last_token = if stream_len == self.end {
&self.stream.tokens[stream_len - 1]
} else {
self.token(self.end - self.start)
};
SourceRange::new(first_token.start, last_token.end, last_token.module_id)
}
}
impl<'a> IntoIterator for TokenSlice<'a> {
@ -294,6 +310,14 @@ impl<'a> winnow::stream::StreamIsPartial for TokenSlice<'a> {
}
}
impl<'a> winnow::stream::FindSlice<&str> for TokenSlice<'a> {
fn find_slice(&self, substr: &str) -> Option<std::ops::Range<usize>> {
self.iter()
.enumerate()
.find_map(|(i, b)| if b.value == substr { Some(i..self.end) } else { None })
}
}
#[derive(Clone, Debug)]
pub struct Checkpoint(usize, usize);

File diff suppressed because it is too large Load Diff

View File

@ -34,10 +34,10 @@ fn parse(dir_name: &str, dir_path: &Path) {
}
#[kcl_directory_test_macro::test_all_dirs("../public/kcl-samples")]
fn unparse(dir_name: &str, dir_path: &Path) {
// kcl-samples don't always use correct formatting. We don't ignore the
// test because we want to allow the just command to work. It's actually
// fine when no test runs.
async fn unparse(dir_name: &str, dir_path: &Path) {
// TODO: turn this on when we fix the comments recasting.
// let t = test(dir_name, dir_path.join("main.kcl").to_str().unwrap().to_owned());
// super::unparse_test(&t).await;
}
#[kcl_directory_test_macro::test_all_dirs("../public/kcl-samples")]

View File

@ -12,7 +12,10 @@ use validator::Validate;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue, Solid, SolidSet},
execution::{
kcl_value::{ArrayLen, RuntimeType},
ExecState, KclValue, PrimitiveType, Solid,
},
std::Args,
};
@ -38,8 +41,12 @@ struct AppearanceData {
}
/// Set the appearance of a solid. This only works on solids, not sketches or individual paths.
pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid_set: SolidSet = args.get_unlabeled_kw_arg("solidSet")?;
pub async fn appearance(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solids = args.get_unlabeled_kw_arg_typed(
"solids",
&RuntimeType::Array(PrimitiveType::Solid, ArrayLen::NonEmpty),
exec_state,
)?;
let color: String = args.get_kw_arg("color")?;
let metalness: Option<f64> = args.get_kw_arg_opt("metalness")?;
@ -66,7 +73,7 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
}));
}
let result = inner_appearance(solid_set, data.color, data.metalness, data.roughness, args).await?;
let result = inner_appearance(solids, data.color, data.metalness, data.roughness, args).await?;
Ok(result.into())
}
@ -276,21 +283,19 @@ pub async fn appearance(_exec_state: &mut ExecState, args: Args) -> Result<KclVa
keywords = true,
unlabeled_first = true,
args = {
solid_set = { docs = "The solid(s) whose appearance is being set" },
solids = { docs = "The solid(s) whose appearance is being set" },
color = { docs = "Color of the new material, a hex string like '#ff0000'"},
metalness = { docs = "Metalness of the new material, a percentage like 95.7." },
roughness = { docs = "Roughness of the new material, a percentage like 95.7." },
}
}]
async fn inner_appearance(
solid_set: SolidSet,
solids: Vec<Solid>,
color: String,
metalness: Option<f64>,
roughness: Option<f64>,
args: Args,
) -> Result<SolidSet, KclError> {
let solids: Vec<Box<Solid>> = solid_set.into();
) -> Result<Vec<Solid>, KclError> {
for solid in &solids {
// Set the material properties.
let rgb = rgba_simple::RGB::<f32>::from_hex(&color).map_err(|err| {
@ -323,5 +328,5 @@ async fn inner_appearance(
// I can't think of a use case for it.
}
Ok(SolidSet::from(solids))
Ok(solids)
}

View File

@ -1,22 +1,28 @@
use std::{any::type_name, collections::HashMap, num::NonZeroU32};
use anyhow::Result;
use kcmc::{websocket::OkWebSocketResponseData, ModelingCmd};
use kcmc::{
websocket::{ModelingCmdReq, OkWebSocketResponseData},
ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::shapes::PolygonType;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
kcl_value::{FunctionSource, NumericType},
ExecState, ExecutorContext, ExtrudeSurface, Helix, KclObjectFields, KclValue, Metadata, Sketch, SketchSet,
SketchSurface, Solid, SolidSet, TagIdentifier,
kcl_value::{ArrayLen, FunctionSource, NumericType, RuntimeType},
ExecState, ExecutorContext, ExtrudeSurface, Helix, KclObjectFields, KclValue, Metadata, PrimitiveType, Sketch,
SketchSurface, Solid, TagIdentifier,
},
parsing::ast::types::TagNode,
source_range::SourceRange,
std::{shapes::SketchOrSurface, sketch::FaceTag, sweep::SweepPath},
std::{
shapes::{PolygonType, SketchOrSurface},
sketch::FaceTag,
sweep::SweepPath,
},
ModuleId,
};
@ -227,9 +233,41 @@ impl Args {
T::from_kcl_val(&arg.value).ok_or_else(|| {
let expected_type_name = tynm::type_name::<T>();
let actual_type_name = arg.value.human_friendly_type();
let msg_base = format!("This function expected the input argument to be of type {expected_type_name} but it's actually of type {actual_type_name}");
let suggestion = match (expected_type_name.as_str(), actual_type_name) {
("SolidSet", "Sketch") => Some(
let message = format!("This function expected the input argument to be of type {expected_type_name} but it's actually of type {actual_type_name}");
KclError::Semantic(KclErrorDetails {
source_ranges: arg.source_ranges(),
message,
})
})
}
/// Get the unlabeled keyword argument. If not set, returns Err. If it
/// can't be converted to the given type, returns Err.
pub(crate) fn get_unlabeled_kw_arg_typed<T>(
&self,
label: &str,
ty: &RuntimeType,
exec_state: &mut ExecState,
) -> Result<T, KclError>
where
T: for<'a> FromKclValue<'a>,
{
let arg = self
.unlabeled_kw_arg_unconverted()
.ok_or(KclError::Semantic(KclErrorDetails {
source_ranges: vec![self.source_range],
message: format!("This function requires a value for the special unlabeled first parameter, '{label}'"),
}))?;
let arg = arg.value.coerce(ty, exec_state).ok_or_else(|| {
let actual_type_name = arg.value.human_friendly_type();
let msg_base = format!(
"This function expected the input argument to be {} but it's actually of type {actual_type_name}",
ty.human_friendly_type(),
);
let suggestion = match (ty, actual_type_name) {
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch")
| (RuntimeType::Array(PrimitiveType::Solid, _), "Sketch") => Some(
"You can convert a sketch (2D) into a Solid (3D) by calling a function like `extrude` or `revolve`",
),
_ => None,
@ -242,7 +280,10 @@ impl Args {
source_ranges: arg.source_ranges(),
message,
})
})
})?;
// TODO unnecessary cloning
Ok(T::from_kcl_val(&arg).unwrap())
}
// Add a modeling command to the batch but don't fire it right away.
@ -254,6 +295,11 @@ impl Args {
self.ctx.engine.batch_modeling_cmd(id, self.source_range, &cmd).await
}
// Add multiple modeling commands to the batch but don't fire them right away.
pub(crate) async fn batch_modeling_cmds(&self, cmds: &[ModelingCmdReq]) -> Result<(), crate::errors::KclError> {
self.ctx.engine.batch_modeling_cmds(self.source_range, cmds).await
}
// Add a modeling command to the batch that gets executed at the end of the file.
// This is good for something like fillet or chamfer where the engine would
// eat the path id if we executed it right away.
@ -275,13 +321,16 @@ impl Args {
exec_state: &'e mut ExecState,
tag: &'a TagIdentifier,
) -> Result<&'e crate::execution::TagEngineInfo, KclError> {
if let KclValue::TagIdentifier(t) = exec_state.stack().get_from_call_stack(&tag.value, self.source_range)? {
Ok(t.info.as_ref().ok_or_else(|| {
if let (epoch, KclValue::TagIdentifier(t)) =
exec_state.stack().get_from_call_stack(&tag.value, self.source_range)?
{
let info = t.get_info(epoch).ok_or_else(|| {
KclError::Type(KclErrorDetails {
message: format!("Tag `{}` does not have engine info", tag.value),
source_ranges: vec![self.source_range],
})
})?)
})?;
Ok(info)
} else {
Err(KclError::Type(KclErrorDetails {
message: format!("Tag `{}` does not exist", tag.value),
@ -298,7 +347,7 @@ impl Args {
where
'e: 'a,
{
if let Some(info) = &tag.info {
if let Some(info) = tag.get_cur_info() {
return Ok(info);
}
@ -313,7 +362,7 @@ impl Args {
where
'e: 'a,
{
if let Some(info) = &tag.info {
if let Some(info) = tag.get_cur_info() {
if info.surface.is_some() {
return Ok(info);
}
@ -324,10 +373,10 @@ impl Args {
/// Flush just the fillets and chamfers for this specific SolidSet.
#[allow(clippy::vec_box)]
pub(crate) async fn flush_batch_for_solid_set(
pub(crate) async fn flush_batch_for_solids(
&self,
exec_state: &mut ExecState,
solids: Vec<Box<Solid>>,
solids: Vec<Solid>,
) -> Result<(), KclError> {
// Make sure we don't traverse sketches more than once.
let mut traversed_sketches = Vec::new();
@ -496,12 +545,48 @@ impl Args {
Ok((a.n, b.n, ty))
}
pub(crate) fn get_sketches(&self) -> Result<(SketchSet, Sketch), KclError> {
FromArgs::from_args(self, 0)
pub(crate) fn get_sketches(&self, exec_state: &mut ExecState) -> Result<(Vec<Sketch>, Sketch), KclError> {
let sarg = self.args[0]
.value
.coerce(&RuntimeType::Array(PrimitiveType::Sketch, ArrayLen::None), exec_state)
.ok_or(KclError::Type(KclErrorDetails {
message: format!(
"Expected an array of sketches, found {}",
self.args[0].value.human_friendly_type()
),
source_ranges: vec![self.source_range],
}))?;
let sketches = match sarg {
KclValue::HomArray { value, .. } => value.iter().map(|v| v.as_sketch().unwrap().clone()).collect(),
_ => unreachable!(),
};
let sarg = self.args[1]
.value
.coerce(&RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)
.ok_or(KclError::Type(KclErrorDetails {
message: format!("Expected a sketch, found {}", self.args[1].value.human_friendly_type()),
source_ranges: vec![self.source_range],
}))?;
let sketch = match sarg {
KclValue::Sketch { value } => *value,
_ => unreachable!(),
};
Ok((sketches, sketch))
}
pub(crate) fn get_sketch(&self) -> Result<Sketch, KclError> {
FromArgs::from_args(self, 0)
pub(crate) fn get_sketch(&self, exec_state: &mut ExecState) -> Result<Sketch, KclError> {
let sarg = self.args[0]
.value
.coerce(&RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)
.ok_or(KclError::Type(KclErrorDetails {
message: format!("Expected a sketch, found {}", self.args[0].value.human_friendly_type()),
source_ranges: vec![self.source_range],
}))?;
match sarg {
KclValue::Sketch { value } => Ok(*value),
_ => unreachable!(),
}
}
pub(crate) fn get_data<'a, T>(&'a self) -> Result<T, KclError>
@ -522,25 +607,55 @@ impl Args {
FromArgs::from_args(self, 0)
}
pub(crate) fn get_data_and_sketch<'a, T>(&'a self) -> Result<(T, Sketch), KclError>
pub(crate) fn get_data_and_sketches<'a, T>(
&'a self,
exec_state: &mut ExecState,
) -> Result<(T, Vec<Sketch>), KclError>
where
T: serde::de::DeserializeOwned + FromArgs<'a>,
{
FromArgs::from_args(self, 0)
let data: T = FromArgs::from_args(self, 0)?;
let sarg = self.args[1]
.value
.coerce(&RuntimeType::Array(PrimitiveType::Sketch, ArrayLen::None), exec_state)
.ok_or(KclError::Type(KclErrorDetails {
message: format!(
"Expected an array of sketches for second argument, found {}",
self.args[1].value.human_friendly_type()
),
source_ranges: vec![self.source_range],
}))?;
let sketches = match sarg {
KclValue::HomArray { value, .. } => value.iter().map(|v| v.as_sketch().unwrap().clone()).collect(),
_ => unreachable!(),
};
Ok((data, sketches))
}
pub(crate) fn get_data_and_sketch_set<'a, T>(&'a self) -> Result<(T, SketchSet), KclError>
where
T: serde::de::DeserializeOwned + FromArgs<'a>,
{
FromArgs::from_args(self, 0)
}
pub(crate) fn get_data_and_sketch_and_tag<'a, T>(&'a self) -> Result<(T, Sketch, Option<TagNode>), KclError>
pub(crate) fn get_data_and_sketch_and_tag<'a, T>(
&'a self,
exec_state: &mut ExecState,
) -> Result<(T, Sketch, Option<TagNode>), KclError>
where
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
{
FromArgs::from_args(self, 0)
let data: T = FromArgs::from_args(self, 0)?;
let sarg = self.args[1]
.value
.coerce(&RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)
.ok_or(KclError::Type(KclErrorDetails {
message: format!(
"Expected a sketch for second argument, found {}",
self.args[1].value.human_friendly_type()
),
source_ranges: vec![self.source_range],
}))?;
let sketch = match sarg {
KclValue::Sketch { value } => *value,
_ => unreachable!(),
};
let tag: Option<TagNode> = FromArgs::from_args(self, 2)?;
Ok((data, sketch, tag))
}
pub(crate) fn get_data_and_sketch_surface<'a, T>(&'a self) -> Result<(T, SketchSurface, Option<TagNode>), KclError>
@ -550,11 +665,26 @@ impl Args {
FromArgs::from_args(self, 0)
}
pub(crate) fn get_data_and_solid<'a, T>(&'a self) -> Result<(T, Box<Solid>), KclError>
pub(crate) fn get_data_and_solid<'a, T>(&'a self, exec_state: &mut ExecState) -> Result<(T, Box<Solid>), KclError>
where
T: serde::de::DeserializeOwned + FromKclValue<'a> + Sized,
{
FromArgs::from_args(self, 0)
let data: T = FromArgs::from_args(self, 0)?;
let sarg = self.args[1]
.value
.coerce(&RuntimeType::Primitive(PrimitiveType::Solid), exec_state)
.ok_or(KclError::Type(KclErrorDetails {
message: format!(
"Expected a solid for second argument, found {}",
self.args[1].value.human_friendly_type()
),
source_ranges: vec![self.source_range],
}))?;
let solid = match sarg {
KclValue::Solid { value } => value,
_ => unreachable!(),
};
Ok((data, solid))
}
pub(crate) fn get_tag_to_number_sketch(&self) -> Result<(TagIdentifier, f64, Sketch), KclError> {
@ -1297,7 +1427,6 @@ impl_from_kcl_for_vec!(crate::execution::EdgeCut);
impl_from_kcl_for_vec!(crate::execution::Metadata);
impl_from_kcl_for_vec!(super::fillet::EdgeReference);
impl_from_kcl_for_vec!(ExtrudeSurface);
impl_from_kcl_for_vec!(Sketch);
impl<'a> FromKclValue<'a> for SourceRange {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
@ -1333,7 +1462,10 @@ impl<'a> FromKclValue<'a> for crate::execution::Solid {
impl<'a> FromKclValue<'a> for crate::execution::SolidOrImportedGeometry {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
match arg {
KclValue::Solid { value } => Some(Self::Solid(value.clone())),
KclValue::Solid { value } => Some(Self::SolidSet(vec![(**value).clone()])),
KclValue::HomArray { value, .. } => Some(Self::SolidSet(
value.iter().map(|v| v.as_solid().unwrap().clone()).collect(),
)),
KclValue::ImportedGeometry(value) => Some(Self::ImportedGeometry(Box::new(value.clone()))),
_ => None,
}
@ -1346,11 +1478,13 @@ impl<'a> FromKclValue<'a> for super::sketch::SketchData {
let case1 = crate::execution::Plane::from_kcl_val;
let case2 = super::sketch::PlaneData::from_kcl_val;
let case3 = crate::execution::Solid::from_kcl_val;
let case4 = <Vec<Solid>>::from_kcl_val;
case1(arg)
.map(Box::new)
.map(Self::Plane)
.or_else(|| case2(arg).map(Self::PlaneOrientation))
.or_else(|| case3(arg).map(Box::new).map(Self::Solid))
.or_else(|| case4(arg).map(|v| Box::new(v[0].clone())).map(Self::Solid))
}
}
@ -1523,6 +1657,7 @@ impl<'a> FromKclValue<'a> for TyF64 {
}
}
}
impl<'a> FromKclValue<'a> for Sketch {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let KclValue::Sketch { value } = arg else {
@ -1540,13 +1675,16 @@ impl<'a> FromKclValue<'a> for Helix {
Some(value.as_ref().to_owned())
}
}
impl<'a> FromKclValue<'a> for SweepPath {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let case1 = Sketch::from_kcl_val;
let case2 = Helix::from_kcl_val;
let case2 = <Vec<Sketch>>::from_kcl_val;
let case3 = Helix::from_kcl_val;
case1(arg)
.map(Self::Sketch)
.or_else(|| case2(arg).map(|arg0: Helix| Self::Helix(Box::new(arg0))))
.or_else(|| case2(arg).map(|arg0: Vec<Sketch>| Self::Sketch(arg0[0].clone())))
.or_else(|| case3(arg).map(|arg0: Helix| Self::Helix(Box::new(arg0))))
}
}
impl<'a> FromKclValue<'a> for String {
@ -1574,20 +1712,6 @@ impl<'a> FromKclValue<'a> for bool {
}
}
impl<'a> FromKclValue<'a> for SketchSet {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
match arg {
KclValue::Sketch { value: sketch } => Some(SketchSet::from(sketch.to_owned())),
KclValue::Sketches { value } => Some(SketchSet::from(value.to_owned())),
KclValue::MixedArray { .. } => {
let v: Option<Vec<Sketch>> = FromKclValue::from_kcl_val(arg);
Some(SketchSet::Sketches(v?.iter().cloned().map(Box::new).collect()))
}
_ => None,
}
}
}
impl<'a> FromKclValue<'a> for Box<Solid> {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
let KclValue::Solid { value } = arg else {
@ -1597,15 +1721,27 @@ impl<'a> FromKclValue<'a> for Box<Solid> {
}
}
impl<'a> FromKclValue<'a> for &'a FunctionSource {
impl<'a> FromKclValue<'a> for Vec<Solid> {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
arg.get_function()
let KclValue::HomArray { value, .. } = arg else {
return None;
};
value.iter().map(Solid::from_kcl_val).collect()
}
}
impl<'a> FromKclValue<'a> for SolidSet {
impl<'a> FromKclValue<'a> for Vec<Sketch> {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
arg.get_solid_set().ok()
let KclValue::HomArray { value, .. } = arg else {
return None;
};
value.iter().map(Sketch::from_kcl_val).collect()
}
}
impl<'a> FromKclValue<'a> for &'a FunctionSource {
fn from_kcl_val(arg: &'a KclValue) -> Option<Self> {
arg.get_function()
}
}

View File

@ -7,7 +7,10 @@ use kittycad_modeling_cmds as kcmc;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, Solid},
execution::{
kcl_value::RuntimeType, ChamferSurface, EdgeCut, ExecState, ExtrudeSurface, GeoMeta, KclValue, PrimitiveType,
Solid,
},
parsing::ast::types::TagNode,
std::{fillet::EdgeReference, Args},
};
@ -16,7 +19,7 @@ pub(crate) const DEFAULT_TOLERANCE: f64 = 0.0000001;
/// Create chamfers on tagged paths.
pub async fn chamfer(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid = args.get_unlabeled_kw_arg("solid")?;
let solid = args.get_unlabeled_kw_arg_typed("solid", &RuntimeType::Primitive(PrimitiveType::Solid), exec_state)?;
let length = args.get_kw_arg("length")?;
let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?;
let tag = args.get_kw_arg_opt("tag")?;

View File

@ -5,8 +5,13 @@ use std::collections::HashMap;
use anyhow::Result;
use kcl_derive_docs::stdlib;
use kcmc::{
each_cmd as mcmd, length_unit::LengthUnit, ok_response::OkModelingCmdResponse, output::ExtrusionFaceInfo,
shared::ExtrusionFaceCapType, websocket::OkWebSocketResponseData, ModelingCmd,
each_cmd as mcmd,
length_unit::LengthUnit,
ok_response::OkModelingCmdResponse,
output::ExtrusionFaceInfo,
shared::ExtrusionFaceCapType,
websocket::{ModelingCmdReq, OkWebSocketResponseData},
ModelingCmd,
};
use kittycad_modeling_cmds as kcmc;
use uuid::Uuid;
@ -14,18 +19,22 @@ use uuid::Uuid;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
ArtifactId, ExecState, ExtrudeSurface, GeoMeta, KclValue, Path, Sketch, SketchSet, SketchSurface, Solid,
SolidSet,
kcl_value::{ArrayLen, RuntimeType},
ArtifactId, ExecState, ExtrudeSurface, GeoMeta, KclValue, Path, PrimitiveType, Sketch, SketchSurface, Solid,
},
std::Args,
};
/// Extrudes by a given amount.
pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch_set = args.get_unlabeled_kw_arg("sketch_set")?;
let sketches = args.get_unlabeled_kw_arg_typed(
"sketches",
&RuntimeType::Array(PrimitiveType::Sketch, ArrayLen::NonEmpty),
exec_state,
)?;
let length = args.get_kw_arg("length")?;
let result = inner_extrude(sketch_set, length, exec_state, args).await?;
let result = inner_extrude(sketches, length, exec_state, args).await?;
Ok(result.into())
}
@ -34,6 +43,9 @@ pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// create new 3-dimensional volume, or if extruded into an existing volume,
/// cut into an existing solid.
///
/// You can provide more than one sketch to extrude, and they will all be
/// extruded in the same direction.
///
/// ```no_run
/// example = startSketchOn('XZ')
/// |> startProfileAt([0, 0], %)
@ -82,64 +94,37 @@ pub async fn extrude(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
keywords = true,
unlabeled_first = true,
args = {
sketch_set = { docs = "Which sketches should be extruded"},
sketches = { docs = "Which sketch or sketches should be extruded"},
length = { docs = "How far to extrude the given sketches"},
}
}]
async fn inner_extrude(
sketch_set: SketchSet,
sketches: Vec<Sketch>,
length: f64,
exec_state: &mut ExecState,
args: Args,
) -> Result<SolidSet, KclError> {
let id = exec_state.next_uuid();
) -> Result<Vec<Solid>, KclError> {
// Extrude the element(s).
let sketches: Vec<Sketch> = sketch_set.into();
let mut solids = Vec::new();
for sketch in &sketches {
// Before we extrude, we need to enable the sketch mode.
// We do this here in case extrude is called out of order.
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::EnableSketchMode {
animated: false,
ortho: false,
entity_id: sketch.on.id(),
adjust_camera: false,
planar_normal: if let SketchSurface::Plane(plane) = &sketch.on {
// We pass in the normal for the plane here.
Some(plane.z_axis.into())
} else {
None
},
}),
)
let id = exec_state.next_uuid();
args.batch_modeling_cmds(&sketch.build_sketch_mode_cmds(
exec_state,
ModelingCmdReq {
cmd_id: id.into(),
cmd: ModelingCmd::from(mcmd::Extrude {
target: sketch.id.into(),
distance: LengthUnit(length),
faces: Default::default(),
}),
},
))
.await?;
// TODO: We're reusing the same UUID for multiple commands. This seems
// like the artifact graph would never be able to find all the
// responses.
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::Extrude {
target: sketch.id.into(),
distance: LengthUnit(length),
faces: Default::default(),
}),
)
.await?;
// Disable the sketch mode.
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
)
.await?;
solids.push(do_post_extrude(sketch.clone(), id.into(), length, exec_state, args.clone()).await?);
}
Ok(solids.into())
Ok(solids)
}
pub(crate) async fn do_post_extrude(
@ -148,7 +133,7 @@ pub(crate) async fn do_post_extrude(
length: f64,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
) -> Result<Solid, KclError> {
// Bring the object to the front of the scene.
// See: https://github.com/KittyCAD/modeling-app/issues/806
args.batch_modeling_cmd(
@ -267,6 +252,17 @@ pub(crate) async fn do_post_extrude(
});
Some(extrude_surface)
}
Path::ArcThreePoint { .. } => {
let extrude_surface = ExtrudeSurface::ExtrudeArc(crate::execution::ExtrudeArc {
face_id: *actual_face_id,
tag: path.get_base().tag.clone(),
geo_meta: GeoMeta {
id: path.get_base().geo_meta.id,
metadata: path.get_base().geo_meta.metadata,
},
});
Some(extrude_surface)
}
}
} else if no_engine_commands {
// Only pre-populate the extrude surface if we are in mock mode.
@ -287,7 +283,7 @@ pub(crate) async fn do_post_extrude(
})
.collect();
Ok(Box::new(Solid {
Ok(Solid {
// Ok so you would think that the id would be the id of the solid,
// that we passed in to the function, but it's actually the id of the
// sketch.
@ -301,7 +297,7 @@ pub(crate) async fn do_post_extrude(
start_cap_id,
end_cap_id,
edge_cuts: vec![],
}))
})
}
#[derive(Default)]

View File

@ -14,7 +14,10 @@ use uuid::Uuid;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, Solid, TagIdentifier},
execution::{
kcl_value::RuntimeType, EdgeCut, ExecState, ExtrudeSurface, FilletSurface, GeoMeta, KclValue, PrimitiveType,
Solid, TagIdentifier,
},
parsing::ast::types::TagNode,
settings::types::UnitLength,
std::Args,
@ -64,8 +67,7 @@ pub(super) fn validate_unique<T: Eq + std::hash::Hash>(tags: &[(T, SourceRange)]
/// Create fillets on tagged paths.
pub async fn fillet(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
// Get all args:
let solid = args.get_unlabeled_kw_arg("solid")?;
let solid = args.get_unlabeled_kw_arg_typed("solid", &RuntimeType::Primitive(PrimitiveType::Solid), exec_state)?;
let radius = args.get_kw_arg("radius")?;
let tolerance = args.get_kw_arg_opt("tolerance")?;
let tags = args.kw_arg_array_and_source::<EdgeReference>("tags")?;
@ -334,6 +336,7 @@ async fn inner_get_next_adjacent_edge(
}),
)
.await?;
let OkWebSocketResponseData::Modeling {
modeling_response: OkModelingCmdResponse::Solid3dGetNextAdjacentEdge(adjacent_edge),
} = &resp

View File

@ -196,7 +196,7 @@ pub struct HelixRevolutionsData {
/// Create a helix on a cylinder.
pub async fn helix_revolutions(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, solid): (HelixRevolutionsData, Box<Solid>) = args.get_data_and_solid()?;
let (data, solid): (HelixRevolutionsData, Box<Solid>) = args.get_data_and_solid(exec_state)?;
let value = inner_helix_revolutions(data, solid, exec_state, args).await?;
Ok(KclValue::Solid { value })

View File

@ -9,7 +9,10 @@ use kittycad_modeling_cmds as kcmc;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue, Sketch, Solid},
execution::{
kcl_value::{ArrayLen, RuntimeType},
ExecState, KclValue, PrimitiveType, Sketch, Solid,
},
std::{extrude::do_post_extrude, fillet::default_tolerance, Args},
};
@ -17,7 +20,11 @@ const DEFAULT_V_DEGREE: u32 = 2;
/// Create a 3D surface or solid by interpolating between two or more sketches.
pub async fn loft(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketches = args.get_unlabeled_kw_arg("sketches")?;
let sketches = args.get_unlabeled_kw_arg_typed(
"sketches",
&RuntimeType::Array(PrimitiveType::Sketch, ArrayLen::NonEmpty),
exec_state,
)?;
let v_degree: NonZeroU32 = args
.get_kw_arg_opt("vDegree")?
.unwrap_or(NonZeroU32::new(DEFAULT_V_DEGREE).unwrap());
@ -159,5 +166,7 @@ async fn inner_loft(
let mut sketch = sketches[0].clone();
// Override its id with the loft id so we can get its faces later
sketch.id = id;
do_post_extrude(sketch, id.into(), 0.0, exec_state, args).await
Ok(Box::new(
do_post_extrude(sketch, id.into(), 0.0, exec_state, args).await?,
))
}

View File

@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use crate::{
errors::KclError,
execution::{ExecState, KclValue, Sketch, SketchSet},
execution::{ExecState, KclValue, Sketch},
std::{axis_or_reference::Axis2dOrEdgeReference, Args},
};
@ -26,7 +26,7 @@ pub struct Mirror2dData {
///
/// Only works on unclosed sketches for now.
pub async fn mirror_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch_set): (Mirror2dData, SketchSet) = args.get_data_and_sketch_set()?;
let (data, sketch_set): (Mirror2dData, Vec<Sketch>) = args.get_data_and_sketches(exec_state)?;
let sketches = inner_mirror_2d(data, sketch_set, exec_state, args).await?;
Ok(sketches.into())
@ -103,14 +103,11 @@ pub async fn mirror_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValu
}]
async fn inner_mirror_2d(
data: Mirror2dData,
sketch_set: SketchSet,
sketches: Vec<Sketch>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Box<Sketch>>, KclError> {
let starting_sketches = match sketch_set {
SketchSet::Sketch(sketch) => vec![sketch],
SketchSet::Sketches(sketches) => sketches,
};
) -> Result<Vec<Sketch>, KclError> {
let starting_sketches = sketches;
if args.ctx.no_engine_commands().await {
return Ok(starting_sketches);

View File

@ -20,9 +20,8 @@ use super::args::Arg;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
kcl_value::{FunctionSource, NumericType},
ExecState, Geometries, Geometry, KclObjectFields, KclValue, Point2d, Point3d, Sketch, SketchSet, Solid,
SolidSet,
kcl_value::{ArrayLen, FunctionSource, NumericType, RuntimeType},
ExecState, Geometries, Geometry, KclObjectFields, KclValue, Point2d, Point3d, PrimitiveType, Sketch, Solid,
},
std::Args,
ExecutorContext, SourceRange,
@ -48,25 +47,32 @@ pub struct LinearPattern3dData {
/// Repeat some 3D solid, changing each repetition slightly.
pub async fn pattern_transform(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid_set = args.get_unlabeled_kw_arg("solidSet")?;
let solids = args.get_unlabeled_kw_arg_typed(
"solids",
&RuntimeType::Array(PrimitiveType::Solid, ArrayLen::NonEmpty),
exec_state,
)?;
let instances: u32 = args.get_kw_arg("instances")?;
let transform: &FunctionSource = args.get_kw_arg("transform")?;
let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
let solids = inner_pattern_transform(solid_set, instances, transform, use_original, exec_state, &args).await?;
Ok(KclValue::Solids { value: solids })
let solids = inner_pattern_transform(solids, instances, transform, use_original, exec_state, &args).await?;
Ok(solids.into())
}
/// Repeat some 2D sketch, changing each repetition slightly.
pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch_set = args.get_unlabeled_kw_arg("sketchSet")?;
let sketches = args.get_unlabeled_kw_arg_typed(
"sketches",
&RuntimeType::Array(PrimitiveType::Sketch, ArrayLen::NonEmpty),
exec_state,
)?;
let instances: u32 = args.get_kw_arg("instances")?;
let transform: &FunctionSource = args.get_kw_arg("transform")?;
let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
let sketches =
inner_pattern_transform_2d(sketch_set, instances, transform, use_original, exec_state, &args).await?;
Ok(KclValue::Sketches { value: sketches })
let sketches = inner_pattern_transform_2d(sketches, instances, transform, use_original, exec_state, &args).await?;
Ok(sketches.into())
}
/// Repeat a 3-dimensional solid, changing it each time.
@ -258,20 +264,20 @@ pub async fn pattern_transform_2d(exec_state: &mut ExecState, args: Args) -> Res
keywords = true,
unlabeled_first = true,
args = {
solid_set = { docs = "The solid(s) to duplicate" },
solids = { docs = "The solid(s) to duplicate" },
instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect." },
transform = { docs = "How each replica should be transformed. The transform function takes a single parameter: an integer representing which number replication the transform is for. E.g. the first replica to be transformed will be passed the argument `1`. This simplifies your math: the transform function can rely on id `0` being the original instance passed into the `patternTransform`. See the examples." },
use_original = { docs = "If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false." },
}
}]
async fn inner_pattern_transform<'a>(
solid_set: SolidSet,
solids: Vec<Solid>,
instances: u32,
transform: &'a FunctionSource,
use_original: Option<bool>,
exec_state: &mut ExecState,
args: &'a Args,
) -> Result<Vec<Box<Solid>>, KclError> {
) -> Result<Vec<Solid>, KclError> {
// Build the vec of transforms, one for each repetition.
let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
if instances < 1 {
@ -281,12 +287,12 @@ async fn inner_pattern_transform<'a>(
}));
}
for i in 1..instances {
let t = make_transform::<Box<Solid>>(i, transform, args.source_range, exec_state, &args.ctx).await?;
let t = make_transform::<Solid>(i, transform, args.source_range, exec_state, &args.ctx).await?;
transform_vec.push(t);
}
execute_pattern_transform(
transform_vec,
solid_set,
solids,
use_original.unwrap_or_default(),
exec_state,
args,
@ -311,20 +317,20 @@ async fn inner_pattern_transform<'a>(
keywords = true,
unlabeled_first = true,
args = {
sketch_set = { docs = "The sketch(es) to duplicate" },
sketches = { docs = "The sketch(es) to duplicate" },
instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect." },
transform = { docs = "How each replica should be transformed. The transform function takes a single parameter: an integer representing which number replication the transform is for. E.g. the first replica to be transformed will be passed the argument `1`. This simplifies your math: the transform function can rely on id `0` being the original instance passed into the `patternTransform`. See the examples." },
use_original = { docs = "If the target was sketched on an extrusion, setting this will use the original sketch as the target, not the entire joined solid. Defaults to false." },
}
}]
async fn inner_pattern_transform_2d<'a>(
sketch_set: SketchSet,
sketches: Vec<Sketch>,
instances: u32,
transform: &'a FunctionSource,
use_original: Option<bool>,
exec_state: &mut ExecState,
args: &'a Args,
) -> Result<Vec<Box<Sketch>>, KclError> {
) -> Result<Vec<Sketch>, KclError> {
// Build the vec of transforms, one for each repetition.
let mut transform_vec = Vec::with_capacity(usize::try_from(instances).unwrap());
if instances < 1 {
@ -334,12 +340,12 @@ async fn inner_pattern_transform_2d<'a>(
}));
}
for i in 1..instances {
let t = make_transform::<Box<Sketch>>(i, transform, args.source_range, exec_state, &args.ctx).await?;
let t = make_transform::<Sketch>(i, transform, args.source_range, exec_state, &args.ctx).await?;
transform_vec.push(t);
}
execute_pattern_transform(
transform_vec,
sketch_set,
sketches,
use_original.unwrap_or_default(),
exec_state,
args,
@ -611,8 +617,8 @@ trait GeometryTrait: Clone {
async fn flush_batch(args: &Args, exec_state: &mut ExecState, set: Self::Set) -> Result<(), KclError>;
}
impl GeometryTrait for Box<Sketch> {
type Set = SketchSet;
impl GeometryTrait for Sketch {
type Set = Vec<Sketch>;
fn set_id(&mut self, id: Uuid) {
self.id = id;
}
@ -632,8 +638,8 @@ impl GeometryTrait for Box<Sketch> {
}
}
impl GeometryTrait for Box<Solid> {
type Set = SolidSet;
impl GeometryTrait for Solid {
type Set = Vec<Solid>;
fn set_id(&mut self, id: Uuid) {
self.id = id;
}
@ -651,7 +657,7 @@ impl GeometryTrait for Box<Solid> {
}
async fn flush_batch(args: &Args, exec_state: &mut ExecState, solid_set: Self::Set) -> Result<(), KclError> {
args.flush_batch_for_solid_set(exec_state, solid_set.into()).await
args.flush_batch_for_solids(exec_state, solid_set).await
}
}
@ -690,7 +696,11 @@ mod tests {
/// A linear pattern on a 2D sketch.
pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch_set: SketchSet = args.get_unlabeled_kw_arg("sketchSet")?;
let sketches = args.get_unlabeled_kw_arg_typed(
"sketches",
&RuntimeType::Array(PrimitiveType::Sketch, ArrayLen::NonEmpty),
exec_state,
)?;
let instances: u32 = args.get_kw_arg("instances")?;
let distance: f64 = args.get_kw_arg("distance")?;
let axis: [f64; 2] = args.get_kw_arg("axis")?;
@ -705,8 +715,7 @@ pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result
}));
}
let sketches =
inner_pattern_linear_2d(sketch_set, instances, distance, axis, use_original, exec_state, args).await?;
let sketches = inner_pattern_linear_2d(sketches, instances, distance, axis, use_original, exec_state, args).await?;
Ok(sketches.into())
}
@ -729,7 +738,7 @@ pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result
keywords = true,
unlabeled_first = true,
args = {
sketch_set = { docs = "The sketch(es) to duplicate" },
sketches = { docs = "The sketch(es) to duplicate" },
instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect." },
distance = { docs = "Distance between each repetition. Also known as 'spacing'."},
axis = { docs = "The axis of the pattern. A 2D vector." },
@ -737,14 +746,14 @@ pub async fn pattern_linear_2d(exec_state: &mut ExecState, args: Args) -> Result
}
}]
async fn inner_pattern_linear_2d(
sketch_set: SketchSet,
sketches: Vec<Sketch>,
instances: u32,
distance: f64,
axis: [f64; 2],
use_original: Option<bool>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Box<Sketch>>, KclError> {
) -> Result<Vec<Sketch>, KclError> {
let [x, y] = axis;
let axis_len = f64::sqrt(x * x + y * y);
let normalized_axis = kcmc::shared::Point2d::from([x / axis_len, y / axis_len]);
@ -760,7 +769,7 @@ async fn inner_pattern_linear_2d(
.collect();
execute_pattern_transform(
transforms,
sketch_set,
sketches,
use_original.unwrap_or_default(),
exec_state,
&args,
@ -770,7 +779,11 @@ async fn inner_pattern_linear_2d(
/// A linear pattern on a 3D model.
pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid_set: SolidSet = args.get_unlabeled_kw_arg("solidSet")?;
let solids = args.get_unlabeled_kw_arg_typed(
"solids",
&RuntimeType::Array(PrimitiveType::Solid, ArrayLen::NonEmpty),
exec_state,
)?;
let instances: u32 = args.get_kw_arg("instances")?;
let distance: f64 = args.get_kw_arg("distance")?;
let axis: [f64; 3] = args.get_kw_arg("axis")?;
@ -785,7 +798,7 @@ pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result
}));
}
let solids = inner_pattern_linear_3d(solid_set, instances, distance, axis, use_original, exec_state, args).await?;
let solids = inner_pattern_linear_3d(solids, instances, distance, axis, use_original, exec_state, args).await?;
Ok(solids.into())
}
@ -866,7 +879,7 @@ pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result
keywords = true,
unlabeled_first = true,
args = {
solid_set = { docs = "The solid(s) to duplicate" },
solids = { docs = "The solid(s) to duplicate" },
instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect." },
distance = { docs = "Distance between each repetition. Also known as 'spacing'."},
axis = { docs = "The axis of the pattern. A 2D vector." },
@ -874,14 +887,14 @@ pub async fn pattern_linear_3d(exec_state: &mut ExecState, args: Args) -> Result
}
}]
async fn inner_pattern_linear_3d(
solid_set: SolidSet,
solids: Vec<Solid>,
instances: u32,
distance: f64,
axis: [f64; 3],
use_original: Option<bool>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Box<Solid>>, KclError> {
) -> Result<Vec<Solid>, KclError> {
let [x, y, z] = axis;
let axis_len = f64::sqrt(x * x + y * y + z * z);
let normalized_axis = kcmc::shared::Point3d::from([x / axis_len, y / axis_len, z / axis_len]);
@ -895,14 +908,7 @@ async fn inner_pattern_linear_3d(
}]
})
.collect();
execute_pattern_transform(
transforms,
solid_set,
use_original.unwrap_or_default(),
exec_state,
&args,
)
.await
execute_pattern_transform(transforms, solids, use_original.unwrap_or_default(), exec_state, &args).await
}
/// Data for a circular pattern on a 2D sketch.
@ -1022,7 +1028,11 @@ impl CircularPattern {
/// A circular pattern on a 2D sketch.
pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch_set: SketchSet = args.get_unlabeled_kw_arg("sketchSet")?;
let sketches = args.get_unlabeled_kw_arg_typed(
"sketches",
&RuntimeType::Array(PrimitiveType::Sketch, ArrayLen::NonEmpty),
exec_state,
)?;
let instances: u32 = args.get_kw_arg("instances")?;
let center: [f64; 2] = args.get_kw_arg("center")?;
let arc_degrees: f64 = args.get_kw_arg("arcDegrees")?;
@ -1030,7 +1040,7 @@ pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Resu
let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
let sketches = inner_pattern_circular_2d(
sketch_set,
sketches,
instances,
center,
arc_degrees,
@ -1079,7 +1089,7 @@ pub async fn pattern_circular_2d(exec_state: &mut ExecState, args: Args) -> Resu
}]
#[allow(clippy::too_many_arguments)]
async fn inner_pattern_circular_2d(
sketch_set: SketchSet,
sketch_set: Vec<Sketch>,
instances: u32,
center: [f64; 2],
arc_degrees: f64,
@ -1087,8 +1097,8 @@ async fn inner_pattern_circular_2d(
use_original: Option<bool>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Box<Sketch>>, KclError> {
let starting_sketches: Vec<Box<Sketch>> = sketch_set.into();
) -> Result<Vec<Sketch>, KclError> {
let starting_sketches = sketch_set;
if args.ctx.context_type == crate::execution::ContextType::Mock {
return Ok(starting_sketches);
@ -1126,7 +1136,11 @@ async fn inner_pattern_circular_2d(
/// A circular pattern on a 3D model.
pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid_set: SolidSet = args.get_unlabeled_kw_arg("solidSet")?;
let solids = args.get_unlabeled_kw_arg_typed(
"solids",
&RuntimeType::Array(PrimitiveType::Solid, ArrayLen::NonEmpty),
exec_state,
)?;
// The number of total instances. Must be greater than or equal to 1.
// This includes the original entity. For example, if instances is 2,
// there will be two copies -- the original, and one new copy.
@ -1145,7 +1159,7 @@ pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Resu
let use_original: Option<bool> = args.get_kw_arg_opt("useOriginal")?;
let solids = inner_pattern_circular_3d(
solid_set,
solids,
instances,
axis,
center,
@ -1183,7 +1197,7 @@ pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Resu
keywords = true,
unlabeled_first = true,
args = {
solid_set = { docs = "Which solid(s) to pattern" },
solids = { docs = "Which solid(s) to pattern" },
instances = { docs = "The number of total instances. Must be greater than or equal to 1. This includes the original entity. For example, if instances is 2, there will be two copies -- the original, and one new copy. If instances is 1, this has no effect."},
axis = { docs = "The axis around which to make the pattern. This is a 3D vector"},
center = { docs = "The center about which to make the pattern. This is a 3D vector."},
@ -1194,7 +1208,7 @@ pub async fn pattern_circular_3d(exec_state: &mut ExecState, args: Args) -> Resu
}]
#[allow(clippy::too_many_arguments)]
async fn inner_pattern_circular_3d(
solid_set: SolidSet,
solids: Vec<Solid>,
instances: u32,
axis: [f64; 3],
center: [f64; 3],
@ -1203,14 +1217,13 @@ async fn inner_pattern_circular_3d(
use_original: Option<bool>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Vec<Box<Solid>>, KclError> {
) -> Result<Vec<Solid>, KclError> {
// Flush the batch for our fillets/chamfers if there are any.
// If we do not flush these, then you won't be able to pattern something with fillets.
// Flush just the fillets/chamfers that apply to these solids.
args.flush_batch_for_solid_set(exec_state, solid_set.clone().into())
.await?;
args.flush_batch_for_solids(exec_state, solids.clone()).await?;
let starting_solids: Vec<Box<Solid>> = solid_set.into();
let starting_solids = solids;
if args.ctx.context_type == crate::execution::ContextType::Mock {
return Ok(starting_solids);

View File

@ -28,12 +28,12 @@ pub struct RevolveData {
pub tolerance: Option<f64>,
}
/// Revolve a sketch around an axis.
/// Revolve a sketch or set of sketches around an axis.
pub async fn revolve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch): (RevolveData, Sketch) = args.get_data_and_sketch()?;
let (data, sketches): (RevolveData, _) = args.get_data_and_sketches(exec_state)?;
let value = inner_revolve(data, sketch, exec_state, args).await?;
Ok(KclValue::Solid { value })
let value = inner_revolve(data, sketches, exec_state, args).await?;
Ok(value.into())
}
/// Rotate a sketch around some provided axis, creating a solid from its extent.
@ -46,6 +46,9 @@ pub async fn revolve(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
///
/// Revolve occurs around a local sketch axis rather than a global axis.
///
/// You can provide more than one sketch to revolve, and they will all be
/// revolved around the same axis.
///
/// ```no_run
/// part001 = startSketchOn('XY')
/// |> startProfileAt([4, 12], %)
@ -174,16 +177,39 @@ pub async fn revolve(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// }
/// }, sketch001)
/// ```
///
/// ```no_run
/// // Revolve two sketches around the same axis.
///
/// sketch001 = startSketchOn('XY')
/// profile001 = startProfileAt([4, 8], sketch001)
/// |> xLine(length = 3)
/// |> yLine(length = -3)
/// |> xLine(length = -3)
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// profile002 = startProfileAt([-5, 8], sketch001)
/// |> xLine(length = 3)
/// |> yLine(length = -3)
/// |> xLine(length = -3)
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// revolve({
/// axis = "X",
/// }, [profile001, profile002])
/// ```
#[stdlib {
name = "revolve",
feature_tree_operation = true,
}]
async fn inner_revolve(
data: RevolveData,
sketch: Sketch,
sketches: Vec<Sketch>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
) -> Result<Vec<Solid>, KclError> {
if let Some(angle) = data.angle {
// Return an error if the angle is zero.
// We don't use validate() here because we want to return a specific error message that is
@ -198,37 +224,43 @@ async fn inner_revolve(
let angle = Angle::from_degrees(data.angle.unwrap_or(360.0));
let id = exec_state.next_uuid();
match data.axis {
Axis2dOrEdgeReference::Axis(axis) => {
let (axis, origin) = axis.axis_and_origin()?;
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::Revolve {
angle,
target: sketch.id.into(),
axis,
origin,
tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
axis_is_2d: true,
}),
)
.await?;
}
Axis2dOrEdgeReference::Edge(edge) => {
let edge_id = edge.get_engine_id(exec_state, &args)?;
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::RevolveAboutEdge {
angle,
target: sketch.id.into(),
edge_id,
tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
}),
)
.await?;
let mut solids = Vec::new();
for sketch in &sketches {
let id = exec_state.next_uuid();
match &data.axis {
Axis2dOrEdgeReference::Axis(axis) => {
let (axis, origin) = axis.axis_and_origin()?;
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::Revolve {
angle,
target: sketch.id.into(),
axis,
origin,
tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
axis_is_2d: true,
}),
)
.await?;
}
Axis2dOrEdgeReference::Edge(edge) => {
let edge_id = edge.get_engine_id(exec_state, &args)?;
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::RevolveAboutEdge {
angle,
target: sketch.id.into(),
edge_id,
tolerance: LengthUnit(data.tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
}),
)
.await?;
}
}
solids.push(do_post_extrude(sketch.clone(), id.into(), 0.0, exec_state, args.clone()).await?);
}
do_post_extrude(sketch, id.into(), 0.0, exec_state, args).await
Ok(solids)
}

View File

@ -6,7 +6,7 @@ use kittycad_modeling_cmds::shared::Angle;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue, Point2d, Sketch, TagIdentifier},
execution::{kcl_value::RuntimeType, ExecState, KclValue, Point2d, PrimitiveType, Sketch, TagIdentifier},
std::{utils::between, Args},
};
@ -282,8 +282,9 @@ fn inner_segment_start_y(tag: &TagIdentifier, exec_state: &mut ExecState, args:
Ok(path.get_from()[1])
}
/// Returns the last segment of x.
pub async fn last_segment_x(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch = args.get_unlabeled_kw_arg("sketch")?;
pub async fn last_segment_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch =
args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
let result = inner_last_segment_x(sketch, args.clone())?;
Ok(args.make_user_val_from_f64(result))
@ -327,8 +328,9 @@ fn inner_last_segment_x(sketch: Sketch, args: Args) -> Result<f64, KclError> {
}
/// Returns the last segment of y.
pub async fn last_segment_y(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch = args.get_unlabeled_kw_arg("sketch")?;
pub async fn last_segment_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch =
args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
let result = inner_last_segment_y(sketch, args.clone())?;
Ok(args.make_user_val_from_f64(result))

View File

@ -139,7 +139,7 @@ async fn inner_circle(
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
new_sketch.add_tag(tag, &current_path, exec_state);
}
new_sketch.paths.push(current_path);
@ -251,7 +251,7 @@ async fn inner_circle_three_point(
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
new_sketch.add_tag(tag, &current_path, exec_state);
}
new_sketch.paths.push(current_path);
@ -414,7 +414,7 @@ async fn inner_polygon(
};
if let Some(tag) = &tag {
sketch.add_tag(tag, &current_path);
sketch.add_tag(tag, &current_path, exec_state);
}
sketch.paths.push(current_path);
@ -450,7 +450,7 @@ async fn inner_polygon(
};
if let Some(tag) = &tag {
sketch.add_tag(tag, &current_path);
sketch.add_tag(tag, &current_path, exec_state);
}
sketch.paths.push(current_path);

View File

@ -7,17 +7,24 @@ use kittycad_modeling_cmds as kcmc;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue, Solid, SolidSet},
execution::{
kcl_value::{ArrayLen, RuntimeType},
ExecState, KclValue, PrimitiveType, Solid,
},
std::{sketch::FaceTag, Args},
};
/// Create a shell.
pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid_set = args.get_unlabeled_kw_arg("solidSet")?;
let solids = args.get_unlabeled_kw_arg_typed(
"solids",
&RuntimeType::Array(PrimitiveType::Solid, ArrayLen::NonEmpty),
exec_state,
)?;
let thickness = args.get_kw_arg("thickness")?;
let faces = args.get_kw_arg("faces")?;
let result = inner_shell(solid_set, thickness, faces, exec_state, args).await?;
let result = inner_shell(solids, thickness, faces, exec_state, args).await?;
Ok(result.into())
}
@ -173,18 +180,18 @@ pub async fn shell(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
keywords = true,
unlabeled_first = true,
args = {
solid_set = { docs = "Which solid (or solids) to shell out"},
solids = { docs = "Which solid (or solids) to shell out"},
thickness = {docs = "The thickness of the shell"},
faces = {docs = "The faces you want removed"},
}
}]
async fn inner_shell(
solid_set: SolidSet,
solids: Vec<Solid>,
thickness: f64,
faces: Vec<FaceTag>,
exec_state: &mut ExecState,
args: Args,
) -> Result<SolidSet, KclError> {
) -> Result<Vec<Solid>, KclError> {
if faces.is_empty() {
return Err(KclError::Type(KclErrorDetails {
message: "You must shell at least one face".to_string(),
@ -192,7 +199,6 @@ async fn inner_shell(
}));
}
let solids: Vec<Box<Solid>> = solid_set.clone().into();
if solids.is_empty() {
return Err(KclError::Type(KclErrorDetails {
message: "You must shell at least one solid".to_string(),
@ -204,7 +210,7 @@ async fn inner_shell(
for solid in &solids {
// Flush the batch for our fillets/chamfers if there are any.
// If we do not do these for sketch on face, things will fail with face does not exist.
args.flush_batch_for_solid_set(exec_state, solid.clone().into()).await?;
args.flush_batch_for_solids(exec_state, vec![solid.clone()]).await?;
for tag in &faces {
let extrude_plane_id = tag.get_face_id(solid, exec_state, &args, false).await?;
@ -241,12 +247,12 @@ async fn inner_shell(
)
.await?;
Ok(solid_set)
Ok(solids)
}
/// Make the inside of a 3D object hollow.
pub async fn hollow(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (thickness, solid): (f64, Box<Solid>) = args.get_data_and_solid()?;
let (thickness, solid): (f64, Box<Solid>) = args.get_data_and_solid(exec_state)?;
let value = inner_hollow(thickness, solid, exec_state, args).await?;
Ok(KclValue::Solid { value })
@ -314,7 +320,7 @@ async fn inner_hollow(
) -> Result<Box<Solid>, KclError> {
// Flush the batch for our fillets/chamfers if there are any.
// If we do not do these for sketch on face, things will fail with face does not exist.
args.flush_batch_for_solid_set(exec_state, solid.clone().into()).await?;
args.flush_batch_for_solids(exec_state, vec![(*solid).clone()]).await?;
args.batch_modeling_cmd(
exec_state.next_uuid(),

View File

@ -4,25 +4,27 @@ use anyhow::Result;
use indexmap::IndexMap;
use kcl_derive_docs::stdlib;
use kcmc::shared::Point2d as KPoint2d; // Point2d is already defined in this pkg, to impl ts_rs traits.
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, ModelingCmd};
use kcmc::{each_cmd as mcmd, length_unit::LengthUnit, shared::Angle, websocket::ModelingCmdReq, ModelingCmd};
use kittycad_modeling_cmds as kcmc;
use kittycad_modeling_cmds::shared::PathSegment;
use parse_display::{Display, FromStr};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::execution::kcl_value::RuntimeType;
use crate::execution::PrimitiveType;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{
Artifact, ArtifactId, BasePath, CodeRef, ExecState, Face, GeoMeta, KclValue, Path, Plane, Point2d, Point3d,
Sketch, SketchSet, SketchSurface, Solid, StartSketchOnFace, StartSketchOnPlane, TagEngineInfo, TagIdentifier,
Sketch, SketchSurface, Solid, StartSketchOnFace, StartSketchOnPlane, TagEngineInfo, TagIdentifier,
},
parsing::ast::types::TagNode,
std::{
args::{Args, TyF64},
utils::{
arc_angles, arc_center_and_end, calculate_circle_center, get_tangential_arc_to_info, get_x_component,
get_y_component, intersection_with_parallel_line, TangentialArcInfoInput,
arc_angles, arc_center_and_end, get_tangential_arc_to_info, get_x_component, get_y_component,
intersection_with_parallel_line, TangentialArcInfoInput,
},
},
};
@ -96,7 +98,8 @@ pub const NEW_TAG_KW: &str = "tag";
/// Draw a line to a point.
pub async fn line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
// let (to, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let sketch = args.get_unlabeled_kw_arg("sketch")?;
let sketch =
args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
let end = args.get_kw_arg_opt("end")?;
let end_absolute = args.get_kw_arg_opt("endAbsolute")?;
let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
@ -230,6 +233,7 @@ async fn straight_line(
}),
)
.await?;
let end = if is_absolute {
point
} else {
@ -252,7 +256,7 @@ async fn straight_line(
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
new_sketch.add_tag(tag, &current_path, exec_state);
}
new_sketch.paths.push(current_path);
@ -262,7 +266,8 @@ async fn straight_line(
/// Draw a line on the x-axis.
pub async fn x_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch = args.get_unlabeled_kw_arg("sketch")?;
let sketch =
args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
let length = args.get_kw_arg_opt("length")?;
let end_absolute = args.get_kw_arg_opt("endAbsolute")?;
let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
@ -330,7 +335,8 @@ async fn inner_x_line(
/// Draw a line on the y-axis.
pub async fn y_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch = args.get_unlabeled_kw_arg("sketch")?;
let sketch =
args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
let length = args.get_kw_arg_opt("length")?;
let end_absolute = args.get_kw_arg_opt("endAbsolute")?;
let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
@ -409,7 +415,8 @@ pub enum AngledLineData {
/// Draw an angled line.
pub async fn angled_line(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch, tag): (AngledLineData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let (data, sketch, tag): (AngledLineData, Sketch, Option<TagNode>) =
args.get_data_and_sketch_and_tag(exec_state)?;
let new_sketch = inner_angled_line(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
@ -488,7 +495,7 @@ async fn inner_angled_line(
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
new_sketch.add_tag(tag, &current_path, exec_state);
}
new_sketch.paths.push(current_path);
@ -497,7 +504,8 @@ async fn inner_angled_line(
/// Draw an angled line of a given x length.
pub async fn angled_line_of_x_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch, tag): (AngledLineData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let (data, sketch, tag): (AngledLineData, Sketch, Option<TagNode>) =
args.get_data_and_sketch_and_tag(exec_state)?;
let new_sketch = inner_angled_line_of_x_length(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
@ -567,7 +575,8 @@ pub struct AngledLineToData {
/// Draw an angled line to a given x coordinate.
pub async fn angled_line_to_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch, tag): (AngledLineToData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let (data, sketch, tag): (AngledLineToData, Sketch, Option<TagNode>) =
args.get_data_and_sketch_and_tag(exec_state)?;
let new_sketch = inner_angled_line_to_x(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
@ -631,7 +640,8 @@ async fn inner_angled_line_to_x(
/// Draw an angled line of a given y length.
pub async fn angled_line_of_y_length(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch, tag): (AngledLineData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let (data, sketch, tag): (AngledLineData, Sketch, Option<TagNode>) =
args.get_data_and_sketch_and_tag(exec_state)?;
let new_sketch = inner_angled_line_of_y_length(data, sketch, tag, exec_state, args).await?;
@ -693,7 +703,8 @@ async fn inner_angled_line_of_y_length(
/// Draw an angled line to a given y coordinate.
pub async fn angled_line_to_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch, tag): (AngledLineToData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let (data, sketch, tag): (AngledLineToData, Sketch, Option<TagNode>) =
args.get_data_and_sketch_and_tag(exec_state)?;
let new_sketch = inner_angled_line_to_y(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
@ -772,7 +783,7 @@ pub struct AngledLineThatIntersectsData {
/// Draw an angled line that intersects with a given line.
pub async fn angled_line_that_intersects(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch, tag): (AngledLineThatIntersectsData, Sketch, Option<TagNode>) =
args.get_data_and_sketch_and_tag()?;
args.get_data_and_sketch_and_tag(exec_state)?;
let new_sketch = inner_angled_line_that_intersects(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
value: Box::new(new_sketch),
@ -1097,6 +1108,8 @@ async fn make_sketch_plane_from_orientation(
let hide = Some(true);
match data {
PlaneData::XY | PlaneData::NegXY | PlaneData::XZ | PlaneData::NegXZ | PlaneData::YZ | PlaneData::NegYZ => {
// TODO: ignoring the default planes here since we already created them, breaks the
// front end for the feature tree which is stupid and we should fix it.
let x_axis = match data {
PlaneData::NegXY => Point3d::new(-1.0, 0.0, 0.0),
PlaneData::NegXZ => Point3d::new(-1.0, 0.0, 0.0),
@ -1199,7 +1212,7 @@ pub(crate) async fn inner_start_profile_at(
SketchSurface::Face(face) => {
// Flush the batch for our fillets/chamfers if there are any.
// If we do not do these for sketch on face, things will fail with face does not exist.
args.flush_batch_for_solid_set(exec_state, face.solid.clone().into())
args.flush_batch_for_solids(exec_state, vec![(*face.solid).clone()])
.await?;
}
SketchSurface::Plane(plane) if !plane.is_standard() => {
@ -1217,38 +1230,43 @@ pub(crate) async fn inner_start_profile_at(
_ => {}
}
// Enter sketch mode on the surface.
// We call this here so you can reuse the sketch surface for multiple sketches.
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::EnableSketchMode {
animated: false,
ortho: false,
entity_id: sketch_surface.id(),
adjust_camera: false,
planar_normal: if let SketchSurface::Plane(plane) = &sketch_surface {
// We pass in the normal for the plane here.
Some(plane.z_axis.into())
} else {
None
},
}),
)
.await?;
let id = exec_state.next_uuid();
let enable_sketch_id = exec_state.next_uuid();
let path_id = exec_state.next_uuid();
args.batch_modeling_cmd(path_id, ModelingCmd::from(mcmd::StartPath::default()))
.await?;
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::MovePathPen {
path: path_id.into(),
to: KPoint2d::from(to).with_z(0.0).map(LengthUnit),
}),
)
let move_pen_id = exec_state.next_uuid();
args.batch_modeling_cmds(&[
// Enter sketch mode on the surface.
// We call this here so you can reuse the sketch surface for multiple sketches.
ModelingCmdReq {
cmd: ModelingCmd::from(mcmd::EnableSketchMode {
animated: false,
ortho: false,
entity_id: sketch_surface.id(),
adjust_camera: false,
planar_normal: if let SketchSurface::Plane(plane) = &sketch_surface {
// We pass in the normal for the plane here.
Some(plane.z_axis.into())
} else {
None
},
}),
cmd_id: enable_sketch_id.into(),
},
ModelingCmdReq {
cmd: ModelingCmd::from(mcmd::StartPath::default()),
cmd_id: path_id.into(),
},
ModelingCmdReq {
cmd: ModelingCmd::from(mcmd::MovePathPen {
path: path_id.into(),
to: KPoint2d::from(to).with_z(0.0).map(LengthUnit),
}),
cmd_id: move_pen_id.into(),
},
ModelingCmdReq {
cmd: ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
cmd_id: exec_state.next_uuid().into(),
},
])
.await?;
let current_path = BasePath {
@ -1257,7 +1275,7 @@ pub(crate) async fn inner_start_profile_at(
tag: tag.clone(),
units: sketch_surface.units(),
geo_meta: GeoMeta {
id,
id: move_pen_id,
metadata: args.source_range.into(),
},
};
@ -1272,14 +1290,17 @@ pub(crate) async fn inner_start_profile_at(
meta: vec![args.source_range.into()],
tags: if let Some(tag) = &tag {
let mut tag_identifier: TagIdentifier = tag.into();
tag_identifier.info = Some(TagEngineInfo {
id: current_path.geo_meta.id,
sketch: path_id,
path: Some(Path::Base {
base: current_path.clone(),
}),
surface: None,
});
tag_identifier.info = vec![(
exec_state.stack().current_epoch(),
TagEngineInfo {
id: current_path.geo_meta.id,
sketch: path_id,
path: Some(Path::Base {
base: current_path.clone(),
}),
surface: None,
},
)];
IndexMap::from([(tag.name.to_string(), tag_identifier)])
} else {
Default::default()
@ -1290,8 +1311,8 @@ pub(crate) async fn inner_start_profile_at(
}
/// Returns the X component of the sketch profile start point.
pub async fn profile_start_x(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_sketch()?;
pub async fn profile_start_x(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_sketch(exec_state)?;
let ty = sketch.units.into();
let x = inner_profile_start_x(sketch)?;
Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
@ -1315,8 +1336,8 @@ pub(crate) fn inner_profile_start_x(sketch: Sketch) -> Result<f64, KclError> {
}
/// Returns the Y component of the sketch profile start point.
pub async fn profile_start_y(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_sketch()?;
pub async fn profile_start_y(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_sketch(exec_state)?;
let ty = sketch.units.into();
let x = inner_profile_start_y(sketch)?;
Ok(args.make_user_val_from_f64_with_type(TyF64::new(x, ty)))
@ -1339,8 +1360,8 @@ pub(crate) fn inner_profile_start_y(sketch: Sketch) -> Result<f64, KclError> {
}
/// Returns the sketch profile start point.
pub async fn profile_start(_exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_sketch()?;
pub async fn profile_start(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch: Sketch = args.get_sketch(exec_state)?;
let ty = sketch.units.into();
let point = inner_profile_start(sketch)?;
Ok(KclValue::from_point2d(point, ty, args.into()))
@ -1367,7 +1388,8 @@ pub(crate) fn inner_profile_start(sketch: Sketch) -> Result<[f64; 2], KclError>
/// Close the current sketch.
pub async fn close(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch = args.get_unlabeled_kw_arg("sketch")?;
let sketch =
args.get_unlabeled_kw_arg_typed("sketch", &RuntimeType::Primitive(PrimitiveType::Sketch), exec_state)?;
let tag = args.get_kw_arg_opt(NEW_TAG_KW)?;
let new_sketch = inner_close(sketch, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
@ -1419,16 +1441,6 @@ pub(crate) async fn inner_close(
args.batch_modeling_cmd(id, ModelingCmd::from(mcmd::ClosePath { path_id: sketch.id }))
.await?;
// If we are sketching on a plane we can close the sketch now.
if let SketchSurface::Plane(_) = sketch.on {
// We were on a plane, disable the sketch mode.
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::SketchModeDisable(mcmd::SketchModeDisable::default()),
)
.await?;
}
let current_path = Path::ToPoint {
base: BasePath {
from: from.into(),
@ -1444,7 +1456,7 @@ pub(crate) async fn inner_close(
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
new_sketch.add_tag(tag, &current_path, exec_state);
}
new_sketch.paths.push(current_path);
@ -1494,7 +1506,7 @@ pub struct ArcToData {
/// Draw an arc.
pub async fn arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch, tag): (ArcData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let (data, sketch, tag): (ArcData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag(exec_state)?;
let new_sketch = inner_arc(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
@ -1597,7 +1609,7 @@ pub(crate) async fn inner_arc(
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
new_sketch.add_tag(tag, &current_path, exec_state);
}
new_sketch.paths.push(current_path);
@ -1607,7 +1619,7 @@ pub(crate) async fn inner_arc(
/// Draw a three point arc.
pub async fn arc_to(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch, tag): (ArcToData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let (data, sketch, tag): (ArcToData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag(exec_state)?;
let new_sketch = inner_arc_to(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
@ -1670,18 +1682,7 @@ pub(crate) async fn inner_arc_to(
let interior = data.interior;
let end = data.end;
// compute the center of the circle since we do not have the value returned from the engine
let center = calculate_circle_center(start, interior, end);
// compute the radius since we do not have the value returned from the engine
// Pick any of the 3 points since they all lie along the circle
let sum_of_square_differences =
(center[0] - start[0] * center[0] - start[0]) + (center[1] - start[1] * center[1] - start[1]);
let radius = sum_of_square_differences.sqrt();
let ccw = is_ccw(start, interior, end);
let current_path = Path::Arc {
let current_path = Path::ArcThreePoint {
base: BasePath {
from: from.into(),
to: data.end,
@ -1692,14 +1693,14 @@ pub(crate) async fn inner_arc_to(
metadata: args.source_range.into(),
},
},
center,
radius,
ccw,
p1: start,
p2: interior,
p3: end,
};
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
new_sketch.add_tag(tag, &current_path, exec_state);
}
new_sketch.paths.push(current_path);
@ -1707,26 +1708,6 @@ pub(crate) async fn inner_arc_to(
Ok(new_sketch)
}
/// Returns true if the three-point arc is counterclockwise. The order of
/// parameters is critical.
///
/// | end
/// | /
/// | | / interior
/// | / /
/// | | /
/// |/_____________
/// start
///
/// If the slope of the line from start to interior is less than the slope of
/// the line from start to end, the arc is counterclockwise.
fn is_ccw(start: [f64; 2], interior: [f64; 2], end: [f64; 2]) -> bool {
let t1 = (interior[0] - start[0]) * (end[1] - start[1]);
let t2 = (end[0] - start[0]) * (interior[1] - start[1]);
// If these terms are equal, the points are collinear.
t1 > t2
}
/// Data to draw a tangential arc.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, ts_rs::TS)]
#[ts(export)]
@ -1743,7 +1724,8 @@ pub enum TangentialArcData {
/// Draw a tangential arc.
pub async fn tangential_arc(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch, tag): (TangentialArcData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let (data, sketch, tag): (TangentialArcData, Sketch, Option<TagNode>) =
args.get_data_and_sketch_and_tag(exec_state)?;
let new_sketch = inner_tangential_arc(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
@ -1850,7 +1832,7 @@ async fn inner_tangential_arc(
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
new_sketch.add_tag(tag, &current_path, exec_state);
}
new_sketch.paths.push(current_path);
@ -1870,7 +1852,7 @@ fn tan_arc_to(sketch: &Sketch, to: &[f64; 2]) -> ModelingCmd {
/// Draw a tangential arc to a specific point.
pub async fn tangential_arc_to(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (to, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = super::args::FromArgs::from_args(&args, 0)?;
let (to, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag(exec_state)?;
let new_sketch = inner_tangential_arc_to(to, sketch, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
@ -1880,7 +1862,7 @@ pub async fn tangential_arc_to(exec_state: &mut ExecState, args: Args) -> Result
/// Draw a tangential arc to point some distance away..
pub async fn tangential_arc_to_relative(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (delta, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = super::args::FromArgs::from_args(&args, 0)?;
let (delta, sketch, tag): ([f64; 2], Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag(exec_state)?;
let new_sketch = inner_tangential_arc_to_relative(delta, sketch, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
@ -1947,7 +1929,7 @@ async fn inner_tangential_arc_to(
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
new_sketch.add_tag(tag, &current_path, exec_state);
}
new_sketch.paths.push(current_path);
@ -2031,7 +2013,7 @@ async fn inner_tangential_arc_to_relative(
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
new_sketch.add_tag(tag, &current_path, exec_state);
}
new_sketch.paths.push(current_path);
@ -2054,7 +2036,7 @@ pub struct BezierData {
/// Draw a bezier curve.
pub async fn bezier_curve(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (data, sketch, tag): (BezierData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag()?;
let (data, sketch, tag): (BezierData, Sketch, Option<TagNode>) = args.get_data_and_sketch_and_tag(exec_state)?;
let new_sketch = inner_bezier_curve(data, sketch, tag, exec_state, args).await?;
Ok(KclValue::Sketch {
@ -2127,7 +2109,7 @@ async fn inner_bezier_curve(
let mut new_sketch = sketch.clone();
if let Some(tag) = &tag {
new_sketch.add_tag(tag, &current_path);
new_sketch.add_tag(tag, &current_path, exec_state);
}
new_sketch.paths.push(current_path);
@ -2137,7 +2119,7 @@ async fn inner_bezier_curve(
/// Use a sketch to cut a hole in another sketch.
pub async fn hole(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let (hole_sketch, sketch): (SketchSet, Sketch) = args.get_sketches()?;
let (hole_sketch, sketch): (Vec<Sketch>, Sketch) = args.get_sketches(exec_state)?;
let new_sketch = inner_hole(hole_sketch, sketch, exec_state, args).await?;
Ok(KclValue::Sketch {
@ -2181,13 +2163,12 @@ pub async fn hole(exec_state: &mut ExecState, args: Args) -> Result<KclValue, Kc
feature_tree_operation = true,
}]
async fn inner_hole(
hole_sketch: SketchSet,
hole_sketch: Vec<Sketch>,
sketch: Sketch,
exec_state: &mut ExecState,
args: Args,
) -> Result<Sketch, KclError> {
let hole_sketches: Vec<Sketch> = hole_sketch.into();
for hole_sketch in hole_sketches {
for hole_sketch in hole_sketch {
args.batch_modeling_cmd(
exec_state.next_uuid(),
ModelingCmd::from(mcmd::Solid2dAddHole {
@ -2256,7 +2237,7 @@ mod tests {
str_json = serde_json::to_string(&TagIdentifier {
value: "thing".to_string(),
info: None,
info: Vec::new(),
meta: Default::default(),
})
.unwrap();
@ -2265,7 +2246,7 @@ mod tests {
data,
crate::std::sketch::FaceTag::Tag(Box::new(TagIdentifier {
value: "thing".to_string(),
info: None,
info: Vec::new(),
meta: Default::default()
}))
);

View File

@ -9,7 +9,10 @@ use serde::{Deserialize, Serialize};
use crate::{
errors::KclError,
execution::{ExecState, Helix, KclValue, Sketch, Solid},
execution::{
kcl_value::{ArrayLen, RuntimeType},
ExecState, Helix, KclValue, PrimitiveType, Sketch, Solid,
},
std::{extrude::do_post_extrude, fillet::default_tolerance, Args},
};
@ -24,13 +27,17 @@ pub enum SweepPath {
/// Extrude a sketch along a path.
pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let sketch = args.get_unlabeled_kw_arg("sketch")?;
let sketches = args.get_unlabeled_kw_arg_typed(
"sketches",
&RuntimeType::Array(PrimitiveType::Sketch, ArrayLen::NonEmpty),
exec_state,
)?;
let path: SweepPath = args.get_kw_arg("path")?;
let sectional = args.get_kw_arg_opt("sectional")?;
let tolerance = args.get_kw_arg_opt("tolerance")?;
let value = inner_sweep(sketch, path, sectional, tolerance, exec_state, args).await?;
Ok(KclValue::Solid { value })
let value = inner_sweep(sketches, path, sectional, tolerance, exec_state, args).await?;
Ok(value.into())
}
/// Extrude a sketch along a path.
@ -41,6 +48,9 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// creating more complex shapes that can't be created with a simple
/// extrusion.
///
/// You can provide more than one sketch to sweep, and they will all be
/// swept along the same path.
///
/// ```no_run
/// // Create a pipe using a sweep.
///
@ -94,40 +104,78 @@ pub async fn sweep(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// |> circle( center = [0, 0], radius = 1)
/// |> sweep(path = helixPath)
/// ```
///
/// ```
/// // Sweep two sketches along the same path.
///
/// sketch001 = startSketchOn('XY')
/// rectangleSketch = startProfileAt([-200, 23.86], sketch001)
/// |> angledLine([0, 73.47], %, $rectangleSegmentA001)
/// |> angledLine([
/// segAng(rectangleSegmentA001) - 90,
/// 50.61
/// ], %)
/// |> angledLine([
/// segAng(rectangleSegmentA001),
/// -segLen(rectangleSegmentA001)
/// ], %)
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
///
/// sketch002 = startSketchOn('YZ')
/// sweepPath = startProfileAt([0, 0], sketch002)
/// |> yLine(length = 231.81)
/// |> tangentialArc({
/// radius = 80,
/// offset = -90,
/// }, %)
/// |> xLine(length = 384.93)
///
/// sweep([rectangleSketch, circleSketch], path = sweepPath)
/// ```
#[stdlib {
name = "sweep",
feature_tree_operation = true,
keywords = true,
unlabeled_first = true,
args = {
sketch = { docs = "The sketch that should be swept in space" },
sketches = { docs = "The sketch or set of sketches that should be swept in space" },
path = { docs = "The path to sweep the sketch along" },
sectional = { docs = "If true, the sweep will be broken up into sub-sweeps (extrusions, revolves, sweeps) based on the trajectory path components." },
tolerance = { docs = "Tolerance for this operation" },
}
}]
async fn inner_sweep(
sketch: Sketch,
sketches: Vec<Sketch>,
path: SweepPath,
sectional: Option<bool>,
tolerance: Option<f64>,
exec_state: &mut ExecState,
args: Args,
) -> Result<Box<Solid>, KclError> {
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::Sweep {
target: sketch.id.into(),
trajectory: match path {
SweepPath::Sketch(sketch) => sketch.id.into(),
SweepPath::Helix(helix) => helix.value.into(),
},
sectional: sectional.unwrap_or(false),
tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
}),
)
.await?;
) -> Result<Vec<Solid>, KclError> {
let trajectory = match path {
SweepPath::Sketch(sketch) => sketch.id.into(),
SweepPath::Helix(helix) => helix.value.into(),
};
do_post_extrude(sketch, id.into(), 0.0, exec_state, args).await
let mut solids = Vec::new();
for sketch in &sketches {
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::Sweep {
target: sketch.id.into(),
trajectory,
sectional: sectional.unwrap_or(false),
tolerance: LengthUnit(tolerance.unwrap_or(default_tolerance(&args.ctx.settings.units))),
}),
)
.await?;
solids.push(do_post_extrude(sketch.clone(), id.into(), 0.0, exec_state, args.clone()).await?);
}
Ok(solids)
}

View File

@ -13,18 +13,28 @@ use kittycad_modeling_cmds as kcmc;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{ExecState, KclValue, SolidOrImportedGeometry},
execution::{
kcl_value::{ArrayLen, RuntimeType},
ExecState, KclValue, PrimitiveType, SolidOrImportedGeometry,
},
std::Args,
};
/// Scale a solid.
pub async fn scale(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid = args.get_unlabeled_kw_arg("solid")?;
let solids = args.get_unlabeled_kw_arg_typed(
"solids",
&RuntimeType::Union(vec![
RuntimeType::Array(PrimitiveType::Solid, ArrayLen::NonEmpty),
RuntimeType::Primitive(PrimitiveType::ImportedGeometry),
]),
exec_state,
)?;
let scale = args.get_kw_arg("scale")?;
let global = args.get_kw_arg_opt("global")?;
let solid = inner_scale(solid, scale, global, exec_state, args).await?;
Ok(solid.into())
let solids = inner_scale(solids, scale, global, exec_state, args).await?;
Ok(solids.into())
}
/// Scale a solid.
@ -84,59 +94,102 @@ pub async fn scale(exec_state: &mut ExecState, args: Args) -> Result<KclValue, K
/// scale = [1.0, 1.0, 2.5],
/// )
/// ```
///
/// ```
/// // Sweep two sketches along the same path.
///
/// sketch001 = startSketchOn('XY')
/// rectangleSketch = startProfileAt([-200, 23.86], sketch001)
/// |> angledLine([0, 73.47], %, $rectangleSegmentA001)
/// |> angledLine([
/// segAng(rectangleSegmentA001) - 90,
/// 50.61
/// ], %)
/// |> angledLine([
/// segAng(rectangleSegmentA001),
/// -segLen(rectangleSegmentA001)
/// ], %)
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
///
/// sketch002 = startSketchOn('YZ')
/// sweepPath = startProfileAt([0, 0], sketch002)
/// |> yLine(length = 231.81)
/// |> tangentialArc({
/// radius = 80,
/// offset = -90,
/// }, %)
/// |> xLine(length = 384.93)
///
/// parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
///
/// // Scale the sweep.
/// scale(parts, scale = [1.0, 1.0, 0.5])
/// ```
#[stdlib {
name = "scale",
feature_tree_operation = false,
keywords = true,
unlabeled_first = true,
args = {
solid = {docs = "The solid to scale."},
solids = {docs = "The solid or set of solids to scale."},
scale = {docs = "The scale factor for the x, y, and z axes."},
global = {docs = "If true, the transform is applied in global space. The origin of the model will move. By default, the transform is applied in local sketch axis, therefore the origin will not move."}
}
}]
async fn inner_scale(
solid: SolidOrImportedGeometry,
solids: SolidOrImportedGeometry,
scale: [f64; 3],
global: Option<bool>,
exec_state: &mut ExecState,
args: Args,
) -> Result<SolidOrImportedGeometry, KclError> {
let id = exec_state.next_uuid();
for solid_id in solids.ids() {
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::SetObjectTransform {
object_id: solid.id(),
transforms: vec![shared::ComponentTransform {
scale: Some(shared::TransformBy::<Point3d<f64>> {
property: Point3d {
x: scale[0],
y: scale[1],
z: scale[2],
},
set: false,
is_local: !global.unwrap_or(false),
}),
translate: None,
rotate_rpy: None,
rotate_angle_axis: None,
}],
}),
)
.await?;
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::SetObjectTransform {
object_id: solid_id,
transforms: vec![shared::ComponentTransform {
scale: Some(shared::TransformBy::<Point3d<f64>> {
property: Point3d {
x: scale[0],
y: scale[1],
z: scale[2],
},
set: false,
is_local: !global.unwrap_or(false),
}),
translate: None,
rotate_rpy: None,
rotate_angle_axis: None,
}],
}),
)
.await?;
}
Ok(solid)
Ok(solids)
}
/// Move a solid.
pub async fn translate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid = args.get_unlabeled_kw_arg("solid")?;
let solids = args.get_unlabeled_kw_arg_typed(
"solids",
&RuntimeType::Union(vec![
RuntimeType::Array(PrimitiveType::Solid, ArrayLen::NonEmpty),
RuntimeType::Primitive(PrimitiveType::ImportedGeometry),
]),
exec_state,
)?;
let translate = args.get_kw_arg("translate")?;
let global = args.get_kw_arg_opt("global")?;
let solid = inner_translate(solid, translate, global, exec_state, args).await?;
Ok(solid.into())
let solids = inner_translate(solids, translate, global, exec_state, args).await?;
Ok(solids.into())
}
/// Move a solid.
@ -188,54 +241,97 @@ pub async fn translate(exec_state: &mut ExecState, args: Args) -> Result<KclValu
/// translate = [1.0, 1.0, 2.5],
/// )
/// ```
///
/// ```
/// // Sweep two sketches along the same path.
///
/// sketch001 = startSketchOn('XY')
/// rectangleSketch = startProfileAt([-200, 23.86], sketch001)
/// |> angledLine([0, 73.47], %, $rectangleSegmentA001)
/// |> angledLine([
/// segAng(rectangleSegmentA001) - 90,
/// 50.61
/// ], %)
/// |> angledLine([
/// segAng(rectangleSegmentA001),
/// -segLen(rectangleSegmentA001)
/// ], %)
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
///
/// sketch002 = startSketchOn('YZ')
/// sweepPath = startProfileAt([0, 0], sketch002)
/// |> yLine(length = 231.81)
/// |> tangentialArc({
/// radius = 80,
/// offset = -90,
/// }, %)
/// |> xLine(length = 384.93)
///
/// parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
///
/// // Move the sweeps.
/// translate(parts, translate = [1.0, 1.0, 2.5])
/// ```
#[stdlib {
name = "translate",
feature_tree_operation = false,
keywords = true,
unlabeled_first = true,
args = {
solid = {docs = "The solid to move."},
solids = {docs = "The solid or set of solids to move."},
translate = {docs = "The amount to move the solid in all three axes."},
global = {docs = "If true, the transform is applied in global space. The origin of the model will move. By default, the transform is applied in local sketch axis, therefore the origin will not move."}
}
}]
async fn inner_translate(
solid: SolidOrImportedGeometry,
solids: SolidOrImportedGeometry,
translate: [f64; 3],
global: Option<bool>,
exec_state: &mut ExecState,
args: Args,
) -> Result<SolidOrImportedGeometry, KclError> {
let id = exec_state.next_uuid();
for solid_id in solids.ids() {
let id = exec_state.next_uuid();
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::SetObjectTransform {
object_id: solid.id(),
transforms: vec![shared::ComponentTransform {
translate: Some(shared::TransformBy::<Point3d<LengthUnit>> {
property: shared::Point3d {
x: LengthUnit(translate[0]),
y: LengthUnit(translate[1]),
z: LengthUnit(translate[2]),
},
set: false,
is_local: !global.unwrap_or(false),
}),
scale: None,
rotate_rpy: None,
rotate_angle_axis: None,
}],
}),
)
.await?;
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::SetObjectTransform {
object_id: solid_id,
transforms: vec![shared::ComponentTransform {
translate: Some(shared::TransformBy::<Point3d<LengthUnit>> {
property: shared::Point3d {
x: LengthUnit(translate[0]),
y: LengthUnit(translate[1]),
z: LengthUnit(translate[2]),
},
set: false,
is_local: !global.unwrap_or(false),
}),
scale: None,
rotate_rpy: None,
rotate_angle_axis: None,
}],
}),
)
.await?;
}
Ok(solid)
Ok(solids)
}
/// Rotate a solid.
pub async fn rotate(exec_state: &mut ExecState, args: Args) -> Result<KclValue, KclError> {
let solid = args.get_unlabeled_kw_arg("solid")?;
let solids = args.get_unlabeled_kw_arg_typed(
"solid",
&RuntimeType::Union(vec![
RuntimeType::Array(PrimitiveType::Solid, ArrayLen::NonEmpty),
RuntimeType::Primitive(PrimitiveType::ImportedGeometry),
]),
exec_state,
)?;
let roll = args.get_kw_arg_opt("roll")?;
let pitch = args.get_kw_arg_opt("pitch")?;
let yaw = args.get_kw_arg_opt("yaw")?;
@ -343,8 +439,8 @@ pub async fn rotate(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
}
}
let solid = inner_rotate(solid, roll, pitch, yaw, axis, angle, global, exec_state, args).await?;
Ok(solid.into())
let solids = inner_rotate(solids, roll, pitch, yaw, axis, angle, global, exec_state, args).await?;
Ok(solids.into())
}
/// Rotate a solid.
@ -459,13 +555,47 @@ pub async fn rotate(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
/// angle = 90,
/// )
/// ```
///
/// ```
/// // Sweep two sketches along the same path.
///
/// sketch001 = startSketchOn('XY')
/// rectangleSketch = startProfileAt([-200, 23.86], sketch001)
/// |> angledLine([0, 73.47], %, $rectangleSegmentA001)
/// |> angledLine([
/// segAng(rectangleSegmentA001) - 90,
/// 50.61
/// ], %)
/// |> angledLine([
/// segAng(rectangleSegmentA001),
/// -segLen(rectangleSegmentA001)
/// ], %)
/// |> line(endAbsolute = [profileStartX(%), profileStartY(%)])
/// |> close()
///
/// circleSketch = circle(sketch001, center = [200, -30.29], radius = 32.63)
///
/// sketch002 = startSketchOn('YZ')
/// sweepPath = startProfileAt([0, 0], sketch002)
/// |> yLine(length = 231.81)
/// |> tangentialArc({
/// radius = 80,
/// offset = -90,
/// }, %)
/// |> xLine(length = 384.93)
///
/// parts = sweep([rectangleSketch, circleSketch], path = sweepPath)
///
/// // Rotate the sweeps.
/// rotate(parts, axis = [0, 0, 1.0], angle = 90)
/// ```
#[stdlib {
name = "rotate",
feature_tree_operation = false,
keywords = true,
unlabeled_first = true,
args = {
solid = {docs = "The solid to rotate."},
solids = {docs = "The solid or set of solids to rotate."},
roll = {docs = "The roll angle in degrees. Must be used with `pitch` and `yaw`. Must be between -360 and 360.", include_in_snippet = true},
pitch = {docs = "The pitch angle in degrees. Must be used with `roll` and `yaw`. Must be between -360 and 360.", include_in_snippet = true},
yaw = {docs = "The yaw angle in degrees. Must be used with `roll` and `pitch`. Must be between -360 and 360.", include_in_snippet = true},
@ -476,7 +606,7 @@ pub async fn rotate(exec_state: &mut ExecState, args: Args) -> Result<KclValue,
}]
#[allow(clippy::too_many_arguments)]
async fn inner_rotate(
solid: SolidOrImportedGeometry,
solids: SolidOrImportedGeometry,
roll: Option<f64>,
pitch: Option<f64>,
yaw: Option<f64>,
@ -486,58 +616,60 @@ async fn inner_rotate(
exec_state: &mut ExecState,
args: Args,
) -> Result<SolidOrImportedGeometry, KclError> {
let id = exec_state.next_uuid();
for solid_id in solids.ids() {
let id = exec_state.next_uuid();
if let (Some(roll), Some(pitch), Some(yaw)) = (roll, pitch, yaw) {
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::SetObjectTransform {
object_id: solid.id(),
transforms: vec![shared::ComponentTransform {
rotate_rpy: Some(shared::TransformBy::<Point3d<f64>> {
property: shared::Point3d {
x: roll,
y: pitch,
z: yaw,
},
set: false,
is_local: !global.unwrap_or(false),
}),
scale: None,
rotate_angle_axis: None,
translate: None,
}],
}),
)
.await?;
if let (Some(roll), Some(pitch), Some(yaw)) = (roll, pitch, yaw) {
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::SetObjectTransform {
object_id: solid_id,
transforms: vec![shared::ComponentTransform {
rotate_rpy: Some(shared::TransformBy::<Point3d<f64>> {
property: shared::Point3d {
x: roll,
y: pitch,
z: yaw,
},
set: false,
is_local: !global.unwrap_or(false),
}),
scale: None,
rotate_angle_axis: None,
translate: None,
}],
}),
)
.await?;
}
if let (Some(axis), Some(angle)) = (axis, angle) {
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::SetObjectTransform {
object_id: solid_id,
transforms: vec![shared::ComponentTransform {
rotate_angle_axis: Some(shared::TransformBy::<Point4d<f64>> {
property: shared::Point4d {
x: axis[0],
y: axis[1],
z: axis[2],
w: angle,
},
set: false,
is_local: !global.unwrap_or(false),
}),
scale: None,
rotate_rpy: None,
translate: None,
}],
}),
)
.await?;
}
}
if let (Some(axis), Some(angle)) = (axis, angle) {
args.batch_modeling_cmd(
id,
ModelingCmd::from(mcmd::SetObjectTransform {
object_id: solid.id(),
transforms: vec![shared::ComponentTransform {
rotate_angle_axis: Some(shared::TransformBy::<Point4d<f64>> {
property: shared::Point4d {
x: axis[0],
y: axis[1],
z: axis[2],
w: angle,
},
set: false,
is_local: !global.unwrap_or(false),
}),
scale: None,
rotate_rpy: None,
translate: None,
}],
}),
)
.await?;
}
Ok(solid)
Ok(solids)
}
#[cfg(test)]

View File

@ -81,7 +81,7 @@ async fn do_execute_and_snapshot(
ctx: &ExecutorContext,
program: Program,
) -> Result<(ExecState, EnvironmentRef, image::DynamicImage), ExecErrorWithState> {
let mut exec_state = ExecState::new(&ctx.settings);
let mut exec_state = ExecState::new(ctx);
let result = ctx
.run(&program, &mut exec_state)
.await
@ -156,7 +156,7 @@ pub async fn execute_and_export_step(
ExecErrorWithState,
> {
let ctx = new_context(units, true, current_file).await?;
let mut exec_state = ExecState::new(&ctx.settings);
let mut exec_state = ExecState::new(&ctx);
let program = Program::parse_no_errs(code)
.map_err(|err| ExecErrorWithState::new(KclErrorWithOutputs::no_outputs(err).into(), exec_state.clone()))?;
let result = ctx

View File

@ -821,7 +821,7 @@ impl Type {
pub fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
match self {
Type::Primitive(t) => t.to_string(),
Type::Array(t) => format!("{t}[]"),
Type::Array(t) => format!("[{t}]"),
Type::Object { properties } => {
let mut result = "{".to_owned();
for p in properties {
@ -842,6 +842,103 @@ impl Type {
}
}
/// Collect all the kcl files in a directory, recursively.
#[cfg(not(target_arch = "wasm32"))]
#[async_recursion::async_recursion]
pub(crate) async fn walk_dir(dir: &std::path::PathBuf) -> Result<Vec<std::path::PathBuf>, anyhow::Error> {
// Make sure we actually have a directory.
if !dir.is_dir() {
anyhow::bail!("`{}` is not a directory", dir.display());
}
let mut entries = tokio::fs::read_dir(dir).await?;
let mut files = Vec::new();
while let Some(entry) = entries.next_entry().await? {
let path = entry.path();
if path.is_dir() {
files.extend(walk_dir(&path).await?);
} else if path.extension().is_some_and(|ext| ext == "kcl") {
files.push(path);
}
}
Ok(files)
}
/// Recast all the kcl files in a directory, recursively.
#[cfg(not(target_arch = "wasm32"))]
pub async fn recast_dir(dir: &std::path::Path, options: &crate::FormatOptions) -> Result<(), crate::KclError> {
let files = walk_dir(&dir.to_path_buf()).await.map_err(|err| {
crate::KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to walk directory `{}`: {:?}", dir.display(), err),
source_ranges: vec![crate::SourceRange::default()],
})
})?;
let futures = files
.into_iter()
.map(|file| {
let options = options.clone();
tokio::spawn(async move {
let contents = tokio::fs::read_to_string(&file).await.map_err(|err| {
crate::KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to read file `{}`: {:?}", file.display(), err),
source_ranges: vec![crate::SourceRange::default()],
})
})?;
let (program, ces) = crate::Program::parse(&contents)?;
for ce in &ces {
if ce.severity != crate::errors::Severity::Warning {
return Err(crate::KclError::Semantic(ce.clone().into()));
}
}
let Some(program) = program else {
return Err(crate::KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to parse file `{}`: {:?}", file.display(), ces),
source_ranges: vec![crate::SourceRange::default()],
}));
};
let recast = program.recast_with_options(&options);
tokio::fs::write(&file, recast).await.map_err(|err| {
crate::KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to write file `{}`: {:?}", file.display(), err),
source_ranges: vec![crate::SourceRange::default()],
})
})?;
Ok::<(), crate::KclError>(())
})
})
.collect::<Vec<_>>();
// Join all futures and await their completion
let results = futures::future::join_all(futures).await;
// Check if any of the futures failed.
let mut errors = Vec::new();
for result in results {
if let Err(err) = result.map_err(|err| {
crate::KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to recast file: {:?}", err),
source_ranges: vec![crate::SourceRange::default()],
})
})? {
errors.push(err);
}
}
if !errors.is_empty() {
return Err(crate::KclError::Internal(crate::errors::KclErrorDetails {
message: format!("Failed to recast files: {:?}", errors),
source_ranges: vec![crate::SourceRange::default()],
}));
}
Ok(())
}
#[cfg(test)]
mod tests {
use pretty_assertions::assert_eq;
@ -1268,7 +1365,7 @@ thing(1)
#[test]
fn test_recast_typed_fn() {
let some_program_string = r#"fn thing(x: string, y: bool[]): number {
let some_program_string = r#"fn thing(x: string, y: [bool]): number {
return x + 1
}
"#;