use std::collections::HashMap;
use std::sync::Arc;
use async_recursion::async_recursion;
use ezpz::Constraint;
use ezpz::NonLinearSystemError;
use ezpz::datatypes::inputs::DatumPoint;
use indexmap::IndexMap;
use kcl_api::Group;
use kcl_api::NumericType;
use kcl_api::Operation;
use kcl_api::UnitAngle;
use crate::CompilationIssue;
use crate::NodePath;
use crate::NodePathExt;
use crate::SourceRange;
use crate::errors::KclError;
use crate::errors::KclErrorDetails;
use crate::exec::Sketch;
use crate::execution::AbstractSegment;
use crate::execution::AngleConstraintMode;
use crate::execution::AngleRayDirection;
use crate::execution::AngleSector;
use crate::execution::Artifact;
use crate::execution::ArtifactId;
use crate::execution::BodyType;
use crate::execution::ConstrainableLine2d;
use crate::execution::ConstraintKind;
use crate::execution::EarlyReturn;
use crate::execution::EnvironmentRef;
use crate::execution::ExecState;
use crate::execution::ExecutorContext;
use crate::execution::KclValue;
use crate::execution::KclValueControlFlow;
use crate::execution::KclVersion;
use crate::execution::LegacyAngleRefactorMeta;
use crate::execution::Metadata;
use crate::execution::ModelingCmdMeta;
use crate::execution::ModuleArtifactState;
use crate::execution::PendingLegacyAngleRefactorMeta;
use crate::execution::PreserveMem;
use crate::execution::RefactorMetadata;
use crate::execution::SKETCH_BLOCK_PARAM_ON;
use crate::execution::SKETCH_OBJECT_META;
use crate::execution::SKETCH_OBJECT_META_SKETCH;
use crate::execution::Segment;
use crate::execution::SegmentKind;
use crate::execution::SegmentRepr;
use crate::execution::SketchConstraintKind;
use crate::execution::SketchSurface;
use crate::execution::SolverArc;
use crate::execution::StatementKind;
use crate::execution::TagIdentifier;
use crate::execution::UnsolvedExpr;
use crate::execution::UnsolvedSegment;
use crate::execution::UnsolvedSegmentKind;
use crate::execution::annotations;
use crate::execution::annotations::FnAttrs;
use crate::execution::cad_op::op_from_kcl_value;
use crate::execution::control_continue;
use crate::execution::early_return;
use crate::execution::fn_call::Arg;
use crate::execution::fn_call::Args;
use crate::execution::fn_call::unexpected_kw_arg_message;
use crate::execution::kcl_value::EnumTypeDef;
use crate::execution::kcl_value::EnumTypeId;
use crate::execution::kcl_value::EnumValue;
use crate::execution::kcl_value::FunctionSource;
use crate::execution::kcl_value::KclFunctionSourceParams;
use crate::execution::kcl_value::KclObjectKind;
use crate::execution::kcl_value::TypeDef;
use crate::execution::memory::SKETCH_PREFIX;
use crate::execution::memory::{self};
use crate::execution::sketch_constraint_status_for_sketch;
use crate::execution::sketch_solve::FreedomAnalysis;
use crate::execution::sketch_solve::Solved;
use crate::execution::sketch_solve::UnsatisfiedDirectionalConstraint;
use crate::execution::sketch_solve::create_segment_scene_objects;
use crate::execution::sketch_solve::normalize_to_solver_angle_unit;
use crate::execution::sketch_solve::normalize_to_solver_distance_unit;
use crate::execution::sketch_solve::solver_numeric_type;
use crate::execution::sketch_solve::substitute_sketch_var_in_segment;
use crate::execution::sketch_solve::substitute_sketch_vars;
use crate::execution::state::ModuleState;
use crate::execution::state::SketchBlockState;
use crate::execution::types::CoercionMode;
use crate::execution::types::NumericTypeExt;
use crate::execution::types::PrimitiveType;
use crate::execution::types::RuntimeType;
use crate::execution::types::resolve_named_type_def;
use crate::execution::types::type_value_named_by_segment;
use crate::front::ArcDirection;
use crate::front::LineCtor;
use crate::front::Object;
use crate::front::ObjectId;
use crate::front::ObjectKind;
use crate::front::PointCtor;
use crate::modules::ModuleExecutionOutcome;
use crate::modules::ModuleId;
use crate::modules::ModulePath;
use crate::modules::ModuleRepr;
use crate::parsing::ast::types::ABSOLUTE_PATHS_NOT_SUPPORTED;
use crate::parsing::ast::types::Annotation;
use crate::parsing::ast::types::ArrayExpression;
use crate::parsing::ast::types::ArrayRangeExpression;
use crate::parsing::ast::types::AscribedExpression;
use crate::parsing::ast::types::BinaryExpression;
use crate::parsing::ast::types::BinaryOperator;
use crate::parsing::ast::types::BinaryPart;
use crate::parsing::ast::types::BodyItem;
use crate::parsing::ast::types::CodeBlock;
use crate::parsing::ast::types::Expr;
use crate::parsing::ast::types::FunctionExpression;
use crate::parsing::ast::types::Identifier;
use crate::parsing::ast::types::IfExpression;
use crate::parsing::ast::types::ImportPath;
use crate::parsing::ast::types::ImportSelector;
use crate::parsing::ast::types::ImportStatement;
use crate::parsing::ast::types::ItemVisibility;
use crate::parsing::ast::types::MemberExpression;
use crate::parsing::ast::types::Name;
use crate::parsing::ast::types::Node;
use crate::parsing::ast::types::ObjectExpression;
use crate::parsing::ast::types::PipeExpression;
use crate::parsing::ast::types::Program;
use crate::parsing::ast::types::ReturnStatement;
use crate::parsing::ast::types::SketchBlock;
use crate::parsing::ast::types::SketchVar;
use crate::parsing::ast::types::TagDeclarator;
use crate::parsing::ast::types::Type;
use crate::parsing::ast::types::TypeDeclaration;
use crate::parsing::ast::types::TypeDeclarationDefinition;
use crate::parsing::ast::types::UnaryExpression;
use crate::parsing::ast::types::UnaryOperator;
use crate::parsing::ast::types::VariableDeclaration;
use crate::std::StdFnProps;
use crate::std::args::FromKclValue;
use crate::std::args::TyF64;
use crate::std::shapes::SketchOrSurface;
use crate::std::sketch::ensure_sketch_plane_in_engine;
use crate::std::solver::SOLVER_CONVERGENCE_TOLERANCE;
use crate::std::solver::create_segments_in_engine;
use crate::std::utils::intersect_lines_2d;
use crate::std::utils::normalize_rad;
use crate::std::utils::vec2_dot;
use crate::std::utils::vec2_len;
use crate::std::utils::vec2_sub;
use crate::walk::Visitable;
fn internal_err(message: impl Into<String>, range: impl Into<SourceRange>) -> KclError {
KclError::new_internal(KclErrorDetails::new(message.into(), vec![range.into()]))
}
fn signed_distance_conflict_hint(solve_outcome: &Solved) -> String {
let hints = solve_outcome
.unsatisfied_directional_constraints
.iter()
.map(|constraint| match constraint {
UnsatisfiedDirectionalConstraint::Horizontal(expected) if *expected > 0.0 => {
"Unsatisfied signed horizontalDistance constraint: a positive right-hand side requires the second point to be right of the first (second.x - first.x > 0)."
}
UnsatisfiedDirectionalConstraint::Horizontal(expected) if *expected < 0.0 => {
"Unsatisfied signed horizontalDistance constraint: a negative right-hand side requires the second point to be left of the first (second.x - first.x < 0)."
}
UnsatisfiedDirectionalConstraint::Horizontal(_) => {
"Unsatisfied signed horizontalDistance constraint: a zero right-hand side requires both points to have the same X coordinate."
}
UnsatisfiedDirectionalConstraint::Vertical(expected) if *expected > 0.0 => {
"Unsatisfied signed verticalDistance constraint: a positive right-hand side requires the second point to be above the first (second.y - first.y > 0)."
}
UnsatisfiedDirectionalConstraint::Vertical(expected) if *expected < 0.0 => {
"Unsatisfied signed verticalDistance constraint: a negative right-hand side requires the second point to be below the first (second.y - first.y < 0)."
}
UnsatisfiedDirectionalConstraint::Vertical(_) => {
"Unsatisfied signed verticalDistance constraint: a zero right-hand side requires both points to have the same Y coordinate."
}
})
.collect::<Vec<_>>();
if hints.is_empty() {
String::new()
} else {
format!(" {}", hints.join(" "))
}
}
fn datum_point_from_constrainable(
point: &crate::execution::ConstrainablePoint2d,
range: SourceRange,
) -> Result<ezpz::datatypes::inputs::DatumPoint, KclError> {
Ok(ezpz::datatypes::inputs::DatumPoint::new_xy(
point.vars.x.to_constraint_id(range)?,
point.vars.y.to_constraint_id(range)?,
))
}
fn push_fixed_origin_point(
sketch_block_state: &mut SketchBlockState,
sketch_var_ty: NumericType,
range: SourceRange,
) -> Result<ezpz::datatypes::inputs::DatumPoint, KclError> {
let origin_x_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: origin_x_id,
initial_value: 0.0,
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let origin_y_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: origin_y_id,
initial_value: 0.0,
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
sketch_block_state
.solver_constraints
.push(Constraint::Fixed(origin_x_id.to_constraint_id(range)?, 0.0));
sketch_block_state
.solver_constraints
.push(Constraint::Fixed(origin_y_id.to_constraint_id(range)?, 0.0));
Ok(ezpz::datatypes::inputs::DatumPoint::new_xy(
origin_x_id.to_constraint_id(range)?,
origin_y_id.to_constraint_id(range)?,
))
}
fn datum_point_from_constrainable_or_origin(
sketch_block_state: &mut SketchBlockState,
sketch_var_ty: NumericType,
point: &crate::execution::ConstrainablePoint2dOrOrigin,
range: SourceRange,
) -> Result<ezpz::datatypes::inputs::DatumPoint, KclError> {
match point {
crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => datum_point_from_constrainable(point, range),
crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
push_fixed_origin_point(sketch_block_state, sketch_var_ty, range)
}
}
}
fn datum_line_from_constrainable(
line: &crate::execution::ConstrainableLine2d,
range: SourceRange,
) -> Result<ezpz::datatypes::inputs::DatumLineSegment, KclError> {
Ok(ezpz::datatypes::inputs::DatumLineSegment::new(
ezpz::datatypes::inputs::DatumPoint::new_xy(
line.vars[0].x.to_constraint_id(range)?,
line.vars[0].y.to_constraint_id(range)?,
),
ezpz::datatypes::inputs::DatumPoint::new_xy(
line.vars[1].x.to_constraint_id(range)?,
line.vars[1].y.to_constraint_id(range)?,
),
))
}
fn push_hidden_sketch_point(
sketch_block_state: &mut SketchBlockState,
sketch_var_ty: NumericType,
initial: [f64; 2],
range: SourceRange,
) -> Result<DatumPoint, KclError> {
let x_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: x_id,
initial_value: initial[0],
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let y_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: y_id,
initial_value: initial[1],
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
Ok(DatumPoint::new_xy(
x_id.to_constraint_id(range)?,
y_id.to_constraint_id(range)?,
))
}
fn front_angle_sector(sector: AngleSector) -> u8 {
match sector {
AngleSector::One => 1,
AngleSector::Two => 2,
AngleSector::Three => 3,
AngleSector::Four => 4,
}
}
#[derive(Clone, Copy)]
struct AngleSectorRay {
line_index: usize,
direction: AngleRayDirection,
}
fn angle_sector_rays(sector: AngleSector, is_inverse: bool) -> [AngleSectorRay; 2] {
let rays = match sector {
AngleSector::One => [
AngleSectorRay {
line_index: 0,
direction: AngleRayDirection::Forward,
},
AngleSectorRay {
line_index: 1,
direction: AngleRayDirection::Forward,
},
],
AngleSector::Two => [
AngleSectorRay {
line_index: 1,
direction: AngleRayDirection::Forward,
},
AngleSectorRay {
line_index: 0,
direction: AngleRayDirection::Reverse,
},
],
AngleSector::Three => [
AngleSectorRay {
line_index: 0,
direction: AngleRayDirection::Reverse,
},
AngleSectorRay {
line_index: 1,
direction: AngleRayDirection::Reverse,
},
],
AngleSector::Four => [
AngleSectorRay {
line_index: 1,
direction: AngleRayDirection::Reverse,
},
AngleSectorRay {
line_index: 0,
direction: AngleRayDirection::Forward,
},
],
};
if is_inverse { [rays[1], rays[0]] } else { rays }
}
fn line_endpoint_datum(
line: &ConstrainableLine2d,
endpoint_index: usize,
range: SourceRange,
) -> Result<DatumPoint, KclError> {
let Some(endpoint) = line.vars.get(endpoint_index) else {
return Err(internal_err("Invalid angle line endpoint index", range));
};
Ok(DatumPoint::new_xy(
endpoint.x.to_constraint_id(range)?,
endpoint.y.to_constraint_id(range)?,
))
}
fn representative_angle_endpoint(
line: &ConstrainableLine2d,
initial_line: ([f64; 2], [f64; 2]),
vertex: [f64; 2],
range: SourceRange,
) -> Result<(DatumPoint, AngleRayDirection), KclError> {
let start_delta = vec2_sub(initial_line.0, vertex);
let end_delta = vec2_sub(initial_line.1, vertex);
let endpoint_index = if vec2_len(end_delta) >= vec2_len(start_delta) {
1
} else {
0
};
let endpoint_delta = if endpoint_index == 1 { end_delta } else { start_delta };
if vec2_len(endpoint_delta) <= 1e-9 {
return Err(KclError::new_semantic(KclErrorDetails::new(
"angleDimension(lines = ..., sector = ...) requires each line to have an endpoint away from the intersection"
.to_owned(),
vec![range],
)));
}
let line_direction = vec2_sub(initial_line.1, initial_line.0);
let direction = if vec2_dot(endpoint_delta, line_direction) >= 0.0 {
AngleRayDirection::Forward
} else {
AngleRayDirection::Reverse
};
Ok((line_endpoint_datum(line, endpoint_index, range)?, direction))
}
fn remap_angle_for_representative_rays(
requested_rays: [AngleSectorRay; 2],
representative_directions: [AngleRayDirection; 2],
desired_angle: ezpz::datatypes::Angle,
) -> ezpz::datatypes::Angle {
let mut requested_directions = representative_directions;
for ray in requested_rays {
requested_directions[ray.line_index] = ray.direction;
}
let sign_offset = if (requested_directions[0] != representative_directions[0])
^ (requested_directions[1] != representative_directions[1])
{
std::f64::consts::PI
} else {
0.0
};
let desired = desired_angle.to_radians();
let representative_angle = if requested_rays[0].line_index == 0 {
desired - sign_offset
} else {
-desired - sign_offset
};
ezpz::datatypes::Angle::from_radians(normalize_rad(representative_angle))
}
struct PointsAtAngleLineData {
initial_vertex: [f64; 2],
representative_points: [DatumPoint; 2],
angle_kind: ezpz::datatypes::AngleKind,
}
enum AngleConstraintLowering {
LinesAtAngle(Box<PendingLegacyAngleRefactorMeta>),
PointsAtAngle(PointsAtAngleLineData),
}
fn solved_angle_line(line: &ConstrainableLine2d, final_values: &[f64]) -> Option<([f64; 2], [f64; 2])> {
let point = |index: usize| {
let point = line.vars.get(index)?;
Some([*final_values.get(point.x.0)?, *final_values.get(point.y.0)?])
};
Some((point(0)?, point(1)?))
}
fn angle_ray_vector(lines: [[f64; 2]; 2], ray: AngleSectorRay) -> [f64; 2] {
let direction = lines[ray.line_index];
match ray.direction {
AngleRayDirection::Forward => direction,
AngleRayDirection::Reverse => [-direction[0], -direction[1]],
}
}
fn directed_angle(from: [f64; 2], to: [f64; 2]) -> f64 {
let cross = from[0] * to[1] - from[1] * to[0];
libm::atan2(cross, vec2_dot(from, to)).rem_euclid(std::f64::consts::TAU)
}
fn circular_angle_distance(a: f64, b: f64) -> f64 {
let delta = (a - b).abs().rem_euclid(std::f64::consts::TAU);
libm::fmin(delta, std::f64::consts::TAU - delta)
}
fn legacy_angle_arc_midpoint_angle(
lines: [([f64; 2], [f64; 2]); 2],
directions: [[f64; 2]; 2],
vertex: [f64; 2],
desired: f64,
) -> f64 {
let signed_distances = lines.map(|line| {
let direction = vec2_sub(line.1, line.0);
let length = vec2_len(direction);
[
vec2_dot(vec2_sub(line.0, vertex), direction) / length,
vec2_dot(vec2_sub(line.1, vertex), direction) / length,
]
});
let overlap = [
libm::fmax(signed_distances[0][0], signed_distances[1][0]),
libm::fmin(signed_distances[0][1], signed_distances[1][1]),
];
let radius = if overlap[1] >= overlap[0] {
let near_start = overlap[0] + (overlap[1] - overlap[0]) * 0.15;
let near_end = overlap[0] + (overlap[1] - overlap[0]) * 0.85;
if near_start.abs() < near_end.abs() {
near_start
} else {
near_end
}
} else {
let mut distances = signed_distances.into_iter().flatten().collect::<Vec<_>>();
distances.sort_by(f64::total_cmp);
distances[1]
};
let start = if radius < 0.0 {
[-directions[0][0], -directions[0][1]]
} else {
directions[0]
};
(libm::atan2(start[1], start[0]) + desired * 0.5).rem_euclid(std::f64::consts::TAU)
}
fn finalize_legacy_angle_refactor_meta(
pending: &PendingLegacyAngleRefactorMeta,
final_values: &[f64],
) -> Option<LegacyAngleRefactorMeta> {
let line0 = solved_angle_line(&pending.lines[0], final_values)?;
let line1 = solved_angle_line(&pending.lines[1], final_values)?;
let vertex = intersect_lines_2d(line0, line1)?;
let directions = [vec2_sub(line0.1, line0.0), vec2_sub(line1.1, line1.0)];
if directions.iter().any(|direction| vec2_len(*direction) <= 1e-9) {
return None;
}
let desired = pending.desired_angle_radians.rem_euclid(std::f64::consts::TAU);
let sectors = [
AngleSector::One,
AngleSector::Two,
AngleSector::Three,
AngleSector::Four,
];
let mut candidates = Vec::new();
for sector in sectors {
for inverse in [false, true] {
let rays = angle_sector_rays(sector, inverse);
let from = angle_ray_vector(directions, rays[0]);
let to = angle_ray_vector(directions, rays[1]);
if circular_angle_distance(directed_angle(from, to), desired) <= 1e-5 {
let midpoint = libm::atan2(from[1], from[0]) + desired * 0.5;
candidates.push((sector, inverse, midpoint.rem_euclid(std::f64::consts::TAU)));
}
}
}
let arc_midpoint_angle = legacy_angle_arc_midpoint_angle([line0, line1], directions, vertex, desired);
let selected = candidates.into_iter().min_by(|a, b| {
circular_angle_distance(a.2, arc_midpoint_angle).total_cmp(&circular_angle_distance(b.2, arc_midpoint_angle))
})?;
Some(LegacyAngleRefactorMeta {
source_range: pending.source_range,
sector: front_angle_sector(selected.0),
inverse: selected.1,
})
}
fn push_points_at_angle_for_lines(
sketch_block_state: &mut SketchBlockState,
sketch_var_ty: NumericType,
lines: [&ConstrainableLine2d; 2],
data: PointsAtAngleLineData,
range: SourceRange,
) -> Result<(), KclError> {
let solver_line0 = datum_line_from_constrainable(lines[0], range)?;
let solver_line1 = datum_line_from_constrainable(lines[1], range)?;
let vertex = push_hidden_sketch_point(sketch_block_state, sketch_var_ty, data.initial_vertex, range)?;
sketch_block_state
.solver_constraints
.push(Constraint::PointLineDistance(vertex, solver_line0, 0.0));
sketch_block_state
.solver_constraints
.push(Constraint::PointLineDistance(vertex, solver_line1, 0.0));
sketch_block_state.solver_constraints.push(Constraint::PointsAtAngle(
vertex,
data.representative_points[0],
data.representative_points[1],
data.angle_kind,
));
Ok(())
}
fn sketch_var_initial_value(
sketch_vars: &[KclValue],
id: crate::execution::SketchVarId,
exec_state: &mut ExecState,
range: SourceRange,
description: &str,
) -> Result<f64, KclError> {
sketch_vars
.get(id.0)
.and_then(KclValue::as_sketch_var)
.map(|sketch_var| {
sketch_var
.initial_value_to_solver_units(exec_state, range, description)
.map(|value| value.n)
})
.transpose()?
.ok_or_else(|| internal_err(format!("Missing sketch variable initial value for id {}", id.0), range))
}
fn constrainable_point_initial_position(
sketch_vars: &[KclValue],
point: &crate::execution::ConstrainablePoint2d,
exec_state: &mut ExecState,
range: SourceRange,
description: &str,
) -> Result<[f64; 2], KclError> {
Ok([
sketch_var_initial_value(sketch_vars, point.vars.x, exec_state, range, description)?,
sketch_var_initial_value(sketch_vars, point.vars.y, exec_state, range, description)?,
])
}
fn constrainable_point_or_origin_initial_position(
sketch_vars: &[KclValue],
point: &crate::execution::ConstrainablePoint2dOrOrigin,
exec_state: &mut ExecState,
range: SourceRange,
description: &str,
) -> Result<[f64; 2], KclError> {
match point {
crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
constrainable_point_initial_position(sketch_vars, point, exec_state, range, description)
}
crate::execution::ConstrainablePoint2dOrOrigin::Origin => Ok([0.0, 0.0]),
}
}
fn constrainable_line_initial_positions(
sketch_vars: &[KclValue],
line: &crate::execution::ConstrainableLine2d,
exec_state: &mut ExecState,
range: SourceRange,
description: &str,
) -> Result<([f64; 2], [f64; 2]), KclError> {
let start = crate::execution::ConstrainablePoint2d {
vars: line.vars[0].clone(),
object_id: line.object_id,
};
let end = crate::execution::ConstrainablePoint2d {
vars: line.vars[1].clone(),
object_id: line.object_id,
};
Ok((
constrainable_point_initial_position(sketch_vars, &start, exec_state, range, description)?,
constrainable_point_initial_position(sketch_vars, &end, exec_state, range, description)?,
))
}
fn projected_point_on_line_initial_position(
sketch_vars: &[KclValue],
point: &crate::execution::ConstrainablePoint2dOrOrigin,
line: &crate::execution::ConstrainableLine2d,
exec_state: &mut ExecState,
range: SourceRange,
) -> Result<[f64; 2], KclError> {
let point = constrainable_point_or_origin_initial_position(
sketch_vars,
point,
exec_state,
range,
"point-line distance initial point",
)?;
let (line_start, line_end) =
constrainable_line_initial_positions(sketch_vars, line, exec_state, range, "point-line distance initial line")?;
let dx = line_end[0] - line_start[0];
let dy = line_end[1] - line_start[1];
let len_sq = dx * dx + dy * dy;
if len_sq == 0.0 {
return Err(KclError::new_semantic(KclErrorDetails::new(
"distance() line input must have non-zero length".to_owned(),
vec![range],
)));
}
let t = ((point[0] - line_start[0]) * dx + (point[1] - line_start[1]) * dy) / len_sq;
Ok([line_start[0] + t * dx, line_start[1] + t * dy])
}
fn constrainable_points_initial_distance(
sketch_vars: &[KclValue],
point0: &crate::execution::ConstrainablePoint2d,
point1: &crate::execution::ConstrainablePoint2d,
exec_state: &mut ExecState,
range: SourceRange,
description: &str,
) -> Result<f64, KclError> {
let p0 = constrainable_point_initial_position(sketch_vars, point0, exec_state, range, description)?;
let p1 = constrainable_point_initial_position(sketch_vars, point1, exec_state, range, description)?;
Ok(libm::hypot(p0[0] - p1[0], p0[1] - p1[1]))
}
#[derive(Clone, Copy)]
struct CircularDistanceDatums {
center: ezpz::datatypes::inputs::DatumPoint,
start: ezpz::datatypes::inputs::DatumPoint,
end: Option<ezpz::datatypes::inputs::DatumPoint>,
radius_initial_value: f64,
}
fn circular_distance_datums(
sketch_vars: &[KclValue],
center: &crate::execution::ConstrainablePoint2d,
start: &crate::execution::ConstrainablePoint2d,
end: Option<&crate::execution::ConstrainablePoint2d>,
exec_state: &mut ExecState,
range: SourceRange,
) -> Result<CircularDistanceDatums, KclError> {
Ok(CircularDistanceDatums {
center: datum_point_from_constrainable(center, range)?,
start: datum_point_from_constrainable(start, range)?,
end: end.map(|end| datum_point_from_constrainable(end, range)).transpose()?,
radius_initial_value: constrainable_points_initial_distance(
sketch_vars,
center,
start,
exec_state,
range,
"circular distance radius initial value",
)?,
})
}
fn circular_circular_support_initial_position(
sketch_vars: &[KclValue],
center0: &crate::execution::ConstrainablePoint2d,
center1: &crate::execution::ConstrainablePoint2d,
radius0: f64,
distance_value: f64,
exec_state: &mut ExecState,
range: SourceRange,
) -> Result<[f64; 2], KclError> {
let center0_initial =
constrainable_point_initial_position(sketch_vars, center0, exec_state, range, "circular distance center")?;
let center1_initial =
constrainable_point_initial_position(sketch_vars, center1, exec_state, range, "circular distance center")?;
let dx = center1_initial[0] - center0_initial[0];
let dy = center1_initial[1] - center0_initial[1];
let center_distance = libm::hypot(dx, dy);
let support_distance = radius0 + distance_value / 2.0;
if center_distance <= f64::EPSILON {
return Ok([center0_initial[0] + support_distance, center0_initial[1]]);
}
Ok([
center0_initial[0] + dx / center_distance * support_distance,
center0_initial[1] + dy / center_distance * support_distance,
])
}
fn push_circular_radius_constraints(
sketch_block_state: &mut SketchBlockState,
sketch_var_ty: NumericType,
circular: CircularDistanceDatums,
range: SourceRange,
) -> Result<ezpz::datatypes::inputs::DatumCircle, KclError> {
let circular_radius_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: circular_radius_id,
initial_value: circular.radius_initial_value,
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let circular_radius = ezpz::datatypes::inputs::DatumDistance::new(circular_radius_id.to_constraint_id(range)?);
sketch_block_state.solver_constraints.push(Constraint::DistanceVar(
circular.start,
circular.center,
circular_radius,
));
if let Some(end) = circular.end {
sketch_block_state
.solver_constraints
.push(Constraint::DistanceVar(end, circular.center, circular_radius));
}
Ok(ezpz::datatypes::inputs::DatumCircle {
center: circular.center,
radius: circular_radius,
})
}
fn push_circular_distance_constraints(
sketch_block_state: &mut SketchBlockState,
sketch_var_ty: NumericType,
target_point: ezpz::datatypes::inputs::DatumPoint,
circular: CircularDistanceDatums,
distance_value: f64,
range: SourceRange,
) -> Result<(), KclError> {
let circular_target = push_circular_radius_constraints(sketch_block_state, sketch_var_ty, circular, range)?;
let target_distance_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: target_distance_id,
initial_value: distance_value,
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let target_distance = ezpz::datatypes::inputs::DatumDistance::new(target_distance_id.to_constraint_id(range)?);
sketch_block_state
.solver_constraints
.push(Constraint::Fixed(target_distance.id, distance_value));
let target_circle = ezpz::datatypes::inputs::DatumCircle {
center: target_point,
radius: target_distance,
};
sketch_block_state
.solver_constraints
.push(Constraint::CircleTangentToCircle(
target_circle,
circular_target,
ezpz::CircleSide::Exterior,
));
Ok(())
}
fn sketch_on_cache_name(sketch_id: ObjectId) -> String {
format!("{SKETCH_PREFIX}{}_on", sketch_id.0)
}
fn default_plane_name_from_expr(expr: &Expr) -> Option<crate::engine::PlaneName> {
fn parse_name(name: &str, negative: bool) -> Option<crate::engine::PlaneName> {
use crate::engine::PlaneName;
match (name, negative) {
("XY", false) => Some(PlaneName::Xy),
("XY", true) => Some(PlaneName::NegXy),
("XZ", false) => Some(PlaneName::Xz),
("XZ", true) => Some(PlaneName::NegXz),
("YZ", false) => Some(PlaneName::Yz),
("YZ", true) => Some(PlaneName::NegYz),
_ => None,
}
}
match expr {
Expr::Name(name) => {
if !name.path.is_empty() {
return None;
}
parse_name(&name.name.name, false)
}
Expr::UnaryExpression(unary) => {
if unary.operator != UnaryOperator::Neg {
return None;
}
let crate::parsing::ast::types::BinaryPart::Name(name) = &unary.argument else {
return None;
};
if !name.path.is_empty() {
return None;
}
parse_name(&name.name.name, true)
}
_ => None,
}
}
fn sketch_on_frontend_plane(
arguments: &[crate::parsing::ast::types::LabeledArg],
on_object_id: crate::front::ObjectId,
) -> crate::front::Plane {
for arg in arguments {
let Some(label) = &arg.label else {
continue;
};
if label.name != SKETCH_BLOCK_PARAM_ON {
continue;
}
if let Some(name) = default_plane_name_from_expr(&arg.arg) {
return crate::front::Plane::Default(name);
}
break;
}
crate::front::Plane::Object(on_object_id)
}
impl<'a> StatementKind<'a> {
fn expect_name(&self) -> &'a str {
match self {
StatementKind::Declaration { name } => name,
StatementKind::Expression => unreachable!(),
}
}
}
impl ExecutorContext {
pub(super) async fn handle_annotations(
&self,
annotations: impl Iterator<Item = &Node<Annotation>>,
body_type: BodyType,
exec_state: &mut ExecState,
) -> Result<bool, KclError> {
let mut no_prelude = false;
for annotation in annotations {
let diagnostics_attr = annotations::diagnostics_attr_name(exec_state.kcl_version());
if annotation.name() == Some(annotations::SETTINGS) {
if matches!(body_type, BodyType::Root) {
let (updated_len, updated_angle) =
exec_state.mod_local.settings.update_from_annotation(annotation)?;
if updated_len {
exec_state.mod_local.explicit_length_units = true;
}
if updated_angle {
if exec_state.kcl_version() >= KclVersion::V3Preview {
return Err(KclError::new_semantic(KclErrorDetails::new(
"The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles"
.to_owned(),
annotation.as_source_ranges(),
)));
}
exec_state.warn(
CompilationIssue::err(
annotation.as_source_range(),
"The `defaultAngleUnit` setting is deprecated; use explicit units for angles",
),
annotations::WARN_ANGLE_UNITS,
);
}
} else {
exec_state.err(CompilationIssue::err(
annotation.as_source_range(),
"Settings can only be modified at the top level scope of a file",
));
}
} else if annotation.name() == Some(annotations::NO_PRELUDE) {
if matches!(body_type, BodyType::Root) {
no_prelude = true;
} else {
exec_state.err(CompilationIssue::err(
annotation.as_source_range(),
"The standard library can only be skipped at the top level scope of a file",
));
}
} else if annotation.name() == Some(diagnostics_attr) {
if matches!(body_type, BodyType::Root) {
let props = annotations::expect_properties(diagnostics_attr, annotation)?;
for p in props {
match &*p.inner.key.name {
annotations::WARN_ALLOW => {
let allowed = annotations::many_of(
&p.inner.value,
&annotations::WARN_VALUES,
diagnostics_attr,
annotation.as_source_range(),
)?;
exec_state.mod_local.allowed_warnings = allowed;
}
annotations::WARN_DENY => {
let denied = annotations::many_of(
&p.inner.value,
&annotations::WARN_VALUES,
diagnostics_attr,
annotation.as_source_range(),
)?;
exec_state.mod_local.denied_warnings = denied;
}
name => {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Unexpected {diagnostics_attr} key: `{name}`; expected one of `{}`, `{}`",
annotations::WARN_ALLOW,
annotations::WARN_DENY,
),
vec![annotation.as_source_range()],
)));
}
}
}
} else {
let message = match diagnostics_attr {
annotations::WARNINGS => "Warnings can only be customized at the top level scope of a file",
_ => "Diagnostics can only be customized at the top level scope of a file",
};
exec_state.err(CompilationIssue::err(annotation.as_source_range(), message));
}
} else if annotation.name() == Some(annotations::WARNINGS) {
let mut issue = CompilationIssue::err(
annotation.as_source_range(),
format!(
"The `@{old}` attribute was renamed to `@{new}` in KCL 3.0, so this attribute is ignored. Replace `@{old}` with `@{new}`; its `{allow}` and `{deny}` properties are unchanged.",
old = annotations::WARNINGS,
new = annotations::DIAGNOSTICS,
allow = annotations::WARN_ALLOW,
deny = annotations::WARN_DENY,
),
);
if let Some(name) = &annotation.name {
issue = issue.with_suggestion(
format!("Rename to `@{}`", annotations::DIAGNOSTICS),
annotations::DIAGNOSTICS,
Some(name.as_source_range()),
crate::errors::Tag::None,
);
}
exec_state.err(issue);
} else {
exec_state.warn(
CompilationIssue::err(annotation.as_source_range(), "Unknown annotation"),
annotations::WARN_UNKNOWN_ATTR,
);
}
}
Ok(no_prelude)
}
pub(super) async fn exec_module_body(
&self,
program: &Node<Program>,
exec_state: &mut ExecState,
preserve_mem: PreserveMem,
module_id: ModuleId,
path: &ModulePath,
) -> Result<ModuleExecutionOutcome, (KclError, Option<EnvironmentRef>, Option<ModuleArtifactState>)> {
crate::log::log(format!("enter module {path} {}", exec_state.stack()));
exec_state
.check_imported_module_kcl_version(path, program, None)
.map_err(|err| (err, None, None))?;
let mut local_state = ModuleState::new(
path.clone(),
exec_state.stack().memory.clone(),
Some(module_id),
exec_state.mod_local.sketch_mode,
exec_state.mod_local.freedom_analysis,
);
match preserve_mem {
PreserveMem::Always => {
exec_state
.mod_local
.artifacts
.restore_scene_objects(&exec_state.global.root_module_artifacts.scene_objects);
}
PreserveMem::Normal => {
local_state
.artifacts
.restore_scene_objects(&exec_state.mod_local.artifacts.scene_objects);
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
}
}
let no_prelude = self
.handle_annotations(program.inner_attrs.iter(), crate::execution::BodyType::Root, exec_state)
.await
.map_err(|err| (err, None, None))?;
if preserve_mem.normal() {
exec_state
.mut_stack()
.push_new_root_env(!no_prelude)
.map_err(|err| (err, None, None))?;
}
let result = self
.exec_block(program, exec_state, crate::execution::BodyType::Root)
.await;
let env_ref = match preserve_mem {
PreserveMem::Always => exec_state.mut_stack().pop_and_preserve_env(),
PreserveMem::Normal => exec_state.mut_stack().pop_env(),
}
.map_err(|err| (err, None, None))?;
let module_artifacts = match preserve_mem {
PreserveMem::Always => std::mem::take(&mut exec_state.mod_local.artifacts),
PreserveMem::Normal => {
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
local_state.artifacts
}
};
crate::log::log(format!("leave {path}"));
result
.map_err(|err| (err, Some(env_ref), Some(module_artifacts.clone())))
.map(|last_expr| ModuleExecutionOutcome {
last_expr: last_expr.map(|value_cf| value_cf.into_value()),
environment: env_ref,
exports: local_state.module_exports,
artifacts: module_artifacts,
})
}
#[async_recursion]
pub(super) async fn exec_block<'a, B>(
&'a self,
block: &'a B,
exec_state: &mut ExecState,
body_type: BodyType,
) -> Result<Option<KclValueControlFlow>, KclError>
where
B: CodeBlock + crate::execution::machine::ToMachineBlock + Sync,
{
if self.is_machine_executor() {
return crate::execution::machine::run_block(self, block.to_machine_block(), exec_state, body_type).await;
}
let mut last_expr = None;
for statement in block.body() {
match statement {
BodyItem::ImportStatement(import_stmt) => {
if exec_state.sketch_mode() {
continue;
}
self.exec_import_statement(import_stmt, body_type, exec_state).await?;
last_expr = None;
}
BodyItem::ExpressionStatement(expression_statement) => {
if exec_state.sketch_mode() && sketch_mode_should_skip(&expression_statement.expression) {
continue;
}
let metadata = Metadata::from(expression_statement);
let value = self
.execute_expr(
&expression_statement.expression,
exec_state,
&metadata,
&[],
StatementKind::Expression,
)
.await?;
let is_return = value.is_some_return();
last_expr = Some(value);
if is_return {
break;
}
}
BodyItem::VariableDeclaration(variable_declaration) => {
if exec_state.sketch_mode() && sketch_mode_should_skip(&variable_declaration.declaration.init) {
continue;
}
let var_name = variable_declaration.declaration.id.name.to_string();
let source_range = SourceRange::from(&variable_declaration.declaration.init);
let metadata = Metadata { source_range };
let annotations = &variable_declaration.outer_attrs;
let lhs = variable_declaration.inner.name().to_owned();
let prev_being_declared = exec_state.mod_local.being_declared.take();
exec_state.mod_local.being_declared = Some(lhs);
let rhs_result = self
.execute_expr(
&variable_declaration.declaration.init,
exec_state,
&metadata,
annotations,
StatementKind::Declaration { name: &var_name },
)
.await;
exec_state.mod_local.being_declared = prev_being_declared;
let rhs = rhs_result?;
if rhs.is_some_return() {
last_expr = Some(rhs);
break;
}
let rhs =
self.bind_variable_declaration(variable_declaration, rhs.into_value(), body_type, exec_state)?;
last_expr = matches!(body_type, BodyType::Root).then_some(rhs.continue_());
}
BodyItem::TypeDeclaration(ty) => {
if exec_state.sketch_mode() {
continue;
}
self.exec_type_declaration(ty, body_type, exec_state).await?;
last_expr = None;
}
BodyItem::ReturnStatement(return_statement) => {
if exec_state.sketch_mode() && sketch_mode_should_skip(&return_statement.argument) {
continue;
}
let metadata = Metadata::from(return_statement);
if matches!(body_type, BodyType::Root) {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Cannot return from outside a function.".to_owned(),
vec![metadata.source_range],
)));
}
let value_cf = self
.execute_expr(
&return_statement.argument,
exec_state,
&metadata,
&[],
StatementKind::Expression,
)
.await?;
if value_cf.is_some_return() {
last_expr = Some(value_cf);
break;
}
let value = value_cf.into_value();
if exec_state.entry_point_version_is_v3_or_higher() {
last_expr = Some(value.return_());
break;
}
Self::bind_return_value(return_statement, value, exec_state)?;
last_expr = None;
}
}
}
if matches!(body_type, BodyType::Root)
&& let Some(cf) = &last_expr
&& cf.is_return()
{
return Err(KclError::new_semantic(KclErrorDetails::new(
"Cannot return from outside a function.".to_owned(),
cf.source_ranges(),
)));
}
if matches!(body_type, BodyType::Root) {
exec_state
.flush_batch(
ModelingCmdMeta::new(exec_state, self, block.to_source_range()),
true,
)
.await?;
}
Ok(last_expr)
}
pub(super) async fn exec_import_statement(
&self,
import_stmt: &Node<ImportStatement>,
body_type: BodyType,
exec_state: &mut ExecState,
) -> Result<(), KclError> {
if !matches!(body_type, BodyType::Root) {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Imports are only supported at the top-level of a file.".to_owned(),
vec![import_stmt.into()],
)));
}
let source_range = SourceRange::from(import_stmt);
let attrs = &import_stmt.outer_attrs;
let module_path = ModulePath::from_import_path(
&import_stmt.path,
&self.settings.project_directory,
&exec_state.mod_local.path,
)?;
let module_id = self
.open_module(&import_stmt.path, attrs, &module_path, exec_state, source_range)
.await?;
if let ImportPath::Kcl { .. } = &import_stmt.path
&& let Some(ModuleRepr::Kcl(program, _)) =
exec_state.global.module_infos.get(&module_id).map(|info| &info.repr)
{
exec_state.check_imported_module_kcl_version(&module_path, program, Some(source_range))?;
}
if let ModulePath::Local { value, .. } = &module_path {
let name = import_stmt
.module_name()
.unwrap_or_else(|| value.file_name().unwrap_or_default());
exec_state.push_op(Operation::ModuleInstance {
name,
module_id,
glob: matches!(import_stmt.selector, ImportSelector::Glob(_)),
node_path: NodePath::placeholder(),
source_range,
});
}
match &import_stmt.selector {
ImportSelector::List { items } => {
let (env_ref, module_exports) = self.exec_module_for_items(module_id, exec_state, source_range).await?;
for import_item in items {
let mem = &exec_state.stack().memory;
let mut value = mem.get_from_owned(&import_item.name.name, env_ref, import_item.into(), 0);
let ty_name = format!("{}{}", memory::TYPE_PREFIX, import_item.name.name);
let mut ty = mem.get_from_owned(&ty_name, env_ref, import_item.into(), 0);
let mod_name = format!("{}{}", memory::MODULE_PREFIX, import_item.name.name);
let mut mod_value = mem.get_from_owned(&mod_name, env_ref, import_item.into(), 0);
if value.is_err() && ty.is_err() && mod_value.is_err() {
return Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("{} is not defined in module", import_item.name.name),
vec![SourceRange::from(&import_item.name)],
),
None,
));
}
if value.is_ok() && !module_exports.contains(&import_item.name.name) {
value = Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
import_item.name.name
),
vec![SourceRange::from(&import_item.name)],
)));
}
if ty.is_ok() && !module_exports.contains(&ty_name) {
ty = Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
import_item.name.name
),
vec![SourceRange::from(&import_item.name)],
)));
}
if mod_value.is_ok() && !module_exports.contains(&mod_name) {
mod_value = Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
import_item.name.name
),
vec![SourceRange::from(&import_item.name)],
)));
}
if value.is_err() && ty.is_err() && mod_value.is_err() {
return value.map(|_| ());
}
if let Ok(value) = value {
exec_state.mut_stack().add(
import_item.identifier().to_owned(),
value,
SourceRange::from(&import_item.name),
)?;
if let ItemVisibility::Export = import_stmt.visibility {
exec_state
.mod_local
.module_exports
.push(import_item.identifier().to_owned());
}
}
if let Ok(ty) = ty {
let ty_name = format!("{}{}", memory::TYPE_PREFIX, import_item.identifier());
if matches!(
&ty,
KclValue::Type {
value: TypeDef::Enum(_),
..
}
) {
reject_enum_clashing_with_module(
exec_state,
import_item.identifier(),
SourceRange::from(&import_item.name),
)?;
}
exec_state
.mut_stack()
.add(ty_name.clone(), ty, SourceRange::from(&import_item.name))?;
if let ItemVisibility::Export = import_stmt.visibility {
exec_state.mod_local.module_exports.push(ty_name);
}
}
if let Ok(mod_value) = mod_value {
let mod_name = format!("{}{}", memory::MODULE_PREFIX, import_item.identifier());
reject_module_clashing_with_enum(
exec_state,
import_item.identifier(),
SourceRange::from(&import_item.name),
)?;
exec_state.mut_stack().add(
mod_name.clone(),
mod_value,
SourceRange::from(&import_item.name),
)?;
if let ItemVisibility::Export = import_stmt.visibility {
exec_state.mod_local.module_exports.push(mod_name);
}
}
}
}
ImportSelector::Glob(_) => {
let (env_ref, module_exports) = self.exec_module_for_items(module_id, exec_state, source_range).await?;
for name in module_exports.iter() {
let item = exec_state
.stack()
.memory
.get_from_owned(name, env_ref, source_range, 0)
.map_err(|_err| {
internal_err(
format!("{name} is not defined in module (but was exported?)"),
source_range,
)
})?;
reject_glob_import_clash(exec_state, name, &item, source_range)?;
exec_state.mut_stack().add(name.to_owned(), item, source_range)?;
if let ItemVisibility::Export = import_stmt.visibility {
exec_state.mod_local.module_exports.push(name.clone());
}
}
}
ImportSelector::None { .. } => {
let name = import_stmt.module_name().unwrap();
reject_module_clashing_with_enum(exec_state, &name, source_range)?;
let item = KclValue::Module {
value: module_id,
meta: vec![source_range.into()],
};
exec_state
.mut_stack()
.add(format!("{}{}", memory::MODULE_PREFIX, name), item, source_range)?;
}
}
Ok(())
}
pub(super) async fn exec_type_declaration(
&self,
ty: &Node<TypeDeclaration>,
body_type: BodyType,
exec_state: &mut ExecState,
) -> Result<(), KclError> {
let metadata = Metadata::from(ty);
let attrs = annotations::get_fn_attrs(&ty.outer_attrs, metadata.source_range)?.unwrap_or_default();
match attrs.impl_ {
annotations::Impl::Rust | annotations::Impl::RustConstrainable | annotations::Impl::RustConstraint => {
let std_path = match &exec_state.mod_local.path {
ModulePath::Std { value } => value,
ModulePath::Local { .. } | ModulePath::Main => {
return Err(KclError::new_semantic(KclErrorDetails::new(
"User-defined types are not yet supported.".to_owned(),
vec![metadata.source_range],
)));
}
};
let (t, props) = crate::std::std_ty(std_path, &ty.name.name);
let value = KclValue::Type {
value: TypeDef::RustRepr(t, props),
meta: vec![metadata],
experimental: attrs.experimental,
};
let name_in_mem = format!("{}{}", memory::TYPE_PREFIX, ty.name.name);
exec_state
.mut_stack()
.add(name_in_mem.clone(), value, metadata.source_range)
.map_err(|_| {
KclError::new_semantic(KclErrorDetails::new(
format!("Redefinition of type {}.", ty.name.name),
vec![metadata.source_range],
))
})?;
if let ItemVisibility::Export = ty.visibility {
exec_state.mod_local.module_exports.push(name_in_mem);
}
}
annotations::Impl::Primitive => {}
annotations::Impl::Kcl | annotations::Impl::KclConstrainable => match &ty.definition {
TypeDeclarationDefinition::Alias { ty: alias } => {
let type_def = match alias.inner.clone() {
Type::Named { name } => {
match resolve_named_type_def(&name, exec_state, self, metadata.source_range, false).await? {
def @ TypeDef::Enum(_) => def,
def => TypeDef::Alias(def.into_runtime_type()),
}
}
alias => TypeDef::Alias(
RuntimeType::from_parsed(
alias,
exec_state,
self,
metadata.source_range,
attrs.impl_ == annotations::Impl::KclConstrainable,
false,
)
.await?,
),
};
if matches!(&type_def, TypeDef::Enum(_)) {
reject_enum_clashing_with_module(exec_state, &ty.name.name, metadata.source_range)?;
}
let value = KclValue::Type {
value: type_def,
meta: vec![metadata],
experimental: attrs.experimental,
};
let name_in_mem = format!("{}{}", memory::TYPE_PREFIX, ty.name.name);
exec_state
.mut_stack()
.add(name_in_mem.clone(), value, metadata.source_range)
.map_err(|_| {
KclError::new_semantic(KclErrorDetails::new(
format!("Redefinition of type {}.", ty.name.name),
vec![metadata.source_range],
))
})?;
if let ItemVisibility::Export = ty.visibility {
exec_state.mod_local.module_exports.push(name_in_mem);
}
}
TypeDeclarationDefinition::Bare => {
return Err(KclError::new_semantic(KclErrorDetails::new(
"User-defined types are not yet supported.".to_owned(),
vec![metadata.source_range],
)));
}
TypeDeclarationDefinition::Enum(decl) => {
if !matches!(body_type, BodyType::Root) {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Enum declarations are only supported at the top-level of a file. Move `type {}` to the top-level.",
ty.name.name
),
vec![metadata.source_range],
)));
}
reject_enum_clashing_with_module(exec_state, &ty.name.name, metadata.source_range)?;
let variants = decl.variants.iter().map(|v| v.name.name.clone()).collect();
let id = EnumTypeId::new(metadata.source_range.module_id(), ty.name.name.clone());
let def = EnumTypeDef::new(id, variants).map_err(|duplicate| {
KclError::new_semantic(KclErrorDetails::new(
format!("Duplicate variant `{}` in enum `{}`.", duplicate.name, ty.name.name),
vec![
decl.variants[duplicate.first_index].as_source_range(),
decl.variants[duplicate.duplicate_index].as_source_range(),
],
))
})?;
let value = KclValue::Type {
value: TypeDef::Enum(Arc::new(def)),
meta: vec![metadata],
experimental: attrs.experimental,
};
let name_in_mem = format!("{}{}", memory::TYPE_PREFIX, ty.name.name);
exec_state
.mut_stack()
.add(name_in_mem.clone(), value, metadata.source_range)
.map_err(|_| {
KclError::new_semantic(KclErrorDetails::new(
format!("Redefinition of type {}.", ty.name.name),
vec![metadata.source_range],
))
})?;
if let ItemVisibility::Export = ty.visibility {
exec_state.mod_local.module_exports.push(name_in_mem);
}
}
},
}
Ok(())
}
pub(super) fn bind_variable_declaration(
&self,
variable_declaration: &Node<VariableDeclaration>,
rhs: KclValue,
body_type: BodyType,
exec_state: &mut ExecState,
) -> Result<KclValue, KclError> {
let var_name = variable_declaration.declaration.id.name.to_string();
let source_range = SourceRange::from(&variable_declaration.declaration.init);
let mut rhs = rhs;
if let KclValue::Segment { value } = &mut rhs
&& let SegmentRepr::Unsolved { segment } = &mut value.repr
{
segment.tag = Some(TagIdentifier {
value: variable_declaration.declaration.id.name.clone(),
info: Default::default(),
meta: vec![SourceRange::from(&variable_declaration.declaration.id).into()],
});
}
let rhs = rhs;
let should_bind_name = if let Some(fn_name) = variable_declaration.declaration.init.fn_declaring_name() {
var_name != fn_name
} else {
true
};
if should_bind_name {
exec_state
.mut_stack()
.add(var_name.clone(), rhs.clone(), source_range)?;
}
if let Some(sketch_block_state) = exec_state.mod_local.sketch_block.as_mut()
&& let KclValue::Segment { value } = &rhs
{
let segment_object_id = match &value.repr {
SegmentRepr::Unsolved { segment } => segment.object_id,
SegmentRepr::Solved { segment } => segment.object_id,
};
sketch_block_state
.segment_tags
.entry(segment_object_id)
.or_insert_with(|| {
let id_node = &variable_declaration.declaration.id;
Node::new(
TagDeclarator {
name: id_node.name.clone(),
digest: None,
},
id_node.start,
id_node.end,
id_node.module_id,
)
});
}
let should_show_in_feature_tree = !exec_state.mod_local.inside_stdlib && rhs.show_variable_in_feature_tree();
if should_show_in_feature_tree {
exec_state.push_op(Operation::VariableDeclaration {
name: var_name.clone(),
value: op_from_kcl_value(&rhs),
visibility: variable_declaration.visibility,
node_path: NodePath::placeholder(),
source_range,
});
}
if let ItemVisibility::Export = variable_declaration.visibility {
if matches!(body_type, BodyType::Root) {
exec_state.mod_local.module_exports.push(var_name);
} else {
exec_state.err(CompilationIssue::err(
variable_declaration.as_source_range(),
"Exports are only supported at the top-level of a file. Remove `export` or move it to the top-level.",
));
}
}
Ok(rhs)
}
pub(super) fn bind_return_value(
return_statement: &Node<ReturnStatement>,
value: KclValue,
exec_state: &mut ExecState,
) -> Result<(), KclError> {
let metadata = Metadata::from(return_statement);
exec_state
.mut_stack()
.add(memory::RETURN_NAME.to_owned(), value, metadata.source_range)
.map_err(|_| {
KclError::new_semantic(KclErrorDetails::new(
"Multiple returns from a single function.".to_owned(),
vec![metadata.source_range],
))
})?;
Ok(())
}
pub async fn open_module(
&self,
path: &ImportPath,
attrs: &[Node<Annotation>],
resolved_path: &ModulePath,
exec_state: &mut ExecState,
source_range: SourceRange,
) -> Result<ModuleId, KclError> {
match path {
ImportPath::Kcl { .. } => {
exec_state.global.mod_loader.cycle_check(resolved_path, source_range)?;
if let Some(id) = exec_state.id_for_module(resolved_path) {
return Ok(id);
}
let id = exec_state.next_module_id();
exec_state.add_path_to_source_id(resolved_path.clone(), id);
let source = resolved_path.source(&self.fs, source_range).await?;
exec_state.add_id_to_source(id, source.clone());
let parsed = crate::parsing::parse_str(&source.source, id).parse_errs_as_err()?;
exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Kcl(parsed, None));
Ok(id)
}
ImportPath::Foreign { .. } => {
if let Some(id) = exec_state.id_for_module(resolved_path) {
return Ok(id);
}
let id = exec_state.next_module_id();
let path = resolved_path.expect_path();
exec_state.add_path_to_source_id(resolved_path.clone(), id);
let format = super::import::format_from_annotations(attrs, path, source_range)?;
let geom = super::import::import_foreign(path, format, exec_state, self, source_range).await?;
exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Foreign(geom, None));
Ok(id)
}
ImportPath::Std { .. } => {
if resolved_path.is_solver_module() && exec_state.mod_local.sketch_block.is_none() {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!("The `{resolved_path}` module is only available inside sketch blocks."),
vec![source_range],
)));
}
if let Some(id) = exec_state.id_for_module(resolved_path) {
return Ok(id);
}
let id = exec_state.next_module_id();
exec_state.add_path_to_source_id(resolved_path.clone(), id);
let source = resolved_path.source(&self.fs, source_range).await?;
exec_state.add_id_to_source(id, source.clone());
let parsed = crate::parsing::parse_str(&source.source, id)
.parse_errs_as_err()
.unwrap();
exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Kcl(parsed, None));
Ok(id)
}
}
}
pub(super) async fn exec_module_for_items(
&self,
module_id: ModuleId,
exec_state: &mut ExecState,
source_range: SourceRange,
) -> Result<(EnvironmentRef, Vec<String>), KclError> {
let path = exec_state.global.module_infos[&module_id].path.clone();
let mut repr = exec_state.global.module_infos[&module_id].take_repr();
let result = match &mut repr {
ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
ModuleRepr::Kcl(_, Some(outcome)) => Ok((outcome.environment, outcome.exports.clone())),
ModuleRepr::Kcl(program, cache) => self
.exec_module_from_ast(program, module_id, &path, exec_state, source_range, PreserveMem::Normal)
.await
.map(|outcome| {
*cache = Some(outcome.clone());
(outcome.environment, outcome.exports)
}),
ModuleRepr::Foreign(geom, _) => Err(KclError::new_semantic(KclErrorDetails::new(
"Cannot import items from foreign modules".to_owned(),
vec![geom.source_range],
))),
ModuleRepr::Dummy => unreachable!("Looking up {}, but it is still being interpreted", path),
};
exec_state.global.module_infos[&module_id].restore_repr(repr);
result
}
async fn exec_module_for_result(
&self,
module_id: ModuleId,
exec_state: &mut ExecState,
source_range: SourceRange,
) -> Result<Option<KclValue>, KclError> {
let path = exec_state.global.module_infos[&module_id].path.clone();
let mut repr = exec_state.global.module_infos[&module_id].take_repr();
let result = match &mut repr {
ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
ModuleRepr::Kcl(_, Some(outcome)) => Ok(outcome.last_expr.clone()),
ModuleRepr::Kcl(program, cached_items) => {
let result = self
.exec_module_from_ast(program, module_id, &path, exec_state, source_range, PreserveMem::Normal)
.await;
match result {
Ok(outcome) => {
let value = outcome.last_expr.clone();
*cached_items = Some(outcome);
Ok(value)
}
Err(e) => Err(e),
}
}
ModuleRepr::Foreign(_, Some((imported, _))) => Ok(imported.clone()),
ModuleRepr::Foreign(geom, cached) => {
let caller_artifacts = std::mem::take(&mut exec_state.mod_local.artifacts);
let result = super::import::send_to_engine(geom.clone(), exec_state, self)
.await
.map(|geom| Some(KclValue::ImportedGeometry(geom)));
let module_artifacts = std::mem::replace(&mut exec_state.mod_local.artifacts, caller_artifacts);
match result {
Ok(val) => {
*cached = Some((val.clone(), module_artifacts));
Ok(val)
}
Err(e) => {
exec_state.mod_local.artifacts.extend(module_artifacts);
Err(e.add_import_location(&path.import_name(), source_range))
}
}
}
ModuleRepr::Dummy => unreachable!(),
};
exec_state.global.module_infos[&module_id].restore_repr(repr);
result
}
pub async fn exec_module_from_ast(
&self,
program: &Node<Program>,
module_id: ModuleId,
path: &ModulePath,
exec_state: &mut ExecState,
source_range: SourceRange,
preserve_mem: PreserveMem,
) -> Result<ModuleExecutionOutcome, 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, source_range)?;
result.map_err(|(err, _, _)| {
match err {
KclError::ImportCycle { .. } => {
err.override_source_ranges(vec![source_range])
}
_ => err.add_import_location(&path.import_name(), source_range),
}
})
}
pub(super) async fn resolve_name_for_eval(
&self,
name: &Node<Name>,
metadata: &Metadata,
exec_state: &mut ExecState,
) -> Result<KclValue, KclError> {
let value = name.get_result(exec_state, self).await?;
if let KclValue::Module { value: module_id, meta } = value {
Ok(self
.exec_module_for_result(module_id, exec_state, metadata.source_range)
.await?
.unwrap_or_else(|| {
exec_state.warn(
CompilationIssue::err(
metadata.source_range,
"Imported module has no return value. The last statement of the module must be an expression, usually the Solid.",
),
annotations::WARN_MOD_RETURN_VALUE,
);
let mut new_meta = vec![metadata.to_owned()];
new_meta.extend(meta);
KclValue::KclNone {
value: Default::default(),
meta: new_meta,
}
}))
} else {
Ok(value)
}
}
#[async_recursion]
pub(crate) async fn execute_expr<'a: 'async_recursion>(
&self,
init: &Expr,
exec_state: &mut ExecState,
metadata: &Metadata,
annotations: &[Node<Annotation>],
statement_kind: StatementKind<'a>,
) -> Result<KclValueControlFlow, KclError> {
let item = match init {
Expr::None(none) => KclValue::from(none).continue_(),
Expr::Literal(literal) => KclValue::from_literal((**literal).clone(), exec_state).continue_(),
Expr::TagDeclarator(tag) => tag.execute(exec_state).await?.continue_(),
Expr::Name(name) => self
.resolve_name_for_eval(name, metadata, exec_state)
.await?
.continue_(),
Expr::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, self).await?,
Expr::FunctionExpression(function_expression) => self
.create_function_closure(function_expression, annotations, metadata, statement_kind, exec_state)
.await?
.continue_(),
Expr::CallExpressionKw(call_expression) => call_expression.execute(exec_state, self).await?,
Expr::PipeExpression(pipe_expression) => pipe_expression.get_result(exec_state, self).await?,
Expr::PipeSubstitution(pipe_substitution) => match statement_kind {
StatementKind::Declaration { name } => {
let message = format!(
"you cannot declare variable {name} as %, because % can only be used in function calls"
);
return Err(KclError::new_semantic(KclErrorDetails::new(
message,
vec![pipe_substitution.into()],
)));
}
StatementKind::Expression => match exec_state.mod_local.pipe_value.clone() {
Some(x) => x.continue_(),
None => {
return Err(KclError::new_semantic(KclErrorDetails::new(
"cannot use % outside a pipe expression".to_owned(),
vec![pipe_substitution.into()],
)));
}
},
},
Expr::ArrayExpression(array_expression) => array_expression.execute(exec_state, self).await?,
Expr::ArrayRangeExpression(range_expression) => range_expression.execute(exec_state, self).await?,
Expr::ObjectExpression(object_expression) => object_expression.execute(exec_state, self).await?,
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state, self).await?,
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?,
Expr::LabelledExpression(expr) => {
let value_cf = self
.execute_expr(&expr.expr, exec_state, metadata, &[], statement_kind)
.await?;
let value = control_continue!(value_cf);
exec_state
.mut_stack()
.add(expr.label.name.clone(), value.clone(), init.into())?;
value.continue_()
}
Expr::AscribedExpression(expr) => expr.get_result(exec_state, self).await?,
Expr::SketchBlock(expr) => expr.get_result(exec_state, self).await?,
Expr::SketchVar(expr) => expr.get_result(exec_state, self).await?.continue_(),
};
Ok(item)
}
pub(crate) async fn eval_expr_fresh_root(
&self,
expr: &Expr,
exec_state: &mut ExecState,
metadata: &Metadata,
) -> Result<KclValueControlFlow, KclError> {
if self.is_machine_executor() {
return crate::execution::machine::run_expr(self, expr, exec_state, metadata).await;
}
self.execute_expr(expr, exec_state, metadata, &[], StatementKind::Expression)
.await
}
pub(super) async fn create_function_closure(
&self,
function_expression: &crate::parsing::ast::types::BoxNode<FunctionExpression>,
annotations: &[Node<Annotation>],
metadata: &Metadata,
statement_kind: StatementKind<'_>,
exec_state: &mut ExecState,
) -> Result<KclValue, KclError> {
let attrs = annotations::get_fn_attrs(annotations, metadata.source_range)?;
let experimental = attrs
.as_ref()
.map(|a| a.experimental)
.unwrap_or_else(|| FnAttrs::default().experimental);
let include_in_feature_tree = attrs
.as_ref()
.map(|a| a.include_in_feature_tree)
.unwrap_or_else(|| FnAttrs::default().include_in_feature_tree);
let (mut closure, placeholder_env_ref) = if let Some(attrs) = attrs
&& (attrs.impl_ == annotations::Impl::Rust
|| attrs.impl_ == annotations::Impl::RustConstrainable
|| attrs.impl_ == annotations::Impl::RustConstraint)
{
if let ModulePath::Std { value: std_path } = &exec_state.mod_local.path {
let (func, props) = crate::std::std_fn(std_path, statement_kind.expect_name());
(
KclValue::Function {
value: Box::new(FunctionSource::rust(func, function_expression.clone(), props, attrs)),
meta: vec![metadata.to_owned()],
},
None,
)
} else {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Rust implementation of functions is restricted to the standard library".to_owned(),
vec![metadata.source_range],
)));
}
} else {
let std_props = function_expression
.name_str()
.and_then(|name| exec_state.mod_local.path.build_std_fully_qualified_name(name))
.map(|name| StdFnProps::default(&name));
let (env_ref, placeholder_env_ref) = if function_expression.name.is_some() {
let dummy = EnvironmentRef::dummy();
(dummy, Some(dummy))
} else {
(exec_state.mut_stack().snapshot()?, None)
};
(
KclValue::Function {
value: Box::new(FunctionSource::kcl(
function_expression.clone(),
env_ref,
KclFunctionSourceParams {
std_props,
experimental,
include_in_feature_tree,
},
)),
meta: vec![metadata.to_owned()],
},
placeholder_env_ref,
)
};
if let KclValue::Function { value, .. } = &mut closure {
value.resolve_signature_types(exec_state, self).await?;
}
if let Some(fn_name) = &function_expression.name {
if let Some(placeholder_env_ref) = placeholder_env_ref {
closure = exec_state.mut_stack().add_recursive_closure(
fn_name.name.to_owned(),
closure,
placeholder_env_ref,
metadata.source_range,
)?;
} else {
exec_state
.mut_stack()
.add(fn_name.name.clone(), closure.clone(), metadata.source_range)?;
}
}
Ok(closure)
}
}
fn module_enum_clash(name: &str, source_range: SourceRange) -> KclError {
KclError::new_semantic(KclErrorDetails::new(
format!(
"An enum and a module cannot share the name `{name}` in the same scope, because `{name}::x` would be ambiguous. Rename one of them."
),
vec![source_range],
))
}
fn reject_enum_clashing_with_module(
exec_state: &ExecState,
name: &str,
source_range: SourceRange,
) -> Result<(), KclError> {
let key = format!("{}{}", memory::MODULE_PREFIX, name);
if !exec_state.stack().cur_frame_contains(&key)? {
return Ok(());
}
Err(module_enum_clash(name, source_range))
}
fn reject_module_clashing_with_enum(
exec_state: &ExecState,
name: &str,
source_range: SourceRange,
) -> Result<(), KclError> {
let key = format!("{}{}", memory::TYPE_PREFIX, name);
if !exec_state.stack().cur_frame_contains(&key)? {
return Ok(());
}
let Ok(KclValue::Type {
value: TypeDef::Enum(_),
..
}) = exec_state.stack().get(&key, source_range)
else {
return Ok(());
};
Err(module_enum_clash(name, source_range))
}
fn different_enums_err(left: &EnumValue, right: &EnumValue, source_range: SourceRange) -> KclError {
let left_name = left.enum_id().declared_name();
let right_name = right.enum_id().declared_name();
let message = if left_name == right_name {
format!(
"Cannot compare two different enums that are both named `{left_name}`. They come from separate declarations."
)
} else {
format!("Cannot compare enum `{left_name}` with enum `{right_name}`. They are different types.")
};
KclError::new_semantic(KclErrorDetails::new(message, vec![source_range]))
}
fn type_used_as_value(exec_state: &ExecState, name: &Node<Identifier>) -> Option<KclError> {
let key = format!("{}{}", memory::TYPE_PREFIX, name.name);
let KclValue::Type { value: def, .. } = exec_state.stack().get(&key, name.as_source_range()).ok()? else {
return None;
};
let suggestion = match &def {
TypeDef::Enum(def) => def
.variants()
.first()
.map(|variant| format!(" Use one of its variants, such as `{}::{variant}`.", name.name))
.unwrap_or_default(),
_ => String::new(),
};
Some(KclError::new_semantic(KclErrorDetails::new(
format!("`{}` is a type, not a value.{suggestion}", name.name),
name.as_source_ranges(),
)))
}
enum EnumPathHead {
Enum(Arc<EnumTypeDef>),
NonEnumType,
}
fn enum_named_by_segment(
exec_state: &ExecState,
segment: &Node<Identifier>,
within: Option<&(EnvironmentRef, Vec<String>)>,
) -> Option<EnumPathHead> {
match type_value_named_by_segment(exec_state, segment, within)? {
KclValue::Type {
value: TypeDef::Enum(def),
..
} => Some(EnumPathHead::Enum(def)),
KclValue::Type { .. } => Some(EnumPathHead::NonEnumType),
_ => None,
}
}
fn non_enum_type_in_path(segment: &Node<Identifier>) -> KclError {
KclError::new_semantic(KclErrorDetails::new(
format!(
"`{}` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
segment.name
),
segment.as_source_ranges(),
))
}
fn enum_variant_value(
def: Arc<EnumTypeDef>,
variant: &Node<Identifier>,
exec_state: &mut ExecState,
) -> Result<KclValue, KclError> {
let enum_name = def.id().declared_name();
if !def.has_variant(&variant.name) {
let known = if def.variants().is_empty() {
format!("Enum `{enum_name}` has no variants")
} else {
format!("Its variants are: {}", def.variants().join(", "))
};
return Err(KclError::new_semantic(KclErrorDetails::new(
format!("`{}` is not a variant of enum `{enum_name}`. {known}.", variant.name),
variant.as_source_ranges(),
)));
}
exec_state.warn_experimental(&format!("the enum `{enum_name}`"), variant.as_source_range());
Ok(KclValue::Enum {
value: Box::new(EnumValue::new(
def,
variant.name.clone(),
vec![Metadata {
source_range: variant.as_source_range(),
}],
)),
})
}
fn reject_glob_import_clash(
exec_state: &ExecState,
key: &str,
item: &KclValue,
source_range: SourceRange,
) -> Result<(), KclError> {
if let Some(name) = key.strip_prefix(memory::MODULE_PREFIX) {
return reject_module_clashing_with_enum(exec_state, name, source_range);
}
if let Some(name) = key.strip_prefix(memory::TYPE_PREFIX)
&& matches!(
item,
KclValue::Type {
value: TypeDef::Enum(_),
..
}
)
{
return reject_enum_clashing_with_module(exec_state, name, source_range);
}
Ok(())
}
pub(super) fn sketch_mode_should_skip(expr: &Expr) -> bool {
fn contains_edited_sketch_block(node: crate::walk::Node<'_>) -> bool {
if let crate::walk::Node::SketchBlock(sketch_block) = node {
return sketch_block.is_being_edited;
}
node.children().into_iter().any(contains_edited_sketch_block)
}
!contains_edited_sketch_block(expr.into())
}
fn var_in_own_ref_err(e: KclError, being_declared: &Option<String>) -> KclError {
let KclError::UndefinedValue { name, mut details } = e else {
return e;
};
if let (Some(name0), Some(name1)) = (&being_declared, &name)
&& name0 == name1
{
details.message = format!(
"You can't use `{name0}` because you're currently trying to define it. Use a different variable here instead."
);
}
KclError::UndefinedValue { details, name }
}
impl Node<AscribedExpression> {
#[async_recursion]
pub(super) async fn get_result(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
let metadata = Metadata {
source_range: SourceRange::from(self),
};
let result = ctx
.execute_expr(&self.expr, exec_state, &metadata, &[], StatementKind::Expression)
.await?;
let result = control_continue!(result);
apply_ascription(&result, &self.ty, exec_state, ctx, self.into())
.await
.map(KclValue::continue_)
}
}
impl Node<SketchBlock> {
pub(super) async fn get_result(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
if exec_state.mod_local.sketch_block.is_some() {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Cannot execute a sketch block from within another sketch block".to_owned(),
vec![SourceRange::from(self)],
)));
}
let range = SourceRange::from(self);
let (sketch_id, sketch_surface) = match self.exec_arguments(exec_state, ctx).await {
Ok(x) => x,
Err(cf_error) => match cf_error {
EarlyReturn::Value(cf_value) => return Ok(cf_value),
EarlyReturn::Error(err) => return Err(err),
},
};
let sketch_block_artifact_id = self.scene_setup(sketch_id, &sketch_surface, exec_state)?;
let (return_result, variables, sketch_block_state) = {
self.prep_mem(exec_state.mut_stack().snapshot()?, exec_state)?;
let initial_sketch_block_state = {
SketchBlockState {
sketch_id: Some(sketch_id),
..Default::default()
}
};
let original_value = exec_state.mod_local.sketch_block.replace(initial_sketch_block_state);
let original_sketch_mode = std::mem::replace(&mut exec_state.mod_local.sketch_mode, false);
let (result, block_variables) = match self.load_sketch2_into_current_scope(exec_state, ctx, range).await {
Ok(()) => {
let parent = exec_state.mut_stack().snapshot()?;
exec_state.mut_stack().push_new_env_for_call(parent)?;
let result = ctx.exec_block(&self.body, exec_state, BodyType::Block).await;
let (result, block_variables) = match exec_state.stack().find_all_in_current_env() {
Ok(block_variables) => (result, block_variables.into_iter().collect::<IndexMap<_, _>>()),
Err(err) => (Err(err), IndexMap::new()),
};
let result = match exec_state.mut_stack().pop_env() {
Ok(_) => result,
Err(err) => Err(err),
};
(result, block_variables)
}
Err(err) => (Err(err), IndexMap::new()),
};
exec_state.mod_local.sketch_mode = original_sketch_mode;
let sketch_block_state = std::mem::replace(&mut exec_state.mod_local.sketch_block, original_value);
let result = match exec_state.mut_stack().pop_env() {
Ok(_) => result,
Err(err) => Err(err),
};
(result, block_variables, sketch_block_state)
};
let return_control_flow = return_result?;
if let Some(control_flow) = return_control_flow
&& control_flow.is_some_return()
{
exec_state.push_op(Operation::GroupEnd);
return Ok(control_flow);
}
let Some(sketch_block_state) = sketch_block_state else {
debug_assert!(false, "Sketch block state should still be set to Some from just above");
return Err(internal_err(
"Sketch block state should still be set to Some from just above",
self,
));
};
let return_value = self
.finalize_sketch_block(
sketch_id,
&sketch_surface,
sketch_block_artifact_id,
variables,
sketch_block_state,
exec_state,
ctx,
)
.await?;
Ok(if self.is_being_edited {
return_value.exit()
} else {
return_value.continue_()
})
}
async fn exec_arguments(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<(ObjectId, SketchSurface), EarlyReturn> {
if !exec_state.sketch_mode() {
let mut labeled = IndexMap::new();
for labeled_arg in &self.arguments {
let source_range = SourceRange::from(labeled_arg.arg.clone());
let metadata = Metadata { source_range };
let value_cf = ctx
.execute_expr(&labeled_arg.arg, exec_state, &metadata, &[], StatementKind::Expression)
.await?;
let value = early_return!(value_cf);
let arg = Arg::new(value, source_range);
match &labeled_arg.label {
Some(label) => {
labeled.insert(label.name.clone(), arg);
}
None => {
let name = labeled_arg.arg.ident_name();
if let Some(name) = name {
labeled.insert(name.to_owned(), arg);
} else {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Arguments to sketch blocks must be either labeled or simple identifiers".to_owned(),
vec![SourceRange::from(&labeled_arg.arg)],
))
.into());
}
}
}
}
self.finish_arguments_after_eval(labeled, exec_state, ctx).await
} else {
self.arguments_from_cache(exec_state)
}
}
pub(super) async fn finish_arguments_after_eval(
&self,
labeled: IndexMap<String, Arg>,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<(ObjectId, SketchSurface), EarlyReturn> {
let range = SourceRange::from(self);
let mut args = Args::new_no_args(
range,
self.node_path.clone(),
ctx.clone(),
Some(SketchBlock::CALLEE_NAME.to_owned()),
);
args.labeled = labeled;
self.check_for_unexpected_arguments(&args, exec_state)?;
let arg_on_value: KclValue =
args.get_kw_arg(SKETCH_BLOCK_PARAM_ON, &RuntimeType::sketch_or_surface(), exec_state)?;
let Some(arg_on) = SketchOrSurface::from_kcl_val(&arg_on_value) else {
let message = "The `on` argument to a sketch block must be convertible to a sketch or surface.".to_owned();
debug_assert!(false, "{message}");
return Err(KclError::new_semantic(KclErrorDetails::new(message, vec![range])).into());
};
let mut sketch_surface = arg_on.into_sketch_surface();
match &mut sketch_surface {
SketchSurface::Plane(plane) => {
ensure_sketch_plane_in_engine(plane, exec_state, ctx, range, self.node_path.clone()).await?;
}
SketchSurface::Face(_) => {
}
}
let sketch_id = exec_state.next_object_id();
exec_state.add_placeholder_scene_object(sketch_id, range, self.node_path.clone());
let on_cache_name = sketch_on_cache_name(sketch_id);
exec_state.mut_stack().add(on_cache_name, arg_on_value, range)?;
Ok((sketch_id, sketch_surface))
}
pub(super) fn arguments_from_cache(
&self,
exec_state: &mut ExecState,
) -> Result<(ObjectId, SketchSurface), EarlyReturn> {
let range = SourceRange::from(self);
{
let sketch_id = exec_state.next_object_id();
exec_state.add_placeholder_scene_object(sketch_id, range, self.node_path.clone());
let on_cache_name = sketch_on_cache_name(sketch_id);
let arg_on_value = exec_state.stack().get_owned(&on_cache_name, range)?;
let Some(arg_on) = SketchOrSurface::from_kcl_val(&arg_on_value) else {
let message =
"The `on` argument to a sketch block must be convertible to a sketch or surface.".to_owned();
debug_assert!(false, "{message}");
return Err(KclError::new_semantic(KclErrorDetails::new(message, vec![range])).into());
};
let mut sketch_surface = arg_on.into_sketch_surface();
if sketch_surface.object_id().is_none() {
let Some(last_object) = exec_state.mod_local.artifacts.scene_objects.last() else {
return Err(internal_err(
"In sketch mode, the `on` plane argument must refer to an existing plane object.",
range,
)
.into());
};
sketch_surface.set_object_id(last_object.id);
}
Ok((sketch_id, sketch_surface))
}
}
pub(super) fn scene_setup(
&self,
sketch_id: ObjectId,
sketch_surface: &SketchSurface,
exec_state: &mut ExecState,
) -> Result<ArtifactId, KclError> {
let range = SourceRange::from(self);
let on_object_id = if let Some(object_id) = sketch_surface.object_id() {
object_id
} else {
let message = "The `on` argument should have an object after ensure_sketch_plane_in_engine".to_owned();
debug_assert!(false, "{message}");
return Err(internal_err(message, range));
};
let sketch_ctor_on = sketch_on_frontend_plane(&self.arguments, on_object_id);
let sketch_block_artifact_id = {
use crate::execution::CodeRef;
use crate::execution::SketchBlock;
use crate::front::Plane;
use crate::front::SourceRef;
let on_object = exec_state.mod_local.artifacts.scene_object_by_id(on_object_id);
let plane_artifact_id = on_object.map(|object| object.artifact_id);
let plane_info = match &sketch_surface {
SketchSurface::Plane(plane) => Some(super::artifact::artifact_plane_info(&plane.info)),
SketchSurface::Face(_) => None,
};
let standard_plane = match &sketch_ctor_on {
Plane::Default(plane) => Some(*plane),
Plane::Object(_) | Plane::PrimitiveFace(_) => None,
};
let artifact_id = ArtifactId::from(exec_state.next_uuid());
let label = exec_state.mod_local.being_declared.clone().unwrap_or_default();
let sketch_scene_object = Object {
id: sketch_id,
kind: ObjectKind::Sketch(crate::frontend::sketch::Sketch {
args: crate::front::SketchCtor { on: sketch_ctor_on },
plane: on_object_id,
segments: Default::default(),
constraints: Default::default(),
}),
label,
comments: Default::default(),
artifact_id,
source: SourceRef::new(self.into(), self.node_path.clone()),
};
exec_state.set_scene_object(sketch_scene_object);
exec_state.add_artifact(Artifact::SketchBlock(SketchBlock {
id: artifact_id,
standard_plane,
plane_id: plane_artifact_id,
plane_info,
path_id: None,
code_ref: CodeRef::placeholder(range),
sketch_id,
}));
exec_state.push_op(Operation::GroupBegin {
group: Group::SketchBlock { sketch_id },
node_path: NodePath::placeholder(),
source_range: range,
});
artifact_id
};
Ok(sketch_block_artifact_id)
}
#[allow(clippy::too_many_arguments)]
pub(super) async fn finalize_sketch_block(
&self,
sketch_id: ObjectId,
sketch_surface: &SketchSurface,
sketch_block_artifact_id: ArtifactId,
variables: IndexMap<String, KclValue>,
mut sketch_block_state: SketchBlockState,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValue, KclError> {
let range = SourceRange::from(self);
let constraints = sketch_block_state
.solver_constraints
.iter()
.cloned()
.map(ezpz::ConstraintRequest::highest_priority)
.chain(
sketch_block_state
.solver_optional_constraints
.iter()
.cloned()
.map(|c| ezpz::ConstraintRequest::new(c, 1)),
)
.collect::<Vec<_>>();
let initial_guesses = sketch_block_state
.sketch_vars
.iter()
.map(|v| {
let Some(sketch_var) = v.as_sketch_var() else {
return Err(internal_err("Expected sketch variable", self));
};
let constraint_id = sketch_var.id.to_constraint_id(range)?;
let number_value = KclValue::Number {
value: sketch_var.initial_value,
ty: sketch_var.ty,
meta: sketch_var.meta.clone(),
};
let initial_guess_value = normalize_to_solver_distance_unit(
&number_value,
v.into(),
exec_state,
"sketch variable initial value",
)?;
let initial_guess = if let Some(n) = initial_guess_value.as_ty_f64() {
n.n
} else {
let message = format!(
"Expected number after coercion, but found {}",
initial_guess_value.human_friendly_type()
);
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
Ok((constraint_id, initial_guess))
})
.collect::<Result<Vec<_>, KclError>>()?;
let config = ezpz::Config::default()
.with_max_iterations(50)
.with_convergence_tolerance(SOLVER_CONVERGENCE_TOLERANCE);
let solve_result = if exec_state.mod_local.freedom_analysis {
ezpz::solve_analysis(&constraints, initial_guesses.clone(), config).map(|outcome| {
let freedom_analysis = FreedomAnalysis::from_ezpz_analysis(outcome.analysis, constraints.len());
(outcome.outcome, Some(freedom_analysis))
})
} else {
ezpz::solve(&constraints, initial_guesses.clone(), config).map(|outcome| (outcome, None))
};
let num_required_constraints = sketch_block_state.solver_constraints.len();
let all_constraints: Vec<ezpz::Constraint> = sketch_block_state
.solver_constraints
.iter()
.cloned()
.chain(sketch_block_state.solver_optional_constraints.iter().cloned())
.collect();
let (solve_outcome, solve_analysis) = match solve_result {
Ok((solved, freedom)) => {
if solved
.final_values()
.iter()
.any(|number| number.is_infinite() || number.is_nan())
{
return Err(KclError::new_internal(KclErrorDetails::new(
"KCL's 2D constraint solver returned an invalid number".to_owned(),
vec![SourceRange::from(self)],
)));
}
let outcome = Solved::from_ezpz_outcome(solved, &all_constraints, num_required_constraints);
if !outcome.converged {
exec_state.warn(
CompilationIssue::err(range, "Constraint solver failed to find a solution".to_owned()),
annotations::WARN_SOLVER,
);
}
(outcome, freedom)
}
Err(failure) => {
match &failure.error {
NonLinearSystemError::FaerMatrix { .. }
| NonLinearSystemError::Faer { .. }
| NonLinearSystemError::FaerSolve { .. }
| NonLinearSystemError::FaerSvd(..) => {
exec_state.warn(
CompilationIssue::err(range, "Internal error in constraint solver".to_owned()),
annotations::WARN_SOLVER,
);
let final_values = initial_guesses.iter().map(|(_, v)| *v).collect::<Vec<_>>();
(
Solved {
final_values,
iterations: Default::default(),
warnings: failure.warnings,
priority_solved: Default::default(),
variables_in_conflicts: Default::default(),
unsatisfied_directional_constraints: Default::default(),
converged: false,
},
None,
)
}
NonLinearSystemError::EmptySystemNotAllowed
| NonLinearSystemError::WrongNumberGuesses { .. }
| NonLinearSystemError::MissingGuess { .. }
| NonLinearSystemError::NotFound(..) => {
#[cfg(target_arch = "wasm32")]
web_sys::console::error_1(
&format!("Internal error from constraint solver: {}", failure.error).into(),
);
return Err(internal_err(
format!("Internal error from constraint solver: {}", failure.error),
self,
));
}
_ => {
return Err(internal_err(
format!("Error from constraint solver: {}", failure.error),
self,
));
}
}
}
};
for warning in &solve_outcome.warnings {
let message = if let Some(index) = warning.about_constraint.as_ref() {
format!("{}; constraint index {}", warning.content, index)
} else {
format!("{}", warning.content)
};
exec_state.warn(CompilationIssue::err(range, message), annotations::WARN_SOLVER);
}
if solve_outcome.converged {
exec_state.mod_local.artifacts.refactor_metadata.extend(
sketch_block_state
.pending_legacy_angle_refactor_metadata
.iter()
.filter_map(|pending| {
finalize_legacy_angle_refactor_meta(pending, &solve_outcome.final_values)
.map(RefactorMetadata::LegacyAngle)
}),
);
}
let sketch_engine_id = exec_state.next_uuid();
let solution_ty = solver_numeric_type(exec_state);
let mut solved_segments = Vec::with_capacity(sketch_block_state.needed_by_engine.len());
for unsolved_segment in &sketch_block_state.needed_by_engine {
solved_segments.push(substitute_sketch_var_in_segment(
unsolved_segment.clone(),
sketch_surface,
sketch_engine_id,
None,
&solve_outcome,
solver_numeric_type(exec_state),
solve_analysis.as_ref(),
)?);
}
exec_state.mod_local.artifacts.var_solutions =
sketch_block_state.var_solutions(&solve_outcome, solution_ty, SourceRange::from(self))?;
let scene_objects = create_segment_scene_objects(&solved_segments, range, exec_state)?;
let sketch = create_segments_in_engine(
sketch_surface,
sketch_engine_id,
&mut solved_segments,
&sketch_block_state.segment_tags,
ctx,
exec_state,
range,
)
.await?;
if let Some(sketch_artifact_id) = sketch.as_ref().map(|s| s.artifact_id) {
if let Some(Artifact::SketchBlock(sketch_block_artifact)) =
exec_state.artifact_mut(sketch_block_artifact_id)
{
sketch_block_artifact.path_id = Some(sketch_artifact_id);
} else {
let message = "Sketch block artifact not found, so path couldn't be linked to it".to_owned();
debug_assert!(false, "{message}");
return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
}
}
let variables = substitute_sketch_vars(
variables,
sketch_surface,
sketch_engine_id,
sketch.as_ref(),
&solve_outcome,
solution_ty,
solve_analysis.as_ref(),
)?;
let mut segment_object_ids = Vec::with_capacity(scene_objects.len());
for scene_object in scene_objects {
segment_object_ids.push(scene_object.id);
exec_state.set_scene_object(scene_object);
}
let Some(sketch_object) = exec_state.mod_local.artifacts.scene_object_by_id_mut(sketch_id) else {
let message = format!("Sketch object not found after it was just created; id={:?}", sketch_id);
debug_assert!(false, "{}", &message);
return Err(internal_err(message, range));
};
let ObjectKind::Sketch(front_sketch) = &mut sketch_object.kind else {
let message = format!(
"Expected Sketch object after it was just created to be a sketch kind; id={:?}, actual={:?}",
sketch_id, sketch_object
);
debug_assert!(
false,
"{}; scene_objects={:#?}",
message, exec_state.mod_local.artifacts.scene_objects
);
return Err(internal_err(message, range));
};
front_sketch.segments.extend(segment_object_ids);
front_sketch
.constraints
.extend(std::mem::take(&mut sketch_block_state.sketch_constraints));
exec_state.push_op(Operation::GroupEnd);
if exec_state.mod_local.freedom_analysis {
let status = {
let scene_objects = &exec_state.mod_local.artifacts.scene_objects;
scene_objects
.get(sketch_id.0)
.and_then(|obj| sketch_constraint_status_for_sketch(scene_objects, obj))
};
if let Some(status) = status
&& status.status == ConstraintKind::OverConstrained
{
let description = if status.conflict_count == 1 {
"segment has"
} else {
"segments have"
};
let message = format!(
"Sketch is over-constrained: {} {description} conflicting constraints.{}",
status.conflict_count,
signed_distance_conflict_hint(&solve_outcome),
);
exec_state.warn(
CompilationIssue::err(range, message),
annotations::WARN_OVER_CONSTRAINED_SKETCH,
);
}
}
let properties = self.sketch_properties(sketch, variables);
let metadata = Metadata {
source_range: SourceRange::from(self),
};
let return_value = KclValue::Object {
value: properties,
constrainable: Default::default(),
object_kind: KclObjectKind::Default,
meta: vec![metadata],
};
Ok(return_value)
}
fn check_for_unexpected_arguments(&self, args: &Args, exec_state: &mut ExecState) -> Result<(), KclError> {
if !args.unlabeled.is_empty() {
let message = "Sketch block doesn't support unlabeled arguments; argument shorthand should have already been desugared";
debug_assert!(false, "{message}");
return Err(KclError::new_internal(KclErrorDetails::new(
message.to_owned(),
vec![args.source_range],
)));
}
for (label, arg) in &args.labeled {
if label == SKETCH_BLOCK_PARAM_ON {
continue;
}
exec_state.err(CompilationIssue::err(
arg.source_range,
unexpected_kw_arg_message(label, Some(SketchBlock::CALLEE_NAME)),
));
}
Ok(())
}
pub(super) async fn load_sketch2_into_current_scope(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
source_range: SourceRange,
) -> Result<(), KclError> {
let path = vec!["std".to_owned(), "solver".to_owned()];
let resolved_path = ModulePath::from_std_import_path(&path)?;
let module_id = ctx
.open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
.await?;
let (env_ref, exports) = ctx.exec_module_for_items(module_id, exec_state, source_range).await?;
for name in exports {
let value = exec_state
.stack()
.memory
.get_from_owned(&name, env_ref, source_range, 0)?;
exec_state.mut_stack().add(name, value, source_range)?;
}
Ok(())
}
pub(crate) fn sketch_properties(
&self,
sketch: Option<Sketch>,
variables: HashMap<String, KclValue>,
) -> HashMap<String, KclValue> {
let Some(sketch) = sketch else {
return variables;
};
let mut properties = variables;
let sketch_value = KclValue::Sketch {
value: Box::new(sketch),
};
let mut meta_map = HashMap::with_capacity(1);
meta_map.insert(SKETCH_OBJECT_META_SKETCH.to_owned(), sketch_value);
let meta_value = KclValue::Object {
value: meta_map,
constrainable: false,
object_kind: KclObjectKind::Default,
meta: vec![Metadata {
source_range: SourceRange::from(self),
}],
};
properties.insert(SKETCH_OBJECT_META.to_owned(), meta_value);
properties
}
}
impl SketchBlock {
pub(super) fn prep_mem(&self, parent: EnvironmentRef, exec_state: &mut ExecState) -> Result<(), KclError> {
exec_state.mut_stack().push_new_env_for_call(parent)
}
}
impl Node<SketchVar> {
pub async fn get_result(&self, exec_state: &mut ExecState, _ctx: &ExecutorContext) -> Result<KclValue, KclError> {
let Some(sketch_block_state) = &exec_state.mod_local.sketch_block else {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Cannot use a sketch variable outside of a sketch block".to_owned(),
vec![SourceRange::from(self)],
)));
};
let id = sketch_block_state.next_sketch_var_id();
let sketch_var = if let Some(initial) = &self.initial {
KclValue::from_sketch_var_literal(initial, id, self.node_path.clone(), exec_state)
} else {
let metadata = Metadata {
source_range: SourceRange::from(self),
};
KclValue::SketchVar {
value: Box::new(super::SketchVar {
id,
initial_value: 0.0,
ty: NumericType::default(),
node_path: self.node_path.clone(),
meta: vec![metadata],
}),
}
};
let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Cannot use a sketch variable outside of a sketch block".to_owned(),
vec![SourceRange::from(self)],
)));
};
sketch_block_state.sketch_vars.push(sketch_var.clone());
Ok(sketch_var)
}
}
pub(super) async fn apply_ascription(
value: &KclValue,
ty: &Node<Type>,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
source_range: SourceRange,
) -> Result<KclValue, KclError> {
let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, ctx, value.into(), false, false).await?;
if matches!(&ty, &RuntimeType::Primitive(PrimitiveType::Number(..))) {
exec_state.clear_units_warnings(&source_range);
}
value.coerce(&ty, CoercionMode::explicit(), exec_state).map_err(|e| {
if let Some(message) = e.message {
return KclError::new_semantic(KclErrorDetails::new(message, vec![source_range]));
}
let suggestion = if ty == RuntimeType::length() {
", you might try coercing to a fully specified numeric type such as `mm`"
} else if ty == RuntimeType::angle() {
", you might try coercing to a fully specified numeric type such as `deg`"
} 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 {} {ty_str}to type `{ty}`{suggestion}",
value.human_friendly_type()
),
vec![source_range],
))
})
}
impl BinaryPart {
#[async_recursion]
pub(super) async fn get_result(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
match self {
BinaryPart::Literal(literal) => Ok(KclValue::from_literal((**literal).clone(), exec_state).continue_()),
BinaryPart::Name(name) => {
let metadata = Metadata {
source_range: SourceRange::from(&**name),
};
ctx.resolve_name_for_eval(name, &metadata, exec_state)
.await
.map(KclValue::continue_)
}
BinaryPart::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, ctx).await,
BinaryPart::CallExpressionKw(call_expression) => call_expression.execute(exec_state, ctx).await,
BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, ctx).await,
BinaryPart::MemberExpression(member_expression) => member_expression.get_result(exec_state, ctx).await,
BinaryPart::ArrayExpression(e) => e.execute(exec_state, ctx).await,
BinaryPart::ArrayRangeExpression(e) => e.execute(exec_state, ctx).await,
BinaryPart::ObjectExpression(e) => e.execute(exec_state, ctx).await,
BinaryPart::IfExpression(e) => e.get_result(exec_state, ctx).await,
BinaryPart::AscribedExpression(e) => e.get_result(exec_state, ctx).await,
BinaryPart::SketchVar(e) => e.get_result(exec_state, ctx).await.map(KclValue::continue_),
}
}
}
impl Node<Name> {
pub(super) async fn get_result(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValue, KclError> {
let result = self.get_result_inner(exec_state, ctx).await;
result.map_err(|e| var_in_own_ref_err(e, &exec_state.mod_local.being_declared))
}
async fn get_result_inner(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
if self.abs_path {
return Err(KclError::new_semantic(KclErrorDetails::new(
ABSOLUTE_PATHS_NOT_SUPPORTED.to_owned(),
self.as_source_ranges(),
)));
}
if self.path.is_empty() {
if let Ok(item_value) = exec_state.stack().get(&self.name.name, self.into()) {
return Ok(item_value);
}
let mod_name = format!("{}{}", memory::MODULE_PREFIX, self.name.name);
let not_defined = match exec_state.stack().get(&mod_name, self.into()) {
Ok(module) => return Ok(module),
Err(err) => err,
};
return Err(type_used_as_value(exec_state, &self.name).unwrap_or(not_defined));
}
let mut mem_spec: Option<(EnvironmentRef, Vec<String>)> = None;
for (index, p) in self.path.iter().enumerate() {
let non_enum_type = match enum_named_by_segment(exec_state, p, mem_spec.as_ref()) {
Some(EnumPathHead::Enum(def)) => {
if let Some(next) = self.path.get(index + 1) {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"`{}` is an enum, so only a variant name can follow it. There is nothing to reach through `{}::{}`.",
p.name, p.name, next.name
),
p.as_source_ranges(),
)));
}
return enum_variant_value(def, &self.name, exec_state);
}
Some(EnumPathHead::NonEnumType) => true,
None => false,
};
let value = match mem_spec {
Some((env, exports)) => {
if !exports.contains(&p.name) {
if non_enum_type {
return Err(non_enum_type_in_path(p));
}
return Err(KclError::new_semantic(KclErrorDetails::new(
format!("Item {} not found in module's exported items", p.name),
p.as_source_ranges(),
)));
}
exec_state
.stack()
.memory
.get_from_owned(&p.name, env, p.as_source_range(), 0)?
}
None => match exec_state
.stack()
.get(&format!("{}{}", memory::MODULE_PREFIX, p.name), self.into())
{
Ok(value) => value,
Err(_) if non_enum_type => return Err(non_enum_type_in_path(p)),
Err(err) => return Err(err),
},
};
let module_id = match value {
KclValue::Module { value, .. } => value,
value => {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Identifier in path must refer to a module, found {}",
value.human_friendly_type()
),
p.as_source_ranges(),
)));
}
};
mem_spec = Some(
ctx.exec_module_for_items(module_id, exec_state, p.as_source_range())
.await?,
);
}
let (env, exports) = mem_spec.unwrap();
let item_exported = exports.contains(&self.name.name);
let item_value = exec_state
.stack()
.memory
.get_from_owned(&self.name.name, env, self.name.as_source_range(), 0);
if item_exported && item_value.is_ok() {
return item_value;
}
let mod_name = format!("{}{}", memory::MODULE_PREFIX, self.name.name);
let mod_exported = exports.contains(&mod_name);
let mod_value = exec_state
.stack()
.memory
.get_from_owned(&mod_name, env, self.name.as_source_range(), 0);
if mod_exported && mod_value.is_ok() {
return mod_value;
}
if item_value.is_err() && mod_value.is_err() {
return item_value;
}
debug_assert!((item_value.is_ok() && !item_exported) || (mod_value.is_ok() && !mod_exported));
Err(KclError::new_semantic(KclErrorDetails::new(
format!("Item {} not found in module's exported items", self.name.name),
self.name.as_source_ranges(),
)))
}
}
fn mock_array_may_have_engine_dependent_cardinality(ty: &RuntimeType) -> bool {
match ty {
RuntimeType::Primitive(
PrimitiveType::Sketch
| PrimitiveType::Solid
| PrimitiveType::Face
| PrimitiveType::Edge
| PrimitiveType::BoundedEdge
| PrimitiveType::ImportedGeometry,
) => true,
RuntimeType::Union(types) => types.iter().any(mock_array_may_have_engine_dependent_cardinality),
_ => false,
}
}
impl Node<MemberExpression> {
async fn get_result(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
if exec_state.entry_point_version_is_v3_or_higher() {
let object = control_continue!(self.eval_object(exec_state, ctx).await?);
let property = match self.eval_property(exec_state, ctx).await {
Ok(property) => property,
Err(EarlyReturn::Value(cf)) => return Ok(cf),
Err(EarlyReturn::Error(err)) => return Err(err),
};
return self.apply_member(object, property, exec_state, ctx).await;
}
let property = match self.eval_property(exec_state, ctx).await {
Ok(property) => property,
Err(EarlyReturn::Value(cf)) => return Ok(cf),
Err(EarlyReturn::Error(err)) => return Err(err),
};
let object = control_continue!(self.eval_object(exec_state, ctx).await?);
self.apply_member(object, property, exec_state, ctx).await
}
async fn eval_object(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
let object_meta = Metadata {
source_range: SourceRange::from(&self.object),
};
ctx.execute_expr(&self.object, exec_state, &object_meta, &[], StatementKind::Expression)
.await
}
async fn eval_property(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<Property, EarlyReturn> {
let property_meta = Metadata {
source_range: SourceRange::from(&self.property),
};
Property::try_from(
self.computed,
self.property.clone(),
exec_state,
self.into(),
ctx,
&property_meta,
&[],
StatementKind::Expression,
)
.await
}
pub(super) async fn apply_member(
&self,
object: KclValue,
property: Property,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
let meta = Metadata {
source_range: SourceRange::from(self),
};
match (object, property, self.computed) {
(KclValue::Segment { value: segment }, Property::String(property), false) => match property.as_str() {
"at" => match &segment.repr {
SegmentRepr::Unsolved { segment } => {
match &segment.kind {
UnsolvedSegmentKind::Point { position, .. } => {
Ok(KclValue::HomArray {
value: vec![
KclValue::from_unsolved_expr(position[0].clone(), segment.meta.clone()),
KclValue::from_unsolved_expr(position[1].clone(), segment.meta.clone()),
],
ty: RuntimeType::any(),
}
.continue_())
}
_ => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in segment"),
vec![self.clone().into()],
),
None,
)),
}
}
SegmentRepr::Solved { segment } => {
match &segment.kind {
SegmentKind::Point { position, .. } => {
Ok(KclValue::array_from_point2d(
[position[0].n, position[1].n],
position[0].ty,
segment.meta.clone(),
)
.continue_())
}
_ => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in segment"),
vec![self.clone().into()],
),
None,
)),
}
}
},
"start" => match &segment.repr {
SegmentRepr::Unsolved { segment } => match &segment.kind {
UnsolvedSegmentKind::Point { .. } => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in point segment"),
vec![self.clone().into()],
),
None,
)),
UnsolvedSegmentKind::Line {
start,
ctor,
start_object_id,
..
} => Ok(KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Unsolved {
segment: Box::new(UnsolvedSegment {
id: segment.id,
object_id: *start_object_id,
kind: UnsolvedSegmentKind::Point {
position: start.clone(),
ctor: Box::new(PointCtor {
position: ctor.start.clone(),
}),
},
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
}
.continue_()),
UnsolvedSegmentKind::Arc {
start,
ctor,
start_object_id,
..
} => Ok(KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Unsolved {
segment: Box::new(UnsolvedSegment {
id: segment.id,
object_id: *start_object_id,
kind: UnsolvedSegmentKind::Point {
position: start.clone(),
ctor: Box::new(PointCtor {
position: ctor.start.clone(),
}),
},
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
}
.continue_()),
UnsolvedSegmentKind::Circle {
start,
ctor,
start_object_id,
..
} => Ok(KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Unsolved {
segment: Box::new(UnsolvedSegment {
id: segment.id,
object_id: *start_object_id,
kind: UnsolvedSegmentKind::Point {
position: start.clone(),
ctor: Box::new(PointCtor {
position: ctor.start.clone(),
}),
},
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
}
.continue_()),
UnsolvedSegmentKind::ControlPointSpline { .. } => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in segment"),
vec![self.clone().into()],
),
None,
)),
},
SegmentRepr::Solved { segment } => match &segment.kind {
SegmentKind::Point { .. } => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in point segment"),
vec![self.clone().into()],
),
None,
)),
SegmentKind::Line {
start,
ctor,
start_object_id,
start_freedom,
..
} => Ok(KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Solved {
segment: Box::new(Segment {
id: segment.id,
object_id: *start_object_id,
kind: SegmentKind::Point {
position: start.clone(),
ctor: Box::new(PointCtor {
position: ctor.start.clone(),
}),
freedom: *start_freedom,
},
surface: segment.surface.clone(),
sketch_id: segment.sketch_id,
sketch: segment.sketch.clone(),
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
}
.continue_()),
SegmentKind::Arc {
start,
ctor,
start_object_id,
start_freedom,
..
} => Ok(KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Solved {
segment: Box::new(Segment {
id: segment.id,
object_id: *start_object_id,
kind: SegmentKind::Point {
position: start.clone(),
ctor: Box::new(PointCtor {
position: ctor.start.clone(),
}),
freedom: *start_freedom,
},
surface: segment.surface.clone(),
sketch_id: segment.sketch_id,
sketch: segment.sketch.clone(),
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
}
.continue_()),
SegmentKind::Circle {
start,
ctor,
start_object_id,
start_freedom,
..
} => Ok(KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Solved {
segment: Box::new(Segment {
id: segment.id,
object_id: *start_object_id,
kind: SegmentKind::Point {
position: start.clone(),
ctor: Box::new(PointCtor {
position: ctor.start.clone(),
}),
freedom: *start_freedom,
},
surface: segment.surface.clone(),
sketch_id: segment.sketch_id,
sketch: segment.sketch.clone(),
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
}
.continue_()),
SegmentKind::ControlPointSpline { .. } => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in segment"),
vec![self.clone().into()],
),
None,
)),
},
},
"end" => match &segment.repr {
SegmentRepr::Unsolved { segment } => match &segment.kind {
UnsolvedSegmentKind::Point { .. } => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in point segment"),
vec![self.clone().into()],
),
None,
)),
UnsolvedSegmentKind::Line {
end,
ctor,
end_object_id,
..
} => Ok(KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Unsolved {
segment: Box::new(UnsolvedSegment {
id: segment.id,
object_id: *end_object_id,
kind: UnsolvedSegmentKind::Point {
position: end.clone(),
ctor: Box::new(PointCtor {
position: ctor.end.clone(),
}),
},
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
}
.continue_()),
UnsolvedSegmentKind::Arc {
end,
ctor,
end_object_id,
..
} => Ok(KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Unsolved {
segment: Box::new(UnsolvedSegment {
id: segment.id,
object_id: *end_object_id,
kind: UnsolvedSegmentKind::Point {
position: end.clone(),
ctor: Box::new(PointCtor {
position: ctor.end.clone(),
}),
},
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
}
.continue_()),
UnsolvedSegmentKind::Circle { .. } => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in segment"),
vec![self.into()],
),
None,
)),
UnsolvedSegmentKind::ControlPointSpline { .. } => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in segment"),
vec![self.clone().into()],
),
None,
)),
},
SegmentRepr::Solved { segment } => match &segment.kind {
SegmentKind::Point { .. } => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in point segment"),
vec![self.clone().into()],
),
None,
)),
SegmentKind::Line {
end,
ctor,
end_object_id,
end_freedom,
..
} => Ok(KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Solved {
segment: Box::new(Segment {
id: segment.id,
object_id: *end_object_id,
kind: SegmentKind::Point {
position: end.clone(),
ctor: Box::new(PointCtor {
position: ctor.end.clone(),
}),
freedom: *end_freedom,
},
surface: segment.surface.clone(),
sketch_id: segment.sketch_id,
sketch: segment.sketch.clone(),
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
}
.continue_()),
SegmentKind::Arc {
end,
ctor,
end_object_id,
end_freedom,
..
} => Ok(KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Solved {
segment: Box::new(Segment {
id: segment.id,
object_id: *end_object_id,
kind: SegmentKind::Point {
position: end.clone(),
ctor: Box::new(PointCtor {
position: ctor.end.clone(),
}),
freedom: *end_freedom,
},
surface: segment.surface.clone(),
sketch_id: segment.sketch_id,
sketch: segment.sketch.clone(),
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
}
.continue_()),
SegmentKind::Circle { .. } => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in segment"),
vec![self.into()],
),
None,
)),
SegmentKind::ControlPointSpline { .. } => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in segment"),
vec![self.clone().into()],
),
None,
)),
},
},
"center" => match &segment.repr {
SegmentRepr::Unsolved { segment } => match &segment.kind {
UnsolvedSegmentKind::Arc {
center,
ctor,
center_object_id,
..
} => Ok(KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Unsolved {
segment: Box::new(UnsolvedSegment {
id: segment.id,
object_id: *center_object_id,
kind: UnsolvedSegmentKind::Point {
position: center.clone(),
ctor: Box::new(PointCtor {
position: ctor.center.clone(),
}),
},
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
}
.continue_()),
UnsolvedSegmentKind::Circle {
center,
ctor,
center_object_id,
..
} => Ok(KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Unsolved {
segment: Box::new(UnsolvedSegment {
id: segment.id,
object_id: *center_object_id,
kind: UnsolvedSegmentKind::Point {
position: center.clone(),
ctor: Box::new(PointCtor {
position: ctor.center.clone(),
}),
},
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
}
.continue_()),
_ => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in segment"),
vec![self.clone().into()],
),
None,
)),
},
SegmentRepr::Solved { segment } => match &segment.kind {
SegmentKind::Arc {
center,
ctor,
center_object_id,
center_freedom,
..
} => Ok(KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Solved {
segment: Box::new(Segment {
id: segment.id,
object_id: *center_object_id,
kind: SegmentKind::Point {
position: center.clone(),
ctor: Box::new(PointCtor {
position: ctor.center.clone(),
}),
freedom: *center_freedom,
},
surface: segment.surface.clone(),
sketch_id: segment.sketch_id,
sketch: segment.sketch.clone(),
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
}
.continue_()),
SegmentKind::Circle {
center,
ctor,
center_object_id,
center_freedom,
..
} => Ok(KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Solved {
segment: Box::new(Segment {
id: segment.id,
object_id: *center_object_id,
kind: SegmentKind::Point {
position: center.clone(),
ctor: Box::new(PointCtor {
position: ctor.center.clone(),
}),
freedom: *center_freedom,
},
surface: segment.surface.clone(),
sketch_id: segment.sketch_id,
sketch: segment.sketch.clone(),
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
}
.continue_()),
_ => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in segment"),
vec![self.clone().into()],
),
None,
)),
},
},
"controls" => match &segment.repr {
SegmentRepr::Unsolved { segment } => match &segment.kind {
UnsolvedSegmentKind::ControlPointSpline {
controls,
ctor,
control_object_ids,
..
} => Ok(KclValue::HomArray {
value: controls
.iter()
.zip(control_object_ids.iter())
.zip(ctor.points.iter())
.map(|((position, object_id), ctor_point)| KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Unsolved {
segment: Box::new(UnsolvedSegment {
id: segment.id,
object_id: *object_id,
kind: UnsolvedSegmentKind::Point {
position: position.clone(),
ctor: Box::new(PointCtor {
position: ctor_point.clone(),
}),
},
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
})
.collect(),
ty: RuntimeType::segment(),
}
.continue_()),
_ => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in segment"),
vec![self.clone().into()],
),
None,
)),
},
SegmentRepr::Solved { segment } => match &segment.kind {
SegmentKind::ControlPointSpline {
controls,
ctor,
control_object_ids,
control_freedoms,
..
} => Ok(KclValue::HomArray {
value: controls
.iter()
.zip(control_object_ids.iter())
.zip(control_freedoms.iter())
.zip(ctor.points.iter())
.map(|(((position, object_id), freedom), ctor_point)| KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Solved {
segment: Box::new(Segment {
id: segment.id,
object_id: *object_id,
kind: SegmentKind::Point {
position: position.clone(),
ctor: Box::new(PointCtor {
position: ctor_point.clone(),
}),
freedom: *freedom,
},
surface: segment.surface.clone(),
sketch_id: segment.sketch_id,
sketch: segment.sketch.clone(),
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
})
.collect(),
ty: RuntimeType::segment(),
}
.continue_()),
_ => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in segment"),
vec![self.clone().into()],
),
None,
)),
},
},
"edges" => match &segment.repr {
SegmentRepr::Unsolved { segment } => match &segment.kind {
UnsolvedSegmentKind::ControlPointSpline {
controls,
ctor,
control_object_ids,
control_polygon_edge_object_ids,
construction,
..
} => Ok(KclValue::HomArray {
value: control_polygon_edge_object_ids
.iter()
.enumerate()
.map(|(index, object_id)| KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Unsolved {
segment: Box::new(UnsolvedSegment {
id: segment.id,
object_id: *object_id,
kind: UnsolvedSegmentKind::Line {
start: controls[index].clone(),
end: controls[index + 1].clone(),
ctor: Box::new(LineCtor {
start: ctor.points[index].clone(),
end: ctor.points[index + 1].clone(),
construction: Some(*construction),
}),
start_object_id: control_object_ids[index],
end_object_id: control_object_ids[index + 1],
construction: *construction,
},
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
})
.collect(),
ty: RuntimeType::segment(),
}
.continue_()),
_ => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in segment"),
vec![self.clone().into()],
),
None,
)),
},
SegmentRepr::Solved { segment } => match &segment.kind {
SegmentKind::ControlPointSpline {
controls,
ctor,
control_object_ids,
control_polygon_edge_object_ids,
control_freedoms,
construction,
..
} => Ok(KclValue::HomArray {
value: control_polygon_edge_object_ids
.iter()
.enumerate()
.map(|(index, object_id)| KclValue::Segment {
value: Box::new(AbstractSegment {
repr: SegmentRepr::Solved {
segment: Box::new(Segment {
id: segment.id,
object_id: *object_id,
kind: SegmentKind::Line {
start: controls[index].clone(),
end: controls[index + 1].clone(),
ctor: Box::new(LineCtor {
start: ctor.points[index].clone(),
end: ctor.points[index + 1].clone(),
construction: Some(*construction),
}),
start_object_id: control_object_ids[index],
end_object_id: control_object_ids[index + 1],
start_freedom: control_freedoms[index],
end_freedom: control_freedoms[index + 1],
construction: *construction,
},
surface: segment.surface.clone(),
sketch_id: segment.sketch_id,
sketch: segment.sketch.clone(),
tag: segment.tag.clone(),
node_path: segment.node_path.clone(),
meta: segment.meta.clone(),
}),
},
meta: segment.meta.clone(),
}),
})
.collect(),
ty: RuntimeType::segment(),
}
.continue_()),
_ => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in segment"),
vec![self.clone().into()],
),
None,
)),
},
},
other => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{other}' not found in segment"),
vec![self.clone().into()],
),
None,
)),
},
(KclValue::Plane { value: plane }, Property::String(property), false) => match property.as_str() {
"zAxis" => {
let (p, u) = plane.info.z_axis.as_3_dims();
Ok(KclValue::array_from_point3d(p, NumericType::optional_length(u), vec![meta]).continue_())
}
"yAxis" => {
let (p, u) = plane.info.y_axis.as_3_dims();
Ok(KclValue::array_from_point3d(p, NumericType::optional_length(u), vec![meta]).continue_())
}
"xAxis" => {
let (p, u) = plane.info.x_axis.as_3_dims();
Ok(KclValue::array_from_point3d(p, NumericType::optional_length(u), vec![meta]).continue_())
}
"origin" => {
let (p, u) = plane.info.origin.as_3_dims();
Ok(KclValue::array_from_point3d(p, NumericType::optional_length(u), vec![meta]).continue_())
}
other => Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{other}' not found in plane"),
vec![self.clone().into()],
),
None,
)),
},
(
KclValue::Object {
value: map,
object_kind,
..
},
Property::String(property),
false,
) => {
if let Some(value) = map.get(&property) {
if object_kind
.deprecated_solid_tag_names()
.iter()
.any(|tag_name| tag_name == &property)
{
exec_state.warn(
CompilationIssue::err(
SourceRange::from(self),
format!(
"Accessing solid-created face `{property}` through sketch tags is deprecated. Use the body's faces instead, e.g. `body.faces.{property}`."
),
),
annotations::WARN_DEPRECATED,
);
}
Ok(value.to_owned().continue_())
} else {
Err(KclError::new_undefined_value(
KclErrorDetails::new(
format!("Property '{property}' not found in object"),
vec![self.clone().into()],
),
None,
))
}
}
(KclValue::Object { .. }, Property::String(property), true) => {
Err(KclError::new_semantic(KclErrorDetails::new(
format!("Cannot index object with string; use dot notation instead, e.g. `obj.{property}`"),
vec![self.clone().into()],
)))
}
(KclValue::Object { value: map, .. }, p @ Property::UInt(i), _) => {
if i == 0
&& let Some(value) = map.get("x")
{
return Ok(value.to_owned().continue_());
}
if i == 1
&& let Some(value) = map.get("y")
{
return Ok(value.to_owned().continue_());
}
if i == 2
&& let Some(value) = map.get("z")
{
return Ok(value.to_owned().continue_());
}
let t = p.type_name();
let article = article_for(t);
Err(KclError::new_semantic(KclErrorDetails::new(
format!("Only strings can be used as the property of an object, but you're using {article} {t}",),
vec![self.clone().into()],
)))
}
(KclValue::HomArray { value: arr, ty }, Property::UInt(index), _) => {
let value_of_arr = arr.get(index);
let oob_error = KclError::new_undefined_value(
KclErrorDetails::new(
format!("The array doesn't have any item at index {index}"),
vec![self.clone().into()],
),
None,
);
if let Some(value) = value_of_arr {
Ok(value.to_owned().continue_())
} else if ctx.no_engine_commands().await
&& !exec_state.is_sketch_mode_execution()
&& mock_array_may_have_engine_dependent_cardinality(&ty)
{
let value = arr.first();
value.map(|value| value.to_owned().continue_()).ok_or(oob_error)
} else {
Err(oob_error)
}
}
(obj, Property::UInt(0), _) => Ok(obj.continue_()),
(KclValue::HomArray { .. }, p, _) => {
let t = p.type_name();
let article = article_for(t);
Err(KclError::new_semantic(KclErrorDetails::new(
format!("Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",),
vec![self.clone().into()],
)))
}
(KclValue::Solid { value }, Property::String(prop), false) if prop == "sketch" => {
let Some(sketch) = value.sketch() else {
return Err(KclError::new_semantic(KclErrorDetails::new(
"This solid was created without a sketch, so `solid.sketch` is unavailable.".to_owned(),
vec![self.clone().into()],
)));
};
Ok(KclValue::Sketch {
value: Box::new(sketch.clone()),
}
.continue_())
}
(KclValue::Solid { value: solid }, Property::String(prop), false) if prop == "faces" => {
Ok(KclValue::Object {
meta: vec![Metadata {
source_range: SourceRange::from(self.clone()),
}],
value: solid
.faces
.iter()
.map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
.collect(),
constrainable: false,
object_kind: KclObjectKind::Default,
}
.continue_())
}
(geometry @ KclValue::Solid { .. }, Property::String(prop), false) if prop == "tags" => {
Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Property `{prop}` not found on {}. You can get a solid's faces through `exampleSolid.faces`, or its sketch tags through `exampleSolid.sketch.tags`.",
geometry.human_friendly_type()
),
vec![self.clone().into()],
)))
}
(KclValue::Sketch { value: sk }, Property::String(prop), false) if prop == "tags" => Ok(KclValue::Object {
meta: vec![Metadata {
source_range: SourceRange::from(self.clone()),
}],
value: sk
.tags
.iter()
.map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
.collect(),
constrainable: false,
object_kind: KclObjectKind::SketchTags {
deprecated_solid_tag_names: sk
.tags
.iter()
.filter(|(_, tag)| tag.is_body_created_tag())
.map(|(name, _)| name.to_owned())
.collect(),
},
}
.continue_()),
(geometry @ (KclValue::Sketch { .. } | KclValue::Solid { .. }), Property::String(property), false) => {
Err(KclError::new_semantic(KclErrorDetails::new(
format!("Property `{property}` not found on {}", geometry.human_friendly_type()),
vec![self.clone().into()],
)))
}
(being_indexed, _, false) => Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Only objects can have members accessed with dot notation, but you're trying to access {}",
being_indexed.human_friendly_type()
),
vec![self.clone().into()],
))),
(being_indexed, _, true) => 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()],
))),
}
}
}
impl Node<BinaryExpression> {
pub(super) async fn get_result(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
enum State {
EvaluateLeft(Node<BinaryExpression>),
FromLeft {
node: Node<BinaryExpression>,
},
EvaluateRight {
node: Node<BinaryExpression>,
left: KclValue,
},
FromRight {
node: Node<BinaryExpression>,
left: KclValue,
},
}
let mut stack = vec![State::EvaluateLeft(self.clone())];
let mut last_result: Option<KclValue> = None;
while let Some(state) = stack.pop() {
match state {
State::EvaluateLeft(node) => {
let left_part = node.left.clone();
match left_part {
BinaryPart::BinaryExpression(child) => {
stack.push(State::FromLeft { node });
stack.push(State::EvaluateLeft(child.into_node()));
}
part => {
let left_value = part.get_result(exec_state, ctx).await?;
let left_value = control_continue!(left_value);
stack.push(State::EvaluateRight { node, left: left_value });
}
}
}
State::FromLeft { node } => {
let Some(left_value) = last_result.take() else {
return Err(Self::missing_result_error(&node));
};
stack.push(State::EvaluateRight { node, left: left_value });
}
State::EvaluateRight { node, left } => {
let right_part = node.right.clone();
match right_part {
BinaryPart::BinaryExpression(child) => {
stack.push(State::FromRight { node, left });
stack.push(State::EvaluateLeft(child.into_node()));
}
part => {
let right_value = part.get_result(exec_state, ctx).await?;
let right_value = control_continue!(right_value);
let result = node.apply_operator(exec_state, ctx, left, right_value).await?;
last_result = Some(result);
}
}
}
State::FromRight { node, left } => {
let Some(right_value) = last_result.take() else {
return Err(Self::missing_result_error(&node));
};
let result = node.apply_operator(exec_state, ctx, left, right_value).await?;
last_result = Some(result);
}
}
}
last_result
.map(KclValue::continue_)
.ok_or_else(|| Self::missing_result_error(self))
}
pub(super) async fn apply_operator(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
left_value: KclValue,
right_value: KclValue,
) -> Result<KclValue, KclError> {
let mut meta = left_value.metadata();
meta.extend(right_value.metadata());
if self.operator == BinaryOperator::Add
&& let (KclValue::String { value: left, .. }, KclValue::String { value: right, .. }) =
(&left_value, &right_value)
{
return Ok(KclValue::String {
value: format!("{left}{right}"),
meta,
});
}
if self.operator == BinaryOperator::Add || self.operator == BinaryOperator::Or {
if let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value) {
let args = Args::new_no_args(
self.into(),
self.node_path.clone(),
ctx.clone(),
Some("union".to_owned()),
);
let result = crate::std::csg::inner_union(
vec![*left.clone(), *right.clone()],
Default::default(),
crate::std::csg::CsgAlgorithm::Latest,
exec_state,
args,
)
.await?;
return Ok(result.into());
}
} else if self.operator == BinaryOperator::Sub {
if let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value) {
let args = Args::new_no_args(
self.into(),
self.node_path.clone(),
ctx.clone(),
Some("subtract".to_owned()),
);
let result = crate::std::csg::inner_subtract(
vec![*left.clone()],
vec![*right.clone()],
Default::default(),
crate::std::csg::CsgAlgorithm::Latest,
exec_state,
args,
)
.await?;
return Ok(result.into());
}
} else if self.operator == BinaryOperator::And
&& let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value)
{
let args = Args::new_no_args(
self.into(),
self.node_path.clone(),
ctx.clone(),
Some("intersect".to_owned()),
);
let result = crate::std::csg::inner_intersect(
vec![*left.clone(), *right.clone()],
Default::default(),
crate::std::csg::CsgAlgorithm::Latest,
exec_state,
args,
)
.await?;
return Ok(result.into());
}
if self.operator == BinaryOperator::Or || self.operator == BinaryOperator::And {
let KclValue::Bool { value: left_value, .. } = left_value else {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Cannot apply logical operator to non-boolean value: {}",
left_value.human_friendly_type()
),
vec![self.left.clone().into()],
)));
};
let KclValue::Bool { value: right_value, .. } = right_value else {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Cannot apply logical operator to non-boolean value: {}",
right_value.human_friendly_type()
),
vec![self.right.clone().into()],
)));
};
let raw_value = match self.operator {
BinaryOperator::Or => left_value || right_value,
BinaryOperator::And => left_value && right_value,
_ => unreachable!(),
};
return Ok(KclValue::Bool { value: raw_value, meta });
}
if self.operator == BinaryOperator::Eq && exec_state.mod_local.sketch_block.is_some() {
match (&left_value, &right_value) {
(KclValue::SketchVar { value: left_value, .. }, KclValue::SketchVar { value: right_value, .. })
if left_value.id == right_value.id =>
{
return Ok(KclValue::none());
}
(KclValue::SketchVar { value: var0 }, KclValue::SketchVar { value: var1, .. }) => {
let constraint = Constraint::ScalarEqual(
var0.id.to_constraint_id(self.as_source_range())?,
var1.id.to_constraint_id(self.as_source_range())?,
);
let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
let message = "Being inside a sketch block should have already been checked above".to_owned();
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
sketch_block_state.solver_constraints.push(constraint);
return Ok(KclValue::none());
}
(KclValue::SketchVar { value: var, .. }, input_number @ KclValue::Number { .. })
| (input_number @ KclValue::Number { .. }, KclValue::SketchVar { value: var, .. }) => {
let number_value = normalize_to_solver_distance_unit(
input_number,
input_number.into(),
exec_state,
"fixed constraint value",
)?;
let Some(n) = number_value.as_ty_f64() else {
let message = format!(
"Expected number after coercion, but found {}",
number_value.human_friendly_type()
);
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
let constraint = Constraint::Fixed(var.id.to_constraint_id(self.as_source_range())?, n.n);
let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
let message = "Being inside a sketch block should have already been checked above".to_owned();
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
sketch_block_state.solver_constraints.push(constraint);
exec_state.warn_experimental("scalar fixed constraint", self.as_source_range());
return Ok(KclValue::none());
}
(KclValue::SketchConstraint { value: constraint }, input_number @ KclValue::Number { .. })
| (input_number @ KclValue::Number { .. }, KclValue::SketchConstraint { value: constraint }) => {
let number_value = match constraint.kind {
SketchConstraintKind::Angle { .. } => normalize_to_solver_angle_unit(
input_number,
input_number.into(),
exec_state,
"fixed constraint value",
)?,
SketchConstraintKind::Distance { .. }
| SketchConstraintKind::PointLineDistance { .. }
| SketchConstraintKind::LineLineDistance { .. }
| SketchConstraintKind::PointCircularDistance { .. }
| SketchConstraintKind::LineCircularDistance { .. }
| SketchConstraintKind::CircularCircularDistance { .. }
| SketchConstraintKind::Radius { .. }
| SketchConstraintKind::Diameter { .. }
| SketchConstraintKind::HorizontalDistance { .. }
| SketchConstraintKind::VerticalDistance { .. } => normalize_to_solver_distance_unit(
input_number,
input_number.into(),
exec_state,
"fixed constraint value",
)?,
};
let Some(n) = number_value.as_ty_f64() else {
let message = format!(
"Expected number after coercion, but found {}",
number_value.human_friendly_type()
);
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
let number_binary_part = if matches!(&left_value, KclValue::SketchConstraint { .. }) {
&self.right
} else {
&self.left
};
let source = {
use crate::unparser::ExprContext;
let mut buf = String::new();
number_binary_part.recast(&mut buf, &Default::default(), 0, ExprContext::Other);
crate::frontend::sketch::ConstraintSource {
expr: buf,
is_literal: matches!(number_binary_part, BinaryPart::Literal(_)),
}
};
match &constraint.kind {
SketchConstraintKind::Angle {
line0,
line1,
mode,
label_position,
} => {
let range = self.as_source_range();
let desired_angle = match n.ty {
NumericType::Known(crate::exec::UnitType::Angle(crate::exec::UnitAngle::Degrees))
| NumericType::Default {
len: _,
angle: UnitAngle::Degrees,
} => ezpz::datatypes::Angle::from_degrees(n.n),
NumericType::Known(crate::exec::UnitType::Angle(crate::exec::UnitAngle::Radians))
| NumericType::Default {
len: _,
angle: UnitAngle::Radians,
} => ezpz::datatypes::Angle::from_radians(n.n),
NumericType::Known(crate::exec::UnitType::Count)
| NumericType::Known(crate::exec::UnitType::GenericLength)
| NumericType::Known(crate::exec::UnitType::GenericAngle)
| NumericType::Known(crate::exec::UnitType::Length(_))
| NumericType::Unknown
| NumericType::Any => {
let message = format!("Expected angle but found {:?}", n);
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
}
};
let angle_lowering = match *mode {
AngleConstraintMode::LinesAtAngle => {
AngleConstraintLowering::LinesAtAngle(Box::new(PendingLegacyAngleRefactorMeta {
source_range: constraint
.meta
.first()
.map(|meta| meta.source_range)
.unwrap_or(range),
lines: [line0.clone(), line1.clone()],
desired_angle_radians: desired_angle.to_radians(),
}))
}
AngleConstraintMode::PointsAtAngle { sector, inverse } => {
let sketch_vars = exec_state
.mod_local
.sketch_block
.as_ref()
.ok_or_else(|| {
internal_err(
"Being inside a sketch block should have already been checked above",
self,
)
})?
.sketch_vars
.clone();
let initial_line0 = constrainable_line_initial_positions(
&sketch_vars,
line0,
exec_state,
range,
"angle line0",
)?;
let initial_line1 = constrainable_line_initial_positions(
&sketch_vars,
line1,
exec_state,
range,
"angle line1",
)?;
let Some(initial_vertex) = intersect_lines_2d(initial_line0, initial_line1) else {
return Err(KclError::new_semantic(KclErrorDetails::new(
"angleDimension(lines = ..., sector = ...) requires non-parallel lines"
.to_owned(),
vec![range],
)));
};
let (line0_representative, line0_direction) =
representative_angle_endpoint(line0, initial_line0, initial_vertex, range)?;
let (line1_representative, line1_direction) =
representative_angle_endpoint(line1, initial_line1, initial_vertex, range)?;
let sector_rays = angle_sector_rays(sector, inverse);
let angle_kind =
ezpz::datatypes::AngleKind::Other(remap_angle_for_representative_rays(
sector_rays,
[line0_direction, line1_direction],
desired_angle,
));
AngleConstraintLowering::PointsAtAngle(PointsAtAngleLineData {
initial_vertex,
representative_points: [line0_representative, line1_representative],
angle_kind,
})
}
};
let sketch_var_ty = solver_numeric_type(exec_state);
let constraint_id = exec_state.next_object_id();
let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
let message =
"Being inside a sketch block should have already been checked above".to_owned();
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
match angle_lowering {
AngleConstraintLowering::LinesAtAngle(refactor_meta) => {
sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
datum_line_from_constrainable(line0, range)?,
datum_line_from_constrainable(line1, range)?,
ezpz::datatypes::AngleKind::Other(desired_angle),
));
sketch_block_state
.pending_legacy_angle_refactor_metadata
.push(*refactor_meta);
}
AngleConstraintLowering::PointsAtAngle(points_at_angle_data) => {
push_points_at_angle_for_lines(
sketch_block_state,
sketch_var_ty,
[line0, line1],
points_at_angle_data,
range,
)?
}
}
use crate::execution::Artifact;
use crate::execution::CodeRef;
use crate::execution::SketchBlockConstraint;
use crate::front::Angle;
use crate::front::SourceRef;
let Some(sketch_id) = sketch_block_state.sketch_id else {
let message = "Sketch id missing for constraint artifact".to_owned();
debug_assert!(false, "{}", &message);
return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
};
let (sector, inverse) = match *mode {
AngleConstraintMode::LinesAtAngle => (None, None),
AngleConstraintMode::PointsAtAngle { sector, inverse } => {
(Some(front_angle_sector(sector)), Some(inverse))
}
};
let sketch_constraint = crate::front::Constraint::Angle(Angle {
lines: vec![line0.object_id, line1.object_id],
angle: n.try_into().map_err(|_| {
internal_err("Failed to convert angle units numeric suffix:", range)
})?,
sector,
inverse,
label_position: label_position.clone(),
source,
});
sketch_block_state.sketch_constraints.push(constraint_id);
let artifact_id = exec_state.next_artifact_id();
exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
id: artifact_id,
sketch_id,
constraint_id,
constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
code_ref: CodeRef::placeholder(range),
}));
exec_state.add_scene_object(
Object {
id: constraint_id,
kind: ObjectKind::Constraint {
constraint: sketch_constraint,
},
label: Default::default(),
comments: Default::default(),
artifact_id,
source: SourceRef::new(range, self.node_path.clone()),
},
range,
);
}
SketchConstraintKind::Distance { points, label_position } => {
let range = self.as_source_range();
let p0 = &points[0];
let p1 = &points[1];
let sketch_var_ty = solver_numeric_type(exec_state);
let constraint_id = exec_state.next_object_id();
let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
let message =
"Being inside a sketch block should have already been checked above".to_owned();
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
match (p0, p1) {
(
crate::execution::ConstrainablePoint2dOrOrigin::Point(p0),
crate::execution::ConstrainablePoint2dOrOrigin::Point(p1),
) => {
let solver_pt0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
p0.vars.x.to_constraint_id(range)?,
p0.vars.y.to_constraint_id(range)?,
);
let solver_pt1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
p1.vars.x.to_constraint_id(range)?,
p1.vars.y.to_constraint_id(range)?,
);
sketch_block_state
.solver_constraints
.push(Constraint::Distance(solver_pt0, solver_pt1, n.n));
}
(
crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
crate::execution::ConstrainablePoint2dOrOrigin::Origin,
)
| (
crate::execution::ConstrainablePoint2dOrOrigin::Origin,
crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
) => {
let origin_x_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: origin_x_id,
initial_value: 0.0,
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let origin_y_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: origin_y_id,
initial_value: 0.0,
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let origin_x = origin_x_id.to_constraint_id(range)?;
let origin_y = origin_y_id.to_constraint_id(range)?;
sketch_block_state
.solver_constraints
.push(Constraint::Fixed(origin_x, 0.0));
sketch_block_state
.solver_constraints
.push(Constraint::Fixed(origin_y, 0.0));
let solver_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
point.vars.x.to_constraint_id(range)?,
point.vars.y.to_constraint_id(range)?,
);
let origin_point = ezpz::datatypes::inputs::DatumPoint::new_xy(origin_x, origin_y);
sketch_block_state.solver_constraints.push(Constraint::Distance(
solver_point,
origin_point,
n.n,
));
}
(
crate::execution::ConstrainablePoint2dOrOrigin::Origin,
crate::execution::ConstrainablePoint2dOrOrigin::Origin,
) => {
return Err(internal_err(
"distance() cannot constrain ORIGIN against ORIGIN".to_owned(),
range,
));
}
}
use crate::execution::Artifact;
use crate::execution::CodeRef;
use crate::execution::SketchBlockConstraint;
use crate::front::Distance;
use crate::front::SourceRef;
use crate::frontend::sketch::ConstraintSegment;
let Some(sketch_id) = sketch_block_state.sketch_id else {
let message = "Sketch id missing for constraint artifact".to_owned();
debug_assert!(false, "{}", &message);
return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
};
let sketch_constraint = crate::front::Constraint::Distance(Distance {
segments: vec![
match p0 {
crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
ConstraintSegment::from(point.object_id)
}
crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
ConstraintSegment::ORIGIN
}
},
match p1 {
crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
ConstraintSegment::from(point.object_id)
}
crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
ConstraintSegment::ORIGIN
}
},
],
distance: n.try_into().map_err(|_| {
internal_err("Failed to convert distance units numeric suffix:", range)
})?,
label_position: label_position.clone(),
source,
});
sketch_block_state.sketch_constraints.push(constraint_id);
let artifact_id = exec_state.next_artifact_id();
exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
id: artifact_id,
sketch_id,
constraint_id,
constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
code_ref: CodeRef::placeholder(range),
}));
exec_state.add_scene_object(
Object {
id: constraint_id,
kind: ObjectKind::Constraint {
constraint: sketch_constraint,
},
label: Default::default(),
comments: Default::default(),
artifact_id,
source: SourceRef::new(range, self.node_path.clone()),
},
range,
);
}
SketchConstraintKind::PointLineDistance {
point,
line,
input_object_ids,
label_position,
} => {
let range = self.as_source_range();
let sketch_var_ty = solver_numeric_type(exec_state);
let sketch_vars = exec_state
.mod_local
.sketch_block
.as_ref()
.ok_or_else(|| {
internal_err(
"Being inside a sketch block should have already been checked above",
self,
)
})?
.sketch_vars
.clone();
let support_initial =
projected_point_on_line_initial_position(&sketch_vars, point, line, exec_state, range)?;
let solver_line = datum_line_from_constrainable(line, range)?;
let constraint_id = exec_state.next_object_id();
let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
let message =
"Being inside a sketch block should have already been checked above".to_owned();
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
let solver_point = datum_point_from_constrainable_or_origin(
sketch_block_state,
sketch_var_ty,
point,
range,
)?;
let support_x_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: support_x_id,
initial_value: support_initial[0],
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let support_y_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: support_y_id,
initial_value: support_initial[1],
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let support_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
support_x_id.to_constraint_id(range)?,
support_y_id.to_constraint_id(range)?,
);
let support_line =
ezpz::datatypes::inputs::DatumLineSegment::new(solver_point, support_point);
sketch_block_state
.solver_constraints
.push(Constraint::PointLineDistance(support_point, solver_line, 0.0));
sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
support_line,
solver_line,
ezpz::datatypes::AngleKind::Perpendicular,
));
sketch_block_state.solver_constraints.push(Constraint::Distance(
solver_point,
support_point,
n.n,
));
use crate::execution::Artifact;
use crate::execution::CodeRef;
use crate::execution::SketchBlockConstraint;
use crate::front::Distance;
use crate::front::SourceRef;
use crate::frontend::sketch::ConstraintSegment;
let Some(sketch_id) = sketch_block_state.sketch_id else {
let message = "Sketch id missing for constraint artifact".to_owned();
debug_assert!(false, "{}", &message);
return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
};
let sketch_constraint = crate::front::Constraint::Distance(Distance {
segments: input_object_ids
.iter()
.copied()
.map(|id| id.map_or(ConstraintSegment::ORIGIN, ConstraintSegment::from))
.collect(),
distance: n.try_into().map_err(|_| {
internal_err("Failed to convert distance units numeric suffix:", range)
})?,
label_position: label_position.clone(),
source,
});
sketch_block_state.sketch_constraints.push(constraint_id);
let artifact_id = exec_state.next_artifact_id();
exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
id: artifact_id,
sketch_id,
constraint_id,
constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
code_ref: CodeRef::placeholder(range),
}));
exec_state.add_scene_object(
Object {
id: constraint_id,
kind: ObjectKind::Constraint {
constraint: sketch_constraint,
},
label: Default::default(),
comments: Default::default(),
artifact_id,
source: SourceRef::new(range, self.node_path.clone()),
},
range,
);
}
SketchConstraintKind::LineLineDistance {
line0,
line1,
input_object_ids,
label_position,
} => {
let range = self.as_source_range();
let reference_point = crate::execution::ConstrainablePoint2d {
vars: line0.vars[0].clone(),
object_id: line0.object_id,
};
let sketch_var_ty = solver_numeric_type(exec_state);
let sketch_vars = exec_state
.mod_local
.sketch_block
.as_ref()
.ok_or_else(|| {
internal_err(
"Being inside a sketch block should have already been checked above",
self,
)
})?
.sketch_vars
.clone();
let support_initial = projected_point_on_line_initial_position(
&sketch_vars,
&crate::execution::ConstrainablePoint2dOrOrigin::Point(reference_point.clone()),
line1,
exec_state,
range,
)?;
let solver_point = datum_point_from_constrainable(&reference_point, range)?;
let solver_line0 = datum_line_from_constrainable(line0, range)?;
let solver_line1 = datum_line_from_constrainable(line1, range)?;
let constraint_id = exec_state.next_object_id();
let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
let message =
"Being inside a sketch block should have already been checked above".to_owned();
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
let support_x_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: support_x_id,
initial_value: support_initial[0],
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let support_y_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: support_y_id,
initial_value: support_initial[1],
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let support_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
support_x_id.to_constraint_id(range)?,
support_y_id.to_constraint_id(range)?,
);
let support_line =
ezpz::datatypes::inputs::DatumLineSegment::new(solver_point, support_point);
sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
solver_line0,
solver_line1,
ezpz::datatypes::AngleKind::Parallel,
));
sketch_block_state
.solver_constraints
.push(Constraint::PointLineDistance(support_point, solver_line1, 0.0));
sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
support_line,
solver_line1,
ezpz::datatypes::AngleKind::Perpendicular,
));
sketch_block_state.solver_constraints.push(Constraint::Distance(
solver_point,
support_point,
n.n,
));
use crate::execution::Artifact;
use crate::execution::CodeRef;
use crate::execution::SketchBlockConstraint;
use crate::front::Distance;
use crate::front::SourceRef;
use crate::frontend::sketch::ConstraintSegment;
let Some(sketch_id) = sketch_block_state.sketch_id else {
let message = "Sketch id missing for constraint artifact".to_owned();
debug_assert!(false, "{}", &message);
return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
};
let sketch_constraint = crate::front::Constraint::Distance(Distance {
segments: input_object_ids.iter().copied().map(ConstraintSegment::from).collect(),
distance: n.try_into().map_err(|_| {
internal_err("Failed to convert distance units numeric suffix:", range)
})?,
label_position: label_position.clone(),
source,
});
sketch_block_state.sketch_constraints.push(constraint_id);
let artifact_id = exec_state.next_artifact_id();
exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
id: artifact_id,
sketch_id,
constraint_id,
constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
code_ref: CodeRef::placeholder(range),
}));
exec_state.add_scene_object(
Object {
id: constraint_id,
kind: ObjectKind::Constraint {
constraint: sketch_constraint,
},
label: Default::default(),
comments: Default::default(),
artifact_id,
source: SourceRef::new(range, self.node_path.clone()),
},
range,
);
}
SketchConstraintKind::PointCircularDistance {
point,
center,
start,
end,
input_object_ids,
label_position,
} => {
let range = self.as_source_range();
let sketch_var_ty = solver_numeric_type(exec_state);
let sketch_vars = exec_state
.mod_local
.sketch_block
.as_ref()
.ok_or_else(|| {
internal_err(
"Being inside a sketch block should have already been checked above",
self,
)
})?
.sketch_vars
.clone();
let circular =
circular_distance_datums(&sketch_vars, center, start, end.as_ref(), exec_state, range)?;
let constraint_id = exec_state.next_object_id();
let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
let message =
"Being inside a sketch block should have already been checked above".to_owned();
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
let target_point = datum_point_from_constrainable_or_origin(
sketch_block_state,
sketch_var_ty,
point,
range,
)?;
push_circular_distance_constraints(
sketch_block_state,
sketch_var_ty,
target_point,
circular,
n.n,
range,
)?;
use crate::execution::Artifact;
use crate::execution::CodeRef;
use crate::execution::SketchBlockConstraint;
use crate::front::Distance;
use crate::front::SourceRef;
use crate::frontend::sketch::ConstraintSegment;
let Some(sketch_id) = sketch_block_state.sketch_id else {
let message = "Sketch id missing for constraint artifact".to_owned();
debug_assert!(false, "{}", &message);
return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
};
let sketch_constraint = crate::front::Constraint::Distance(Distance {
segments: input_object_ids
.iter()
.copied()
.map(|id| id.map_or(ConstraintSegment::ORIGIN, ConstraintSegment::from))
.collect(),
distance: n.try_into().map_err(|_| {
internal_err("Failed to convert distance units numeric suffix:", range)
})?,
label_position: label_position.clone(),
source,
});
sketch_block_state.sketch_constraints.push(constraint_id);
let artifact_id = exec_state.next_artifact_id();
exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
id: artifact_id,
sketch_id,
constraint_id,
constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
code_ref: CodeRef::placeholder(range),
}));
exec_state.add_scene_object(
Object {
id: constraint_id,
kind: ObjectKind::Constraint {
constraint: sketch_constraint,
},
label: Default::default(),
comments: Default::default(),
artifact_id,
source: SourceRef::new(range, self.node_path.clone()),
},
range,
);
}
SketchConstraintKind::LineCircularDistance {
line,
center,
start,
end,
input_object_ids,
label_position,
} => {
let range = self.as_source_range();
let sketch_var_ty = solver_numeric_type(exec_state);
let sketch_vars = exec_state
.mod_local
.sketch_block
.as_ref()
.ok_or_else(|| {
internal_err(
"Being inside a sketch block should have already been checked above",
self,
)
})?
.sketch_vars
.clone();
let support_initial = projected_point_on_line_initial_position(
&sketch_vars,
&crate::execution::ConstrainablePoint2dOrOrigin::Point(center.clone()),
line,
exec_state,
range,
)?;
let solver_line = datum_line_from_constrainable(line, range)?;
let circular =
circular_distance_datums(&sketch_vars, center, start, end.as_ref(), exec_state, range)?;
let constraint_id = exec_state.next_object_id();
let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
let message =
"Being inside a sketch block should have already been checked above".to_owned();
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
let support_x_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: support_x_id,
initial_value: support_initial[0],
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let support_y_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: support_y_id,
initial_value: support_initial[1],
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let support_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
support_x_id.to_constraint_id(range)?,
support_y_id.to_constraint_id(range)?,
);
let support_line =
ezpz::datatypes::inputs::DatumLineSegment::new(circular.center, support_point);
sketch_block_state
.solver_constraints
.push(Constraint::PointLineDistance(support_point, solver_line, 0.0));
sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
support_line,
solver_line,
ezpz::datatypes::AngleKind::Perpendicular,
));
push_circular_distance_constraints(
sketch_block_state,
sketch_var_ty,
support_point,
circular,
n.n,
range,
)?;
use crate::execution::Artifact;
use crate::execution::CodeRef;
use crate::execution::SketchBlockConstraint;
use crate::front::Distance;
use crate::front::SourceRef;
use crate::frontend::sketch::ConstraintSegment;
let Some(sketch_id) = sketch_block_state.sketch_id else {
let message = "Sketch id missing for constraint artifact".to_owned();
debug_assert!(false, "{}", &message);
return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
};
let sketch_constraint = crate::front::Constraint::Distance(Distance {
segments: input_object_ids.iter().copied().map(ConstraintSegment::from).collect(),
distance: n.try_into().map_err(|_| {
internal_err("Failed to convert distance units numeric suffix:", range)
})?,
label_position: label_position.clone(),
source,
});
sketch_block_state.sketch_constraints.push(constraint_id);
let artifact_id = exec_state.next_artifact_id();
exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
id: artifact_id,
sketch_id,
constraint_id,
constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
code_ref: CodeRef::placeholder(range),
}));
exec_state.add_scene_object(
Object {
id: constraint_id,
kind: ObjectKind::Constraint {
constraint: sketch_constraint,
},
label: Default::default(),
comments: Default::default(),
artifact_id,
source: SourceRef::new(range, self.node_path.clone()),
},
range,
);
}
SketchConstraintKind::CircularCircularDistance {
center0,
start0,
end0,
center1,
start1,
end1,
input_object_ids,
label_position,
} => {
let range = self.as_source_range();
let sketch_var_ty = solver_numeric_type(exec_state);
let sketch_vars = exec_state
.mod_local
.sketch_block
.as_ref()
.ok_or_else(|| {
internal_err(
"Being inside a sketch block should have already been checked above",
self,
)
})?
.sketch_vars
.clone();
let circular0 = circular_distance_datums(
&sketch_vars,
center0,
start0,
end0.as_ref(),
exec_state,
range,
)?;
let circular1 = circular_distance_datums(
&sketch_vars,
center1,
start1,
end1.as_ref(),
exec_state,
range,
)?;
let support_initial = circular_circular_support_initial_position(
&sketch_vars,
center0,
center1,
circular0.radius_initial_value,
n.n,
exec_state,
range,
)?;
let constraint_id = exec_state.next_object_id();
let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
let message =
"Being inside a sketch block should have already been checked above".to_owned();
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
let circular_target0 =
push_circular_radius_constraints(sketch_block_state, sketch_var_ty, circular0, range)?;
let circular_target1 =
push_circular_radius_constraints(sketch_block_state, sketch_var_ty, circular1, range)?;
let support_x_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: support_x_id,
initial_value: support_initial[0],
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let support_y_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: support_y_id,
initial_value: support_initial[1],
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let support_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
support_x_id.to_constraint_id(range)?,
support_y_id.to_constraint_id(range)?,
);
let support_radius_id = sketch_block_state.next_sketch_var_id();
let support_radius_value = n.n / 2.0;
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: support_radius_id,
initial_value: support_radius_value,
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let support_radius =
ezpz::datatypes::inputs::DatumDistance::new(support_radius_id.to_constraint_id(range)?);
let support_circle = ezpz::datatypes::inputs::DatumCircle {
center: support_point,
radius: support_radius,
};
let center_line = ezpz::datatypes::inputs::DatumLineSegment::new(
circular_target0.center,
circular_target1.center,
);
sketch_block_state
.solver_constraints
.push(Constraint::Fixed(support_radius.id, support_radius_value));
sketch_block_state
.solver_constraints
.push(Constraint::PointLineDistance(support_point, center_line, 0.0));
sketch_block_state
.solver_constraints
.push(Constraint::CircleTangentToCircle(
circular_target0,
support_circle,
ezpz::CircleSide::Exterior,
));
sketch_block_state
.solver_constraints
.push(Constraint::CircleTangentToCircle(
support_circle,
circular_target1,
ezpz::CircleSide::Exterior,
));
use crate::execution::Artifact;
use crate::execution::CodeRef;
use crate::execution::SketchBlockConstraint;
use crate::front::Distance;
use crate::front::SourceRef;
use crate::frontend::sketch::ConstraintSegment;
let Some(sketch_id) = sketch_block_state.sketch_id else {
let message = "Sketch id missing for constraint artifact".to_owned();
debug_assert!(false, "{}", &message);
return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
};
let sketch_constraint = crate::front::Constraint::Distance(Distance {
segments: input_object_ids.iter().copied().map(ConstraintSegment::from).collect(),
distance: n.try_into().map_err(|_| {
internal_err("Failed to convert distance units numeric suffix:", range)
})?,
label_position: label_position.clone(),
source,
});
sketch_block_state.sketch_constraints.push(constraint_id);
let artifact_id = exec_state.next_artifact_id();
exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
id: artifact_id,
sketch_id,
constraint_id,
constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
code_ref: CodeRef::placeholder(range),
}));
exec_state.add_scene_object(
Object {
id: constraint_id,
kind: ObjectKind::Constraint {
constraint: sketch_constraint,
},
label: Default::default(),
comments: Default::default(),
artifact_id,
source: SourceRef::new(range, self.node_path.clone()),
},
range,
);
}
SketchConstraintKind::Radius { .. } | SketchConstraintKind::Diameter { .. } => {
#[derive(Clone, Copy)]
enum CircularSegmentConstraintTarget {
Arc {
object_id: ObjectId,
end: [crate::execution::SketchVarId; 2],
direction: ArcDirection,
},
Circle {
object_id: ObjectId,
},
}
fn sketch_var_initial_value(
sketch_vars: &[KclValue],
id: crate::execution::SketchVarId,
exec_state: &mut ExecState,
range: SourceRange,
) -> Result<f64, KclError> {
sketch_vars
.get(id.0)
.and_then(KclValue::as_sketch_var)
.map(|sketch_var| {
sketch_var
.initial_value_to_solver_units(
exec_state,
range,
"circle radius initial value",
)
.map(|value| value.n)
})
.transpose()?
.ok_or_else(|| {
internal_err(
format!("Missing sketch variable initial value for id {}", id.0),
range,
)
})
}
let (points, label_position) = match &constraint.kind {
SketchConstraintKind::Radius { points, label_position } => {
(points, label_position.clone())
}
SketchConstraintKind::Diameter { points, label_position } => {
(points, label_position.clone())
}
_ => unreachable!(),
};
let range = self.as_source_range();
let center = &points[0];
let start = &points[1];
let Some(sketch_block_state) = &exec_state.mod_local.sketch_block else {
return Err(internal_err(
"Being inside a sketch block should have already been checked above",
self,
));
};
let (constraint_name, is_diameter) = match &constraint.kind {
SketchConstraintKind::Radius { .. } => ("radius", false),
SketchConstraintKind::Diameter { .. } => ("diameter", true),
_ => unreachable!(),
};
let sketch_vars = sketch_block_state.sketch_vars.clone();
let target_segment = sketch_block_state
.needed_by_engine
.iter()
.find_map(|seg| match &seg.kind {
UnsolvedSegmentKind::Arc {
center_object_id,
start_object_id,
end,
direction,
..
} if *center_object_id == center.object_id
&& *start_object_id == start.object_id =>
{
let (end_x_var, end_y_var) = match (&end[0], &end[1]) {
(UnsolvedExpr::Unknown(end_x), UnsolvedExpr::Unknown(end_y)) => {
(*end_x, *end_y)
}
_ => return None,
};
Some(CircularSegmentConstraintTarget::Arc {
object_id: seg.object_id,
end: [end_x_var, end_y_var],
direction: *direction,
})
}
UnsolvedSegmentKind::Circle {
center_object_id,
start_object_id,
..
} if *center_object_id == center.object_id
&& *start_object_id == start.object_id =>
{
Some(CircularSegmentConstraintTarget::Circle {
object_id: seg.object_id,
})
}
_ => None,
})
.ok_or_else(|| {
internal_err(
format!("Could not find circular segment for {} constraint", constraint_name),
range,
)
})?;
let radius_value = if is_diameter { n.n / 2.0 } else { n.n };
let center_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
center.vars.x.to_constraint_id(range)?,
center.vars.y.to_constraint_id(range)?,
);
let start_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
start.vars.x.to_constraint_id(range)?,
start.vars.y.to_constraint_id(range)?,
);
let solver_constraint = match target_segment {
CircularSegmentConstraintTarget::Arc { end, direction, .. } => {
let solver_arc = SolverArc::new(
[center.vars.x, center.vars.y],
[start.vars.x, start.vars.y],
end,
direction,
range,
)?;
solver_arc.radius_constraint(radius_value)
}
CircularSegmentConstraintTarget::Circle { .. } => {
let sketch_var_ty = solver_numeric_type(exec_state);
let start_x =
sketch_var_initial_value(&sketch_vars, start.vars.x, exec_state, range)?;
let start_y =
sketch_var_initial_value(&sketch_vars, start.vars.y, exec_state, range)?;
let center_x =
sketch_var_initial_value(&sketch_vars, center.vars.x, exec_state, range)?;
let center_y =
sketch_var_initial_value(&sketch_vars, center.vars.y, exec_state, range)?;
let radius_initial_value = libm::hypot(start_x - center_x, start_y - center_y);
let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
let message =
"Being inside a sketch block should have already been checked above"
.to_owned();
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
let radius_id = sketch_block_state.next_sketch_var_id();
sketch_block_state.sketch_vars.push(KclValue::SketchVar {
value: Box::new(crate::execution::SketchVar {
id: radius_id,
initial_value: radius_initial_value,
ty: sketch_var_ty,
node_path: None,
meta: vec![],
}),
});
let radius =
ezpz::datatypes::inputs::DatumDistance::new(radius_id.to_constraint_id(range)?);
let solver_circle = ezpz::datatypes::inputs::DatumCircle {
center: center_point,
radius,
};
sketch_block_state.solver_constraints.push(Constraint::DistanceVar(
start_point,
center_point,
radius,
));
Constraint::CircleRadius(solver_circle, radius_value)
}
};
let constraint_id = exec_state.next_object_id();
let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
let message =
"Being inside a sketch block should have already been checked above".to_owned();
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
sketch_block_state.solver_constraints.push(solver_constraint);
use crate::execution::Artifact;
use crate::execution::CodeRef;
use crate::execution::SketchBlockConstraint;
use crate::front::SourceRef;
let segment_object_id = match target_segment {
CircularSegmentConstraintTarget::Arc { object_id, .. }
| CircularSegmentConstraintTarget::Circle { object_id } => object_id,
};
let constraint = if is_diameter {
use crate::frontend::sketch::Diameter;
crate::front::Constraint::Diameter(Diameter {
arc: segment_object_id,
diameter: n.try_into().map_err(|_| {
internal_err("Failed to convert diameter units numeric suffix:", range)
})?,
label_position,
source,
})
} else {
use crate::frontend::sketch::Radius;
crate::front::Constraint::Radius(Radius {
arc: segment_object_id,
radius: n.try_into().map_err(|_| {
internal_err("Failed to convert radius units numeric suffix:", range)
})?,
label_position,
source,
})
};
sketch_block_state.sketch_constraints.push(constraint_id);
let Some(sketch_id) = sketch_block_state.sketch_id else {
let message = "Sketch id missing for constraint artifact".to_owned();
debug_assert!(false, "{}", &message);
return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
};
let artifact_id = exec_state.next_artifact_id();
exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
id: artifact_id,
sketch_id,
constraint_id,
constraint_type: super::artifact::sketch_block_constraint_type(&constraint),
code_ref: CodeRef::placeholder(range),
}));
exec_state.add_scene_object(
Object {
id: constraint_id,
kind: ObjectKind::Constraint { constraint },
label: Default::default(),
comments: Default::default(),
artifact_id,
source: SourceRef::new(range, self.node_path.clone()),
},
range,
);
}
SketchConstraintKind::HorizontalDistance { points, label_position } => {
let range = self.as_source_range();
let p0 = &points[0];
let p1 = &points[1];
let constraint_id = exec_state.next_object_id();
let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
let message =
"Being inside a sketch block should have already been checked above".to_owned();
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
match (p0, p1) {
(
crate::execution::ConstrainablePoint2dOrOrigin::Point(p0),
crate::execution::ConstrainablePoint2dOrOrigin::Point(p1),
) => {
let solver_pt0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
p0.vars.x.to_constraint_id(range)?,
p0.vars.y.to_constraint_id(range)?,
);
let solver_pt1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
p1.vars.x.to_constraint_id(range)?,
p1.vars.y.to_constraint_id(range)?,
);
sketch_block_state
.solver_constraints
.push(ezpz::Constraint::HorizontalDistance(solver_pt1, solver_pt0, n.n));
}
(
crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
crate::execution::ConstrainablePoint2dOrOrigin::Origin,
) => {
sketch_block_state
.solver_constraints
.push(ezpz::Constraint::Fixed(point.vars.x.to_constraint_id(range)?, -n.n));
}
(
crate::execution::ConstrainablePoint2dOrOrigin::Origin,
crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
) => {
sketch_block_state
.solver_constraints
.push(ezpz::Constraint::Fixed(point.vars.x.to_constraint_id(range)?, n.n));
}
(
crate::execution::ConstrainablePoint2dOrOrigin::Origin,
crate::execution::ConstrainablePoint2dOrOrigin::Origin,
) => {
return Err(internal_err(
"horizontalDistance() cannot constrain ORIGIN against ORIGIN".to_owned(),
range,
));
}
}
use crate::execution::Artifact;
use crate::execution::CodeRef;
use crate::execution::SketchBlockConstraint;
use crate::front::Distance;
use crate::front::SourceRef;
use crate::frontend::sketch::ConstraintSegment;
let constraint = crate::front::Constraint::HorizontalDistance(Distance {
segments: vec![
match p0 {
crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
ConstraintSegment::from(point.object_id)
}
crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
ConstraintSegment::ORIGIN
}
},
match p1 {
crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
ConstraintSegment::from(point.object_id)
}
crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
ConstraintSegment::ORIGIN
}
},
],
distance: n.try_into().map_err(|_| {
internal_err("Failed to convert distance units numeric suffix:", range)
})?,
label_position: label_position.clone(),
source,
});
sketch_block_state.sketch_constraints.push(constraint_id);
let Some(sketch_id) = sketch_block_state.sketch_id else {
let message = "Sketch id missing for constraint artifact".to_owned();
debug_assert!(false, "{}", &message);
return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
};
let artifact_id = exec_state.next_artifact_id();
exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
id: artifact_id,
sketch_id,
constraint_id,
constraint_type: super::artifact::sketch_block_constraint_type(&constraint),
code_ref: CodeRef::placeholder(range),
}));
exec_state.add_scene_object(
Object {
id: constraint_id,
kind: ObjectKind::Constraint { constraint },
label: Default::default(),
comments: Default::default(),
artifact_id,
source: SourceRef::new(range, self.node_path.clone()),
},
range,
);
}
SketchConstraintKind::VerticalDistance { points, label_position } => {
let range = self.as_source_range();
let p0 = &points[0];
let p1 = &points[1];
let constraint_id = exec_state.next_object_id();
let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
let message =
"Being inside a sketch block should have already been checked above".to_owned();
debug_assert!(false, "{}", &message);
return Err(internal_err(message, self));
};
match (p0, p1) {
(
crate::execution::ConstrainablePoint2dOrOrigin::Point(p0),
crate::execution::ConstrainablePoint2dOrOrigin::Point(p1),
) => {
let solver_pt0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
p0.vars.x.to_constraint_id(range)?,
p0.vars.y.to_constraint_id(range)?,
);
let solver_pt1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
p1.vars.x.to_constraint_id(range)?,
p1.vars.y.to_constraint_id(range)?,
);
sketch_block_state
.solver_constraints
.push(ezpz::Constraint::VerticalDistance(solver_pt1, solver_pt0, n.n));
}
(
crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
crate::execution::ConstrainablePoint2dOrOrigin::Origin,
) => {
sketch_block_state
.solver_constraints
.push(ezpz::Constraint::Fixed(point.vars.y.to_constraint_id(range)?, -n.n));
}
(
crate::execution::ConstrainablePoint2dOrOrigin::Origin,
crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
) => {
sketch_block_state
.solver_constraints
.push(ezpz::Constraint::Fixed(point.vars.y.to_constraint_id(range)?, n.n));
}
(
crate::execution::ConstrainablePoint2dOrOrigin::Origin,
crate::execution::ConstrainablePoint2dOrOrigin::Origin,
) => {
return Err(internal_err(
"verticalDistance() cannot constrain ORIGIN against ORIGIN".to_owned(),
range,
));
}
}
use crate::execution::Artifact;
use crate::execution::CodeRef;
use crate::execution::SketchBlockConstraint;
use crate::front::Distance;
use crate::front::SourceRef;
use crate::frontend::sketch::ConstraintSegment;
let constraint = crate::front::Constraint::VerticalDistance(Distance {
segments: vec![
match p0 {
crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
ConstraintSegment::from(point.object_id)
}
crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
ConstraintSegment::ORIGIN
}
},
match p1 {
crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
ConstraintSegment::from(point.object_id)
}
crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
ConstraintSegment::ORIGIN
}
},
],
distance: n.try_into().map_err(|_| {
internal_err("Failed to convert distance units numeric suffix:", range)
})?,
label_position: label_position.clone(),
source,
});
sketch_block_state.sketch_constraints.push(constraint_id);
let Some(sketch_id) = sketch_block_state.sketch_id else {
let message = "Sketch id missing for constraint artifact".to_owned();
debug_assert!(false, "{}", &message);
return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
};
let artifact_id = exec_state.next_artifact_id();
exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
id: artifact_id,
sketch_id,
constraint_id,
constraint_type: super::artifact::sketch_block_constraint_type(&constraint),
code_ref: CodeRef::placeholder(range),
}));
exec_state.add_scene_object(
Object {
id: constraint_id,
kind: ObjectKind::Constraint { constraint },
label: Default::default(),
comments: Default::default(),
artifact_id,
source: SourceRef::new(range, self.node_path.clone()),
},
range,
);
}
}
return Ok(KclValue::none());
}
_ => {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Cannot create an equivalence constraint between values of these types: {} and {}",
left_value.human_friendly_type(),
right_value.human_friendly_type()
),
vec![self.into()],
)));
}
}
}
if matches!(self.operator, BinaryOperator::Eq | BinaryOperator::Neq)
&& let (KclValue::String { value: left, .. }, KclValue::String { value: right, .. }) =
(&left_value, &right_value)
{
let is_equal = left == right;
let value = if self.operator == BinaryOperator::Eq {
is_equal
} else {
!is_equal
};
return Ok(KclValue::Bool { value, meta });
}
if matches!(self.operator, BinaryOperator::Eq | BinaryOperator::Neq) {
match (&left_value, &right_value) {
(KclValue::Enum { value: left }, KclValue::Enum { value: right }) => {
if left.enum_id() != right.enum_id() {
return Err(different_enums_err(left, right, self.as_source_range()));
}
let is_equal = left.variant() == right.variant();
let value = if self.operator == BinaryOperator::Eq {
is_equal
} else {
!is_equal
};
return Ok(KclValue::Bool { value, meta });
}
(KclValue::Enum { value }, other) | (other, KclValue::Enum { value }) => {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Cannot compare enum `{}` with {}.",
value.qualified_name(),
other.human_friendly_type()
),
vec![self.as_source_range()],
)));
}
_ => {}
}
}
let left = number_as_f64(&left_value, self.left.clone().into())?;
let right = number_as_f64(&right_value, self.right.clone().into())?;
let value = match self.operator {
BinaryOperator::Add => {
let (l, r, ty) = NumericType::combine_eq_coerce(left, right, None);
self.warn_on_unknown(&ty, "Adding", exec_state);
KclValue::Number { value: l + r, meta, ty }
}
BinaryOperator::Sub => {
let (l, r, ty) = NumericType::combine_eq_coerce(left, right, None);
self.warn_on_unknown(&ty, "Subtracting", exec_state);
KclValue::Number { value: l - r, meta, ty }
}
BinaryOperator::Mul => {
let (l, r, ty) = NumericType::combine_mul(left, right);
self.warn_on_unknown(&ty, "Multiplying", exec_state);
KclValue::Number { value: l * r, meta, ty }
}
BinaryOperator::Div => {
let (l, r, ty) = NumericType::combine_div(left, right);
self.warn_on_unknown(&ty, "Dividing", exec_state);
KclValue::Number { value: l / r, meta, ty }
}
BinaryOperator::Mod => {
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 }
}
BinaryOperator::Pow => KclValue::Number {
value: libm::pow(left.n, right.n),
meta,
ty: exec_state.current_default_units(),
},
BinaryOperator::Neq => {
let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
self.warn_on_unknown(&ty, "Comparing", exec_state);
KclValue::Bool { value: l != r, meta }
}
BinaryOperator::Gt => {
let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
self.warn_on_unknown(&ty, "Comparing", exec_state);
KclValue::Bool { value: l > r, meta }
}
BinaryOperator::Gte => {
let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
self.warn_on_unknown(&ty, "Comparing", exec_state);
KclValue::Bool { value: l >= r, meta }
}
BinaryOperator::Lt => {
let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
self.warn_on_unknown(&ty, "Comparing", exec_state);
KclValue::Bool { value: l < r, meta }
}
BinaryOperator::Lte => {
let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
self.warn_on_unknown(&ty, "Comparing", exec_state);
KclValue::Bool { value: l <= r, meta }
}
BinaryOperator::Eq => {
let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
self.warn_on_unknown(&ty, "Comparing", exec_state);
KclValue::Bool { value: l == r, meta }
}
BinaryOperator::And | BinaryOperator::Or => unreachable!(),
};
Ok(value)
}
fn missing_result_error(node: &Node<BinaryExpression>) -> KclError {
internal_err("missing result while evaluating binary expression", node)
}
fn warn_on_unknown(&self, ty: &NumericType, verb: &str, exec_state: &mut ExecState) {
if ty == &NumericType::Unknown {
let sr = self.as_source_range();
exec_state.clear_units_warnings(&sr);
let mut err = CompilationIssue::err(
sr,
format!(
"{verb} 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)`."
),
);
err.tag = crate::errors::Tag::UnknownNumericUnits;
exec_state.warn(err, annotations::WARN_UNKNOWN_UNITS);
}
}
}
impl Node<UnaryExpression> {
pub(super) async fn get_result(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
let value = self.argument.get_result(exec_state, ctx).await?;
let value = control_continue!(value);
self.apply_unary(value, exec_state).map(KclValue::continue_)
}
pub(super) fn apply_unary(&self, value: KclValue, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
match self.operator {
UnaryOperator::Not => {
let KclValue::Bool {
value: bool_value,
meta: _,
} = value
else {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Cannot apply unary operator ! to non-boolean value: {}",
value.human_friendly_type()
),
vec![self.into()],
)));
};
let meta = vec![Metadata {
source_range: self.into(),
}];
let negated = KclValue::Bool {
value: !bool_value,
meta,
};
Ok(negated)
}
UnaryOperator::Neg => {
let err = || {
KclError::new_semantic(KclErrorDetails::new(
format!(
"You can only negate numbers, planes, or lines, but this is a {}",
value.human_friendly_type()
),
vec![self.into()],
))
};
match &value {
KclValue::Number { value, ty, .. } => {
let meta = vec![Metadata {
source_range: self.into(),
}];
Ok(KclValue::Number {
value: -value,
meta,
ty: *ty,
})
}
KclValue::Plane { value } => {
let mut plane = value.clone();
if plane.info.x_axis.x != 0.0 {
plane.info.x_axis.x *= -1.0;
}
if plane.info.x_axis.y != 0.0 {
plane.info.x_axis.y *= -1.0;
}
if plane.info.x_axis.z != 0.0 {
plane.info.x_axis.z *= -1.0;
}
plane.info.z_axis = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
plane.info.z_axis.canonicalize_signed_zero();
plane.id = exec_state.next_uuid();
plane.object_id = None;
Ok(KclValue::Plane { value: plane })
}
KclValue::Object {
value: values, meta, ..
} => {
let Some(direction) = values.get("direction") else {
return Err(err());
};
let direction = match direction {
KclValue::Tuple { value: values, meta } => {
let values = values
.iter()
.map(|v| match v {
KclValue::Number { value, ty, meta } => Ok(KclValue::Number {
value: *value * -1.0,
ty: *ty,
meta: meta.clone(),
}),
_ => Err(err()),
})
.collect::<Result<Vec<_>, _>>()?;
KclValue::Tuple {
value: values,
meta: meta.clone(),
}
}
KclValue::HomArray {
value: values,
ty: ty @ RuntimeType::Primitive(PrimitiveType::Number(_)),
} => {
let values = values
.iter()
.map(|v| match v {
KclValue::Number { value, ty, meta } => Ok(KclValue::Number {
value: *value * -1.0,
ty: *ty,
meta: meta.clone(),
}),
_ => Err(err()),
})
.collect::<Result<Vec<_>, _>>()?;
KclValue::HomArray {
value: values,
ty: ty.clone(),
}
}
_ => return Err(err()),
};
let mut value = values.clone();
value.insert("direction".to_owned(), direction);
Ok(KclValue::Object {
value,
meta: meta.clone(),
constrainable: false,
object_kind: KclObjectKind::Default,
})
}
_ => Err(err()),
}
}
UnaryOperator::Plus => match value {
KclValue::Number { .. } | KclValue::Plane { .. } => Ok(value),
_ => Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"You can only apply unary + to numbers or planes, but this is a {}",
value.human_friendly_type()
),
vec![self.into()],
))),
},
}
}
}
pub(crate) async fn execute_pipe_body(
exec_state: &mut ExecState,
body: &[Expr],
source_range: SourceRange,
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
let Some((first, body)) = body.split_first() else {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Pipe expressions cannot be empty".to_owned(),
vec![source_range],
)));
};
let meta = Metadata {
source_range: SourceRange::from(first),
};
let output = ctx
.execute_expr(first, exec_state, &meta, &[], StatementKind::Expression)
.await?;
let output = control_continue!(output);
let previous_pipe_value = exec_state.mod_local.pipe_value.replace(output);
let result = inner_execute_pipe_body(exec_state, body, ctx).await;
exec_state.mod_local.pipe_value = previous_pipe_value;
result
}
#[async_recursion]
async fn inner_execute_pipe_body(
exec_state: &mut ExecState,
body: &[Expr],
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
for expression in body {
if let Expr::TagDeclarator(_) = expression {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!("This cannot be in a PipeExpression: {expression:?}"),
vec![expression.into()],
)));
}
let metadata = Metadata {
source_range: SourceRange::from(expression),
};
let output = ctx
.execute_expr(expression, exec_state, &metadata, &[], StatementKind::Expression)
.await?;
let output = control_continue!(output);
exec_state.mod_local.pipe_value = Some(output);
}
let final_output = exec_state.mod_local.pipe_value.take().unwrap();
Ok(final_output.continue_())
}
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: Vec::new(),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
exec_state
.mut_stack()
.add(self.name.clone(), memory_item, self.into())?;
Ok(self.into())
}
}
impl Node<ArrayExpression> {
#[async_recursion]
pub(super) async fn execute(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
let mut results = Vec::with_capacity(self.elements.len());
for element in &self.elements {
let metadata = Metadata::from(element);
let value = ctx
.execute_expr(element, exec_state, &metadata, &[], StatementKind::Expression)
.await?;
let value = control_continue!(value);
results.push(value);
}
Ok(KclValue::HomArray {
value: results,
ty: RuntimeType::Primitive(PrimitiveType::Any),
}
.continue_())
}
}
impl Node<ArrayRangeExpression> {
#[async_recursion]
pub(super) async fn execute(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
let metadata = Metadata::from(&self.start_element);
let start_val = ctx
.execute_expr(
&self.start_element,
exec_state,
&metadata,
&[],
StatementKind::Expression,
)
.await?;
let start_val_for_build = control_continue!(start_val);
self.validate_range_start(&start_val_for_build)?;
let metadata = Metadata::from(&self.end_element);
let end_val = ctx
.execute_expr(&self.end_element, exec_state, &metadata, &[], StatementKind::Expression)
.await?;
let end_val = control_continue!(end_val);
self.build_range(start_val_for_build, end_val, exec_state)
.map(KclValue::continue_)
}
pub(super) fn validate_range_start(&self, start_val: &KclValue) -> Result<(), KclError> {
if start_val.as_ty_f64().is_none() {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"Expected number for range start but found {}",
start_val.human_friendly_type()
),
vec![self.into()],
)));
}
Ok(())
}
pub(super) fn build_range(
&self,
start_val: KclValue,
end_val: KclValue,
exec_state: &mut ExecState,
) -> Result<KclValue, KclError> {
let start = start_val
.as_ty_f64()
.ok_or(KclError::new_semantic(KclErrorDetails::new(
format!(
"Expected number for range start but found {}",
start_val.human_friendly_type()
),
vec![self.into()],
)))?;
let end = end_val.as_ty_f64().ok_or(KclError::new_semantic(KclErrorDetails::new(
format!(
"Expected number for range end but found {}",
end_val.human_friendly_type()
),
vec![self.into()],
)))?;
let (start, end, ty) = NumericType::combine_range(start, end, exec_state, self.as_source_range())?;
let Some(start) = crate::try_f64_to_i64(start) else {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!("Range start must be an integer, but found {start}"),
vec![self.into()],
)));
};
let Some(end) = crate::try_f64_to_i64(end) else {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!("Range end must be an integer, but found {end}"),
vec![self.into()],
)));
};
if end < start {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!("Range start is greater than range end: {start} .. {end}"),
vec![self.into()],
)));
}
let range: Vec<_> = if self.end_inclusive {
(start..=end).collect()
} else {
(start..end).collect()
};
let meta = vec![Metadata {
source_range: self.into(),
}];
Ok(KclValue::HomArray {
value: range
.into_iter()
.map(|num| KclValue::Number {
value: num as f64,
ty,
meta: meta.clone(),
})
.collect(),
ty: RuntimeType::Primitive(PrimitiveType::Number(ty)),
})
}
}
impl Node<ObjectExpression> {
#[async_recursion]
pub(super) async fn execute(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
let mut object = HashMap::with_capacity(self.properties.len());
for property in &self.properties {
let metadata = Metadata::from(&property.value);
let result = ctx
.execute_expr(&property.value, exec_state, &metadata, &[], StatementKind::Expression)
.await?;
let result = control_continue!(result);
object.insert(property.key.name.clone(), result);
}
Ok(KclValue::Object {
value: object,
meta: vec![Metadata {
source_range: self.into(),
}],
constrainable: false,
object_kind: KclObjectKind::Default,
}
.continue_())
}
}
fn article_for<S: AsRef<str>>(s: S) -> &'static str {
if s.as_ref().starts_with(['a', 'e', 'i', 'o', 'u', '[']) {
"an"
} else {
"a"
}
}
fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<TyF64, KclError> {
v.as_ty_f64().ok_or_else(|| {
let actual_type = v.human_friendly_type();
KclError::new_semantic(KclErrorDetails::new(
format!("Expected a number, but found {actual_type}",),
vec![source_range],
))
})
}
impl Node<IfExpression> {
#[async_recursion]
pub(super) async fn get_result(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
let cond_value = ctx
.execute_expr(
&self.cond,
exec_state,
&Metadata::from(self),
&[],
StatementKind::Expression,
)
.await?;
let cond_value = control_continue!(cond_value);
if cond_value.get_bool()? {
return exec_if_arm(ctx, &self.then_val, exec_state).await;
}
for else_if in &self.else_ifs {
let cond_value = ctx
.execute_expr(
&else_if.cond,
exec_state,
&Metadata::from(self),
&[],
StatementKind::Expression,
)
.await?;
let cond_value = control_continue!(cond_value);
if cond_value.get_bool()? {
return exec_if_arm(ctx, &else_if.then_val, exec_state).await;
}
}
exec_if_arm(ctx, &self.final_else, exec_state).await
}
}
pub(super) fn if_arm_scope_begin(exec_state: &mut ExecState) -> Result<bool, KclError> {
if !exec_state.entry_point_version_is_v3_or_higher() {
return Ok(false);
}
exec_state.mut_stack().push_new_env_for_block()?;
Ok(true)
}
async fn exec_if_arm(
ctx: &ExecutorContext,
block: &Node<Program>,
exec_state: &mut ExecState,
) -> Result<KclValueControlFlow, KclError> {
let scoped = if_arm_scope_begin(exec_state)?;
let result = ctx.exec_block(block, exec_state, BodyType::Block).await;
if scoped {
exec_state.mut_stack().pop_env()?;
}
let Some(cf) = result? else {
let message = "if-expression arm produced no value";
debug_assert!(false, "{message}");
return Err(KclError::new_internal(KclErrorDetails::new(
message.to_owned(),
vec![block.to_source_range()],
)));
};
Ok(cf)
}
#[derive(Debug)]
pub(super) enum Property {
UInt(usize),
String(String),
}
impl Property {
#[allow(clippy::too_many_arguments)]
async fn try_from<'a>(
computed: bool,
value: Expr,
exec_state: &mut ExecState,
sr: SourceRange,
ctx: &ExecutorContext,
metadata: &Metadata,
annotations: &[Node<Annotation>],
statement_kind: StatementKind<'a>,
) -> Result<Self, EarlyReturn> {
if !computed {
return Ok(Self::from_static_name(&value, sr)?);
}
let prop_value = ctx
.execute_expr(&value, exec_state, metadata, annotations, statement_kind)
.await?;
let prop_value = early_return!(prop_value);
Ok(Self::from_value(prop_value, sr)?)
}
pub(super) fn from_static_name(property: &Expr, sr: SourceRange) -> Result<Self, KclError> {
let Expr::Name(identifier) = property else {
return Err(KclError::new_semantic(KclErrorDetails::new(
"Object expressions like `obj.property` must use simple identifier names, not complex expressions"
.to_owned(),
vec![sr],
)));
};
Ok(Property::String(identifier.to_string()))
}
pub(super) fn from_value(prop_value: KclValue, sr: SourceRange) -> Result<Self, KclError> {
let property_sr = vec![sr];
match prop_value {
KclValue::Number { value, ty, meta: _ } => {
if !matches!(
ty,
NumericType::Unknown
| NumericType::Default { .. }
| NumericType::Known(crate::exec::UnitType::Count)
) {
return Err(KclError::new_semantic(KclErrorDetails::new(
format!(
"{value} is not a valid index, indices must be non-dimensional numbers. If you're sure this is correct, you can add `: number(Count)` to tell KCL this number is an index"
),
property_sr,
)));
}
if let Some(x) = crate::try_f64_to_usize(value) {
Ok(Property::UInt(x))
} else {
Err(KclError::new_semantic(KclErrorDetails::new(
format!("{value} is not a valid index, indices must be whole numbers >= 0"),
property_sr,
)))
}
}
_ => Err(KclError::new_semantic(KclErrorDetails::new(
"Only numbers (>= 0) can be indexes".to_owned(),
vec![sr],
))),
}
}
}
impl Property {
fn type_name(&self) -> &'static str {
match self {
Property::UInt(_) => "number",
Property::String(_) => "string",
}
}
}
impl Node<PipeExpression> {
#[async_recursion]
pub(super) async fn get_result(
&self,
exec_state: &mut ExecState,
ctx: &ExecutorContext,
) -> Result<KclValueControlFlow, KclError> {
execute_pipe_body(exec_state, &self.body, self.into(), ctx).await
}
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use kcl_api::UnitLength;
use tokio::io::AsyncWriteExt;
use super::*;
use crate::ExecutorSettings;
use crate::engine::engine_manager;
use crate::errors::Severity;
use crate::exec::UnitType;
use crate::execution::ContextType;
use crate::execution::machine::ExecutorKind;
use crate::execution::parse_execute;
fn assert_angle_degrees(actual: ezpz::datatypes::Angle, expected: f64) {
assert!(
(actual.to_degrees() - expected).abs() < 1e-9,
"expected {expected}deg, got {}deg",
actual.to_degrees()
);
}
#[test]
fn remaps_sector_angles_to_existing_representative_endpoint_rays() {
let representative_directions = [AngleRayDirection::Forward, AngleRayDirection::Forward];
assert_angle_degrees(
remap_angle_for_representative_rays(
angle_sector_rays(AngleSector::One, false),
representative_directions,
ezpz::datatypes::Angle::from_degrees(60.0),
),
60.0,
);
assert_angle_degrees(
remap_angle_for_representative_rays(
angle_sector_rays(AngleSector::Two, false),
representative_directions,
ezpz::datatypes::Angle::from_degrees(120.0),
),
60.0,
);
assert_angle_degrees(
remap_angle_for_representative_rays(
angle_sector_rays(AngleSector::Three, false),
representative_directions,
ezpz::datatypes::Angle::from_degrees(60.0),
),
60.0,
);
assert_angle_degrees(
remap_angle_for_representative_rays(
angle_sector_rays(AngleSector::Four, false),
representative_directions,
ezpz::datatypes::Angle::from_degrees(120.0),
),
60.0,
);
assert_angle_degrees(
remap_angle_for_representative_rays(
angle_sector_rays(AngleSector::One, true),
representative_directions,
ezpz::datatypes::Angle::from_degrees(300.0),
),
60.0,
);
}
#[test]
fn remaps_sector_angles_when_representative_endpoint_is_on_reverse_ray() {
assert_angle_degrees(
remap_angle_for_representative_rays(
angle_sector_rays(AngleSector::One, false),
[AngleRayDirection::Forward, AngleRayDirection::Reverse],
ezpz::datatypes::Angle::from_degrees(60.0),
),
240.0,
);
}
#[tokio::test(flavor = "multi_thread")]
async fn angle_unlabeled_keeps_legacy_lines_at_angle() {
let code = r#"
sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
lines = [line1, line2]
angle(lines) == 60deg
}
"#;
let result = parse_execute(code).await.unwrap();
let metadata = result
.exec_state
.global
.root_module_artifacts
.legacy_angle_refactor_metadata();
assert_eq!(metadata.len(), 1);
assert_eq!(metadata[0].sector, 1);
assert!(!metadata[0].inverse);
let program = crate::Program::parse_no_errs(code).unwrap();
let findings = program.lint(crate::lint::checks::lint_legacy_angle).unwrap();
assert_eq!(metadata[0].source_range, findings[0].pos);
}
#[tokio::test(flavor = "multi_thread")]
async fn legacy_angle_refactor_metadata_matches_the_default_label_side() {
let result = parse_execute(
r#"
sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
line2 = line(start = [var 0mm, var 0mm], end = [var -2mm, var -3.464mm])
angle([line1, line2]) == 60deg
}
"#,
)
.await
.unwrap();
let metadata = result
.exec_state
.global
.root_module_artifacts
.legacy_angle_refactor_metadata();
assert_eq!(metadata.len(), 1);
assert_eq!(metadata[0].sector, 4);
assert!(metadata[0].inverse);
}
#[tokio::test(flavor = "multi_thread")]
async fn legacy_angle_refactor_metadata_uses_reverse_segment_rays() {
let result = parse_execute(
r#"
sketch(on = XY) {
line1 = line(start = [var -4mm, var 0mm], end = [var 0mm, var 0mm])
line2 = line(start = [var -2mm, var -3.464mm], end = [var 0mm, var 0mm])
angle([line1, line2]) == 60deg
}
"#,
)
.await
.unwrap();
let metadata = result
.exec_state
.global
.root_module_artifacts
.legacy_angle_refactor_metadata();
assert_eq!(metadata.len(), 1);
assert_eq!(metadata[0].sector, 3);
assert!(!metadata[0].inverse);
}
#[tokio::test(flavor = "multi_thread")]
async fn legacy_angle_label_position_does_not_change_the_sector() {
let result = parse_execute(
r#"
sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
angle([line1, line2], labelPosition = [-3mm, -1.7mm]) == 60deg
}
"#,
)
.await
.unwrap();
let metadata = result
.exec_state
.global
.root_module_artifacts
.legacy_angle_refactor_metadata();
assert_eq!(metadata.len(), 1);
assert_eq!(metadata[0].sector, 1);
assert!(!metadata[0].inverse);
}
#[tokio::test(flavor = "multi_thread")]
async fn parallel_legacy_angle_has_no_refactor_metadata() {
let result = parse_execute(
r#"
sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
line2 = line(start = [var 0mm, var 1mm], end = [var 4mm, var 1mm])
angle([line1, line2]) == 0deg
}
"#,
)
.await
.unwrap();
assert!(
result
.exec_state
.global
.root_module_artifacts
.legacy_angle_refactor_metadata()
.is_empty()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn angle_dimension_with_sector_uses_named_lines() {
parse_execute(
r#"
sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
line2 = line(start = [var 0mm, var 1mm], end = [var 2mm, var 3mm])
angleDimension(lines = [line1, line2], sector = 2) == 60deg
}
"#,
)
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn angle_dimension_requires_sector() {
let err = parse_execute(
r#"
sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
angleDimension(lines = [line1, line2]) == 60deg
}
"#,
)
.await
.unwrap_err();
assert!(
err.to_string()
.contains("The `angleDimension` function requires a keyword argument `sector`"),
"unexpected error: {err:?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn angle_dimension_accepts_label_position() {
let result = parse_execute(
r#"
sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm]) == 60deg
}
"#,
)
.await
.unwrap();
let angle = result
.exec_state
.global
.root_module_artifacts
.scene_objects
.iter()
.find_map(|object| match &object.kind {
ObjectKind::Constraint {
constraint: crate::front::Constraint::Angle(angle),
} => Some(angle),
_ => None,
})
.unwrap();
let label_position = angle.label_position.as_ref().unwrap();
assert_eq!(label_position.x.value, 10.0);
assert_eq!(label_position.y.value, 11.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn angle_dimension_accepts_all_four_sectors() {
parse_execute(
r#"
sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
angleDimension(lines = [line1, line2], sector = 1) == 60deg
angleDimension(lines = [line1, line2], sector = 2) == 120deg
angleDimension(lines = [line1, line2], sector = 3) == 60deg
angleDimension(lines = [line1, line2], sector = 4) == 120deg
}
"#,
)
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn angle_dimension_accepts_inverse_angle_for_sector() {
let result = parse_execute(
r#"
sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
angleDimension(lines = [line1, line2], sector = 1, inverse = true) == 360deg - 60deg
}
"#,
)
.await
.unwrap();
let angle = result
.exec_state
.global
.root_module_artifacts
.scene_objects
.iter()
.find_map(|object| match &object.kind {
ObjectKind::Constraint {
constraint: crate::front::Constraint::Angle(angle),
} => Some(angle),
_ => None,
})
.unwrap();
assert_eq!(angle.sector, Some(1));
assert_eq!(angle.inverse, Some(true));
}
#[tokio::test(flavor = "multi_thread")]
async fn angle_dimension_rejects_invalid_sector() {
let err = parse_execute(
r#"
sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
line2 = line(start = [var 0mm, var 1mm], end = [var 2mm, var 3mm])
angleDimension(lines = [line1, line2], sector = 5) == 60deg
}
"#,
)
.await
.unwrap_err();
assert!(
err.to_string()
.contains("angleDimension() sector must be 1, 2, 3, or 4"),
"unexpected error: {err:?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn angle_dimension_rejects_parallel_lines() {
let err = parse_execute(
r#"
sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
line2 = line(start = [var 0mm, var 1mm], end = [var 4mm, var 1mm])
angleDimension(lines = [line1, line2], sector = 2) == 60deg
}
"#,
)
.await
.unwrap_err();
assert!(
err.to_string()
.contains("angleDimension(lines = ..., sector = ...) requires non-parallel lines"),
"unexpected error: {err:?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn angle_accepts_label_position() {
let result = parse_execute(
r#"
sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
line2 = line(start = [var 0mm, var 1mm], end = [var 2mm, var 3mm])
angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
}
"#,
)
.await
.unwrap();
let angle = result
.exec_state
.global
.root_module_artifacts
.scene_objects
.iter()
.find_map(|object| match &object.kind {
ObjectKind::Constraint {
constraint: crate::front::Constraint::Angle(angle),
} => Some(angle),
_ => None,
})
.unwrap();
let label_position = angle.label_position.as_ref().unwrap();
assert_eq!(label_position.x.value, 10.0);
assert_eq!(label_position.y.value, 11.0);
}
#[tokio::test(flavor = "multi_thread")]
async fn angle_requires_unlabeled_lines() {
parse_execute(
r#"
sketch(on = XY) {
angle() == 60deg
}
"#,
)
.await
.unwrap_err();
}
#[tokio::test(flavor = "multi_thread")]
async fn ascription() {
let program = r#"
a = 42: number
b = a: number
p = {
origin = { x = 0, y = 0, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
zAxis = { x = 0, y = 0, z = 1 }
}: Plane
arr1 = [42]: [number(cm)]
"#;
let result = parse_execute(program).await.unwrap();
let mem = result.exec_state.stack();
assert!(matches!(
mem.memory
.get_from_owned("p", result.mem_env, SourceRange::default(), 0)
.unwrap(),
KclValue::Plane { .. }
));
let arr1 = mem
.memory
.get_from_owned("arr1", result.mem_env, SourceRange::default(), 0)
.unwrap();
if let KclValue::HomArray { value, ty } = arr1 {
assert_eq!(value.len(), 1, "Expected Vec with specific length: found {value:?}");
assert_eq!(ty, RuntimeType::known_length(UnitLength::Centimeters));
if let KclValue::Number { value, ty, .. } = &value[0] {
assert_eq!(*value, 42.0);
assert_eq!(*ty, NumericType::Known(UnitType::Length(UnitLength::Centimeters)));
} else {
panic!("Expected a number; found {:?}", value[0]);
}
} else {
panic!("Expected HomArray; found {arr1:?}");
}
let program = r#"
a = 42: string
"#;
let result = parse_execute(program).await;
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("could not coerce a number (with type `number`) to type `string`"),
"Expected error but found {err:?}"
);
let program = r#"
a = 42: Plane
"#;
let result = parse_execute(program).await;
let err = result.unwrap_err();
assert!(
err.to_string()
.contains("could not coerce a number (with type `number`) to type `Plane`"),
"Expected error but found {err:?}"
);
let program = r#"
arr = [0]: [string]
"#;
let result = parse_execute(program).await;
let err = result.unwrap_err();
assert!(
err.to_string().contains(
"could not coerce an array of `number` with 1 value (with type `[any; 1]`) to type `[string]`"
),
"Expected error but found {err:?}"
);
let program = r#"
mixedArr = [0, "a"]: [number(mm)]
"#;
let result = parse_execute(program).await;
let err = result.unwrap_err();
assert!(
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:?}"
);
let program = r#"
mixedArr = [0, "a"]: [mm]
"#;
let result = parse_execute(program).await;
let err = result.unwrap_err();
assert!(
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:?}"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn neg_plane() {
let program = r#"
p = {
origin = { x = 0, y = 0, z = 0 },
xAxis = { x = 1, y = 0, z = 0 },
yAxis = { x = 0, y = 1, z = 0 },
}: Plane
p2 = -p
"#;
let result = parse_execute(program).await.unwrap();
let mem = result.exec_state.stack();
match mem
.memory
.get_from_owned("p2", result.mem_env, SourceRange::default(), 0)
.unwrap()
{
KclValue::Plane { value } => {
assert_eq!(value.info.x_axis.x, -1.0);
assert_eq!(value.info.x_axis.y, 0.0);
assert_eq!(value.info.x_axis.z, 0.0);
}
_ => unreachable!(),
}
}
#[tokio::test(flavor = "multi_thread")]
async fn multiple_returns() {
let program = r#"fn foo() {
return 0
return 42
}
a = foo()
"#;
let result = parse_execute(program).await;
assert!(result.unwrap_err().to_string().contains("return"));
}
#[tokio::test(flavor = "multi_thread")]
async fn load_all_modules() {
let program_a_kcl = r#"
export a = 1
"#;
let program_b_kcl = r#"
import a from 'a.kcl'
export b = a + 1
"#;
let program_c_kcl = r#"
import a from 'a.kcl'
export c = a + 2
"#;
let main_kcl = r#"
import b from 'b.kcl'
import c from 'c.kcl'
d = b + c
"#;
let main = crate::parsing::parse_str(main_kcl, ModuleId::default())
.parse_errs_as_err()
.unwrap();
let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_load_all_modules").unwrap();
tokio::fs::File::create(tmpdir.path().join("main.kcl"))
.await
.unwrap()
.write_all(main_kcl.as_bytes())
.await
.unwrap();
tokio::fs::File::create(tmpdir.path().join("a.kcl"))
.await
.unwrap()
.write_all(program_a_kcl.as_bytes())
.await
.unwrap();
tokio::fs::File::create(tmpdir.path().join("b.kcl"))
.await
.unwrap()
.write_all(program_b_kcl.as_bytes())
.await
.unwrap();
tokio::fs::File::create(tmpdir.path().join("c.kcl"))
.await
.unwrap()
.write_all(program_c_kcl.as_bytes())
.await
.unwrap();
let exec_ctxt = ExecutorContext {
engine: Arc::new(engine_manager::EngineManager::new_mock()),
engine_batch: crate::engine::EngineBatchContext::default(),
fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
settings: ExecutorSettings {
project_directory: Some(crate::TypedPath(tmpdir.path().into())),
..Default::default()
},
context_type: ContextType::Mock,
execution_callbacks: Default::default(),
executor_kind: ExecutorKind::resolve(),
machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
};
let mut exec_state = ExecState::new(&exec_ctxt);
exec_ctxt
.run(
&crate::Program {
ast: main.clone(),
original_file_contents: "".to_owned(),
},
&mut exec_state,
)
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn user_coercion() {
let program = r#"fn foo(x: Axis2d) {
return 0
}
foo(x = { direction = [0, 0], origin = [0, 0]})
"#;
parse_execute(program).await.unwrap();
let program = r#"fn foo(x: Axis3d) {
return 0
}
foo(x = { direction = [0, 0], origin = [0, 0]})
"#;
parse_execute(program).await.unwrap_err();
}
#[tokio::test(flavor = "multi_thread")]
async fn coerce_return() {
let program = r#"fn foo(): number(mm) {
return 42
}
a = foo()
"#;
parse_execute(program).await.unwrap();
let program = r#"fn foo(): mm {
return 42
}
a = foo()
"#;
parse_execute(program).await.unwrap();
let program = r#"fn foo(): number(mm) {
return { bar: 42 }
}
a = foo()
"#;
parse_execute(program).await.unwrap_err();
let program = r#"fn foo(): mm {
return { bar: 42 }
}
a = foo()
"#;
parse_execute(program).await.unwrap_err();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_sensible_error_when_missing_equals_in_kwarg() {
for (i, call) in ["f(x=1,3,0)", "f(x=1,3,z)", "f(x=1,0,z=1)", "f(x=1, 3 + 4, z)"]
.into_iter()
.enumerate()
{
let program = format!(
"fn foo() {{ return 0 }}
z = 0
fn f(x, y, z) {{ return 0 }}
{call}"
);
let err = parse_execute(&program).await.unwrap_err();
let msg = err.message();
assert!(
msg.contains("This argument needs a label, but it doesn't have one"),
"failed test {i}: {msg}"
);
assert!(msg.contains("`y`"), "failed test {i}, missing `y`: {msg}");
if i == 0 {
assert!(msg.contains("`z`"), "failed test {i}, missing `z`: {msg}");
}
}
}
#[tokio::test(flavor = "multi_thread")]
async fn default_param_for_unlabeled() {
let ast = r#"fn myExtrude(@sk, length) {
return extrude(sk, length)
}
sketch001 = startSketchOn(XY)
|> circle(center = [0, 0], radius = 93.75)
|> myExtrude(length = 40)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn dont_use_unlabelled_as_input() {
let ast = r#"length = 10
startSketchOn(XY)
|> circle(center = [0, 0], radius = 93.75)
|> extrude(length)
"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn ascription_in_binop() {
let ast = r#"foo = tan(0): number(rad) - 4deg"#;
parse_execute(ast).await.unwrap();
let ast = r#"foo = tan(0): rad - 4deg"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn neg_sqrt() {
let ast = r#"bad = sqrt(-2)"#;
let e = parse_execute(ast).await.unwrap_err();
assert!(e.message().contains("sqrt"), "Error message: '{}'", e.message());
}
#[tokio::test(flavor = "multi_thread")]
async fn non_array_fns() {
let ast = r#"push(1, item = 2)
pop(1)
map(1, f = fn(@x) { return x + 1 })
reduce(1, f = fn(@x, accum) { return accum + x}, initial = 0)"#;
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn non_array_indexing() {
let good = r#"a = 42
good = a[0]
"#;
let result = parse_execute(good).await.unwrap();
let mem = result.exec_state.stack();
let num = mem
.memory
.get_from_owned("good", result.mem_env, SourceRange::default(), 0)
.unwrap()
.as_ty_f64()
.unwrap();
assert_eq!(num.n, 42.0);
let bad = r#"a = 42
bad = a[1]
"#;
parse_execute(bad).await.unwrap_err();
}
#[tokio::test(flavor = "multi_thread")]
async fn coerce_unknown_to_length() {
let ast = r#"x = 2mm * 2mm
y = x: number(Length)"#;
let e = parse_execute(ast).await.unwrap_err();
assert!(
e.message().contains("could not coerce"),
"Error message: '{}'",
e.message()
);
let ast = r#"x = 2mm
y = x: number(Length)"#;
let result = parse_execute(ast).await.unwrap();
let mem = result.exec_state.stack();
let num = mem
.memory
.get_from_owned("y", result.mem_env, SourceRange::default(), 0)
.unwrap()
.as_ty_f64()
.unwrap();
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.issues().len(), 2);
}
#[tokio::test(flavor = "multi_thread")]
async fn non_count_indexing() {
let ast = r#"x = [0, 0]
y = x[1mm]
"#;
parse_execute(ast).await.unwrap_err();
let ast = r#"x = [0, 0]
y = 1deg
z = x[y]
"#;
parse_execute(ast).await.unwrap_err();
let ast = r#"x = [0, 0]
y = x[0mm + 1]
"#;
parse_execute(ast).await.unwrap_err();
}
#[tokio::test(flavor = "multi_thread")]
async fn getting_property_of_plane() {
let ast = std::fs::read_to_string("tests/inputs/planestuff.kcl").unwrap();
parse_execute(&ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn no_artifacts_from_within_hole_call() {
let ast = std::fs::read_to_string("tests/inputs/sample_hole.kcl").unwrap();
let out = parse_execute(&ast).await.unwrap();
let actual_operations = out.exec_state.global.root_module_artifacts.operations;
let expected = 5;
assert_eq!(
actual_operations.len(),
expected,
"expected {expected} operations, received {}:\n{actual_operations:#?}",
actual_operations.len(),
);
}
#[tokio::test(flavor = "multi_thread")]
async fn feature_tree_annotation_on_user_defined_kcl() {
let ast = std::fs::read_to_string("tests/inputs/feature_tree_annotation_on_user_defined_kcl.kcl").unwrap();
let out = parse_execute(&ast).await.unwrap();
let actual_operations = out.exec_state.global.root_module_artifacts.operations;
let expected = 0;
assert_eq!(
actual_operations.len(),
expected,
"expected {expected} operations, received {}:\n{actual_operations:#?}",
actual_operations.len(),
);
}
#[tokio::test(flavor = "multi_thread")]
async fn no_feature_tree_annotation_on_user_defined_kcl() {
let ast = std::fs::read_to_string("tests/inputs/no_feature_tree_annotation_on_user_defined_kcl.kcl").unwrap();
let out = parse_execute(&ast).await.unwrap();
let actual_operations = out.exec_state.global.root_module_artifacts.operations;
let expected = 2;
assert_eq!(
actual_operations.len(),
expected,
"expected {expected} operations, received {}:\n{actual_operations:#?}",
actual_operations.len(),
);
assert!(matches!(actual_operations[0], Operation::GroupBegin { .. }));
assert!(matches!(actual_operations[1], Operation::GroupEnd));
}
#[tokio::test(flavor = "multi_thread")]
async fn custom_warning() {
let warn = r#"
a = PI * 2
"#;
let result = parse_execute(warn).await.unwrap();
assert_eq!(result.exec_state.issues().len(), 1);
assert_eq!(result.exec_state.issues()[0].severity, Severity::Warning);
let allow = r#"
@warnings(allow = unknownUnits)
a = PI * 2
"#;
let result = parse_execute(allow).await.unwrap();
assert_eq!(result.exec_state.issues().len(), 0);
let deny = r#"
@warnings(deny = [unknownUnits])
a = PI * 2
"#;
let result = parse_execute(deny).await.unwrap();
assert_eq!(result.exec_state.issues().len(), 1);
assert_eq!(result.exec_state.issues()[0].severity, Severity::Error);
}
#[tokio::test(flavor = "multi_thread")]
async fn diagnostics_attribute_in_v3() {
let warn = "@settings(kclVersion = \"3.0-preview\")\na = PI * 2\n";
let result = parse_execute(warn).await.unwrap();
let issues = result.exec_state.issues();
assert_eq!(issues.len(), 1, "{issues:#?}");
assert_eq!(issues[0].severity, Severity::Warning);
assert_eq!(issues[0].tag, crate::errors::Tag::UnknownNumericUnits);
let allow = "@settings(kclVersion = \"3.0-preview\")\n@diagnostics(allow = unknownUnits)\na = PI * 2\n";
let result = parse_execute(allow).await.unwrap();
assert!(
result.exec_state.issues().is_empty(),
"{:#?}",
result.exec_state.issues()
);
let deny = "@settings(kclVersion = \"3.0-preview\")\n@diagnostics(deny = [unknownUnits])\na = PI * 2\n";
let result = parse_execute(deny).await.unwrap();
let issues = result.exec_state.issues();
assert_eq!(issues.len(), 1, "{issues:#?}");
assert_eq!(issues[0].severity, Severity::Error);
assert_eq!(issues[0].tag, crate::errors::Tag::UnknownNumericUnits);
}
#[tokio::test(flavor = "multi_thread")]
async fn diagnostics_attribute_is_unknown_before_v3() {
for version in ["1.0", "2.0"] {
let code = format!("@settings(kclVersion = {version})\n@diagnostics(allow = unknownUnits)\na = PI * 2\n");
let result = parse_execute(&code).await.unwrap();
let issues = result.exec_state.issues();
assert_eq!(issues.len(), 2, "code={code}, issues={issues:#?}");
assert_eq!(issues[0].severity, Severity::Warning);
assert_eq!(issues[0].message, "Unknown annotation");
assert_eq!(
&code[issues[0].source_range.start()..issues[0].source_range.end()],
"@diagnostics(allow = unknownUnits)"
);
assert_eq!(issues[1].severity, Severity::Warning);
assert_eq!(issues[1].tag, crate::errors::Tag::UnknownNumericUnits);
let code = format!("@settings(kclVersion = {version})\n@warnings(allow = unknownUnits)\na = PI * 2\n");
let result = parse_execute(&code).await.unwrap();
assert!(
result.exec_state.issues().is_empty(),
"code={code}, issues={:#?}",
result.exec_state.issues()
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn diagnostics_attribute_errors_use_the_attribute_name() {
for (version, attr, noun) in [
("1.0", "warnings", "warning"),
("2.0", "warnings", "warning"),
("\"3.0-preview\"", "diagnostics", "diagnostic"),
] {
let settings = format!("@settings(kclVersion = {version})\n");
let code = format!("{settings}@{attr}\n");
let error = parse_execute(&code).await.unwrap_err();
assert_eq!(error.message(), format!("Empty `{attr}` annotation"), "code={code}");
let code = format!("{settings}@{attr}(warn = unknownUnits)\n");
let error = parse_execute(&code).await.unwrap_err();
assert_eq!(
error.message(),
format!("Unexpected {attr} key: `warn`; expected one of `allow`, `deny`"),
"code={code}"
);
let code = format!("{settings}@{attr}(allow = 1)\n");
let error = parse_execute(&code).await.unwrap_err();
assert_eq!(
error.message(),
format!(
"Unexpected {attr} value, expected a name or array of names, e.g., `unknownUnits` or `[unknownUnits, deprecated]`"
),
"code={code}"
);
let code = format!("{settings}@{attr}(allow = bogus)\n");
let error = parse_execute(&code).await.unwrap_err();
let expected_prefix = format!("Unexpected {noun} value: `bogus`; accepted values: unknownUnits, ");
assert!(
error.message().starts_with(&expected_prefix),
"code={code}, message={}",
error.message()
);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn warnings_attribute_is_renamed_in_v3() {
let code = "@settings(kclVersion = \"3.0-preview\")\n@warnings(allow = unknownUnits)\na = PI * 2\n";
let result = parse_execute(code).await.unwrap();
let issues = result.exec_state.issues();
assert_eq!(issues.len(), 2, "{issues:#?}");
let renamed = &issues[0];
assert_eq!(renamed.severity, Severity::Error);
assert_eq!(
renamed.message,
"The `@warnings` attribute was renamed to `@diagnostics` in KCL 3.0, so this attribute is ignored. Replace `@warnings` with `@diagnostics`; its `allow` and `deny` properties are unchanged."
);
assert_eq!(
&code[renamed.source_range.start()..renamed.source_range.end()],
"@warnings(allow = unknownUnits)"
);
let suggestion = renamed.suggestion.as_ref().unwrap();
assert_eq!(suggestion.title, "Rename to `@diagnostics`");
assert_eq!(
suggestion.apply(code),
"@settings(kclVersion = \"3.0-preview\")\n@diagnostics(allow = unknownUnits)\na = PI * 2\n"
);
assert_eq!(issues[1].severity, Severity::Warning);
assert_eq!(issues[1].tag, crate::errors::Tag::UnknownNumericUnits);
let code = "@settings(kclVersion = \"3.0-preview\")\n@warnings(allow = bogus)\n";
let result = parse_execute(code).await.unwrap();
let issues = result.exec_state.issues();
assert_eq!(issues.len(), 1, "{issues:#?}");
assert_eq!(issues[0].severity, Severity::Error);
assert!(issues[0].message.starts_with("The `@warnings` attribute was renamed"));
}
#[tokio::test(flavor = "multi_thread")]
async fn sketch_block_unqualified_functions_use_sketch2() {
let ast = r#"
s = sketch(on = XY) {
line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
line2 = line(start = [var 1mm, var 0mm], end = [var 1mm, var 1mm])
coincident([line1.end, line2.start])
}
"#;
let result = parse_execute(ast).await.unwrap();
let mem = result.exec_state.stack();
let sketch_value = mem
.memory
.get_from_owned("s", result.mem_env, SourceRange::default(), 0)
.unwrap();
let KclValue::Object { value, .. } = sketch_value else {
panic!("Expected sketch block to return an object, got {sketch_value:?}");
};
assert!(value.contains_key("line1"));
assert!(value.contains_key("line2"));
assert!(!value.contains_key("line"));
assert!(!value.contains_key("coincident"));
}
#[tokio::test(flavor = "multi_thread")]
async fn solver_module_is_not_available_outside_sketch_blocks() {
let err = parse_execute("a = solver::ORIGIN").await.unwrap_err();
assert!(err.message().contains("solver"), "Error message: '{}'", err.message());
let err = parse_execute(
r#"@settings(experimentalFeatures = allow)
import "std::solver""#,
)
.await
.unwrap_err();
assert!(
err.message().contains("only available inside sketch blocks"),
"Error message: '{}'",
err.message()
);
}
#[tokio::test(flavor = "multi_thread")]
async fn cannot_solid_extrude_an_open_profile() {
let code = std::fs::read_to_string("tests/inputs/cannot_solid_extrude_an_open_profile.kcl").unwrap();
let program = crate::Program::parse_no_errs(&code).expect("should parse");
let exec_ctxt = ExecutorContext::new_mock(None).await;
let mut exec_state = ExecState::new(&exec_ctxt);
let err = exec_ctxt.run(&program, &mut exec_state).await.unwrap_err().error;
assert!(matches!(err, KclError::Semantic { .. }));
exec_ctxt.close().await;
}
}