merge main

This commit is contained in:
benjamaan476
2025-06-18 12:09:02 +01:00
1012 changed files with 11785231 additions and 11653259 deletions

View File

@ -32,6 +32,8 @@ pub(crate) const IMPL_KCL: &str = "kcl";
pub(crate) const IMPL_PRIMITIVE: &str = "primitive";
pub(super) const IMPL_VALUES: [&str; 3] = [IMPL_RUST, IMPL_KCL, IMPL_PRIMITIVE];
pub(crate) const DEPRECATED: &str = "deprecated";
#[derive(Clone, Copy, Eq, PartialEq, Debug, Default)]
pub enum Impl {
#[default]

View File

@ -45,26 +45,6 @@ pub struct ArtifactCommand {
pub command: ModelingCmd,
}
impl PartialOrd for ArtifactCommand {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
// Order by the source range.
let range = self.range.cmp(&other.range);
if range != std::cmp::Ordering::Equal {
return Some(range);
}
#[cfg(test)]
{
// If the ranges are equal, order by the serde variant.
Some(
crate::variant_name::variant_name(&self.command)
.cmp(&crate::variant_name::variant_name(&other.command)),
)
}
#[cfg(not(test))]
self.cmd_id.partial_cmp(&other.cmd_id)
}
}
pub type DummyPathToNode = Vec<()>;
fn serialize_dummy_path_to_node<S>(_path_to_node: &DummyPathToNode, serializer: S) -> Result<S::Ok, S::Error>
@ -185,11 +165,12 @@ pub struct Sweep {
pub code_ref: CodeRef,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, PartialOrd, Ord, ts_rs::TS)]
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub enum SweepSubType {
Extrusion,
ExtrusionTwist,
Revolve,
RevolveAboutEdge,
Loft,
@ -258,7 +239,7 @@ pub struct Cap {
pub cmd_id: uuid::Uuid,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Ord, PartialOrd, Eq, ts_rs::TS)]
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub enum CapSubType {
@ -282,7 +263,7 @@ pub struct SweepEdge {
pub common_surface_ids: Vec<ArtifactId>,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Ord, PartialOrd, Eq, ts_rs::TS)]
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub enum SweepEdgeSubType {
@ -304,7 +285,7 @@ pub struct EdgeCut {
pub code_ref: CodeRef,
}
#[derive(Debug, Clone, Copy, Serialize, PartialEq, PartialOrd, Ord, Eq, ts_rs::TS)]
#[derive(Debug, Clone, Copy, Serialize, PartialEq, Eq, ts_rs::TS)]
#[ts(export_to = "Artifact.ts")]
#[serde(rename_all = "camelCase")]
pub enum EdgeCutSubType {
@ -361,135 +342,6 @@ pub enum Artifact {
Helix(Helix),
}
impl Artifact {
pub(crate) fn rank(&self) -> u8 {
match self {
Artifact::Plane(_) => 0,
Artifact::StartSketchOnPlane(_) => 1,
Artifact::StartSketchOnFace(_) => 2,
Artifact::Path(_) => 3,
Artifact::Segment(_) => 4,
Artifact::Solid2d(_) => 5,
Artifact::Sweep(_) => 6,
Artifact::CompositeSolid(_) => 7,
Artifact::Wall(_) => 8,
Artifact::Cap(Cap { sub_type, .. }) if *sub_type == CapSubType::Start => 9,
Artifact::Cap(Cap { sub_type, .. }) if *sub_type == CapSubType::Start => 10,
Artifact::Cap(_) => 11,
Artifact::SweepEdge(SweepEdge { sub_type, .. }) if *sub_type == SweepEdgeSubType::Adjacent => 12,
Artifact::SweepEdge(SweepEdge { sub_type, .. }) if *sub_type == SweepEdgeSubType::Opposite => 13,
Artifact::SweepEdge(_) => 14,
Artifact::EdgeCut(_) => 15,
Artifact::EdgeCutEdge(_) => 16,
Artifact::Helix(_) => 17,
}
}
}
impl PartialOrd for Artifact {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
// The only thing we want to sort is if we have two sweep edges, we want
// to sort them by the sub_type.
match (self, other) {
(Artifact::SweepEdge(a), Artifact::SweepEdge(b)) => {
if a.sub_type != b.sub_type {
return Some(a.sub_type.cmp(&b.sub_type));
}
if a.sweep_id != b.sweep_id {
return Some(a.sweep_id.cmp(&b.sweep_id));
}
if a.cmd_id != b.cmd_id {
return Some(a.cmd_id.cmp(&b.cmd_id));
}
if a.index != b.index {
return Some(a.index.cmp(&b.index));
}
Some(a.id.cmp(&b.id))
}
(Artifact::EdgeCut(a), Artifact::EdgeCut(b)) => {
if a.code_ref.range != b.code_ref.range {
return Some(a.code_ref.range.cmp(&b.code_ref.range));
}
Some(a.id.cmp(&b.id))
}
(Artifact::EdgeCutEdge(a), Artifact::EdgeCutEdge(b)) => Some(a.edge_cut_id.cmp(&b.edge_cut_id)),
(Artifact::Sweep(a), Artifact::Sweep(b)) => {
if a.code_ref.range != b.code_ref.range {
return Some(a.code_ref.range.cmp(&b.code_ref.range));
}
Some(a.id.cmp(&b.id))
}
// Sort the planes by their code_ref range.
(Artifact::Plane(a), Artifact::Plane(b)) => {
if a.code_ref.range != b.code_ref.range {
return Some(a.code_ref.range.cmp(&b.code_ref.range));
}
Some(a.id.cmp(&b.id))
}
// Sort the paths by their code_ref range.
(Artifact::Path(a), Artifact::Path(b)) => {
if a.code_ref.range != b.code_ref.range {
return Some(a.code_ref.range.cmp(&b.code_ref.range));
}
Some(a.id.cmp(&b.id))
}
// Sort the segments by their code_ref range.
(Artifact::Segment(a), Artifact::Segment(b)) => {
if a.code_ref.range != b.code_ref.range {
return Some(a.code_ref.range.cmp(&b.code_ref.range));
}
Some(a.id.cmp(&b.id))
}
// Sort the solid2d by their id.
(Artifact::Solid2d(a), Artifact::Solid2d(b)) => {
if a.path_id != b.path_id {
return Some(a.path_id.cmp(&b.path_id));
}
Some(a.id.cmp(&b.id))
}
// Sort the walls by their code_ref range.
(Artifact::Wall(a), Artifact::Wall(b)) => {
if a.sweep_id != b.sweep_id {
return Some(a.sweep_id.cmp(&b.sweep_id));
}
if a.cmd_id != b.cmd_id {
return Some(a.cmd_id.cmp(&b.cmd_id));
}
if a.face_code_ref.range != b.face_code_ref.range {
return Some(a.face_code_ref.range.cmp(&b.face_code_ref.range));
}
if a.seg_id != b.seg_id {
return Some(a.seg_id.cmp(&b.seg_id));
}
Some(a.id.cmp(&b.id))
}
// Sort the caps by their code_ref range.
(Artifact::Cap(a), Artifact::Cap(b)) => {
if a.sub_type != b.sub_type {
return Some(a.sub_type.cmp(&b.sub_type));
}
if a.cmd_id != b.cmd_id {
return Some(a.cmd_id.cmp(&b.cmd_id));
}
if a.sweep_id != b.sweep_id {
return Some(a.sweep_id.cmp(&b.sweep_id));
}
if a.face_code_ref.range != b.face_code_ref.range {
return Some(a.face_code_ref.range.cmp(&b.face_code_ref.range));
}
Some(a.id.cmp(&b.id))
}
(Artifact::CompositeSolid(a), Artifact::CompositeSolid(b)) => Some(a.id.cmp(&b.id)),
(Artifact::StartSketchOnFace(a), Artifact::StartSketchOnFace(b)) => Some(a.id.cmp(&b.id)),
(Artifact::StartSketchOnPlane(a), Artifact::StartSketchOnPlane(b)) => Some(a.id.cmp(&b.id)),
// Planes are first, then paths, then segments, then solids2ds, then sweeps, then
// walls, then caps, then sweep edges, then edge cuts, then edge cut edges, then
// helixes.
_ => Some(self.rank().cmp(&other.rank())),
}
}
}
impl Artifact {
pub(crate) fn id(&self) -> ArtifactId {
match self {
@ -692,17 +544,15 @@ impl ArtifactGraph {
self.map.values()
}
pub fn clear(&mut self) {
self.map.clear();
self.item_count = 0;
}
/// Consume the artifact graph and return the map of artifacts.
fn into_map(self) -> IndexMap<ArtifactId, Artifact> {
self.map
}
/// Used to make the mermaid tests deterministic.
#[cfg(test)]
pub(crate) fn sort(&mut self) {
self.map
.sort_by(|_ak, av, _bk, bv| av.partial_cmp(bv).unwrap_or(std::cmp::Ordering::Equal));
}
}
/// Build the artifact graph from the artifact commands and the responses. The
@ -1102,11 +952,13 @@ fn artifacts_to_update(
return Ok(return_arr);
}
ModelingCmd::Extrude(kcmc::Extrude { target, .. })
| ModelingCmd::TwistExtrude(kcmc::TwistExtrude { target, .. })
| ModelingCmd::Revolve(kcmc::Revolve { target, .. })
| ModelingCmd::RevolveAboutEdge(kcmc::RevolveAboutEdge { target, .. })
| ModelingCmd::Sweep(kcmc::Sweep { target, .. }) => {
let sub_type = match cmd {
ModelingCmd::Extrude(_) => SweepSubType::Extrusion,
ModelingCmd::TwistExtrude(_) => SweepSubType::ExtrusionTwist,
ModelingCmd::Revolve(_) => SweepSubType::Revolve,
ModelingCmd::RevolveAboutEdge(_) => SweepSubType::RevolveAboutEdge,
ModelingCmd::Sweep(_) => SweepSubType::Sweep,

View File

@ -109,9 +109,7 @@ impl GlobalState {
variables: self.main.exec_state.variables(self.main.result_env),
filenames: self.exec_state.filenames(),
#[cfg(feature = "artifact-graph")]
operations: self.exec_state.artifacts.operations,
#[cfg(feature = "artifact-graph")]
artifact_commands: self.exec_state.artifacts.commands,
operations: self.exec_state.root_module_artifacts.operations,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.exec_state.artifacts.graph,
errors: self.exec_state.errors,

View File

@ -2,7 +2,9 @@ use indexmap::IndexMap;
use serde::Serialize;
use super::{types::NumericType, ArtifactId, KclValue};
use crate::{ModuleId, NodePath, SourceRange};
#[cfg(feature = "artifact-graph")]
use crate::parsing::ast::types::{Node, Program};
use crate::{parsing::ast::types::ItemVisibility, ModuleId, NodePath, SourceRange};
/// A CAD modeling operation for display in the feature tree, AKA operations
/// timeline.
@ -26,6 +28,20 @@ pub enum Operation {
is_error: bool,
},
#[serde(rename_all = "camelCase")]
VariableDeclaration {
/// The variable name.
name: String,
/// The value of the variable.
value: OpKclValue,
/// The visibility modifier of the variable, e.g. `export`. `Default`
/// means there is no visibility modifier.
visibility: ItemVisibility,
/// The node path of the operation in the source code.
node_path: NodePath,
/// The source range of the operation in the source code.
source_range: SourceRange,
},
#[serde(rename_all = "camelCase")]
GroupBegin {
/// The details of the group.
group: Group,
@ -37,32 +53,36 @@ pub enum Operation {
GroupEnd,
}
/// A way for sorting the operations in the timeline. This is used to sort
/// operations in the timeline and to determine the order of operations.
/// We use this for the multi-threaded snapshotting, so that we can have deterministic
/// output.
impl PartialOrd for Operation {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(match (self, other) {
(Self::StdLibCall { source_range: a, .. }, Self::StdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::StdLibCall { source_range: a, .. }, Self::GroupBegin { source_range: b, .. }) => a.cmp(b),
(Self::StdLibCall { .. }, Self::GroupEnd) => std::cmp::Ordering::Less,
(Self::GroupBegin { source_range: a, .. }, Self::GroupBegin { source_range: b, .. }) => a.cmp(b),
(Self::GroupBegin { source_range: a, .. }, Self::StdLibCall { source_range: b, .. }) => a.cmp(b),
(Self::GroupBegin { .. }, Self::GroupEnd) => std::cmp::Ordering::Less,
(Self::GroupEnd, Self::StdLibCall { .. }) => std::cmp::Ordering::Greater,
(Self::GroupEnd, Self::GroupBegin { .. }) => std::cmp::Ordering::Greater,
(Self::GroupEnd, Self::GroupEnd) => std::cmp::Ordering::Equal,
})
}
}
impl Operation {
/// If the variant is `StdLibCall`, set the `is_error` field.
pub(crate) fn set_std_lib_call_is_error(&mut self, is_err: bool) {
match self {
Self::StdLibCall { ref mut is_error, .. } => *is_error = is_err,
Self::GroupBegin { .. } | Self::GroupEnd => {}
Self::VariableDeclaration { .. } | Self::GroupBegin { .. } | Self::GroupEnd => {}
}
}
#[cfg(feature = "artifact-graph")]
pub(crate) fn fill_node_paths(&mut self, program: &Node<Program>, cached_body_items: usize) {
match self {
Operation::StdLibCall {
node_path,
source_range,
..
}
| Operation::VariableDeclaration {
node_path,
source_range,
..
}
| Operation::GroupBegin {
node_path,
source_range,
..
} => {
node_path.fill_placeholder(program, cached_body_items, *source_range);
}
Operation::GroupEnd => {}
}
}
}

View File

@ -6,13 +6,14 @@ use crate::{
errors::{KclError, KclErrorDetails},
execution::{
annotations,
cad_op::OpKclValue,
fn_call::Args,
kcl_value::{FunctionSource, TypeDef},
memory,
state::ModuleState,
types::{NumericType, PrimitiveType, RuntimeType},
BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, PlaneType, StatementKind,
TagIdentifier,
BodyType, EnvironmentRef, ExecState, ExecutorContext, KclValue, Metadata, ModelingCmdMeta, ModuleArtifactState,
Operation, PlaneType, StatementKind, TagIdentifier,
},
fmt,
modules::{ModuleId, ModulePath, ModuleRepr},
@ -24,7 +25,7 @@ use crate::{
},
source_range::SourceRange,
std::args::TyF64,
CompilationError,
CompilationError, NodePath,
};
impl<'a> StatementKind<'a> {
@ -83,7 +84,10 @@ impl ExecutorContext {
preserve_mem: bool,
module_id: ModuleId,
path: &ModulePath,
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>), KclError> {
) -> Result<
(Option<KclValue>, EnvironmentRef, Vec<String>, ModuleArtifactState),
(KclError, Option<ModuleArtifactState>),
> {
crate::log::log(format!("enter module {path} {}", exec_state.stack()));
let mut local_state = ModuleState::new(path.clone(), exec_state.stack().memory.clone(), Some(module_id));
@ -93,7 +97,8 @@ impl ExecutorContext {
let no_prelude = self
.handle_annotations(program.inner_attrs.iter(), crate::execution::BodyType::Root, exec_state)
.await?;
.await
.map_err(|err| (err, None))?;
if !preserve_mem {
exec_state.mut_stack().push_new_root_env(!no_prelude);
@ -108,13 +113,18 @@ impl ExecutorContext {
} else {
exec_state.mut_stack().pop_env()
};
if !preserve_mem {
let module_artifacts = if !preserve_mem {
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
}
local_state.artifacts
} else {
std::mem::take(&mut exec_state.mod_local.artifacts)
};
crate::log::log(format!("leave {path}"));
result.map(|result| (result, env_ref, local_state.module_exports))
result
.map_err(|err| (err, Some(module_artifacts.clone())))
.map(|result| (result, env_ref, local_state.module_exports, module_artifacts))
}
/// Execute an AST's program.
@ -326,6 +336,16 @@ impl ExecutorContext {
.mut_stack()
.add(var_name.clone(), rhs.clone(), source_range)?;
if rhs.show_variable_in_feature_tree() {
exec_state.push_op(Operation::VariableDeclaration {
name: var_name.clone(),
value: OpKclValue::from(&rhs),
visibility: variable_declaration.visibility,
node_path: NodePath::placeholder(),
source_range,
});
}
// Track exports.
if let ItemVisibility::Export = variable_declaration.visibility {
if matches!(body_type, BodyType::Root) {
@ -450,12 +470,12 @@ impl ExecutorContext {
if matches!(body_type, BodyType::Root) {
// Flush the batch queue.
self.engine
exec_state
.flush_batch(
ModelingCmdMeta::new(self, SourceRange::new(program.end, program.end, program.module_id)),
// True here tells the engine to flush all the end commands as well like fillets
// and chamfers where the engine would otherwise eat the ID of the segments.
true,
SourceRange::new(program.end, program.end, program.module_id),
)
.await?;
}
@ -535,12 +555,12 @@ impl ExecutorContext {
let result = match &mut repr {
ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
ModuleRepr::Kcl(_, Some((_, env_ref, items))) => Ok((*env_ref, items.clone())),
ModuleRepr::Kcl(_, Some((_, env_ref, items, _))) => Ok((*env_ref, items.clone())),
ModuleRepr::Kcl(program, cache) => self
.exec_module_from_ast(program, module_id, &path, exec_state, source_range, false)
.await
.map(|(val, er, items)| {
*cache = Some((val, er, items.clone()));
.map(|(val, er, items, module_artifacts)| {
*cache = Some((val, er, items.clone(), module_artifacts.clone()));
(er, items)
}),
ModuleRepr::Foreign(geom, _) => Err(KclError::new_semantic(KclErrorDetails::new(
@ -566,28 +586,28 @@ impl ExecutorContext {
let result = match &mut repr {
ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
ModuleRepr::Kcl(_, Some((val, _, _))) => Ok(val.clone()),
ModuleRepr::Kcl(_, Some((val, _, _, _))) => Ok(val.clone()),
ModuleRepr::Kcl(program, cached_items) => {
let result = self
.exec_module_from_ast(program, module_id, &path, exec_state, source_range, false)
.await;
match result {
Ok((val, env, items)) => {
*cached_items = Some((val.clone(), env, items));
Ok((val, env, items, module_artifacts)) => {
*cached_items = Some((val.clone(), env, items, module_artifacts));
Ok(val)
}
Err(e) => Err(e),
}
}
ModuleRepr::Foreign(_, Some(imported)) => Ok(Some(imported.clone())),
ModuleRepr::Foreign(_, Some((imported, _))) => Ok(imported.clone()),
ModuleRepr::Foreign(geom, cached) => {
let result = super::import::send_to_engine(geom.clone(), self)
let result = super::import::send_to_engine(geom.clone(), exec_state, self)
.await
.map(|geom| Some(KclValue::ImportedGeometry(geom)));
match result {
Ok(val) => {
*cached = val.clone();
*cached = Some((val.clone(), exec_state.mod_local.artifacts.clone()));
Ok(val)
}
Err(e) => Err(e),
@ -609,14 +629,16 @@ impl ExecutorContext {
exec_state: &mut ExecState,
source_range: SourceRange,
preserve_mem: bool,
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>), KclError> {
) -> Result<(Option<KclValue>, EnvironmentRef, Vec<String>, ModuleArtifactState), KclError> {
exec_state.global.mod_loader.enter_module(path);
let result = self
.exec_module_body(program, exec_state, preserve_mem, module_id, path)
.await;
exec_state.global.mod_loader.leave_module(path);
result.map_err(|err| {
// TODO: ModuleArtifactState is getting dropped here when there's an
// error. Should we propagate it for non-root modules?
result.map_err(|(err, _)| {
if let KclError::ImportCycle { .. } = err {
// It was an import cycle. Keep the original message.
err.override_source_ranges(vec![source_range])
@ -798,6 +820,10 @@ fn apply_ascription(
let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, value.into())
.map_err(|e| KclError::new_semantic(e.into()))?;
if matches!(&ty, &RuntimeType::Primitive(PrimitiveType::Number(..))) {
exec_state.clear_units_warnings(&source_range);
}
value.coerce(&ty, false, exec_state).map_err(|_| {
let suggestion = if ty == RuntimeType::length() {
", you might try coercing to a fully specified numeric type such as `number(mm)`"
@ -806,9 +832,14 @@ fn apply_ascription(
} else {
""
};
let ty_str = if let Some(ty) = value.principal_type() {
format!("(with type `{ty}`) ")
} else {
String::new()
};
KclError::new_semantic(KclErrorDetails::new(
format!(
"could not coerce value of type {} to type {ty}{suggestion}",
"could not coerce {} {ty_str}to type `{ty}`{suggestion}",
value.human_friendly_type()
),
vec![source_range],
@ -1018,14 +1049,13 @@ impl Node<MemberExpression> {
.map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
.collect(),
}),
(being_indexed, _, _) => {
let t = being_indexed.human_friendly_type();
let article = article_for(&t);
Err(KclError::new_semantic(KclErrorDetails::new(
format!("Only arrays can be indexed, but you're trying to index {article} {t}"),
vec![self.clone().into()],
)))
}
(being_indexed, _, _) => Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Only arrays can be indexed, but you're trying to index {}",
being_indexed.human_friendly_type()
),
vec![self.clone().into()],
))),
}
}
}
@ -1153,7 +1183,7 @@ impl Node<BinaryExpression> {
KclValue::Number { value: l / r, meta, ty }
}
BinaryOperator::Mod => {
let (l, r, ty) = NumericType::combine_div(left, right);
let (l, r, ty) = NumericType::combine_mod(left, right);
self.warn_on_unknown(&ty, "Modulo of", exec_state);
KclValue::Number { value: l % r, meta, ty }
}
@ -1200,11 +1230,14 @@ impl Node<BinaryExpression> {
fn warn_on_unknown(&self, ty: &NumericType, verb: &str, exec_state: &mut ExecState) {
if ty == &NumericType::Unknown {
// TODO suggest how to fix this
exec_state.warn(CompilationError::err(
self.as_source_range(),
format!("{} numbers which have unknown or incompatible units.", verb),
));
let sr = self.as_source_range();
exec_state.clear_units_warnings(&sr);
let mut err = CompilationError::err(
sr,
format!("{} numbers which have unknown or incompatible units.\nYou can probably fix this error by specifying the units using type ascription, e.g., `len: number(mm)` or `(a * b): number(deg)`.", verb),
);
err.tag = crate::errors::Tag::UnknownNumericUnits;
exec_state.warn(err);
}
}
}
@ -1753,7 +1786,7 @@ a = 42: string
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("could not coerce value of type number(default units) to type string"),
.contains("could not coerce a number (with type `number`) to type `string`"),
"Expected error but found {err:?}"
);
@ -1764,7 +1797,7 @@ a = 42: Plane
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("could not coerce value of type number(default units) to type Plane"),
.contains("could not coerce a number (with type `number`) to type `Plane`"),
"Expected error but found {err:?}"
);
@ -1775,7 +1808,7 @@ arr = [0]: [string]
let err = result.unwrap_err();
assert!(
err.to_string().contains(
"could not coerce value of type array of number(default units) with 1 value to type [string]"
"could not coerce an array of `number` with 1 value (with type `[any; 1]`) to type `[string]`"
),
"Expected error but found {err:?}"
);
@ -1786,8 +1819,9 @@ mixedArr = [0, "a"]: [number(mm)]
let result = parse_execute(program).await;
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("could not coerce value of type array of number(default units), string with 2 values to type [number(mm)]"),
err.to_string().contains(
"could not coerce an array of `number`, `string` (with type `[any; 2]`) to type `[number(mm)]`"
),
"Expected error but found {err:?}"
);
}
@ -2092,4 +2126,19 @@ y = x: number(Length)"#;
assert_eq!(num.n, 2.0);
assert_eq!(num.ty, NumericType::mm());
}
#[tokio::test(flavor = "multi_thread")]
async fn one_warning_unknown() {
let ast = r#"
// Should warn once
a = PI * 2
// Should warn once
b = (PI * 2) / 3
// Should not warn
c = ((PI * 2) / 3): number(deg)
"#;
let result = parse_execute(ast).await.unwrap();
assert_eq!(result.exec_state.errors().len(), 2);
}
}

View File

@ -532,6 +532,44 @@ fn update_memory_for_tags_of_geometry(result: &mut KclValue, exec_state: &mut Ex
Ok(())
}
fn type_err_str(expected: &Type, found: &KclValue, source_range: &SourceRange, exec_state: &mut ExecState) -> String {
fn strip_backticks(s: &str) -> &str {
let mut result = s;
if s.starts_with('`') {
result = &result[1..]
}
if s.ends_with('`') {
result = &result[..result.len() - 1]
}
result
}
let expected_human = expected.human_friendly_type();
let expected_ty = expected.to_string();
let expected_str =
if expected_human == expected_ty || expected_human == format!("a value with type `{expected_ty}`") {
format!("a value with type `{expected_ty}`")
} else {
format!("{expected_human} (`{expected_ty}`)")
};
let found_human = found.human_friendly_type();
let found_ty = found.principal_type_string();
let found_str = if found_human == found_ty || found_human == format!("a {}", strip_backticks(&found_ty)) {
format!("a value with type {}", found_ty)
} else {
format!("{found_human} (with type {})", found_ty)
};
let mut result = format!("{expected_str}, but found {found_str}.");
if found.is_unknown_number() {
exec_state.clear_units_warnings(source_range);
result.push_str("\nThe found value is a number but has incomplete units information. You can probably fix this error by specifying the units using type ascription, e.g., `len: number(mm)` or `(a * b): number(deg)`.");
}
result
}
fn type_check_params_kw(
fn_name: Option<&str>,
fn_def: &FunctionDefinition<'_>,
@ -556,18 +594,19 @@ fn type_check_params_kw(
// For optional args, passing None should be the same as not passing an arg.
if !(def.is_some() && matches!(arg.value, KclValue::KclNone { .. })) {
if let Some(ty) = ty {
let rty = RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range)
.map_err(|e| KclError::new_semantic(e.into()))?;
arg.value = arg
.value
.coerce(
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.source_range).map_err(|e| KclError::new_semantic(e.into()))?,
&rty,
true,
exec_state,
)
.map_err(|e| {
let mut message = format!(
"{label} requires a value with type `{}`, but found {}",
ty,
arg.value.human_friendly_type(),
"{label} requires {}",
type_err_str(ty, &arg.value, &arg.source_range, exec_state),
);
if let Some(ty) = e.explicit_coercion {
// TODO if we have access to the AST for the argument we could choose which example to suggest.
@ -630,28 +669,20 @@ fn type_check_params_kw(
if let Some(arg) = &mut args.unlabeled {
if let Some((_, Some(ty))) = &fn_def.input_arg {
arg.1.value = arg
.1
.value
.coerce(
&RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range)
.map_err(|e| KclError::new_semantic(e.into()))?,
true,
exec_state,
)
.map_err(|_| {
KclError::new_semantic(KclErrorDetails::new(
format!(
"The input argument of {} requires a value with type `{}`, but found {}",
fn_name
.map(|n| format!("`{}`", n))
.unwrap_or_else(|| "this function".to_owned()),
ty,
arg.1.value.human_friendly_type()
),
vec![arg.1.source_range],
))
})?;
let rty = RuntimeType::from_parsed(ty.clone(), exec_state, arg.1.source_range)
.map_err(|e| KclError::new_semantic(e.into()))?;
arg.1.value = arg.1.value.coerce(&rty, true, exec_state).map_err(|_| {
KclError::new_semantic(KclErrorDetails::new(
format!(
"The input argument of {} requires {}",
fn_name
.map(|n| format!("`{}`", n))
.unwrap_or_else(|| "this function".to_owned()),
type_err_str(ty, &arg.1.value, &arg.1.source_range, exec_state),
),
vec![arg.1.source_range],
))
})?;
}
} else if let Some((name, _)) = &fn_def.input_arg {
if let Some(arg) = args.labeled.get(name) {
@ -747,9 +778,8 @@ fn coerce_result_type(
let val = val.coerce(&ty, true, exec_state).map_err(|_| {
KclError::new_semantic(KclErrorDetails::new(
format!(
"This function requires its result to be of type `{}`, but found {}",
ty.human_friendly_type(),
val.human_friendly_type(),
"This function requires its result to be {}",
type_err_str(ret_ty, &val, &(&val).into(), exec_state)
),
ret_ty.as_source_ranges(),
))
@ -928,7 +958,7 @@ msg2 = makeMessage(prefix = 1, suffix = 3)"#;
let err = parse_execute(program).await.unwrap_err();
assert_eq!(
err.message(),
"prefix requires a value with type `string`, but found number(default units)"
"prefix requires a value with type `string`, but found a value with type `number`.\nThe found value is a number but has incomplete units information. You can probably fix this error by specifying the units using type ascription, e.g., `len: number(mm)` or `(a * b): number(deg)`."
)
}
}

View File

@ -15,7 +15,10 @@ use uuid::Uuid;
use crate::{
errors::{KclError, KclErrorDetails},
execution::{annotations, typed_path::TypedPath, types::UnitLen, ExecState, ExecutorContext, ImportedGeometry},
execution::{
annotations, typed_path::TypedPath, types::UnitLen, ExecState, ExecutorContext, ImportedGeometry,
ModelingCmdMeta,
},
fs::FileSystem,
parsing::ast::types::{Annotation, Node},
source_range::SourceRange,
@ -257,15 +260,22 @@ pub struct PreImportedGeometry {
pub source_range: SourceRange,
}
pub async fn send_to_engine(pre: PreImportedGeometry, ctxt: &ExecutorContext) -> Result<ImportedGeometry, KclError> {
pub async fn send_to_engine(
pre: PreImportedGeometry,
exec_state: &mut ExecState,
ctxt: &ExecutorContext,
) -> Result<ImportedGeometry, KclError> {
let imported_geometry = ImportedGeometry::new(
pre.id,
pre.command.files.iter().map(|f| f.path.to_string()).collect(),
vec![pre.source_range.into()],
);
ctxt.engine
.async_modeling_cmd(pre.id, pre.source_range, &ModelingCmd::from(pre.command.clone()))
exec_state
.async_modeling_cmd(
ModelingCmdMeta::with_id(ctxt, pre.source_range, pre.id),
&ModelingCmd::from(pre.command.clone()),
)
.await?;
Ok(imported_geometry)

View File

@ -4,7 +4,6 @@ use anyhow::Result;
use schemars::JsonSchema;
use serde::Serialize;
use super::types::UnitType;
use crate::{
errors::KclErrorDetails,
execution::{
@ -278,72 +277,85 @@ impl KclValue {
}
}
/// Returns true if we should generate an [`crate::execution::Operation`] to
/// display in the Feature Tree for variable declarations initialized with
/// this value.
pub(crate) fn show_variable_in_feature_tree(&self) -> bool {
match self {
KclValue::Uuid { .. } => false,
KclValue::Bool { .. } | KclValue::Number { .. } | KclValue::String { .. } => true,
KclValue::Tuple { .. }
| KclValue::HomArray { .. }
| KclValue::Object { .. }
| KclValue::TagIdentifier(_)
| KclValue::TagDeclarator(_)
| KclValue::Plane { .. }
| KclValue::Face { .. }
| KclValue::Sketch { .. }
| KclValue::Solid { .. }
| KclValue::Helix { .. }
| KclValue::ImportedGeometry(_)
| KclValue::Function { .. }
| KclValue::Module { .. }
| KclValue::Type { .. }
| KclValue::KclNone { .. } => false,
}
}
/// Human readable type name used in error messages. Should not be relied
/// on for program logic.
pub(crate) fn human_friendly_type(&self) -> String {
self.inner_human_friendly_type(1)
}
fn inner_human_friendly_type(&self, max_depth: usize) -> String {
if let Some(pt) = self.principal_type() {
if max_depth > 0 {
// The principal type of an array uses the array's element type,
// which is oftentimes `any`, and that's not a helpful message. So
// we show the actual elements.
if let KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } = self {
// If it's empty, we want to show the type of the array.
if !value.is_empty() {
// A max of 3 is good because it's common to use 3D points.
let max = 3;
let len = value.len();
let ellipsis = if len > max { ", ..." } else { "" };
let element_label = if len == 1 { "value" } else { "values" };
let element_tys = value
.iter()
.take(max)
.map(|elem| elem.inner_human_friendly_type(max_depth - 1))
.collect::<Vec<_>>()
.join(", ");
return format!("array of {element_tys}{ellipsis} with {len} {element_label}");
}
}
}
return pt.to_string();
}
match self {
KclValue::Uuid { .. } => "Unique ID (uuid)",
KclValue::TagDeclarator(_) => "TagDeclarator",
KclValue::TagIdentifier(_) => "TagIdentifier",
KclValue::Solid { .. } => "Solid",
KclValue::Sketch { .. } => "Sketch",
KclValue::Helix { .. } => "Helix",
KclValue::ImportedGeometry(_) => "ImportedGeometry",
KclValue::Function { .. } => "Function",
KclValue::Plane { .. } => "Plane",
KclValue::Face { .. } => "Face",
KclValue::Bool { .. } => "boolean (true/false value)",
KclValue::Uuid { .. } => "a unique ID (uuid)".to_owned(),
KclValue::TagDeclarator(_) => "a tag declarator".to_owned(),
KclValue::TagIdentifier(_) => "a tag identifier".to_owned(),
KclValue::Solid { .. } => "a solid".to_owned(),
KclValue::Sketch { .. } => "a sketch".to_owned(),
KclValue::Helix { .. } => "a helix".to_owned(),
KclValue::ImportedGeometry(_) => "an imported geometry".to_owned(),
KclValue::Function { .. } => "a function".to_owned(),
KclValue::Plane { .. } => "a plane".to_owned(),
KclValue::Face { .. } => "a face".to_owned(),
KclValue::Bool { .. } => "a boolean (`true` or `false`)".to_owned(),
KclValue::Number {
ty: NumericType::Unknown,
..
} => "number(unknown units)",
} => "a number with unknown units".to_owned(),
KclValue::Number {
ty: NumericType::Known(UnitType::Length(_)),
ty: NumericType::Known(units),
..
} => "number(Length)",
KclValue::Number {
ty: NumericType::Known(UnitType::Angle(_)),
..
} => "number(Angle)",
KclValue::Number { .. } => "number",
KclValue::String { .. } => "string (text)",
KclValue::Tuple { .. } => "tuple (list)",
KclValue::HomArray { .. } => "array (list)",
KclValue::Object { .. } => "object",
KclValue::Module { .. } => "module",
KclValue::Type { .. } => "type",
KclValue::KclNone { .. } => "None",
} => format!("a number ({units})"),
KclValue::Number { .. } => "a number".to_owned(),
KclValue::String { .. } => "a string".to_owned(),
KclValue::Object { .. } => "an object".to_owned(),
KclValue::Module { .. } => "a module".to_owned(),
KclValue::Type { .. } => "a type".to_owned(),
KclValue::KclNone { .. } => "none".to_owned(),
KclValue::Tuple { value, .. } | KclValue::HomArray { value, .. } => {
if value.is_empty() {
"an empty array".to_owned()
} else {
// A max of 3 is good because it's common to use 3D points.
const MAX: usize = 3;
let len = value.len();
let element_tys = value
.iter()
.take(MAX)
.map(|elem| elem.principal_type_string())
.collect::<Vec<_>>()
.join(", ");
let mut result = format!("an array of {element_tys}");
if len > MAX {
result.push_str(&format!(", ... with {len} values"));
}
if len == 1 {
result.push_str(" with 1 value");
}
result
}
}
}
.to_owned()
}
pub(crate) fn from_literal(literal: Node<Literal>, exec_state: &mut ExecState) -> Self {
@ -602,6 +614,13 @@ impl KclValue {
})
}
pub fn is_unknown_number(&self) -> bool {
match self {
KclValue::Number { ty, .. } => !ty.is_fully_specified(),
_ => false,
}
}
pub fn value_str(&self) -> Option<String> {
match self {
KclValue::Bool { value, .. } => Some(format!("{value}")),
@ -650,6 +669,7 @@ impl From<GeometryWithImportedGeometry> for KclValue {
#[cfg(test)]
mod tests {
use super::*;
use crate::exec::UnitType;
#[test]
fn test_human_friendly_type() {
@ -658,21 +678,21 @@ mod tests {
ty: NumericType::Known(UnitType::Length(UnitLen::Unknown)),
meta: vec![],
};
assert_eq!(len.human_friendly_type(), "number(Length)".to_string());
assert_eq!(len.human_friendly_type(), "a number (Length)".to_string());
let unknown = KclValue::Number {
value: 1.0,
ty: NumericType::Unknown,
meta: vec![],
};
assert_eq!(unknown.human_friendly_type(), "number(unknown units)".to_string());
assert_eq!(unknown.human_friendly_type(), "a number with unknown units".to_string());
let mm = KclValue::Number {
value: 1.0,
ty: NumericType::Known(UnitType::Length(UnitLen::Mm)),
meta: vec![],
};
assert_eq!(mm.human_friendly_type(), "number(mm)".to_string());
assert_eq!(mm.human_friendly_type(), "a number (mm)".to_string());
let array1_mm = KclValue::HomArray {
value: vec![mm.clone()],
@ -680,7 +700,7 @@ mod tests {
};
assert_eq!(
array1_mm.human_friendly_type(),
"array of number(mm) with 1 value".to_string()
"an array of `number(mm)` with 1 value".to_string()
);
let array2_mm = KclValue::HomArray {
@ -689,7 +709,7 @@ mod tests {
};
assert_eq!(
array2_mm.human_friendly_type(),
"array of number(mm), number(mm) with 2 values".to_string()
"an array of `number(mm)`, `number(mm)`".to_string()
);
let array3_mm = KclValue::HomArray {
@ -698,7 +718,7 @@ mod tests {
};
assert_eq!(
array3_mm.human_friendly_type(),
"array of number(mm), number(mm), number(mm) with 3 values".to_string()
"an array of `number(mm)`, `number(mm)`, `number(mm)`".to_string()
);
let inches = KclValue::Number {
@ -712,14 +732,14 @@ mod tests {
};
assert_eq!(
array4.human_friendly_type(),
"array of number(mm), number(mm), number(in), ... with 4 values".to_string()
"an array of `number(mm)`, `number(mm)`, `number(in)`, ... with 4 values".to_string()
);
let empty_array = KclValue::HomArray {
value: vec![],
ty: RuntimeType::any(),
};
assert_eq!(empty_array.human_friendly_type(), "[any; 0]".to_string());
assert_eq!(empty_array.human_friendly_type(), "an empty array".to_string());
let array_nested = KclValue::HomArray {
value: vec![array2_mm.clone()],
@ -727,7 +747,7 @@ mod tests {
};
assert_eq!(
array_nested.human_friendly_type(),
"array of [any; 2] with 1 value".to_string()
"an array of `[any; 2]` with 1 value".to_string()
);
}
}

View File

@ -8,7 +8,8 @@ pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, CodeRef, StartSketc
use cache::GlobalState;
pub use cache::{bust_cache, clear_mem_cache};
#[cfg(feature = "artifact-graph")]
pub use cad_op::{Group, Operation};
pub use cad_op::Group;
pub use cad_op::Operation;
pub use geometry::*;
pub use id_generator::IdGenerator;
pub(crate) use import::PreImportedGeometry;
@ -22,8 +23,10 @@ use kcmc::{
};
use kittycad_modeling_cmds::{self as kcmc, id::ModelingCmdId};
pub use memory::EnvironmentRef;
pub(crate) use modeling::ModelingCmdMeta;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
pub(crate) use state::ModuleArtifactState;
pub use state::{ExecState, MetaSettings};
use uuid::Uuid;
@ -56,6 +59,7 @@ mod import;
mod import_graph;
pub(crate) mod kcl_value;
mod memory;
mod modeling;
mod state;
pub mod typed_path;
pub(crate) mod types;
@ -76,9 +80,6 @@ pub struct ExecOutcome {
/// the Feature Tree.
#[cfg(feature = "artifact-graph")]
pub operations: Vec<Operation>,
/// Output commands to allow building the artifact graph by the caller.
#[cfg(feature = "artifact-graph")]
pub artifact_commands: Vec<ArtifactCommand>,
/// Output artifact graph.
#[cfg(feature = "artifact-graph")]
pub artifact_graph: ArtifactGraph,
@ -518,6 +519,12 @@ impl ExecutorContext {
exec_state: &mut ExecState,
source_range: crate::execution::SourceRange,
) -> Result<(), KclError> {
// Ensure artifacts are cleared so that we don't accumulate them across
// runs.
exec_state.mod_local.artifacts.clear();
exec_state.global.root_module_artifacts.clear();
exec_state.global.artifacts.clear();
self.engine
.clear_scene(&mut exec_state.mod_local.id_generator, source_range)
.await
@ -575,7 +582,7 @@ impl ExecutorContext {
let mut mem = exec_state.stack().clone();
let module_infos = exec_state.global.module_infos.clone();
let outcome = exec_state.to_mock_exec_outcome(result.0, self).await;
let outcome = exec_state.into_mock_exec_outcome(result.0, self).await;
mem.squash_env(result.0);
cache::write_old_memory((mem, module_infos)).await;
@ -649,8 +656,8 @@ impl ExecutorContext {
let (new_universe, new_universe_map) =
self.get_universe(&program, &mut new_exec_state).await?;
let clear_scene = new_universe.keys().any(|key| {
let id = new_universe[key].1;
let clear_scene = new_universe.values().any(|value| {
let id = value.1;
match (
cached_state.exec_state.get_source(id),
new_exec_state.global.get_source(id),
@ -773,15 +780,12 @@ impl ExecutorContext {
))
.await;
let outcome = exec_state.to_exec_outcome(result.0, self).await;
let outcome = exec_state.into_exec_outcome(result.0, self).await;
Ok(outcome)
}
/// Perform the execution of a program.
///
/// You can optionally pass in some initialization memory for partial
/// execution.
///
/// To access non-fatal errors and warnings, extract them from the `ExecState`.
pub async fn run(
&self,
@ -794,9 +798,6 @@ impl ExecutorContext {
/// Perform the execution of a program using a concurrent
/// execution model.
///
/// You can optionally pass in some initialization memory for partial
/// execution.
///
/// To access non-fatal errors and warnings, extract them from the `ExecState`.
pub async fn run_concurrent(
&self,
@ -842,6 +843,8 @@ impl ExecutorContext {
let module_id = *module_id;
let module_path = module_path.clone();
let source_range = SourceRange::from(import_stmt);
// Clone before mutating.
let module_exec_state = exec_state.clone();
self.add_import_module_ops(
exec_state,
@ -853,7 +856,6 @@ impl ExecutorContext {
);
let repr = repr.clone();
let exec_state = exec_state.clone();
let exec_ctxt = self.clone();
let results_tx = results_tx.clone();
@ -873,11 +875,13 @@ impl ExecutorContext {
result.map(|val| ModuleRepr::Kcl(program.clone(), Some(val)))
}
ModuleRepr::Foreign(geom, _) => {
let result = crate::execution::import::send_to_engine(geom.clone(), exec_ctxt)
let result = crate::execution::import::send_to_engine(geom.clone(), exec_state, exec_ctxt)
.await
.map(|geom| Some(KclValue::ImportedGeometry(geom)));
result.map(|val| ModuleRepr::Foreign(geom.clone(), val))
result.map(|val| {
ModuleRepr::Foreign(geom.clone(), Some((val, exec_state.mod_local.artifacts.clone())))
})
}
ModuleRepr::Dummy | ModuleRepr::Root => Err(KclError::new_internal(KclErrorDetails::new(
format!("Module {module_path} not found in universe"),
@ -889,7 +893,7 @@ impl ExecutorContext {
#[cfg(target_arch = "wasm32")]
{
wasm_bindgen_futures::spawn_local(async move {
let mut exec_state = exec_state;
let mut exec_state = module_exec_state;
let exec_ctxt = exec_ctxt;
let result = exec_module(
@ -911,7 +915,7 @@ impl ExecutorContext {
#[cfg(not(target_arch = "wasm32"))]
{
set.spawn(async move {
let mut exec_state = exec_state;
let mut exec_state = module_exec_state;
let exec_ctxt = exec_ctxt;
let result = exec_module(
@ -964,6 +968,14 @@ impl ExecutorContext {
}
}
// Since we haven't technically started executing the root module yet,
// the operations corresponding to the imports will be missing unless we
// track them here.
exec_state
.global
.root_module_artifacts
.extend(std::mem::take(&mut exec_state.mod_local.artifacts));
self.inner_run(program, exec_state, preserve_mem).await
}
@ -993,6 +1005,18 @@ impl ExecutorContext {
Ok((universe, root_imports))
}
#[cfg(not(feature = "artifact-graph"))]
fn add_import_module_ops(
&self,
_exec_state: &mut ExecState,
_program: &crate::Program,
_module_id: ModuleId,
_module_path: &ModulePath,
_source_range: SourceRange,
_universe_map: &UniverseMap,
) {
}
#[cfg(feature = "artifact-graph")]
fn add_import_module_ops(
&self,
@ -1042,18 +1066,6 @@ impl ExecutorContext {
}
}
#[cfg(not(feature = "artifact-graph"))]
fn add_import_module_ops(
&self,
_exec_state: &mut ExecState,
_program: &crate::Program,
_module_id: ModuleId,
_module_path: &ModulePath,
_source_range: SourceRange,
_universe_map: &UniverseMap,
) {
}
/// Perform the execution of a program. Accept all possible parameters and
/// output everything.
async fn inner_run(
@ -1107,7 +1119,7 @@ impl ExecutorContext {
// Because of execution caching, we may start with operations from a
// previous run.
#[cfg(feature = "artifact-graph")]
let start_op = exec_state.global.artifacts.operations.len();
let start_op = exec_state.global.root_module_artifacts.operations.len();
self.eval_prelude(exec_state, SourceRange::from(program).start_as_range())
.await?;
@ -1120,27 +1132,40 @@ impl ExecutorContext {
ModuleId::default(),
&ModulePath::Main,
)
.await;
.await
.map(|(_, env_ref, _, module_artifacts)| {
// We need to extend because it may already have operations from
// imports.
exec_state.global.root_module_artifacts.extend(module_artifacts);
env_ref
})
.map_err(|(err, module_artifacts)| {
if let Some(module_artifacts) = module_artifacts {
// We need to extend because it may already have operations
// from imports.
exec_state.global.root_module_artifacts.extend(module_artifacts);
}
err
});
#[cfg(feature = "artifact-graph")]
{
// Fill in NodePath for operations.
let cached_body_items = exec_state.global.artifacts.cached_body_items();
for op in exec_state.global.artifacts.operations.iter_mut().skip(start_op) {
match op {
Operation::StdLibCall {
node_path,
source_range,
..
for op in exec_state
.global
.root_module_artifacts
.operations
.iter_mut()
.skip(start_op)
{
op.fill_node_paths(program, cached_body_items);
}
for module in exec_state.global.module_infos.values_mut() {
if let ModuleRepr::Kcl(_, Some((_, _, _, module_artifacts))) = &mut module.repr {
for op in &mut module_artifacts.operations {
op.fill_node_paths(program, cached_body_items);
}
| Operation::GroupBegin {
node_path,
source_range,
..
} => {
node_path.fill_placeholder(program, cached_body_items, *source_range);
}
Operation::GroupEnd => {}
}
}
}
@ -1153,7 +1178,7 @@ impl ExecutorContext {
self.engine.clear_queues().await;
match exec_state.build_artifact_graph(&self.engine, program).await {
Ok(_) => exec_result.map(|(_, env_ref, _)| env_ref),
Ok(_) => exec_result,
Err(err) => exec_result.and(Err(err)),
}
}
@ -1163,6 +1188,9 @@ impl ExecutorContext {
/// SAFETY: the current thread must have sole access to the memory referenced in exec_state.
async fn eval_prelude(&self, exec_state: &mut ExecState, source_range: SourceRange) -> Result<(), KclError> {
if exec_state.stack().memory.requires_std() {
#[cfg(feature = "artifact-graph")]
let initial_ops = exec_state.mod_local.artifacts.operations.len();
let path = vec!["std".to_owned(), "prelude".to_owned()];
let resolved_path = ModulePath::from_std_import_path(&path)?;
let id = self
@ -1171,6 +1199,14 @@ impl ExecutorContext {
let (module_memory, _) = self.exec_module_for_items(id, exec_state, source_range).await?;
exec_state.mut_stack().memory.set_std(module_memory);
// Operations generated by the prelude are not useful, so clear them
// out.
//
// TODO: Should we also clear them out of each module so that they
// don't appear in test output?
#[cfg(feature = "artifact-graph")]
exec_state.mod_local.artifacts.operations.truncate(initial_ops);
}
Ok(())
@ -1919,13 +1955,13 @@ notNull = !myNull
"#;
assert_eq!(
parse_execute(code1).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: number(default units)",
"Cannot apply unary operator ! to non-boolean value: a number",
);
let code2 = "notZero = !0";
assert_eq!(
parse_execute(code2).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: number(default units)",
"Cannot apply unary operator ! to non-boolean value: a number",
);
let code3 = r#"
@ -1933,7 +1969,7 @@ notEmptyString = !""
"#;
assert_eq!(
parse_execute(code3).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: string",
"Cannot apply unary operator ! to non-boolean value: a string",
);
let code4 = r#"
@ -1942,7 +1978,7 @@ notMember = !obj.a
"#;
assert_eq!(
parse_execute(code4).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: number(default units)",
"Cannot apply unary operator ! to non-boolean value: a number",
);
let code5 = "
@ -1950,7 +1986,7 @@ a = []
notArray = !a";
assert_eq!(
parse_execute(code5).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: [any; 0]",
"Cannot apply unary operator ! to non-boolean value: an empty array",
);
let code6 = "
@ -1958,7 +1994,7 @@ x = {}
notObject = !x";
assert_eq!(
parse_execute(code6).await.unwrap_err().message(),
"Cannot apply unary operator ! to non-boolean value: { }",
"Cannot apply unary operator ! to non-boolean value: an object",
);
let code7 = "
@ -1984,7 +2020,7 @@ notTagDeclarator = !myTagDeclarator";
assert!(
tag_declarator_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
.starts_with("Cannot apply unary operator ! to non-boolean value: a tag declarator"),
"Actual error: {:?}",
tag_declarator_err
);
@ -1998,7 +2034,7 @@ notTagIdentifier = !myTag";
assert!(
tag_identifier_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
.starts_with("Cannot apply unary operator ! to non-boolean value: a tag identifier"),
"Actual error: {:?}",
tag_identifier_err
);
@ -2250,6 +2286,39 @@ w = f() + f()
ctx2.close().await;
}
#[cfg(feature = "artifact-graph")]
#[tokio::test(flavor = "multi_thread")]
async fn sim_sketch_mode_real_mock_real() {
let ctx = ExecutorContext::new_with_default_client().await.unwrap();
let code = r#"sketch001 = startSketchOn(XY)
profile001 = startProfile(sketch001, at = [0, 0])
|> line(end = [10, 0])
|> line(end = [0, 10])
|> line(end = [-10, 0])
|> line(end = [0, -10])
|> close()
"#;
let program = crate::Program::parse_no_errs(code).unwrap();
let result = ctx.run_with_caching(program).await.unwrap();
assert_eq!(result.operations.len(), 1);
let mock_ctx = ExecutorContext::new_mock(None).await;
let mock_program = crate::Program::parse_no_errs(code).unwrap();
let mock_result = mock_ctx.run_mock(mock_program, true).await.unwrap();
assert_eq!(mock_result.operations.len(), 0);
let code2 = code.to_owned()
+ r#"
extrude001 = extrude(profile001, length = 10)
"#;
let program2 = crate::Program::parse_no_errs(&code2).unwrap();
let result = ctx.run_with_caching(program2).await.unwrap();
assert_eq!(result.operations.len(), 2);
ctx.close().await;
mock_ctx.close().await;
}
#[tokio::test(flavor = "multi_thread")]
async fn read_tag_version() {
let ast = r#"fn bar(@t) {

View File

@ -0,0 +1,224 @@
use kcmc::ModelingCmd;
use kittycad_modeling_cmds::{
self as kcmc,
websocket::{ModelingCmdReq, OkWebSocketResponseData},
};
use uuid::Uuid;
#[cfg(feature = "artifact-graph")]
use crate::exec::ArtifactCommand;
use crate::{
exec::{IdGenerator, KclValue},
execution::Solid,
std::Args,
ExecState, ExecutorContext, KclError, SourceRange,
};
/// Context and metadata needed to send a single modeling command.
///
/// Many functions consume Self so that the command ID isn't accidentally reused
/// among multiple modeling commands.
pub(crate) struct ModelingCmdMeta<'a> {
/// The executor context, which contains the engine.
pub ctx: &'a ExecutorContext,
/// The source range of the command, used for error reporting.
pub source_range: SourceRange,
/// The id of the command, if it has been set by the caller or generated.
id: Option<Uuid>,
}
impl<'a> ModelingCmdMeta<'a> {
pub fn new(ctx: &'a ExecutorContext, source_range: SourceRange) -> Self {
ModelingCmdMeta {
ctx,
source_range,
id: None,
}
}
pub fn with_id(ctx: &'a ExecutorContext, source_range: SourceRange, id: Uuid) -> Self {
ModelingCmdMeta {
ctx,
source_range,
id: Some(id),
}
}
pub fn from_args_id(args: &'a Args, id: Uuid) -> Self {
ModelingCmdMeta {
ctx: &args.ctx,
source_range: args.source_range,
id: Some(id),
}
}
pub fn id(&mut self, id_generator: &mut IdGenerator) -> Uuid {
if let Some(id) = self.id {
return id;
}
let id = id_generator.next_uuid();
self.id = Some(id);
id
}
}
impl<'a> From<&'a Args> for ModelingCmdMeta<'a> {
fn from(args: &'a Args) -> Self {
ModelingCmdMeta::new(&args.ctx, args.source_range)
}
}
impl ExecState {
/// Add a modeling command to the batch but don't fire it right away.
pub(crate) async fn batch_modeling_cmd(
&mut self,
mut meta: ModelingCmdMeta<'_>,
cmd: ModelingCmd,
) -> Result<(), crate::errors::KclError> {
let id = meta.id(self.id_generator());
#[cfg(feature = "artifact-graph")]
self.push_command(ArtifactCommand {
cmd_id: id,
range: meta.source_range,
command: cmd.clone(),
});
meta.ctx.engine.batch_modeling_cmd(id, meta.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(
&mut self,
meta: ModelingCmdMeta<'_>,
cmds: &[ModelingCmdReq],
) -> Result<(), crate::errors::KclError> {
#[cfg(feature = "artifact-graph")]
for cmd_req in cmds {
self.push_command(ArtifactCommand {
cmd_id: *cmd_req.cmd_id.as_ref(),
range: meta.source_range,
command: cmd_req.cmd.clone(),
});
}
meta.ctx.engine.batch_modeling_cmds(meta.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.
pub(crate) async fn batch_end_cmd(
&mut self,
mut meta: ModelingCmdMeta<'_>,
cmd: ModelingCmd,
) -> Result<(), crate::errors::KclError> {
let id = meta.id(self.id_generator());
// TODO: The order of the tracking of these doesn't match the order that
// they're sent to the engine.
#[cfg(feature = "artifact-graph")]
self.push_command(ArtifactCommand {
cmd_id: id,
range: meta.source_range,
command: cmd.clone(),
});
meta.ctx.engine.batch_end_cmd(id, meta.source_range, &cmd).await
}
/// Send the modeling cmd and wait for the response.
pub(crate) async fn send_modeling_cmd(
&mut self,
mut meta: ModelingCmdMeta<'_>,
cmd: ModelingCmd,
) -> Result<OkWebSocketResponseData, KclError> {
let id = meta.id(self.id_generator());
#[cfg(feature = "artifact-graph")]
self.push_command(ArtifactCommand {
cmd_id: id,
range: meta.source_range,
command: cmd.clone(),
});
meta.ctx.engine.send_modeling_cmd(id, meta.source_range, &cmd).await
}
/// Send the modeling cmd async and don't wait for the response.
/// Add it to our list of async commands.
pub(crate) async fn async_modeling_cmd(
&mut self,
mut meta: ModelingCmdMeta<'_>,
cmd: &ModelingCmd,
) -> Result<(), crate::errors::KclError> {
let id = meta.id(self.id_generator());
#[cfg(feature = "artifact-graph")]
self.push_command(ArtifactCommand {
cmd_id: id,
range: meta.source_range,
command: cmd.clone(),
});
meta.ctx.engine.async_modeling_cmd(id, meta.source_range, cmd).await
}
/// Force flush the batch queue.
pub(crate) async fn flush_batch(
&mut self,
meta: ModelingCmdMeta<'_>,
// Whether or not to flush the end commands as well.
// We only do this at the very end of the file.
batch_end: bool,
) -> Result<OkWebSocketResponseData, KclError> {
meta.ctx.engine.flush_batch(batch_end, meta.source_range).await
}
/// Flush just the fillets and chamfers for this specific SolidSet.
pub(crate) async fn flush_batch_for_solids(
&mut self,
meta: ModelingCmdMeta<'_>,
solids: &[Solid],
) -> Result<(), KclError> {
// Make sure we don't traverse sketches more than once.
let mut traversed_sketches = Vec::new();
// Collect all the fillet/chamfer ids for the solids.
let mut ids = Vec::new();
for solid in solids {
// We need to traverse the solids that share the same sketch.
let sketch_id = solid.sketch.id;
if !traversed_sketches.contains(&sketch_id) {
// Find all the solids on the same shared sketch.
ids.extend(
self.stack()
.walk_call_stack()
.filter(|v| matches!(v, KclValue::Solid { value } if value.sketch.id == sketch_id))
.flat_map(|v| match v {
KclValue::Solid { value } => value.get_all_edge_cut_ids(),
_ => unreachable!(),
}),
);
traversed_sketches.push(sketch_id);
}
ids.extend(solid.get_all_edge_cut_ids());
}
// We can return early if there are no fillets or chamfers.
if ids.is_empty() {
return Ok(());
}
// We want to move these fillets and chamfers from batch_end to batch so they get executed
// before what ever we call next.
for id in ids {
// Pop it off the batch_end and add it to the batch.
let Some(item) = meta.ctx.engine.batch_end().write().await.shift_remove(&id) else {
// It might be in the batch already.
continue;
};
// Add it to the batch.
meta.ctx.engine.batch().write().await.push(item);
}
// Run flush.
// Yes, we do need to actually flush the batch here, or references will fail later.
self.flush_batch(meta, false).await?;
Ok(())
}
}

View File

@ -2,8 +2,6 @@ use std::sync::Arc;
use anyhow::Result;
use indexmap::IndexMap;
#[cfg(feature = "artifact-graph")]
use kittycad_modeling_cmds::websocket::WebSocketResponse;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@ -48,34 +46,46 @@ pub(super) struct GlobalState {
pub mod_loader: ModuleLoader,
/// Errors and warnings.
pub errors: Vec<CompilationError>,
#[cfg_attr(not(feature = "artifact-graph"), allow(dead_code))]
/// Global artifacts that represent the entire program.
pub artifacts: ArtifactState,
/// Artifacts for only the root module.
pub root_module_artifacts: ModuleArtifactState,
}
#[cfg(feature = "artifact-graph")]
#[derive(Debug, Clone, Default)]
pub(super) struct ArtifactState {
/// Output map of UUIDs to artifacts.
/// Internal map of UUIDs to exec artifacts. This needs to persist across
/// executions to allow the graph building to refer to cached artifacts.
pub artifacts: IndexMap<ArtifactId, Artifact>,
/// Output commands to allow building the artifact graph by the caller.
/// These are accumulated in the [`ExecutorContext`] but moved here for
/// convenience of the execution cache.
pub commands: Vec<ArtifactCommand>,
/// Responses from the engine for `artifact_commands`. We need to cache
/// this so that we can build the artifact graph. These are accumulated in
/// the [`ExecutorContext`] but moved here for convenience of the execution
/// cache.
pub responses: IndexMap<Uuid, WebSocketResponse>,
/// Output artifact graph.
pub graph: ArtifactGraph,
}
#[cfg(not(feature = "artifact-graph"))]
#[derive(Debug, Clone, Default)]
pub(super) struct ArtifactState {}
/// Artifact state for a single module.
#[cfg(feature = "artifact-graph")]
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct ModuleArtifactState {
/// Internal map of UUIDs to exec artifacts.
pub artifacts: IndexMap<ArtifactId, Artifact>,
/// Outgoing engine commands that have not yet been processed and integrated
/// into the artifact graph.
#[serde(skip)]
pub unprocessed_commands: Vec<ArtifactCommand>,
/// Outgoing engine commands.
pub commands: Vec<ArtifactCommand>,
/// Operations that have been performed in execution order, for display in
/// the Feature Tree.
pub operations: Vec<Operation>,
}
#[cfg(not(feature = "artifact-graph"))]
#[derive(Debug, Clone, Default)]
pub(super) struct ArtifactState {}
#[derive(Debug, Clone, Default, PartialEq, Serialize)]
pub struct ModuleArtifactState {}
#[derive(Debug, Clone)]
pub(super) struct ModuleState {
@ -96,6 +106,8 @@ pub(super) struct ModuleState {
pub settings: MetaSettings,
pub(super) explicit_length_units: bool,
pub(super) path: ModulePath,
/// Artifacts for only this module.
pub artifacts: ModuleArtifactState,
}
impl ExecState {
@ -126,6 +138,17 @@ impl ExecState {
self.global.errors.push(e);
}
pub fn clear_units_warnings(&mut self, source_range: &SourceRange) {
self.global.errors = std::mem::take(&mut self.global.errors)
.into_iter()
.filter(|e| {
e.severity != Severity::Warning
|| !source_range.contains_range(&e.source_range)
|| e.tag != crate::errors::Tag::UnknownNumericUnits
})
.collect();
}
pub fn errors(&self) -> &[CompilationError] {
&self.global.errors
}
@ -133,16 +156,14 @@ 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 async fn to_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
pub async fn into_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
// Fields are opt-in so that we don't accidentally leak private internal
// state when we add more to ExecState.
ExecOutcome {
variables: self.mod_local.variables(main_ref),
filenames: self.global.filenames(),
#[cfg(feature = "artifact-graph")]
operations: self.global.artifacts.operations,
#[cfg(feature = "artifact-graph")]
artifact_commands: self.global.artifacts.commands,
operations: self.global.root_module_artifacts.operations,
#[cfg(feature = "artifact-graph")]
artifact_graph: self.global.artifacts.graph,
errors: self.global.errors,
@ -150,14 +171,12 @@ impl ExecState {
}
}
pub async fn to_mock_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
pub async fn into_mock_exec_outcome(self, main_ref: EnvironmentRef, ctx: &ExecutorContext) -> ExecOutcome {
ExecOutcome {
variables: self.mod_local.variables(main_ref),
#[cfg(feature = "artifact-graph")]
operations: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_commands: Default::default(),
#[cfg(feature = "artifact-graph")]
artifact_graph: Default::default(),
errors: self.global.errors,
filenames: Default::default(),
@ -184,16 +203,23 @@ impl ExecState {
#[cfg(feature = "artifact-graph")]
pub(crate) fn add_artifact(&mut self, artifact: Artifact) {
let id = artifact.id();
self.global.artifacts.artifacts.insert(id, artifact);
self.mod_local.artifacts.artifacts.insert(id, artifact);
}
pub(crate) fn push_op(&mut self, op: Operation) {
#[cfg(feature = "artifact-graph")]
self.global.artifacts.operations.push(op);
self.mod_local.artifacts.operations.push(op.clone());
#[cfg(not(feature = "artifact-graph"))]
drop(op);
}
#[cfg(feature = "artifact-graph")]
pub(crate) fn push_command(&mut self, command: ArtifactCommand) {
self.mod_local.artifacts.unprocessed_commands.push(command);
#[cfg(not(feature = "artifact-graph"))]
drop(command);
}
pub(super) fn next_module_id(&self) -> ModuleId {
ModuleId::from_usize(self.global.path_to_source_id.len())
}
@ -241,6 +267,16 @@ impl ExecState {
self.global.module_infos.get(&id)
}
#[cfg(all(test, feature = "artifact-graph"))]
pub(crate) fn modules(&self) -> &ModuleInfoMap {
&self.global.module_infos
}
#[cfg(all(test, feature = "artifact-graph"))]
pub(crate) fn root_module_artifact_state(&self) -> &ModuleArtifactState {
&self.global.root_module_artifacts
}
pub fn current_default_units(&self) -> NumericType {
NumericType::Default {
len: self.length_unit(),
@ -293,9 +329,9 @@ impl ExecState {
error,
self.errors().to_vec(),
#[cfg(feature = "artifact-graph")]
self.global.artifacts.operations.clone(),
self.global.root_module_artifacts.operations.clone(),
#[cfg(feature = "artifact-graph")]
self.global.artifacts.commands.clone(),
Default::default(),
#[cfg(feature = "artifact-graph")]
self.global.artifacts.graph.clone(),
module_id_to_module_path,
@ -310,8 +346,30 @@ impl ExecState {
engine: &Arc<Box<dyn EngineManager>>,
program: NodeRef<'_, crate::parsing::ast::types::Program>,
) -> Result<(), KclError> {
let new_commands = engine.take_artifact_commands().await;
let mut new_commands = Vec::new();
let mut new_exec_artifacts = IndexMap::new();
for module in self.global.module_infos.values_mut() {
match &mut module.repr {
ModuleRepr::Kcl(_, Some((_, _, _, module_artifacts)))
| ModuleRepr::Foreign(_, Some((_, module_artifacts))) => {
new_commands.extend(module_artifacts.process_commands());
new_exec_artifacts.extend(module_artifacts.artifacts.clone());
}
ModuleRepr::Root | ModuleRepr::Kcl(_, None) | ModuleRepr::Foreign(_, None) | ModuleRepr::Dummy => {}
}
}
// Take from the module artifacts so that we don't try to process them
// again next time due to execution caching.
new_commands.extend(self.global.root_module_artifacts.process_commands());
// Note: These will get re-processed, but since we're just adding them
// to a map, it's fine.
new_exec_artifacts.extend(self.global.root_module_artifacts.artifacts.clone());
let new_responses = engine.take_responses().await;
// Move the artifacts into ExecState global to simplify cache
// management.
self.global.artifacts.artifacts.extend(new_exec_artifacts);
let initial_graph = self.global.artifacts.graph.clone();
// Build the artifact graph.
@ -322,10 +380,6 @@ impl ExecState {
&mut self.global.artifacts.artifacts,
initial_graph,
);
// Move the artifact commands and responses into ExecState to
// simplify cache management and error creation.
self.global.artifacts.commands.extend(new_commands);
self.global.artifacts.responses.extend(new_responses);
let artifact_graph = graph_result?;
self.global.artifacts.graph = artifact_graph;
@ -349,6 +403,7 @@ impl GlobalState {
path_to_source_id: Default::default(),
module_infos: Default::default(),
artifacts: Default::default(),
root_module_artifacts: Default::default(),
mod_loader: Default::default(),
errors: Default::default(),
id_to_source: Default::default(),
@ -381,11 +436,54 @@ impl GlobalState {
}
}
#[cfg(feature = "artifact-graph")]
impl ArtifactState {
#[cfg(feature = "artifact-graph")]
pub fn cached_body_items(&self) -> usize {
self.graph.item_count
}
pub(crate) fn clear(&mut self) {
#[cfg(feature = "artifact-graph")]
{
self.artifacts.clear();
self.graph.clear();
}
}
}
impl ModuleArtifactState {
pub(crate) fn clear(&mut self) {
#[cfg(feature = "artifact-graph")]
{
self.artifacts.clear();
self.unprocessed_commands.clear();
self.commands.clear();
self.operations.clear();
}
}
#[cfg(not(feature = "artifact-graph"))]
pub(crate) fn extend(&mut self, _other: ModuleArtifactState) {}
/// When self is a cached state, extend it with new state.
#[cfg(feature = "artifact-graph")]
pub(crate) fn extend(&mut self, other: ModuleArtifactState) {
self.artifacts.extend(other.artifacts);
self.unprocessed_commands.extend(other.unprocessed_commands);
self.commands.extend(other.commands);
self.operations.extend(other.operations);
}
// Move unprocessed artifact commands so that we don't try to process them
// again next time due to execution caching. Returns a clone of the
// commands that were moved.
#[cfg(feature = "artifact-graph")]
pub(crate) fn process_commands(&mut self) -> Vec<ArtifactCommand> {
let unprocessed = std::mem::take(&mut self.unprocessed_commands);
let new_module_commands = unprocessed.clone();
self.commands.extend(unprocessed);
new_module_commands
}
}
impl ModuleState {
@ -403,6 +501,7 @@ impl ModuleState {
default_angle_units: Default::default(),
kcl_version: "0.1".to_owned(),
},
artifacts: Default::default(),
}
}

View File

@ -104,6 +104,16 @@ impl TypedPath {
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn strip_prefix(&self, base: impl AsRef<std::path::Path>) -> Result<Self, std::path::StripPrefixError> {
self.0.strip_prefix(base).map(|p| TypedPath(p.to_path_buf()))
}
#[cfg(not(target_arch = "wasm32"))]
pub fn canonicalize(&self) -> Result<Self, std::io::Error> {
self.0.canonicalize().map(|p| TypedPath(p.to_path_buf()))
}
pub fn to_string_lossy(&self) -> String {
#[cfg(target_arch = "wasm32")]
{

View File

@ -84,16 +84,16 @@ impl RuntimeType {
RuntimeType::Primitive(PrimitiveType::Face)
}
pub fn tag() -> Self {
RuntimeType::Primitive(PrimitiveType::Tag)
}
pub fn tag_decl() -> Self {
RuntimeType::Primitive(PrimitiveType::TagDecl)
}
pub fn tag_identifier() -> Self {
RuntimeType::Primitive(PrimitiveType::TagId)
pub fn tagged_face() -> Self {
RuntimeType::Primitive(PrimitiveType::TaggedFace)
}
pub fn tagged_edge() -> Self {
RuntimeType::Primitive(PrimitiveType::TaggedEdge)
}
pub fn bool() -> Self {
@ -196,7 +196,7 @@ impl RuntimeType {
RuntimeType::Primitive(PrimitiveType::Number(ty))
}
AstPrimitiveType::Named { id } => Self::from_alias(&id.name, exec_state, source_range)?,
AstPrimitiveType::Tag => RuntimeType::Primitive(PrimitiveType::Tag),
AstPrimitiveType::TagDecl => RuntimeType::Primitive(PrimitiveType::TagDecl),
AstPrimitiveType::ImportedGeometry => RuntimeType::Primitive(PrimitiveType::ImportedGeometry),
AstPrimitiveType::Function(_) => RuntimeType::Primitive(PrimitiveType::Function),
})
@ -383,8 +383,8 @@ pub enum PrimitiveType {
Number(NumericType),
String,
Boolean,
Tag,
TagId,
TaggedEdge,
TaggedFace,
TagDecl,
Sketch,
Solid,
@ -416,9 +416,9 @@ impl PrimitiveType {
PrimitiveType::Axis3d => "3d axes".to_owned(),
PrimitiveType::ImportedGeometry => "imported geometries".to_owned(),
PrimitiveType::Function => "functions".to_owned(),
PrimitiveType::Tag => "tags".to_owned(),
PrimitiveType::TagDecl => "tag declarators".to_owned(),
PrimitiveType::TagId => "tag identifiers".to_owned(),
PrimitiveType::TaggedEdge => "tagged edges".to_owned(),
PrimitiveType::TaggedFace => "tagged faces".to_owned(),
}
}
@ -426,7 +426,8 @@ impl PrimitiveType {
match (self, other) {
(_, PrimitiveType::Any) => true,
(PrimitiveType::Number(n1), PrimitiveType::Number(n2)) => n1.subtype(n2),
(PrimitiveType::TagId, PrimitiveType::Tag) | (PrimitiveType::TagDecl, PrimitiveType::Tag) => true,
(PrimitiveType::TaggedEdge, PrimitiveType::TaggedFace)
| (PrimitiveType::TaggedEdge, PrimitiveType::Edge) => true,
(t1, t2) => t1 == t2,
}
}
@ -438,13 +439,13 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Any => write!(f, "any"),
PrimitiveType::Number(NumericType::Known(unit)) => write!(f, "number({unit})"),
PrimitiveType::Number(NumericType::Unknown) => write!(f, "number(unknown units)"),
PrimitiveType::Number(NumericType::Default { .. }) => write!(f, "number(default units)"),
PrimitiveType::Number(NumericType::Default { .. }) => write!(f, "number"),
PrimitiveType::Number(NumericType::Any) => write!(f, "number(any units)"),
PrimitiveType::String => write!(f, "string"),
PrimitiveType::Boolean => write!(f, "bool"),
PrimitiveType::Tag => write!(f, "tag"),
PrimitiveType::TagDecl => write!(f, "tag declarator"),
PrimitiveType::TagId => write!(f, "tag identifier"),
PrimitiveType::TaggedEdge => write!(f, "tagged edge"),
PrimitiveType::TaggedFace => write!(f, "tagged face"),
PrimitiveType::Sketch => write!(f, "Sketch"),
PrimitiveType::Solid => write!(f, "Solid"),
PrimitiveType::Plane => write!(f, "Plane"),
@ -453,8 +454,8 @@ impl fmt::Display for PrimitiveType {
PrimitiveType::Axis2d => write!(f, "Axis2d"),
PrimitiveType::Axis3d => write!(f, "Axis3d"),
PrimitiveType::Helix => write!(f, "Helix"),
PrimitiveType::ImportedGeometry => write!(f, "imported geometry"),
PrimitiveType::Function => write!(f, "function"),
PrimitiveType::ImportedGeometry => write!(f, "ImportedGeometry"),
PrimitiveType::Function => write!(f, "fn"),
}
}
}
@ -499,20 +500,6 @@ impl NumericType {
NumericType::Known(UnitType::Angle(UnitAngle::Degrees))
}
pub fn expect_default_length(&self) -> Self {
match self {
NumericType::Default { len, .. } => NumericType::Known(UnitType::Length(*len)),
_ => unreachable!(),
}
}
pub fn expect_default_angle(&self) -> Self {
match self {
NumericType::Default { angle, .. } => NumericType::Known(UnitType::Angle(*angle)),
_ => unreachable!(),
}
}
/// Combine two types when we expect them to be equal, erring on the side of less coercion. To be
/// precise, only adjusting one number or the other when they are of known types.
///
@ -554,15 +541,10 @@ impl NumericType {
(at, Any) => (a.n, b.n, at),
(Any, bt) => (a.n, b.n, bt),
(Default { .. }, Default { .. }) | (_, Unknown) | (Unknown, _) => (a.n, b.n, Unknown),
// Known types and compatible, but needs adjustment.
(t @ Known(UnitType::Length(l1)), Known(UnitType::Length(l2))) => (a.n, l2.adjust_to(b.n, l1).0, t),
(t @ Known(UnitType::Angle(a1)), Known(UnitType::Angle(a2))) => (a.n, a2.adjust_to(b.n, a1).0, t),
// Known but incompatible.
(Known(_), Known(_)) => (a.n, b.n, Unknown),
// Known and unknown => we assume the known one, possibly with adjustment
(Known(UnitType::Count), Default { .. }) | (Default { .. }, Known(UnitType::Count)) => {
(a.n, b.n, Known(UnitType::Count))
@ -570,9 +552,12 @@ impl NumericType {
(t @ Known(UnitType::Length(l1)), Default { len: l2, .. }) => (a.n, l2.adjust_to(b.n, l1).0, t),
(Default { len: l1, .. }, t @ Known(UnitType::Length(l2))) => (l1.adjust_to(a.n, l2).0, b.n, t),
(t @ Known(UnitType::Angle(a1)), Default { angle: a2, .. }) => (a.n, a2.adjust_to(b.n, a1).0, t),
(Default { angle: a1, .. }, t @ Known(UnitType::Angle(a2))) => (a1.adjust_to(a.n, a2).0, b.n, t),
(Known(_), Known(_)) | (Default { .. }, Default { .. }) | (_, Unknown) | (Unknown, _) => {
(a.n, b.n, Unknown)
}
}
}
@ -647,6 +632,20 @@ impl NumericType {
}
}
/// Combine two types for modulo-like operations.
pub fn combine_mod(a: TyF64, b: TyF64) -> (f64, f64, NumericType) {
use NumericType::*;
match (a.ty, b.ty) {
(at @ Default { .. }, bt @ Default { .. }) if at == bt => (a.n, b.n, at),
(at, bt) if at == bt => (a.n, b.n, at),
(Default { .. }, Default { .. }) => (a.n, b.n, Unknown),
(at, Known(UnitType::Count) | Any) => (a.n, b.n, at),
(at @ Known(_), Default { .. }) => (a.n, b.n, at),
(Known(UnitType::Count), _) => (a.n, b.n, Known(UnitType::Count)),
_ => (a.n, b.n, Unknown),
}
}
pub fn from_parsed(suffix: NumericSuffix, settings: &super::MetaSettings) -> Self {
match suffix {
NumericSuffix::None => NumericType::Default {
@ -851,7 +850,7 @@ impl std::fmt::Display for UnitType {
}
}
// TODO called UnitLen so as not to clash with UnitLength in settings)
// TODO called UnitLen so as not to clash with UnitLength in settings.
/// A unit of length.
#[derive(Debug, Default, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)]
@ -1209,6 +1208,17 @@ impl KclValue {
KclValue::TagIdentifier { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::TaggedEdge => match self {
KclValue::TagIdentifier { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::TaggedFace => match self {
KclValue::TagIdentifier { .. } => Ok(self.clone()),
s @ KclValue::String { value, .. } if ["start", "end", "START", "END"].contains(&&**value) => {
Ok(s.clone())
}
_ => Err(self.into()),
},
PrimitiveType::Axis2d => match self {
KclValue::Object { value: values, meta } => {
if values
@ -1297,23 +1307,10 @@ impl KclValue {
KclValue::Function { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::TagId => match self {
KclValue::TagIdentifier { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::TagDecl => match self {
KclValue::TagDeclarator { .. } => Ok(self.clone()),
_ => Err(self.into()),
},
PrimitiveType::Tag => match self {
KclValue::TagDeclarator { .. } | KclValue::TagIdentifier { .. } | KclValue::Uuid { .. } => {
Ok(self.clone())
}
s @ KclValue::String { value, .. } if ["start", "end", "START", "END"].contains(&&**value) => {
Ok(s.clone())
}
_ => Err(self.into()),
},
}
}
@ -1503,13 +1500,30 @@ impl KclValue {
KclValue::HomArray { ty, value, .. } => {
Some(RuntimeType::Array(Box::new(ty.clone()), ArrayLen::Known(value.len())))
}
KclValue::TagIdentifier(_) => Some(RuntimeType::Primitive(PrimitiveType::TagId)),
KclValue::TagIdentifier(_) => Some(RuntimeType::Primitive(PrimitiveType::TaggedEdge)),
KclValue::TagDeclarator(_) => Some(RuntimeType::Primitive(PrimitiveType::TagDecl)),
KclValue::Uuid { .. } => Some(RuntimeType::Primitive(PrimitiveType::Tag)),
KclValue::Uuid { .. } => Some(RuntimeType::Primitive(PrimitiveType::Edge)),
KclValue::Function { .. } => Some(RuntimeType::Primitive(PrimitiveType::Function)),
KclValue::Module { .. } | KclValue::KclNone { .. } | KclValue::Type { .. } => None,
}
}
pub fn principal_type_string(&self) -> String {
if let Some(ty) = self.principal_type() {
return format!("`{ty}`");
}
match self {
KclValue::Module { .. } => "module",
KclValue::KclNone { .. } => "none",
KclValue::Type { .. } => "type",
_ => {
debug_assert!(false);
"<unexpected type>"
}
}
.to_owned()
}
}
#[cfg(test)]
@ -2342,10 +2356,10 @@ d = cos(30)
let result = parse_execute(program).await.unwrap();
assert!(result.exec_state.errors().is_empty());
assert_value_and_type("a", &result, 1.0, NumericType::count());
assert_value_and_type("a", &result, 1.0, NumericType::default());
assert_value_and_type("b", &result, 3.0, NumericType::default());
assert_value_and_type("c", &result, 1.0, NumericType::count());
assert_value_and_type("d", &result, 1.0, NumericType::count());
assert_value_and_type("c", &result, 1.0, NumericType::default());
assert_value_and_type("d", &result, 1.0, NumericType::default());
}
#[tokio::test(flavor = "multi_thread")]