Skip to main content

kcl_lib/execution/
exec_ast.rs

1use std::collections::HashMap;
2use std::collections::HashSet;
3use std::sync::Arc;
4
5use async_recursion::async_recursion;
6use ezpz::Constraint;
7use ezpz::NonLinearSystemError;
8use ezpz::datatypes::inputs::DatumPoint;
9use indexmap::IndexMap;
10use kcl_api::ArcDirection;
11use kcl_api::Group;
12use kcl_api::LineCtor;
13use kcl_api::NumericType;
14use kcl_api::Object;
15use kcl_api::ObjectId;
16use kcl_api::ObjectKind;
17use kcl_api::Operation;
18use kcl_api::PointCtor;
19use kcl_api::UnitAngle;
20
21use crate::CompilationIssue;
22use crate::NodePath;
23use crate::NodePathExt;
24use crate::SourceRange;
25use crate::errors::KclError;
26use crate::errors::KclErrorDetails;
27use crate::exec::Sketch;
28use crate::execution::AbstractSegment;
29use crate::execution::AngleConstraintMode;
30use crate::execution::AngleRayDirection;
31use crate::execution::AngleSector;
32use crate::execution::Artifact;
33use crate::execution::ArtifactId;
34use crate::execution::BodyType;
35use crate::execution::ConstrainableLine2d;
36use crate::execution::ConstraintKind;
37use crate::execution::EarlyReturn;
38use crate::execution::EnvironmentRef;
39use crate::execution::ExecState;
40use crate::execution::ExecutorContext;
41use crate::execution::KclValue;
42use crate::execution::KclValueControlFlow;
43use crate::execution::KclVersion;
44use crate::execution::LegacyAngleRefactorMeta;
45use crate::execution::Metadata;
46use crate::execution::ModuleArtifactState;
47use crate::execution::PendingLegacyAngleRefactorMeta;
48use crate::execution::PreserveMem;
49use crate::execution::RefactorMetadata;
50use crate::execution::SKETCH_BLOCK_PARAM_ON;
51use crate::execution::SKETCH_OBJECT_META;
52use crate::execution::SKETCH_OBJECT_META_SKETCH;
53use crate::execution::Segment;
54use crate::execution::SegmentKind;
55use crate::execution::SegmentRepr;
56use crate::execution::SketchConstraintKind;
57use crate::execution::SketchSurface;
58use crate::execution::SolverArc;
59use crate::execution::StatementKind;
60use crate::execution::TagIdentifier;
61use crate::execution::UnsolvedExpr;
62use crate::execution::UnsolvedSegment;
63use crate::execution::UnsolvedSegmentKind;
64use crate::execution::annotations;
65use crate::execution::annotations::FnAttrs;
66use crate::execution::cad_op::op_from_kcl_value;
67use crate::execution::fn_call::Arg;
68use crate::execution::fn_call::Args;
69use crate::execution::fn_call::unexpected_kw_arg_message;
70use crate::execution::kcl_value::EnumTypeDef;
71use crate::execution::kcl_value::EnumTypeId;
72use crate::execution::kcl_value::EnumValue;
73use crate::execution::kcl_value::FunctionSource;
74use crate::execution::kcl_value::KclFunctionSourceParams;
75use crate::execution::kcl_value::KclObjectKind;
76use crate::execution::kcl_value::TypeDef;
77use crate::execution::memory::SKETCH_PREFIX;
78use crate::execution::memory::{self};
79use crate::execution::sketch_constraint_status_for_sketch;
80use crate::execution::sketch_solve::FreedomAnalysis;
81use crate::execution::sketch_solve::Solved;
82use crate::execution::sketch_solve::UnsatisfiedDirectionalConstraint;
83use crate::execution::sketch_solve::create_segment_scene_objects;
84use crate::execution::sketch_solve::normalize_to_solver_angle_unit;
85use crate::execution::sketch_solve::normalize_to_solver_distance_unit;
86use crate::execution::sketch_solve::solver_convergence_tolerance;
87use crate::execution::sketch_solve::solver_numeric_type;
88use crate::execution::sketch_solve::substitute_sketch_var_in_segment;
89use crate::execution::sketch_solve::substitute_sketch_vars;
90use crate::execution::state::ModuleState;
91use crate::execution::state::SketchBlockState;
92use crate::execution::types::CoercionMode;
93use crate::execution::types::NumericTypeExt;
94use crate::execution::types::PrimitiveType;
95use crate::execution::types::RuntimeType;
96use crate::execution::types::resolve_named_type_def;
97use crate::execution::types::type_value_named_by_segment;
98use crate::modules::ModuleExecutionOutcome;
99use crate::modules::ModuleId;
100use crate::modules::ModuleItems;
101use crate::modules::ModulePath;
102use crate::modules::ModulePathExt as _;
103use crate::modules::ModuleRepr;
104use crate::parsing::ast::types::ABSOLUTE_PATHS_NOT_SUPPORTED;
105use crate::parsing::ast::types::Annotation;
106use crate::parsing::ast::types::ArrayRangeExpression;
107use crate::parsing::ast::types::BinaryExpression;
108use crate::parsing::ast::types::BinaryOperator;
109use crate::parsing::ast::types::BinaryPart;
110use crate::parsing::ast::types::Expr;
111use crate::parsing::ast::types::FunctionExpression;
112use crate::parsing::ast::types::Identifier;
113use crate::parsing::ast::types::ImportPath;
114use crate::parsing::ast::types::ImportSelector;
115use crate::parsing::ast::types::ImportStatement;
116use crate::parsing::ast::types::ItemVisibility;
117use crate::parsing::ast::types::MemberExpression;
118use crate::parsing::ast::types::Name;
119use crate::parsing::ast::types::Node;
120use crate::parsing::ast::types::Program;
121use crate::parsing::ast::types::ReturnStatement;
122use crate::parsing::ast::types::SketchBlock;
123use crate::parsing::ast::types::SketchVar;
124use crate::parsing::ast::types::TagDeclarator;
125use crate::parsing::ast::types::Type;
126use crate::parsing::ast::types::TypeDeclaration;
127use crate::parsing::ast::types::TypeDeclarationDefinition;
128use crate::parsing::ast::types::UnaryExpression;
129use crate::parsing::ast::types::UnaryOperator;
130use crate::parsing::ast::types::VariableDeclaration;
131use crate::std::StdFnProps;
132use crate::std::args::FromKclValue;
133use crate::std::args::TyF64;
134use crate::std::shapes::SketchOrSurface;
135use crate::std::sketch::ensure_sketch_plane_in_engine;
136use crate::std::solver::create_segments_in_engine;
137use crate::std::utils::intersect_lines_2d;
138use crate::std::utils::normalize_rad;
139use crate::std::utils::vec2_dot;
140use crate::std::utils::vec2_len;
141use crate::std::utils::vec2_sub;
142use crate::walk::Visitable;
143
144fn internal_err(message: impl Into<String>, range: impl Into<SourceRange>) -> KclError {
145    KclError::new_internal(KclErrorDetails::new(message.into(), vec![range.into()]))
146}
147
148fn signed_distance_conflict_hint(solve_outcome: &Solved) -> String {
149    let hints = solve_outcome
150        .unsatisfied_directional_constraints
151        .iter()
152        .map(|constraint| match constraint {
153            UnsatisfiedDirectionalConstraint::Horizontal(expected) if *expected > 0.0 => {
154                "Unsatisfied signed horizontalDistance constraint: a positive right-hand side requires the second point to be right of the first (second.x - first.x > 0)."
155            }
156            UnsatisfiedDirectionalConstraint::Horizontal(expected) if *expected < 0.0 => {
157                "Unsatisfied signed horizontalDistance constraint: a negative right-hand side requires the second point to be left of the first (second.x - first.x < 0)."
158            }
159            UnsatisfiedDirectionalConstraint::Horizontal(_) => {
160                "Unsatisfied signed horizontalDistance constraint: a zero right-hand side requires both points to have the same X coordinate."
161            }
162            UnsatisfiedDirectionalConstraint::Vertical(expected) if *expected > 0.0 => {
163                "Unsatisfied signed verticalDistance constraint: a positive right-hand side requires the second point to be above the first (second.y - first.y > 0)."
164            }
165            UnsatisfiedDirectionalConstraint::Vertical(expected) if *expected < 0.0 => {
166                "Unsatisfied signed verticalDistance constraint: a negative right-hand side requires the second point to be below the first (second.y - first.y < 0)."
167            }
168            UnsatisfiedDirectionalConstraint::Vertical(_) => {
169                "Unsatisfied signed verticalDistance constraint: a zero right-hand side requires both points to have the same Y coordinate."
170            }
171        })
172        .collect::<Vec<_>>();
173
174    if hints.is_empty() {
175        String::new()
176    } else {
177        format!(" {}", hints.join(" "))
178    }
179}
180
181fn datum_point_from_constrainable(
182    point: &crate::execution::ConstrainablePoint2d,
183    range: SourceRange,
184) -> Result<ezpz::datatypes::inputs::DatumPoint, KclError> {
185    Ok(ezpz::datatypes::inputs::DatumPoint::new_xy(
186        point.vars.x.to_constraint_id(range)?,
187        point.vars.y.to_constraint_id(range)?,
188    ))
189}
190
191fn push_fixed_origin_point(
192    sketch_block_state: &mut SketchBlockState,
193    sketch_var_ty: NumericType,
194    range: SourceRange,
195) -> Result<ezpz::datatypes::inputs::DatumPoint, KclError> {
196    let origin_x_id = sketch_block_state.next_sketch_var_id();
197    sketch_block_state.sketch_vars.push(KclValue::SketchVar {
198        value: Box::new(crate::execution::SketchVar {
199            id: origin_x_id,
200            initial_value: 0.0,
201            ty: sketch_var_ty,
202            // Synthesized fixed origin coord; not source-backed.
203            node_path: None,
204            meta: vec![],
205        }),
206    });
207    let origin_y_id = sketch_block_state.next_sketch_var_id();
208    sketch_block_state.sketch_vars.push(KclValue::SketchVar {
209        value: Box::new(crate::execution::SketchVar {
210            id: origin_y_id,
211            initial_value: 0.0,
212            ty: sketch_var_ty,
213            // Synthesized fixed origin coord; not source-backed.
214            node_path: None,
215            meta: vec![],
216        }),
217    });
218
219    sketch_block_state
220        .solver_constraints
221        .push(Constraint::Fixed(origin_x_id.to_constraint_id(range)?, 0.0));
222    sketch_block_state
223        .solver_constraints
224        .push(Constraint::Fixed(origin_y_id.to_constraint_id(range)?, 0.0));
225
226    Ok(ezpz::datatypes::inputs::DatumPoint::new_xy(
227        origin_x_id.to_constraint_id(range)?,
228        origin_y_id.to_constraint_id(range)?,
229    ))
230}
231
232fn datum_point_from_constrainable_or_origin(
233    sketch_block_state: &mut SketchBlockState,
234    sketch_var_ty: NumericType,
235    point: &crate::execution::ConstrainablePoint2dOrOrigin,
236    range: SourceRange,
237) -> Result<ezpz::datatypes::inputs::DatumPoint, KclError> {
238    match point {
239        crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => datum_point_from_constrainable(point, range),
240        crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
241            push_fixed_origin_point(sketch_block_state, sketch_var_ty, range)
242        }
243    }
244}
245
246fn datum_line_from_constrainable(
247    line: &crate::execution::ConstrainableLine2d,
248    range: SourceRange,
249) -> Result<ezpz::datatypes::inputs::DatumLineSegment, KclError> {
250    Ok(ezpz::datatypes::inputs::DatumLineSegment::new(
251        ezpz::datatypes::inputs::DatumPoint::new_xy(
252            line.vars[0].x.to_constraint_id(range)?,
253            line.vars[0].y.to_constraint_id(range)?,
254        ),
255        ezpz::datatypes::inputs::DatumPoint::new_xy(
256            line.vars[1].x.to_constraint_id(range)?,
257            line.vars[1].y.to_constraint_id(range)?,
258        ),
259    ))
260}
261
262fn push_hidden_sketch_point(
263    sketch_block_state: &mut SketchBlockState,
264    sketch_var_ty: NumericType,
265    initial: [f64; 2],
266    range: SourceRange,
267) -> Result<DatumPoint, KclError> {
268    let x_id = sketch_block_state.next_sketch_var_id();
269    sketch_block_state.sketch_vars.push(KclValue::SketchVar {
270        value: Box::new(crate::execution::SketchVar {
271            id: x_id,
272            initial_value: initial[0],
273            ty: sketch_var_ty,
274            node_path: None,
275            meta: vec![],
276        }),
277    });
278    let y_id = sketch_block_state.next_sketch_var_id();
279    sketch_block_state.sketch_vars.push(KclValue::SketchVar {
280        value: Box::new(crate::execution::SketchVar {
281            id: y_id,
282            initial_value: initial[1],
283            ty: sketch_var_ty,
284            node_path: None,
285            meta: vec![],
286        }),
287    });
288
289    Ok(DatumPoint::new_xy(
290        x_id.to_constraint_id(range)?,
291        y_id.to_constraint_id(range)?,
292    ))
293}
294
295fn front_angle_sector(sector: AngleSector) -> u8 {
296    match sector {
297        AngleSector::One => 1,
298        AngleSector::Two => 2,
299        AngleSector::Three => 3,
300        AngleSector::Four => 4,
301    }
302}
303
304#[derive(Clone, Copy)]
305struct AngleSectorRay {
306    line_index: usize,
307    direction: AngleRayDirection,
308}
309
310fn angle_sector_rays(sector: AngleSector, is_inverse: bool) -> [AngleSectorRay; 2] {
311    let rays = match sector {
312        AngleSector::One => [
313            AngleSectorRay {
314                line_index: 0,
315                direction: AngleRayDirection::Forward,
316            },
317            AngleSectorRay {
318                line_index: 1,
319                direction: AngleRayDirection::Forward,
320            },
321        ],
322        AngleSector::Two => [
323            AngleSectorRay {
324                line_index: 1,
325                direction: AngleRayDirection::Forward,
326            },
327            AngleSectorRay {
328                line_index: 0,
329                direction: AngleRayDirection::Reverse,
330            },
331        ],
332        AngleSector::Three => [
333            AngleSectorRay {
334                line_index: 0,
335                direction: AngleRayDirection::Reverse,
336            },
337            AngleSectorRay {
338                line_index: 1,
339                direction: AngleRayDirection::Reverse,
340            },
341        ],
342        AngleSector::Four => [
343            AngleSectorRay {
344                line_index: 1,
345                direction: AngleRayDirection::Reverse,
346            },
347            AngleSectorRay {
348                line_index: 0,
349                direction: AngleRayDirection::Forward,
350            },
351        ],
352    };
353    if is_inverse { [rays[1], rays[0]] } else { rays }
354}
355
356fn line_endpoint_datum(
357    line: &ConstrainableLine2d,
358    endpoint_index: usize,
359    range: SourceRange,
360) -> Result<DatumPoint, KclError> {
361    let Some(endpoint) = line.vars.get(endpoint_index) else {
362        return Err(internal_err("Invalid angle line endpoint index", range));
363    };
364
365    Ok(DatumPoint::new_xy(
366        endpoint.x.to_constraint_id(range)?,
367        endpoint.y.to_constraint_id(range)?,
368    ))
369}
370
371fn representative_angle_endpoint(
372    line: &ConstrainableLine2d,
373    initial_line: ([f64; 2], [f64; 2]),
374    vertex: [f64; 2],
375    range: SourceRange,
376) -> Result<(DatumPoint, AngleRayDirection), KclError> {
377    let start_delta = vec2_sub(initial_line.0, vertex);
378    let end_delta = vec2_sub(initial_line.1, vertex);
379    let endpoint_index = if vec2_len(end_delta) >= vec2_len(start_delta) {
380        1
381    } else {
382        0
383    };
384    let endpoint_delta = if endpoint_index == 1 { end_delta } else { start_delta };
385    if vec2_len(endpoint_delta) <= 1e-9 {
386        return Err(KclError::new_semantic(KclErrorDetails::new(
387            "angleDimension(lines = ..., sector = ...) requires each line to have an endpoint away from the intersection"
388                .to_owned(),
389            vec![range],
390        )));
391    }
392
393    let line_direction = vec2_sub(initial_line.1, initial_line.0);
394    let direction = if vec2_dot(endpoint_delta, line_direction) >= 0.0 {
395        AngleRayDirection::Forward
396    } else {
397        AngleRayDirection::Reverse
398    };
399
400    Ok((line_endpoint_datum(line, endpoint_index, range)?, direction))
401}
402
403fn remap_angle_for_representative_rays(
404    requested_rays: [AngleSectorRay; 2],
405    representative_directions: [AngleRayDirection; 2],
406    desired_angle: ezpz::datatypes::Angle,
407) -> ezpz::datatypes::Angle {
408    let mut requested_directions = representative_directions;
409    for ray in requested_rays {
410        requested_directions[ray.line_index] = ray.direction;
411    }
412
413    let sign_offset = if (requested_directions[0] != representative_directions[0])
414        ^ (requested_directions[1] != representative_directions[1])
415    {
416        std::f64::consts::PI
417    } else {
418        0.0
419    };
420
421    let desired = desired_angle.to_radians();
422    let representative_angle = if requested_rays[0].line_index == 0 {
423        desired - sign_offset
424    } else {
425        -desired - sign_offset
426    };
427
428    ezpz::datatypes::Angle::from_radians(normalize_rad(representative_angle))
429}
430
431struct PointsAtAngleLineData {
432    initial_vertex: [f64; 2],
433    representative_points: [DatumPoint; 2],
434    angle_kind: ezpz::datatypes::AngleKind,
435}
436
437enum AngleConstraintLowering {
438    LinesAtAngle(Box<PendingLegacyAngleRefactorMeta>),
439    PointsAtAngle(PointsAtAngleLineData),
440}
441
442fn solved_angle_line(line: &ConstrainableLine2d, final_values: &[f64]) -> Option<([f64; 2], [f64; 2])> {
443    let point = |index: usize| {
444        let point = line.vars.get(index)?;
445        Some([*final_values.get(point.x.0)?, *final_values.get(point.y.0)?])
446    };
447    Some((point(0)?, point(1)?))
448}
449
450fn angle_ray_vector(lines: [[f64; 2]; 2], ray: AngleSectorRay) -> [f64; 2] {
451    let direction = lines[ray.line_index];
452    match ray.direction {
453        AngleRayDirection::Forward => direction,
454        AngleRayDirection::Reverse => [-direction[0], -direction[1]],
455    }
456}
457
458fn directed_angle(from: [f64; 2], to: [f64; 2]) -> f64 {
459    let cross = from[0] * to[1] - from[1] * to[0];
460    libm::atan2(cross, vec2_dot(from, to)).rem_euclid(std::f64::consts::TAU)
461}
462
463fn circular_angle_distance(a: f64, b: f64) -> f64 {
464    let delta = (a - b).abs().rem_euclid(std::f64::consts::TAU);
465    libm::fmin(delta, std::f64::consts::TAU - delta)
466}
467
468fn legacy_angle_arc_midpoint_angle(
469    lines: [([f64; 2], [f64; 2]); 2],
470    directions: [[f64; 2]; 2],
471    vertex: [f64; 2],
472    desired: f64,
473) -> f64 {
474    let signed_distances = lines.map(|line| {
475        let direction = vec2_sub(line.1, line.0);
476        let length = vec2_len(direction);
477        [
478            vec2_dot(vec2_sub(line.0, vertex), direction) / length,
479            vec2_dot(vec2_sub(line.1, vertex), direction) / length,
480        ]
481    });
482    let overlap = [
483        libm::fmax(signed_distances[0][0], signed_distances[1][0]),
484        libm::fmin(signed_distances[0][1], signed_distances[1][1]),
485    ];
486    // Match calculateArcRenderInput in src/machines/sketchSolve/constraints/AngleConstraintBuilder.ts;
487    // the radius sign chooses the line 0 ray on which the legacy arc starts.
488    let radius = if overlap[1] >= overlap[0] {
489        let near_start = overlap[0] + (overlap[1] - overlap[0]) * 0.15;
490        let near_end = overlap[0] + (overlap[1] - overlap[0]) * 0.85;
491        if near_start.abs() < near_end.abs() {
492            near_start
493        } else {
494            near_end
495        }
496    } else {
497        let mut distances = signed_distances.into_iter().flatten().collect::<Vec<_>>();
498        distances.sort_by(f64::total_cmp);
499        distances[1]
500    };
501    let start = if radius < 0.0 {
502        [-directions[0][0], -directions[0][1]]
503    } else {
504        directions[0]
505    };
506
507    (libm::atan2(start[1], start[0]) + desired * 0.5).rem_euclid(std::f64::consts::TAU)
508}
509
510fn finalize_legacy_angle_refactor_meta(
511    pending: &PendingLegacyAngleRefactorMeta,
512    final_values: &[f64],
513) -> Option<LegacyAngleRefactorMeta> {
514    let line0 = solved_angle_line(&pending.lines[0], final_values)?;
515    let line1 = solved_angle_line(&pending.lines[1], final_values)?;
516    let vertex = intersect_lines_2d(line0, line1)?;
517    let directions = [vec2_sub(line0.1, line0.0), vec2_sub(line1.1, line1.0)];
518    if directions.iter().any(|direction| vec2_len(*direction) <= 1e-9) {
519        return None;
520    }
521
522    let desired = pending.desired_angle_radians.rem_euclid(std::f64::consts::TAU);
523    let sectors = [
524        AngleSector::One,
525        AngleSector::Two,
526        AngleSector::Three,
527        AngleSector::Four,
528    ];
529    let mut candidates = Vec::new();
530    for sector in sectors {
531        for inverse in [false, true] {
532            let rays = angle_sector_rays(sector, inverse);
533            let from = angle_ray_vector(directions, rays[0]);
534            let to = angle_ray_vector(directions, rays[1]);
535            if circular_angle_distance(directed_angle(from, to), desired) <= 1e-5 {
536                let midpoint = libm::atan2(from[1], from[0]) + desired * 0.5;
537                candidates.push((sector, inverse, midpoint.rem_euclid(std::f64::consts::TAU)));
538            }
539        }
540    }
541
542    let arc_midpoint_angle = legacy_angle_arc_midpoint_angle([line0, line1], directions, vertex, desired);
543    let selected = candidates.into_iter().min_by(|a, b| {
544        circular_angle_distance(a.2, arc_midpoint_angle).total_cmp(&circular_angle_distance(b.2, arc_midpoint_angle))
545    })?;
546
547    Some(LegacyAngleRefactorMeta {
548        source_range: pending.source_range,
549        sector: front_angle_sector(selected.0),
550        inverse: selected.1,
551    })
552}
553
554fn push_points_at_angle_for_lines(
555    sketch_block_state: &mut SketchBlockState,
556    sketch_var_ty: NumericType,
557    lines: [&ConstrainableLine2d; 2],
558    data: PointsAtAngleLineData,
559    range: SourceRange,
560) -> Result<(), KclError> {
561    let solver_line0 = datum_line_from_constrainable(lines[0], range)?;
562    let solver_line1 = datum_line_from_constrainable(lines[1], range)?;
563    let vertex = push_hidden_sketch_point(sketch_block_state, sketch_var_ty, data.initial_vertex, range)?;
564
565    sketch_block_state
566        .solver_constraints
567        .push(Constraint::PointLineDistance(vertex, solver_line0, 0.0));
568    sketch_block_state
569        .solver_constraints
570        .push(Constraint::PointLineDistance(vertex, solver_line1, 0.0));
571    sketch_block_state.solver_constraints.push(Constraint::PointsAtAngle(
572        vertex,
573        data.representative_points[0],
574        data.representative_points[1],
575        data.angle_kind,
576    ));
577
578    Ok(())
579}
580
581fn sketch_var_initial_value(
582    sketch_vars: &[KclValue],
583    id: crate::execution::SketchVarId,
584    exec_state: &mut ExecState,
585    range: SourceRange,
586    description: &str,
587) -> Result<f64, KclError> {
588    sketch_vars
589        .get(id.0)
590        .and_then(KclValue::as_sketch_var)
591        .map(|sketch_var| {
592            sketch_var
593                .initial_value_to_solver_units(exec_state, range, description)
594                .map(|value| value.n)
595        })
596        .transpose()?
597        .ok_or_else(|| internal_err(format!("Missing sketch variable initial value for id {}", id.0), range))
598}
599
600fn constrainable_point_initial_position(
601    sketch_vars: &[KclValue],
602    point: &crate::execution::ConstrainablePoint2d,
603    exec_state: &mut ExecState,
604    range: SourceRange,
605    description: &str,
606) -> Result<[f64; 2], KclError> {
607    Ok([
608        sketch_var_initial_value(sketch_vars, point.vars.x, exec_state, range, description)?,
609        sketch_var_initial_value(sketch_vars, point.vars.y, exec_state, range, description)?,
610    ])
611}
612
613fn constrainable_point_or_origin_initial_position(
614    sketch_vars: &[KclValue],
615    point: &crate::execution::ConstrainablePoint2dOrOrigin,
616    exec_state: &mut ExecState,
617    range: SourceRange,
618    description: &str,
619) -> Result<[f64; 2], KclError> {
620    match point {
621        crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
622            constrainable_point_initial_position(sketch_vars, point, exec_state, range, description)
623        }
624        crate::execution::ConstrainablePoint2dOrOrigin::Origin => Ok([0.0, 0.0]),
625    }
626}
627
628// These helpers read the current sketch variable guesses so hidden support
629// geometry starts near the geometry the user selected. The visible constraint
630// value still comes from the KCL RHS. These initial values are only solver
631// seeds for new hidden points/radii, which helps ezpz converge to the intended
632// geometric branch instead of an equivalent but visually surprising one.
633fn constrainable_line_initial_positions(
634    sketch_vars: &[KclValue],
635    line: &crate::execution::ConstrainableLine2d,
636    exec_state: &mut ExecState,
637    range: SourceRange,
638    description: &str,
639) -> Result<([f64; 2], [f64; 2]), KclError> {
640    let start = crate::execution::ConstrainablePoint2d {
641        vars: line.vars[0].clone(),
642        object_id: line.object_id,
643    };
644    let end = crate::execution::ConstrainablePoint2d {
645        vars: line.vars[1].clone(),
646        object_id: line.object_id,
647    };
648    Ok((
649        constrainable_point_initial_position(sketch_vars, &start, exec_state, range, description)?,
650        constrainable_point_initial_position(sketch_vars, &end, exec_state, range, description)?,
651    ))
652}
653
654fn projected_point_on_line_initial_position(
655    sketch_vars: &[KclValue],
656    point: &crate::execution::ConstrainablePoint2dOrOrigin,
657    line: &crate::execution::ConstrainableLine2d,
658    exec_state: &mut ExecState,
659    range: SourceRange,
660) -> Result<[f64; 2], KclError> {
661    let point = constrainable_point_or_origin_initial_position(
662        sketch_vars,
663        point,
664        exec_state,
665        range,
666        "point-line distance initial point",
667    )?;
668    let (line_start, line_end) =
669        constrainable_line_initial_positions(sketch_vars, line, exec_state, range, "point-line distance initial line")?;
670    let dx = line_end[0] - line_start[0];
671    let dy = line_end[1] - line_start[1];
672    let len_sq = dx * dx + dy * dy;
673    if len_sq == 0.0 {
674        return Err(KclError::new_semantic(KclErrorDetails::new(
675            "distance() line input must have non-zero length".to_owned(),
676            vec![range],
677        )));
678    }
679
680    // Project the point onto the infinite target line. `t` is the scalar
681    // projection of the point-start vector onto the line direction.
682    let t = ((point[0] - line_start[0]) * dx + (point[1] - line_start[1]) * dy) / len_sq;
683    Ok([line_start[0] + t * dx, line_start[1] + t * dy])
684}
685
686fn constrainable_points_initial_distance(
687    sketch_vars: &[KclValue],
688    point0: &crate::execution::ConstrainablePoint2d,
689    point1: &crate::execution::ConstrainablePoint2d,
690    exec_state: &mut ExecState,
691    range: SourceRange,
692    description: &str,
693) -> Result<f64, KclError> {
694    let p0 = constrainable_point_initial_position(sketch_vars, point0, exec_state, range, description)?;
695    let p1 = constrainable_point_initial_position(sketch_vars, point1, exec_state, range, description)?;
696    Ok(libm::hypot(p0[0] - p1[0], p0[1] - p1[1]))
697}
698
699// Circular distance lowering needs an ezpz DatumCircle, but arcs/circles in
700// KCL are represented by points. This bundles the center/start/end datums and
701// seeds a hidden radius variable from the current center-start distance.
702#[derive(Clone, Copy)]
703struct CircularDistanceDatums {
704    center: ezpz::datatypes::inputs::DatumPoint,
705    start: ezpz::datatypes::inputs::DatumPoint,
706    end: Option<ezpz::datatypes::inputs::DatumPoint>,
707    radius_initial_value: f64,
708}
709
710fn circular_distance_datums(
711    sketch_vars: &[KclValue],
712    center: &crate::execution::ConstrainablePoint2d,
713    start: &crate::execution::ConstrainablePoint2d,
714    end: Option<&crate::execution::ConstrainablePoint2d>,
715    exec_state: &mut ExecState,
716    range: SourceRange,
717) -> Result<CircularDistanceDatums, KclError> {
718    Ok(CircularDistanceDatums {
719        center: datum_point_from_constrainable(center, range)?,
720        start: datum_point_from_constrainable(start, range)?,
721        end: end.map(|end| datum_point_from_constrainable(end, range)).transpose()?,
722        radius_initial_value: constrainable_points_initial_distance(
723            sketch_vars,
724            center,
725            start,
726            exec_state,
727            range,
728            "circular distance radius initial value",
729        )?,
730    })
731}
732
733fn circular_circular_support_initial_position(
734    sketch_vars: &[KclValue],
735    center0: &crate::execution::ConstrainablePoint2d,
736    center1: &crate::execution::ConstrainablePoint2d,
737    radius0: f64,
738    distance_value: f64,
739    exec_state: &mut ExecState,
740    range: SourceRange,
741) -> Result<[f64; 2], KclError> {
742    let center0_initial =
743        constrainable_point_initial_position(sketch_vars, center0, exec_state, range, "circular distance center")?;
744    let center1_initial =
745        constrainable_point_initial_position(sketch_vars, center1, exec_state, range, "circular distance center")?;
746    let dx = center1_initial[0] - center0_initial[0];
747    let dy = center1_initial[1] - center0_initial[1];
748    let center_distance = libm::hypot(dx, dy);
749    // The circular-circular distance lowering uses a hidden spacer circle
750    // with radius d/2 tangent to both targets. Seed its center on the
751    // center-to-center ray at r0 + d/2 so the nonlinear solver starts on the
752    // intended between-centers tangency branch.
753    let support_distance = radius0 + distance_value / 2.0;
754
755    if center_distance <= f64::EPSILON {
756        // Concentric initial guesses have no center-to-center direction, so
757        // pick a deterministic horizontal ray for the hidden spacer point.
758        return Ok([center0_initial[0] + support_distance, center0_initial[1]]);
759    }
760
761    Ok([
762        center0_initial[0] + dx / center_distance * support_distance,
763        center0_initial[1] + dy / center_distance * support_distance,
764    ])
765}
766
767fn push_circular_radius_constraints(
768    sketch_block_state: &mut SketchBlockState,
769    sketch_var_ty: NumericType,
770    circular: CircularDistanceDatums,
771    range: SourceRange,
772) -> Result<ezpz::datatypes::inputs::DatumCircle, KclError> {
773    // Create a hidden radius variable and constrain the circular segment's
774    // defining points to it. For arcs, both start and end stay on the same
775    // radius; for circles, the start point alone defines the radius.
776    let circular_radius_id = sketch_block_state.next_sketch_var_id();
777    sketch_block_state.sketch_vars.push(KclValue::SketchVar {
778        value: Box::new(crate::execution::SketchVar {
779            id: circular_radius_id,
780            initial_value: circular.radius_initial_value,
781            ty: sketch_var_ty,
782            // Synthesized hidden radius for circular distance; not source-backed.
783            node_path: None,
784            meta: vec![],
785        }),
786    });
787    let circular_radius = ezpz::datatypes::inputs::DatumDistance::new(circular_radius_id.to_constraint_id(range)?);
788
789    sketch_block_state.solver_constraints.push(Constraint::DistanceVar(
790        circular.start,
791        circular.center,
792        circular_radius,
793    ));
794    if let Some(end) = circular.end {
795        sketch_block_state
796            .solver_constraints
797            .push(Constraint::DistanceVar(end, circular.center, circular_radius));
798    }
799
800    Ok(ezpz::datatypes::inputs::DatumCircle {
801        center: circular.center,
802        radius: circular_radius,
803    })
804}
805
806fn push_circular_distance_constraints(
807    sketch_block_state: &mut SketchBlockState,
808    sketch_var_ty: NumericType,
809    target_point: ezpz::datatypes::inputs::DatumPoint,
810    circular: CircularDistanceDatums,
811    distance_value: f64,
812    range: SourceRange,
813) -> Result<(), KclError> {
814    let circular_target = push_circular_radius_constraints(sketch_block_state, sketch_var_ty, circular, range)?;
815
816    // Point-circular distance becomes tangency between the target circle and
817    // a hidden circle centered on the point with radius equal to the distance.
818    let target_distance_id = sketch_block_state.next_sketch_var_id();
819    sketch_block_state.sketch_vars.push(KclValue::SketchVar {
820        value: Box::new(crate::execution::SketchVar {
821            id: target_distance_id,
822            initial_value: distance_value,
823            ty: sketch_var_ty,
824            // Synthesized hidden distance for point-circular tangency; not source-backed.
825            node_path: None,
826            meta: vec![],
827        }),
828    });
829    let target_distance = ezpz::datatypes::inputs::DatumDistance::new(target_distance_id.to_constraint_id(range)?);
830
831    sketch_block_state
832        .solver_constraints
833        .push(Constraint::Fixed(target_distance.id, distance_value));
834
835    let target_circle = ezpz::datatypes::inputs::DatumCircle {
836        center: target_point,
837        radius: target_distance,
838    };
839    sketch_block_state
840        .solver_constraints
841        .push(Constraint::CircleTangentToCircle(
842            target_circle,
843            circular_target,
844            ezpz::CircleSide::Exterior,
845        ));
846
847    Ok(())
848}
849
850fn sketch_on_cache_name(sketch_id: ObjectId) -> String {
851    format!("{SKETCH_PREFIX}{}_on", sketch_id.0)
852}
853
854fn default_plane_name_from_expr(expr: &Expr) -> Option<crate::engine::PlaneName> {
855    fn parse_name(name: &str, negative: bool) -> Option<crate::engine::PlaneName> {
856        use crate::engine::PlaneName;
857
858        match (name, negative) {
859            ("XY", false) => Some(PlaneName::Xy),
860            ("XY", true) => Some(PlaneName::NegXy),
861            ("XZ", false) => Some(PlaneName::Xz),
862            ("XZ", true) => Some(PlaneName::NegXz),
863            ("YZ", false) => Some(PlaneName::Yz),
864            ("YZ", true) => Some(PlaneName::NegYz),
865            _ => None,
866        }
867    }
868
869    match expr {
870        Expr::Name(name) => {
871            if !name.path.is_empty() {
872                return None;
873            }
874            parse_name(&name.name.name, false)
875        }
876        Expr::UnaryExpression(unary) => {
877            if unary.operator != UnaryOperator::Neg {
878                return None;
879            }
880            let crate::parsing::ast::types::BinaryPart::Name(name) = &unary.argument else {
881                return None;
882            };
883            if !name.path.is_empty() {
884                return None;
885            }
886            parse_name(&name.name.name, true)
887        }
888        _ => None,
889    }
890}
891
892fn sketch_on_frontend_plane(
893    arguments: &[crate::parsing::ast::types::LabeledArg],
894    on_object_id: kcl_api::ObjectId,
895) -> kcl_api::Plane {
896    for arg in arguments {
897        let Some(label) = &arg.label else {
898            continue;
899        };
900        if label.name != SKETCH_BLOCK_PARAM_ON {
901            continue;
902        }
903        if let Some(name) = default_plane_name_from_expr(&arg.arg) {
904            return kcl_api::Plane::Default(name);
905        }
906        break;
907    }
908
909    kcl_api::Plane::Object(on_object_id)
910}
911
912impl<'a> StatementKind<'a> {
913    fn expect_name(&self) -> &'a str {
914        match self {
915            StatementKind::Declaration { name } => name,
916            StatementKind::Expression => unreachable!(),
917        }
918    }
919}
920
921impl ExecutorContext {
922    /// Returns true if importing the prelude should be skipped.
923    pub(super) async fn handle_annotations(
924        &self,
925        annotations: impl Iterator<Item = &Node<Annotation>>,
926        body_type: BodyType,
927        exec_state: &mut ExecState,
928    ) -> Result<bool, KclError> {
929        let mut no_prelude = false;
930        for annotation in annotations {
931            // The attribute that customizes diagnostics is `@warnings` before
932            // KCL 3.0 and `@diagnostics` in KCL 3.0 and later. Look it up per
933            // annotation since a preceding `@settings` may have changed the
934            // version.
935            let diagnostics_attr = annotations::diagnostics_attr_name(exec_state.kcl_version());
936            if annotation.name() == Some(annotations::SETTINGS) {
937                if matches!(body_type, BodyType::Root) {
938                    let (updated_len, updated_angle) =
939                        exec_state.mod_local.settings.update_from_annotation(annotation)?;
940                    if updated_len {
941                        exec_state.mod_local.explicit_length_units = true;
942                    }
943                    if updated_angle {
944                        if exec_state.kcl_version() >= KclVersion::V3Preview {
945                            return Err(KclError::new_semantic(KclErrorDetails::new(
946                                "The `defaultAngleUnit` setting was removed in KCL 3.0; use explicit units for angles"
947                                    .to_owned(),
948                                annotation.as_source_ranges(),
949                            )));
950                        }
951                        exec_state.warn(
952                            CompilationIssue::err(
953                                annotation.as_source_range(),
954                                "The `defaultAngleUnit` setting is deprecated; use explicit units for angles",
955                            ),
956                            annotations::WARN_ANGLE_UNITS,
957                        );
958                    }
959                } else {
960                    exec_state.err(CompilationIssue::err(
961                        annotation.as_source_range(),
962                        "Settings can only be modified at the top level scope of a file",
963                    ));
964                }
965            } else if annotation.name() == Some(annotations::NO_PRELUDE) {
966                if matches!(body_type, BodyType::Root) {
967                    no_prelude = true;
968                } else {
969                    exec_state.err(CompilationIssue::err(
970                        annotation.as_source_range(),
971                        "The standard library can only be skipped at the top level scope of a file",
972                    ));
973                }
974            } else if annotation.name() == Some(diagnostics_attr) {
975                // TODO we should support setting warnings for the whole project, not just one file
976                if matches!(body_type, BodyType::Root) {
977                    let props = annotations::expect_properties(diagnostics_attr, annotation)?;
978                    for p in props {
979                        match &*p.inner.key.name {
980                            annotations::WARN_ALLOW => {
981                                let allowed = annotations::many_of(
982                                    &p.inner.value,
983                                    &annotations::WARN_VALUES,
984                                    diagnostics_attr,
985                                    annotation.as_source_range(),
986                                )?;
987                                exec_state.mod_local.allowed_warnings = allowed;
988                            }
989                            annotations::WARN_DENY => {
990                                let denied = annotations::many_of(
991                                    &p.inner.value,
992                                    &annotations::WARN_VALUES,
993                                    diagnostics_attr,
994                                    annotation.as_source_range(),
995                                )?;
996                                exec_state.mod_local.denied_warnings = denied;
997                            }
998                            name => {
999                                return Err(KclError::new_semantic(KclErrorDetails::new(
1000                                    format!(
1001                                        "Unexpected {diagnostics_attr} key: `{name}`; expected one of `{}`, `{}`",
1002                                        annotations::WARN_ALLOW,
1003                                        annotations::WARN_DENY,
1004                                    ),
1005                                    vec![annotation.as_source_range()],
1006                                )));
1007                            }
1008                        }
1009                    }
1010                } else {
1011                    let message = match diagnostics_attr {
1012                        annotations::WARNINGS => "Warnings can only be customized at the top level scope of a file",
1013                        _ => "Diagnostics can only be customized at the top level scope of a file",
1014                    };
1015                    exec_state.err(CompilationIssue::err(annotation.as_source_range(), message));
1016                }
1017            } else if annotation.name() == Some(annotations::WARNINGS) {
1018                // KCL 3.0 renamed `@warnings` to `@diagnostics`. This is only
1019                // reached in KCL 3.0-preview or later, since before that the
1020                // attribute is handled above. Report a non-fatal error with
1021                // the fix, and ignore the attribute.
1022                let mut issue = CompilationIssue::err(
1023                    annotation.as_source_range(),
1024                    format!(
1025                        "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.",
1026                        old = annotations::WARNINGS,
1027                        new = annotations::DIAGNOSTICS,
1028                        allow = annotations::WARN_ALLOW,
1029                        deny = annotations::WARN_DENY,
1030                    ),
1031                );
1032                if let Some(name) = &annotation.name {
1033                    issue = issue.with_suggestion(
1034                        format!("Rename to `@{}`", annotations::DIAGNOSTICS),
1035                        annotations::DIAGNOSTICS,
1036                        Some(name.as_source_range()),
1037                        crate::errors::Tag::None,
1038                    );
1039                }
1040                exec_state.err(issue);
1041            } else {
1042                exec_state.warn(
1043                    CompilationIssue::err(annotation.as_source_range(), "Unknown annotation"),
1044                    annotations::WARN_UNKNOWN_ATTR,
1045                );
1046            }
1047        }
1048        Ok(no_prelude)
1049    }
1050
1051    pub(super) async fn exec_module_body(
1052        &self,
1053        program: &Node<Program>,
1054        exec_state: &mut ExecState,
1055        preserve_mem: PreserveMem,
1056        module_id: ModuleId,
1057        path: &ModulePath,
1058    ) -> Result<ModuleExecutionOutcome, (KclError, Option<EnvironmentRef>, Option<ModuleArtifactState>)> {
1059        crate::log::log(format!("enter module {path} {}", exec_state.stack()));
1060
1061        // Check the imported file's declared version and effective keyword
1062        // restrictions before executing its body.
1063        exec_state
1064            .validate_imported_module(path, module_id, program, None)
1065            .map_err(|err| (err, None, None))?;
1066
1067        // When executing only the new statements in incremental execution or
1068        // mock executing for sketch mode, we need the scene objects that were
1069        // created during the last execution, which are in the execution cache.
1070        // The cache is read to create the initial module state. Depending on
1071        // whether it's mock execution or engine execution, it's rehydrated
1072        // differently, so we need to clone them from a different place. Then
1073        // make sure the object ID generator matches the number of existing
1074        // scene objects.
1075        let mut local_state = ModuleState::new(
1076            path.clone(),
1077            exec_state.stack().memory.clone(),
1078            Some(module_id),
1079            exec_state.mod_local.sketch_mode,
1080            exec_state.mod_local.freedom_analysis,
1081        );
1082        match preserve_mem {
1083            PreserveMem::Always => {
1084                exec_state
1085                    .mod_local
1086                    .artifacts
1087                    .restore_scene_objects(&exec_state.global.root_module_artifacts.scene_objects);
1088            }
1089            PreserveMem::Normal => {
1090                local_state
1091                    .artifacts
1092                    .restore_scene_objects(&exec_state.mod_local.artifacts.scene_objects);
1093                std::mem::swap(&mut exec_state.mod_local, &mut local_state);
1094            }
1095        }
1096
1097        let no_prelude = self
1098            .handle_annotations(program.inner_attrs.iter(), crate::execution::BodyType::Root, exec_state)
1099            .await
1100            .map_err(|err| (err, None, None))?;
1101
1102        if preserve_mem.normal() {
1103            exec_state
1104                .mut_stack()
1105                .push_new_root_env(!no_prelude)
1106                .map_err(|err| (err, None, None))?;
1107        }
1108
1109        let result = self
1110            .exec_block(program, exec_state, crate::execution::BodyType::Root)
1111            .await;
1112
1113        let env_ref = match preserve_mem {
1114            PreserveMem::Always => exec_state.mut_stack().pop_and_preserve_env(),
1115            PreserveMem::Normal => exec_state.mut_stack().pop_env(),
1116        }
1117        .map_err(|err| (err, None, None))?;
1118        let module_artifacts = match preserve_mem {
1119            PreserveMem::Always => std::mem::take(&mut exec_state.mod_local.artifacts),
1120            PreserveMem::Normal => {
1121                std::mem::swap(&mut exec_state.mod_local, &mut local_state);
1122                local_state.artifacts
1123            }
1124        };
1125
1126        crate::log::log(format!("leave {path}"));
1127
1128        result
1129            .map_err(|err| (err, Some(env_ref), Some(module_artifacts.clone())))
1130            .map(|last_expr| ModuleExecutionOutcome {
1131                last_expr: last_expr.map(|value_cf| value_cf.into_value()),
1132                environment: env_ref,
1133                exports: local_state.module_exports,
1134                artifacts: module_artifacts,
1135                not_yet_added: local_state
1136                    .not_yet_added
1137                    .into_iter()
1138                    .filter(|(_, record)| record.exported)
1139                    .map(|(key, record)| (key, record.item))
1140                    .collect(),
1141            })
1142    }
1143
1144    /// Execute an AST's program.
1145    #[async_recursion]
1146    pub(super) async fn exec_block<'a, B>(
1147        &'a self,
1148        block: &'a B,
1149        exec_state: &mut ExecState,
1150        body_type: BodyType,
1151    ) -> Result<Option<KclValueControlFlow>, KclError>
1152    where
1153        B: crate::execution::machine::ToMachineBlock + Sync,
1154    {
1155        crate::execution::machine::run_block(self, block.to_machine_block(), exec_state, body_type).await
1156    }
1157
1158    /// Execute an import statement. Flat: this does not evaluate KCL
1159    /// sub-expressions; module execution happens in its own fresh root.
1160    pub(super) async fn exec_import_statement(
1161        &self,
1162        import_stmt: &Node<ImportStatement>,
1163        body_type: BodyType,
1164        exec_state: &mut ExecState,
1165    ) -> Result<(), KclError> {
1166        if !matches!(body_type, BodyType::Root) {
1167            return Err(KclError::new_semantic(KclErrorDetails::new(
1168                "Imports are only supported at the top-level of a file.".to_owned(),
1169                vec![import_stmt.into()],
1170            )));
1171        }
1172
1173        let source_range = SourceRange::from(import_stmt);
1174        let attrs = &import_stmt.outer_attrs;
1175        let module_path = ModulePath::from_import_path(
1176            &import_stmt.path,
1177            &self.settings.project_directory,
1178            &exec_state.mod_local.path,
1179        )?;
1180        if matches!(&module_path, ModulePath::Std { value } if value == "view")
1181            && !exec_state.entry_point_version_is_v3_or_higher()
1182        {
1183            if matches!(&exec_state.mod_local.path, ModulePath::Std { value } if value == "prelude") {
1184                // The prelude must remain usable before V3, but its view module
1185                // must be absent from those programs.
1186                let added_in = annotations::VersionConstraint::new(3, 0);
1187                exec_state.record_not_yet_added(format!("{}view", memory::MODULE_PREFIX), added_in, true);
1188                return Ok(());
1189            }
1190            return Err(KclError::new_semantic(KclErrorDetails::new(
1191                format!(
1192                    "The `std::view` module requires KCL 3.0-preview, but this program uses KCL {}.",
1193                    exec_state.entry_point_kcl_version().as_str()
1194                ),
1195                vec![source_range],
1196            )));
1197        }
1198        let module_id = self
1199            .open_module(&import_stmt.path, attrs, &module_path, exec_state, source_range)
1200            .await?;
1201
1202        // Validate the imported file at the import site as well. Mock execution
1203        // runs a whole-module import's body only when referenced, so this is
1204        // the only check for an unreferenced module. Engine execution already
1205        // validated every imported module before executing the root body.
1206        if let ImportPath::Kcl { .. } = &import_stmt.path
1207            && let Some(ModuleRepr::Kcl(program, _)) =
1208                exec_state.global.module_infos.get(&module_id).map(|info| &info.repr)
1209        {
1210            exec_state.validate_imported_module(&module_path, module_id, program, Some(source_range))?;
1211        }
1212
1213        if let ModulePath::Local { value, .. } = &module_path {
1214            let name = import_stmt
1215                .module_name()
1216                .unwrap_or_else(|| value.file_name().unwrap_or_default());
1217            exec_state.push_op(Operation::ModuleInstance {
1218                name,
1219                module_id,
1220                glob: matches!(import_stmt.selector, ImportSelector::Glob(_)),
1221                node_path: NodePath::placeholder(),
1222                source_range,
1223            });
1224        }
1225
1226        match &import_stmt.selector {
1227            ImportSelector::List { items } => {
1228                let ModuleItems {
1229                    environment: env_ref,
1230                    exports: module_exports,
1231                    not_yet_added,
1232                } = self.exec_module_for_items(module_id, exec_state, source_range).await?;
1233                for import_item in items {
1234                    // Extract the item from the module.
1235                    let mem = &exec_state.stack().memory;
1236                    let mut value = mem.get_from_owned(&import_item.name.name, env_ref, import_item.into(), 0);
1237                    let ty_name = format!("{}{}", memory::TYPE_PREFIX, import_item.name.name);
1238                    let mut ty = mem.get_from_owned(&ty_name, env_ref, import_item.into(), 0);
1239                    let mod_name = format!("{}{}", memory::MODULE_PREFIX, import_item.name.name);
1240                    let mut mod_value = mem.get_from_owned(&mod_name, env_ref, import_item.into(), 0);
1241
1242                    if value.is_err() && ty.is_err() && mod_value.is_err() {
1243                        let err = KclError::new_undefined_value(
1244                            KclErrorDetails::new(
1245                                format!("{} is not defined in module", import_item.name.name),
1246                                vec![SourceRange::from(&import_item.name)],
1247                            ),
1248                            None,
1249                        );
1250                        return Err(exec_state.with_not_yet_added_hint_from(
1251                            &not_yet_added,
1252                            &[&import_item.name.name, &ty_name],
1253                            err,
1254                        ));
1255                    }
1256
1257                    // Check that the item is allowed to be imported (in at least one namespace).
1258                    if value.is_ok() && !module_exports.contains(&import_item.name.name) {
1259                        value = Err(KclError::new_semantic(KclErrorDetails::new(
1260                            format!(
1261                                "Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
1262                                import_item.name.name
1263                            ),
1264                            vec![SourceRange::from(&import_item.name)],
1265                        )));
1266                    }
1267
1268                    if ty.is_ok() && !module_exports.contains(&ty_name) {
1269                        ty = Err(KclError::new_semantic(KclErrorDetails::new(
1270                            format!(
1271                                "Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
1272                                import_item.name.name
1273                            ),
1274                            vec![SourceRange::from(&import_item.name)],
1275                        )));
1276                    }
1277
1278                    if mod_value.is_ok() && !module_exports.contains(&mod_name) {
1279                        mod_value = Err(KclError::new_semantic(KclErrorDetails::new(
1280                            format!(
1281                                "Cannot import \"{}\" from module because it is not exported. Add \"export\" before the definition to export it.",
1282                                import_item.name.name
1283                            ),
1284                            vec![SourceRange::from(&import_item.name)],
1285                        )));
1286                    }
1287
1288                    if value.is_err() && ty.is_err() && mod_value.is_err() {
1289                        return value.map(|_| ());
1290                    }
1291
1292                    // Add the item to the current module.
1293                    if let Ok(value) = value {
1294                        exec_state.mut_stack().add(
1295                            import_item.identifier().to_owned(),
1296                            value,
1297                            SourceRange::from(&import_item.name),
1298                        )?;
1299
1300                        if let ItemVisibility::Export = import_stmt.visibility {
1301                            exec_state
1302                                .mod_local
1303                                .module_exports
1304                                .push(import_item.identifier().to_owned());
1305                        }
1306                    }
1307
1308                    if let Ok(ty) = ty {
1309                        let ty_name = format!("{}{}", memory::TYPE_PREFIX, import_item.identifier());
1310                        if matches!(
1311                            &ty,
1312                            KclValue::Type {
1313                                value: TypeDef::Enum(_),
1314                                ..
1315                            }
1316                        ) {
1317                            reject_enum_clashing_with_module(
1318                                exec_state,
1319                                import_item.identifier(),
1320                                SourceRange::from(&import_item.name),
1321                            )?;
1322                        }
1323                        exec_state
1324                            .mut_stack()
1325                            .add(ty_name.clone(), ty, SourceRange::from(&import_item.name))?;
1326
1327                        if let ItemVisibility::Export = import_stmt.visibility {
1328                            exec_state.mod_local.module_exports.push(ty_name);
1329                        }
1330                    }
1331
1332                    if let Ok(mod_value) = mod_value {
1333                        let mod_name = format!("{}{}", memory::MODULE_PREFIX, import_item.identifier());
1334                        reject_module_clashing_with_enum(
1335                            exec_state,
1336                            import_item.identifier(),
1337                            SourceRange::from(&import_item.name),
1338                        )?;
1339                        exec_state.mut_stack().add(
1340                            mod_name.clone(),
1341                            mod_value,
1342                            SourceRange::from(&import_item.name),
1343                        )?;
1344
1345                        if let ItemVisibility::Export = import_stmt.visibility {
1346                            exec_state.mod_local.module_exports.push(mod_name);
1347                        }
1348                    }
1349                }
1350            }
1351            ImportSelector::Glob(_) => {
1352                let ModuleItems {
1353                    environment: env_ref,
1354                    exports: module_exports,
1355                    not_yet_added,
1356                } = self.exec_module_for_items(module_id, exec_state, source_range).await?;
1357                for name in module_exports.iter() {
1358                    let item = exec_state
1359                        .stack()
1360                        .memory
1361                        .get_from_owned(name, env_ref, source_range, 0)
1362                        .map_err(|_err| {
1363                            internal_err(
1364                                format!("{name} is not defined in module (but was exported?)"),
1365                                source_range,
1366                            )
1367                        })?;
1368                    reject_glob_import_clash(exec_state, name, &item, source_range)?;
1369                    exec_state.mut_stack().add(name.to_owned(), item, source_range)?;
1370
1371                    if let ItemVisibility::Export = import_stmt.visibility {
1372                        exec_state.mod_local.module_exports.push(name.clone());
1373                    }
1374                }
1375                // The skipped declarations come along with the names.
1376                exec_state
1377                    .import_not_yet_added(&not_yet_added, matches!(import_stmt.visibility, ItemVisibility::Export));
1378            }
1379            ImportSelector::None { .. } => {
1380                let name = import_stmt.module_name().unwrap();
1381                reject_module_clashing_with_enum(exec_state, &name, source_range)?;
1382                let item = KclValue::Module {
1383                    value: module_id,
1384                    meta: vec![source_range.into()],
1385                };
1386                exec_state
1387                    .mut_stack()
1388                    .add(format!("{}{}", memory::MODULE_PREFIX, name), item, source_range)?;
1389            }
1390        }
1391
1392        Ok(())
1393    }
1394
1395    /// Execute a type declaration. Flat.
1396    pub(super) async fn exec_type_declaration(
1397        &self,
1398        ty: &Node<TypeDeclaration>,
1399        body_type: BodyType,
1400        exec_state: &mut ExecState,
1401    ) -> Result<(), KclError> {
1402        let metadata = Metadata::from(ty);
1403        let attrs = annotations::get_fn_attrs(&ty.outer_attrs, metadata.source_range)?.unwrap_or_default();
1404        let v3_only_feature = match (attrs.impl_, &ty.definition) {
1405            (annotations::Impl::Kcl | annotations::Impl::KclConstrainable, TypeDeclarationDefinition::Alias { .. }) => {
1406                Some("Type aliases")
1407            }
1408            (annotations::Impl::Kcl | annotations::Impl::KclConstrainable, TypeDeclarationDefinition::Enum(_)) => {
1409                Some("Enum declarations")
1410            }
1411            _ => None,
1412        };
1413        if let Some(feature) = v3_only_feature
1414            && !matches!(exec_state.mod_local.path, ModulePath::Std { .. })
1415            && !exec_state.entry_point_version_is_v3_or_higher()
1416        {
1417            return Err(KclError::new_semantic(KclErrorDetails::new(
1418                format!(
1419                    "{feature} require KCL 3.0-preview, but this program uses KCL {}.",
1420                    exec_state.entry_point_kcl_version().as_str()
1421                ),
1422                vec![metadata.source_range],
1423            )));
1424        }
1425        match attrs.impl_ {
1426            annotations::Impl::Rust | annotations::Impl::RustConstrainable | annotations::Impl::RustConstraint => {
1427                let std_path = match &exec_state.mod_local.path {
1428                    ModulePath::Std { value } => value,
1429                    ModulePath::Local { .. } | ModulePath::Main => {
1430                        return Err(KclError::new_semantic(KclErrorDetails::new(
1431                            "User-defined types are not yet supported.".to_owned(),
1432                            vec![metadata.source_range],
1433                        )));
1434                    }
1435                };
1436                let (t, props) = crate::std::std_ty(std_path, &ty.name.name);
1437                let value = KclValue::Type {
1438                    value: TypeDef::RustRepr(t, props),
1439                    meta: vec![metadata],
1440                    experimental: attrs.experimental,
1441                };
1442                let name_in_mem = format!("{}{}", memory::TYPE_PREFIX, ty.name.name);
1443                exec_state
1444                    .mut_stack()
1445                    .add(name_in_mem.clone(), value, metadata.source_range)
1446                    .map_err(|_| {
1447                        KclError::new_semantic(KclErrorDetails::new(
1448                            format!("Redefinition of type {}.", ty.name.name),
1449                            vec![metadata.source_range],
1450                        ))
1451                    })?;
1452
1453                if let ItemVisibility::Export = ty.visibility {
1454                    exec_state.mod_local.module_exports.push(name_in_mem);
1455                }
1456            }
1457            // Do nothing for primitive types, they get special treatment and their declarations are just for documentation.
1458            annotations::Impl::Primitive => {}
1459            annotations::Impl::Kcl | annotations::Impl::KclConstrainable => match &ty.definition {
1460                TypeDeclarationDefinition::Alias { ty: alias } => {
1461                    let type_def = match alias.inner.clone() {
1462                        Type::Named { name } => {
1463                            match resolve_named_type_def(&name, exec_state, self, metadata.source_range, false).await? {
1464                                def @ TypeDef::Enum(_) => def,
1465                                def => TypeDef::Alias(def.into_runtime_type()),
1466                            }
1467                        }
1468                        alias => TypeDef::Alias(
1469                            RuntimeType::from_parsed(
1470                                alias,
1471                                exec_state,
1472                                self,
1473                                metadata.source_range,
1474                                attrs.impl_ == annotations::Impl::KclConstrainable,
1475                                false,
1476                            )
1477                            .await?,
1478                        ),
1479                    };
1480                    if matches!(&type_def, TypeDef::Enum(_)) {
1481                        reject_enum_clashing_with_module(exec_state, &ty.name.name, metadata.source_range)?;
1482                    }
1483                    let value = KclValue::Type {
1484                        value: type_def,
1485                        meta: vec![metadata],
1486                        experimental: attrs.experimental,
1487                    };
1488                    let name_in_mem = format!("{}{}", memory::TYPE_PREFIX, ty.name.name);
1489                    exec_state
1490                        .mut_stack()
1491                        .add(name_in_mem.clone(), value, metadata.source_range)
1492                        .map_err(|_| {
1493                            KclError::new_semantic(KclErrorDetails::new(
1494                                format!("Redefinition of type {}.", ty.name.name),
1495                                vec![metadata.source_range],
1496                            ))
1497                        })?;
1498
1499                    if let ItemVisibility::Export = ty.visibility {
1500                        exec_state.mod_local.module_exports.push(name_in_mem);
1501                    }
1502                }
1503                TypeDeclarationDefinition::Bare => {
1504                    return Err(KclError::new_semantic(KclErrorDetails::new(
1505                        "User-defined types are not yet supported.".to_owned(),
1506                        vec![metadata.source_range],
1507                    )));
1508                }
1509                TypeDeclarationDefinition::Enum(decl) => {
1510                    // Identity is module plus declared name, so `type Color` in
1511                    // two function bodies of one file would be one type with two
1512                    // variant sets. A V1 limitation, liftable in enum v2 by
1513                    // giving `EnumTypeId` a declaration site.
1514                    if !matches!(body_type, BodyType::Root) {
1515                        return Err(KclError::new_semantic(KclErrorDetails::new(
1516                            format!(
1517                                "Enum declarations are only supported at the top-level of a file. Move `type {}` to the top-level.",
1518                                ty.name.name
1519                            ),
1520                            vec![metadata.source_range],
1521                        )));
1522                    }
1523
1524                    reject_enum_clashing_with_module(exec_state, &ty.name.name, metadata.source_range)?;
1525
1526                    let variants = decl.variants.iter().map(|v| v.name.name.clone()).collect();
1527                    let id = EnumTypeId::new(metadata.source_range.module_id(), ty.name.name.clone());
1528                    // Constructing the definition is the validation step: nothing
1529                    // below runs, so nothing reaches memory, unless every variant
1530                    // name is distinct.
1531                    let def = EnumTypeDef::new(id, variants, attrs.experimental).map_err(|duplicate| {
1532                        KclError::new_semantic(KclErrorDetails::new(
1533                            format!("Duplicate variant `{}` in enum `{}`.", duplicate.name, ty.name.name),
1534                            vec![
1535                                decl.variants[duplicate.first_index].as_source_range(),
1536                                decl.variants[duplicate.duplicate_index].as_source_range(),
1537                            ],
1538                        ))
1539                    })?;
1540
1541                    let value = KclValue::Type {
1542                        value: TypeDef::Enum(Arc::new(def)),
1543                        meta: vec![metadata],
1544                        experimental: attrs.experimental,
1545                    };
1546                    let name_in_mem = format!("{}{}", memory::TYPE_PREFIX, ty.name.name);
1547                    exec_state
1548                        .mut_stack()
1549                        .add(name_in_mem.clone(), value, metadata.source_range)
1550                        .map_err(|_| {
1551                            KclError::new_semantic(KclErrorDetails::new(
1552                                format!("Redefinition of type {}.", ty.name.name),
1553                                vec![metadata.source_range],
1554                            ))
1555                        })?;
1556
1557                    if let ItemVisibility::Export = ty.visibility {
1558                        exec_state.mod_local.module_exports.push(name_in_mem);
1559                    }
1560                }
1561            },
1562        }
1563
1564        Ok(())
1565    }
1566
1567    /// Bind the evaluated right-hand side of a variable declaration: segment
1568    /// tag attachment, name binding, sketch-block segment tags, feature-tree
1569    /// operation, and export tracking. The evaluation-free second half of
1570    /// variable-declaration execution. Returns the bound value (which becomes
1571    /// the module result when this is the last Root statement).
1572    pub(super) fn bind_variable_declaration(
1573        &self,
1574        variable_declaration: &Node<VariableDeclaration>,
1575        rhs: KclValue,
1576        body_type: BodyType,
1577        exec_state: &mut ExecState,
1578    ) -> Result<KclValue, KclError> {
1579        let var_name = variable_declaration.declaration.id.name.to_string();
1580        let source_range = SourceRange::from(&variable_declaration.declaration.init);
1581        let mut rhs = rhs;
1582
1583        // Attach the variable name to unsolved segments as a tag.
1584        // While executing the body of a sketch block, the segments
1585        // won't have been solved yet.
1586        if let KclValue::Segment { value } = &mut rhs
1587            && let SegmentRepr::Unsolved { segment } = &mut value.repr
1588        {
1589            segment.tag = Some(TagIdentifier {
1590                value: variable_declaration.declaration.id.name.clone(),
1591                info: Default::default(),
1592                meta: vec![SourceRange::from(&variable_declaration.declaration.id).into()],
1593            });
1594        }
1595        let rhs = rhs; // Remove mutability.
1596
1597        let should_bind_name = if let Some(fn_name) = variable_declaration.declaration.init.fn_declaring_name() {
1598            // Declaring a function with a name, so only bind
1599            // the variable name if it differs from the function
1600            // name.
1601            var_name != fn_name
1602        } else {
1603            // Not declaring a function, so we should bind the
1604            // variable name.
1605            true
1606        };
1607        if should_bind_name {
1608            exec_state
1609                .mut_stack()
1610                .add(var_name.clone(), rhs.clone(), source_range)?;
1611        }
1612
1613        if let Some(sketch_block_state) = exec_state.mod_local.sketch_block.as_mut()
1614            && let KclValue::Segment { value } = &rhs
1615        {
1616            // Add segment to mapping so that we can tag it when
1617            // sending to the engine.
1618            let segment_object_id = match &value.repr {
1619                SegmentRepr::Unsolved { segment } => segment.object_id,
1620                SegmentRepr::Solved { segment } => segment.object_id,
1621            };
1622            sketch_block_state
1623                .segment_tags
1624                .entry(segment_object_id)
1625                .or_insert_with(|| {
1626                    let id_node = &variable_declaration.declaration.id;
1627                    Node::new(
1628                        TagDeclarator {
1629                            name: id_node.name.clone(),
1630                            digest: None,
1631                        },
1632                        id_node.start,
1633                        id_node.end,
1634                        id_node.module_id,
1635                    )
1636                });
1637        }
1638
1639        // Track operations, for the feature tree.
1640        // Don't track these operations if the KCL code being executed is in the stdlib,
1641        // because users shouldn't know about stdlib internals -- it's useless noise, to them.
1642        let should_show_in_feature_tree = !exec_state.mod_local.inside_stdlib && rhs.show_variable_in_feature_tree();
1643        if should_show_in_feature_tree {
1644            exec_state.push_op(Operation::VariableDeclaration {
1645                name: var_name.clone(),
1646                value: op_from_kcl_value(&rhs),
1647                visibility: variable_declaration.visibility,
1648                node_path: NodePath::placeholder(),
1649                source_range,
1650            });
1651        }
1652
1653        // Track exports.
1654        if let ItemVisibility::Export = variable_declaration.visibility {
1655            if matches!(body_type, BodyType::Root) {
1656                exec_state.mod_local.module_exports.push(var_name);
1657            } else {
1658                exec_state.err(CompilationIssue::err(
1659                                variable_declaration.as_source_range(),
1660                                "Exports are only supported at the top-level of a file. Remove `export` or move it to the top-level.",
1661                            ));
1662            }
1663        }
1664        Ok(rhs)
1665    }
1666
1667    /// Record a return statement's evaluated value as the function result
1668    /// (`__return`). This is the pre-KCL-3.0 `return` semantics: it
1669    /// deliberately does NOT stop the enclosing block -- statements after a
1670    /// `return` still execute, exactly like the historical behavior (see the
1671    /// return handling in the machine); a second executed `return` is the
1672    /// "Multiple returns" error. Under KCL 3.0, this is never called; `return`
1673    /// unwinds as `Return` control flow instead.
1674    pub(super) fn bind_return_value(
1675        return_statement: &Node<ReturnStatement>,
1676        value: KclValue,
1677        exec_state: &mut ExecState,
1678    ) -> Result<(), KclError> {
1679        let metadata = Metadata::from(return_statement);
1680        exec_state
1681            .mut_stack()
1682            .add(memory::RETURN_NAME.to_owned(), value, metadata.source_range)
1683            .map_err(|_| {
1684                KclError::new_semantic(KclErrorDetails::new(
1685                    "Multiple returns from a single function.".to_owned(),
1686                    vec![metadata.source_range],
1687                ))
1688            })?;
1689        Ok(())
1690    }
1691
1692    pub async fn open_module(
1693        &self,
1694        path: &ImportPath,
1695        attrs: &[Node<Annotation>],
1696        resolved_path: &ModulePath,
1697        exec_state: &mut ExecState,
1698        source_range: SourceRange,
1699    ) -> Result<ModuleId, KclError> {
1700        match path {
1701            ImportPath::Kcl { .. } => {
1702                exec_state.global.mod_loader.cycle_check(resolved_path, source_range)?;
1703
1704                if let Some(id) = exec_state.id_for_module(resolved_path) {
1705                    return Ok(id);
1706                }
1707
1708                let id = exec_state.next_module_id();
1709                // Add file path string to global state even if it fails to import
1710                exec_state.add_path_to_source_id(resolved_path.clone(), id);
1711                let source = resolved_path.source(&self.fs, source_range).await?;
1712                exec_state.add_id_to_source(id, source.clone());
1713                let (_, parsed, never_type_ranges) = crate::parsing::parse_str_syntax(&source.source, id)?;
1714                // Defer validation until module execution or the mock import site.
1715                exec_state.global.never_type_ranges.insert(id, never_type_ranges);
1716                exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Kcl(parsed, None));
1717
1718                Ok(id)
1719            }
1720            ImportPath::Foreign { .. } => {
1721                if let Some(id) = exec_state.id_for_module(resolved_path) {
1722                    return Ok(id);
1723                }
1724
1725                let id = exec_state.next_module_id();
1726                let path = resolved_path.expect_path();
1727                // Add file path string to global state even if it fails to import
1728                exec_state.add_path_to_source_id(resolved_path.clone(), id);
1729                let format = super::import::format_from_annotations(attrs, path, source_range)?;
1730                let geom = super::import::import_foreign(path, format, exec_state, self, source_range).await?;
1731                exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Foreign(geom, None));
1732                Ok(id)
1733            }
1734            ImportPath::Std { .. } => {
1735                if resolved_path.is_solver_module() && exec_state.mod_local.sketch_block.is_none() {
1736                    return Err(KclError::new_semantic(KclErrorDetails::new(
1737                        format!("The `{resolved_path}` module is only available inside sketch blocks."),
1738                        vec![source_range],
1739                    )));
1740                }
1741
1742                if let Some(id) = exec_state.id_for_module(resolved_path) {
1743                    return Ok(id);
1744                }
1745
1746                let id = exec_state.next_module_id();
1747                // Add file path string to global state even if it fails to import
1748                exec_state.add_path_to_source_id(resolved_path.clone(), id);
1749                let source = resolved_path.source(&self.fs, source_range).await?;
1750                exec_state.add_id_to_source(id, source.clone());
1751                let (_, parsed, never_type_ranges) = match crate::parsing::parse_str_syntax(&source.source, id) {
1752                    Ok(v) => v,
1753                    Err(err) => {
1754                        let message = format!("Failed to parse source for {resolved_path}; {err})");
1755                        debug_assert!(false, "{message}");
1756                        return Err(KclError::new_internal(KclErrorDetails::new(
1757                            message,
1758                            vec![source_range],
1759                        )));
1760                    }
1761                };
1762                if let Err(err) = crate::parsing::validate_never_type_ranges(
1763                    &never_type_ranges,
1764                    crate::parsing::SyntaxSource::BundledStdlib,
1765                ) {
1766                    let message = format!("Failed to validate never type ranges for {resolved_path}; {err})");
1767                    debug_assert!(false, "{message}");
1768                    return Err(KclError::new_internal(KclErrorDetails::new(
1769                        message,
1770                        vec![source_range],
1771                    )));
1772                }
1773                exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Kcl(parsed, None));
1774                Ok(id)
1775            }
1776        }
1777    }
1778
1779    pub(super) async fn exec_module_for_items(
1780        &self,
1781        module_id: ModuleId,
1782        exec_state: &mut ExecState,
1783        source_range: SourceRange,
1784    ) -> Result<ModuleItems, KclError> {
1785        let path = exec_state.global.module_infos[&module_id].path.clone();
1786        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1787        // DON'T EARLY RETURN! We need to restore the module repr
1788
1789        let result = match &mut repr {
1790            ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
1791            ModuleRepr::Kcl(_, Some(outcome)) => Ok(outcome.items()),
1792            ModuleRepr::Kcl(program, cache) => self
1793                .exec_module_from_ast(program, module_id, &path, exec_state, source_range, PreserveMem::Normal)
1794                .await
1795                .map(|outcome| {
1796                    let items = outcome.items();
1797                    *cache = Some(outcome);
1798                    items
1799                }),
1800            ModuleRepr::Foreign(geom, _) => Err(KclError::new_semantic(KclErrorDetails::new(
1801                "Cannot import items from foreign modules".to_owned(),
1802                vec![geom.source_range],
1803            ))),
1804            ModuleRepr::Dummy => unreachable!("Looking up {}, but it is still being interpreted", path),
1805        };
1806
1807        exec_state.global.module_infos[&module_id].restore_repr(repr);
1808        result
1809    }
1810
1811    async fn exec_module_for_result(
1812        &self,
1813        module_id: ModuleId,
1814        exec_state: &mut ExecState,
1815        source_range: SourceRange,
1816    ) -> Result<Option<KclValue>, KclError> {
1817        let path = exec_state.global.module_infos[&module_id].path.clone();
1818        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1819        // DON'T EARLY RETURN! We need to restore the module repr
1820
1821        let result = match &mut repr {
1822            ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
1823            ModuleRepr::Kcl(_, Some(outcome)) => Ok(outcome.last_expr.clone()),
1824            ModuleRepr::Kcl(program, cached_items) => {
1825                let result = self
1826                    .exec_module_from_ast(program, module_id, &path, exec_state, source_range, PreserveMem::Normal)
1827                    .await;
1828                match result {
1829                    Ok(outcome) => {
1830                        let value = outcome.last_expr.clone();
1831                        *cached_items = Some(outcome);
1832                        Ok(value)
1833                    }
1834                    Err(e) => Err(e),
1835                }
1836            }
1837            ModuleRepr::Foreign(_, Some((imported, _))) => Ok(imported.clone()),
1838            ModuleRepr::Foreign(geom, cached) => {
1839                let caller_artifacts = std::mem::take(&mut exec_state.mod_local.artifacts);
1840                let result = super::import::send_to_engine(geom.clone(), exec_state, self)
1841                    .await
1842                    .map(|geom| Some(KclValue::ImportedGeometry(geom)));
1843                let module_artifacts = std::mem::replace(&mut exec_state.mod_local.artifacts, caller_artifacts);
1844
1845                match result {
1846                    Ok(val) => {
1847                        *cached = Some((val.clone(), module_artifacts));
1848                        Ok(val)
1849                    }
1850                    Err(e) => {
1851                        // Preserve the failed command in the caller's error artifacts, matching
1852                        // the behavior before foreign module artifact states were isolated.
1853                        exec_state.mod_local.artifacts.extend(module_artifacts);
1854                        // Label the failure with the import so the backtrace
1855                        // names the foreign file, like KCL module failures do.
1856                        Err(e.add_import_location(&path.import_name(), source_range))
1857                    }
1858                }
1859            }
1860            ModuleRepr::Dummy => unreachable!(),
1861        };
1862
1863        exec_state.global.module_infos[&module_id].restore_repr(repr);
1864
1865        result
1866    }
1867
1868    pub async fn exec_module_from_ast(
1869        &self,
1870        program: &Node<Program>,
1871        module_id: ModuleId,
1872        path: &ModulePath,
1873        exec_state: &mut ExecState,
1874        source_range: SourceRange,
1875        preserve_mem: PreserveMem,
1876    ) -> Result<ModuleExecutionOutcome, KclError> {
1877        exec_state.global.mod_loader.enter_module(path);
1878        let result = self
1879            .exec_module_body(program, exec_state, preserve_mem, module_id, path)
1880            .await;
1881        exec_state.global.mod_loader.leave_module(path, source_range)?;
1882
1883        // TODO: ModuleArtifactState is getting dropped here when there's an
1884        // error.  Should we propagate it for non-root modules?
1885        result.map_err(|(err, _, _)| {
1886            match err {
1887                KclError::ImportCycle { .. } => {
1888                    // It was an import cycle.  Keep the original message.
1889                    err.override_source_ranges(vec![source_range])
1890                }
1891                // The module loaded successfully, so preserve execution errors
1892                // exactly as they occurred inside it. Rewrapping them here loses
1893                // the error kind, structured fields, and imported-file location.
1894                _ => err.add_import_location(&path.import_name(), source_range),
1895            }
1896        })
1897    }
1898
1899    /// Resolve a name to its value, running the module body first when the name
1900    /// refers to a module. Flat (module execution happens in its own fresh
1901    /// root).
1902    pub(super) async fn resolve_name_for_eval(
1903        &self,
1904        name: &Node<Name>,
1905        metadata: &Metadata,
1906        exec_state: &mut ExecState,
1907    ) -> Result<KclValue, KclError> {
1908        let value = name.get_result(exec_state, self).await?;
1909        if let KclValue::Module { value: module_id, meta } = value {
1910            Ok(self
1911                .exec_module_for_result(module_id, exec_state, metadata.source_range)
1912                .await?
1913                .unwrap_or_else(|| {
1914                    exec_state.warn(
1915                        CompilationIssue::err(
1916                            metadata.source_range,
1917                            "Imported module has no return value. The last statement of the module must be an expression, usually the Solid.",
1918                        ),
1919                        annotations::WARN_MOD_RETURN_VALUE,
1920                    );
1921
1922                    let mut new_meta = vec![metadata.to_owned()];
1923                    new_meta.extend(meta);
1924                    KclValue::KclNone {
1925                        value: Default::default(),
1926                        meta: new_meta,
1927                    }
1928                }))
1929        } else {
1930            Ok(value)
1931        }
1932    }
1933
1934    /// Evaluate a single expression on the CEK machine as a fresh root. For
1935    /// bounded internal evaluations (e.g. GD&T's constant plane lookup).
1936    pub(crate) async fn eval_expr_fresh_root(
1937        &self,
1938        expr: &Expr,
1939        exec_state: &mut ExecState,
1940        metadata: &Metadata,
1941    ) -> Result<KclValueControlFlow, KclError> {
1942        crate::execution::machine::run_expr(self, expr, exec_state, metadata).await
1943    }
1944
1945    /// Create the closure value for a function expression, including the
1946    /// recursive-closure placeholder fixup and binding a named `fn name() {}`
1947    /// in the current scope. Flat.
1948    pub(super) async fn create_function_closure(
1949        &self,
1950        function_expression: &crate::parsing::ast::types::BoxNode<FunctionExpression>,
1951        annotations: &[Node<Annotation>],
1952        metadata: &Metadata,
1953        statement_kind: StatementKind<'_>,
1954        exec_state: &mut ExecState,
1955    ) -> Result<KclValue, KclError> {
1956        let attrs = annotations::get_fn_attrs(annotations, metadata.source_range)?;
1957        let experimental = attrs
1958            .as_ref()
1959            .map(|a| a.experimental)
1960            // Use the default for the field, not the bool type.
1961            .unwrap_or_else(|| FnAttrs::default().experimental);
1962
1963        // Check the KCL @(feature_tree = ) annotation.
1964        let include_in_feature_tree = attrs
1965            .as_ref()
1966            .map(|a| a.include_in_feature_tree)
1967            // Use the default for the field, not the bool type.
1968            .unwrap_or_else(|| FnAttrs::default().include_in_feature_tree);
1969        let (mut closure, placeholder_env_ref) = if let Some(attrs) = attrs
1970            && (attrs.impl_ == annotations::Impl::Rust
1971                || attrs.impl_ == annotations::Impl::RustConstrainable
1972                || attrs.impl_ == annotations::Impl::RustConstraint)
1973        {
1974            if let ModulePath::Std { value: std_path } = &exec_state.mod_local.path {
1975                let (func, props) = crate::std::std_fn(std_path, statement_kind.expect_name());
1976                (
1977                    KclValue::Function {
1978                        value: Box::new(FunctionSource::rust(func, function_expression.clone(), props, attrs)),
1979                        meta: vec![metadata.to_owned()],
1980                    },
1981                    None,
1982                )
1983            } else {
1984                return Err(KclError::new_semantic(KclErrorDetails::new(
1985                    "Rust implementation of functions is restricted to the standard library".to_owned(),
1986                    vec![metadata.source_range],
1987                )));
1988            }
1989        } else {
1990            let std_props = function_expression
1991                .name_str()
1992                .and_then(|name| exec_state.mod_local.path.build_std_fully_qualified_name(name))
1993                .map(|name| StdFnProps::default(&name));
1994            // Snapshotting memory here is crucial for semantics so that we close
1995            // over variables. Variables defined lexically later shouldn't
1996            // be available to the function body.
1997            let (env_ref, placeholder_env_ref) = if function_expression.name.is_some() {
1998                // Recursive function needs a snapshot that includes
1999                // itself.
2000                let dummy = EnvironmentRef::dummy();
2001                (dummy, Some(dummy))
2002            } else {
2003                (exec_state.mut_stack().snapshot()?, None)
2004            };
2005            (
2006                KclValue::Function {
2007                    value: Box::new(FunctionSource::kcl(
2008                        function_expression.clone(),
2009                        env_ref,
2010                        KclFunctionSourceParams {
2011                            std_props,
2012                            experimental,
2013                            include_in_feature_tree,
2014                        },
2015                    )),
2016                    meta: vec![metadata.to_owned()],
2017                },
2018                placeholder_env_ref,
2019            )
2020        };
2021
2022        // Resolve the signature's type names now, in the scope where
2023        // the declaration is written. Call sites consume the stored
2024        // resolutions and never look type names up themselves.
2025        if let KclValue::Function { value, .. } = &mut closure {
2026            value.resolve_signature_types(exec_state, self).await?;
2027        }
2028
2029        // If the function expression has a name, i.e. `fn name() {}`,
2030        // bind it in the current scope.
2031        if let Some(fn_name) = &function_expression.name {
2032            // If we used a placeholder env ref for recursion, fix it up
2033            // with the name recursively bound so that it's available in
2034            // the function body.
2035            if let Some(placeholder_env_ref) = placeholder_env_ref {
2036                closure = exec_state.mut_stack().add_recursive_closure(
2037                    fn_name.name.to_owned(),
2038                    closure,
2039                    placeholder_env_ref,
2040                    metadata.source_range,
2041                )?;
2042            } else {
2043                // Regular non-recursive binding.
2044                exec_state
2045                    .mut_stack()
2046                    .add(fn_name.name.clone(), closure.clone(), metadata.source_range)?;
2047            }
2048        }
2049
2050        Ok(closure)
2051    }
2052}
2053
2054/// Skip a declaration whose `added_in` the program predates, recording it under
2055/// `key` so a failed lookup can explain why. `key` is only computed on a skip,
2056/// so declarations without attributes cost nothing.
2057pub(super) fn skip_if_not_yet_added(
2058    annotations: &[Node<Annotation>],
2059    key: impl FnOnce() -> String,
2060    exported: bool,
2061    source_range: SourceRange,
2062    exec_state: &mut ExecState,
2063) -> Result<bool, KclError> {
2064    let Some(added_in) = annotations::added_in_version(annotations, source_range)? else {
2065        return Ok(false);
2066    };
2067    if annotations::version_ge(exec_state.entry_point_kcl_version().as_str(), &added_in) {
2068        return Ok(false);
2069    }
2070    exec_state.record_not_yet_added(key(), added_in, exported);
2071    Ok(true)
2072}
2073
2074/// The head of a `Color::Red` path is looked up both as a module and as an enum,
2075/// so one scope must not bind a module and an enum under the same name. Reporting
2076/// the clash where the second name is introduced keeps every `X::y` use site
2077/// unambiguous, so no check is needed at the use site.
2078///
2079/// Type aliases that resolve to enums participate in this rule because they can
2080/// head a `::` path. Other aliases and bare types may continue to share a name
2081/// with a module.
2082fn module_enum_clash(name: &str, source_range: SourceRange) -> KclError {
2083    KclError::new_semantic(KclErrorDetails::new(
2084        format!(
2085            "An enum and a module cannot share the name `{name}` in the same scope, because `{name}::x` would be ambiguous. Rename one of them."
2086        ),
2087        vec![source_range],
2088    ))
2089}
2090
2091/// Call before binding an enum under `name`.
2092fn reject_enum_clashing_with_module(
2093    exec_state: &ExecState,
2094    name: &str,
2095    source_range: SourceRange,
2096) -> Result<(), KclError> {
2097    let key = format!("{}{}", memory::MODULE_PREFIX, name);
2098    if !exec_state.stack().cur_frame_contains(&key)? {
2099        return Ok(());
2100    }
2101
2102    Err(module_enum_clash(name, source_range))
2103}
2104
2105/// Call before binding a module under `name`. The mirror of
2106/// [`reject_enum_clashing_with_module`].
2107fn reject_module_clashing_with_enum(
2108    exec_state: &ExecState,
2109    name: &str,
2110    source_range: SourceRange,
2111) -> Result<(), KclError> {
2112    let key = format!("{}{}", memory::TYPE_PREFIX, name);
2113    if !exec_state.stack().cur_frame_contains(&key)? {
2114        return Ok(());
2115    }
2116
2117    let Ok(KclValue::Type {
2118        value: TypeDef::Enum(_),
2119        ..
2120    }) = exec_state.stack().get(&key, source_range)
2121    else {
2122        return Ok(());
2123    };
2124
2125    Err(module_enum_clash(name, source_range))
2126}
2127
2128/// Builds the error for comparing values of two different enum types.
2129fn different_enums_err(left: &EnumValue, right: &EnumValue, source_range: SourceRange) -> KclError {
2130    let left_name = left.enum_id().declared_name();
2131    let right_name = right.enum_id().declared_name();
2132
2133    let message = if left_name == right_name {
2134        // Identity is the declaration, so two enums can share a name and still be
2135        // different types. Naming both would read as a mistake in the message.
2136        format!(
2137            "Cannot compare two different enums that are both named `{left_name}`. They come from separate declarations."
2138        )
2139    } else {
2140        format!("Cannot compare enum `{left_name}` with enum `{right_name}`. They are different types.")
2141    };
2142
2143    KclError::new_semantic(KclErrorDetails::new(message, vec![source_range]))
2144}
2145
2146/// Builds the error for a name that resolves to a type where a value is needed,
2147/// such as `x = Color`.
2148///
2149/// Returns `None` when no type of that name is in scope, which leaves the
2150/// caller's "is not defined" error in place.
2151fn type_used_as_value(exec_state: &ExecState, name: &Node<Identifier>) -> Option<KclError> {
2152    let key = format!("{}{}", memory::TYPE_PREFIX, name.name);
2153    let KclValue::Type { value: def, .. } = exec_state.stack().get(&key, name.as_source_range()).ok()? else {
2154        return None;
2155    };
2156
2157    // The suggestion uses the name as written, which may be an import alias, so
2158    // that it can be pasted into the file that produced the error.
2159    let suggestion = match &def {
2160        TypeDef::Enum(def) => def
2161            .variants()
2162            .first()
2163            .map(|variant| format!(" Use one of its variants, such as `{}::{variant}`.", name.name))
2164            .unwrap_or_default(),
2165        _ => String::new(),
2166    };
2167
2168    Some(KclError::new_semantic(KclErrorDetails::new(
2169        format!("`{}` is a type, not a value.{suggestion}", name.name),
2170        name.as_source_ranges(),
2171    )))
2172}
2173
2174enum EnumPathHead {
2175    Enum {
2176        def: Arc<EnumTypeDef>,
2177        binding_experimental: bool,
2178    },
2179    NonEnumType,
2180}
2181
2182/// Classifies the type named by a `::` path segment: `Color` in both
2183/// `Color::Red` and `colors::Color::Red`.
2184///
2185/// A non-enum type is retained until module lookup has also failed because a
2186/// type alias and a module may share a name. The module remains the valid path
2187/// head in that case.
2188fn enum_named_by_segment(
2189    exec_state: &ExecState,
2190    segment: &Node<Identifier>,
2191    within: Option<&ModuleItems>,
2192) -> Option<EnumPathHead> {
2193    match type_value_named_by_segment(exec_state, segment, within)? {
2194        KclValue::Type {
2195            value: TypeDef::Enum(def),
2196            experimental,
2197            ..
2198        } => Some(EnumPathHead::Enum {
2199            def,
2200            binding_experimental: experimental,
2201        }),
2202        KclValue::Type { .. } => Some(EnumPathHead::NonEnumType),
2203        _ => None,
2204    }
2205}
2206
2207fn non_enum_type_in_path(segment: &Node<Identifier>) -> KclError {
2208    KclError::new_semantic(KclErrorDetails::new(
2209        format!(
2210            "`{}` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
2211            segment.name
2212        ),
2213        segment.as_source_ranges(),
2214    ))
2215}
2216
2217/// `Red` in `Color::Red`.
2218fn enum_variant_value(
2219    def: Arc<EnumTypeDef>,
2220    binding_experimental: bool,
2221    variant: &Node<Identifier>,
2222    exec_state: &mut ExecState,
2223) -> Result<KclValue, KclError> {
2224    let enum_name = def.id().declared_name();
2225
2226    if !def.has_variant(&variant.name) {
2227        let known = if def.variants().is_empty() {
2228            format!("Enum `{enum_name}` has no variants")
2229        } else {
2230            format!("Its variants are: {}", def.variants().join(", "))
2231        };
2232
2233        return Err(KclError::new_semantic(KclErrorDetails::new(
2234            format!("`{}` is not a variant of enum `{enum_name}`. {known}.", variant.name),
2235            variant.as_source_ranges(),
2236        )));
2237    }
2238
2239    // Older KCL versions report every enum use. In V3, an explicit experimental
2240    // annotation on either the declaration or the binding still applies.
2241    if !exec_state.entry_point_version_is_v3_or_higher() || def.is_experimental() || binding_experimental {
2242        exec_state.warn_experimental(&format!("the enum `{enum_name}`"), variant.as_source_range());
2243    }
2244
2245    // The value holds the declaration itself, so its identity is read off that
2246    // declaration and can never be rebuilt from a name. A later enum v2 adding a
2247    // declaration site to `EnumTypeId` therefore changes nothing here.
2248    Ok(KclValue::Enum {
2249        value: Box::new(EnumValue::new(
2250            def,
2251            variant.name.clone(),
2252            vec![Metadata {
2253                source_range: variant.as_source_range(),
2254            }],
2255        )),
2256    })
2257}
2258
2259/// Glob imports copy exported keys verbatim, prefix included, so which namespace
2260/// an incoming key lands in has to be read back off the key itself.
2261fn reject_glob_import_clash(
2262    exec_state: &ExecState,
2263    key: &str,
2264    item: &KclValue,
2265    source_range: SourceRange,
2266) -> Result<(), KclError> {
2267    if let Some(name) = key.strip_prefix(memory::MODULE_PREFIX) {
2268        return reject_module_clashing_with_enum(exec_state, name, source_range);
2269    }
2270
2271    if let Some(name) = key.strip_prefix(memory::TYPE_PREFIX)
2272        && matches!(
2273            item,
2274            KclValue::Type {
2275                value: TypeDef::Enum(_),
2276                ..
2277            }
2278        )
2279    {
2280        return reject_enum_clashing_with_module(exec_state, name, source_range);
2281    }
2282
2283    Ok(())
2284}
2285
2286/// When executing in sketch mode, whether we should skip executing this
2287/// expression.
2288pub(super) fn sketch_mode_should_skip(expr: &Expr) -> bool {
2289    fn contains_edited_sketch_block(node: crate::walk::Node<'_>) -> bool {
2290        if let crate::walk::Node::SketchBlock(sketch_block) = node {
2291            return sketch_block.is_being_edited;
2292        }
2293        node.children().into_iter().any(contains_edited_sketch_block)
2294    }
2295
2296    !contains_edited_sketch_block(expr.into())
2297}
2298
2299/// If the error is about an undefined name, and that name matches the name being defined,
2300/// make the error message more specific.
2301fn var_in_own_ref_err(e: KclError, being_declared: &Option<String>) -> KclError {
2302    let KclError::UndefinedValue { name, mut details } = e else {
2303        return e;
2304    };
2305    // TODO after June 26th: replace this with a let-chain,
2306    // which will be available in Rust 1.88
2307    // https://rust-lang.github.io/rfcs/2497-if-let-chains.html
2308    if let (Some(name0), Some(name1)) = (&being_declared, &name)
2309        && name0 == name1
2310    {
2311        details.message = format!(
2312            "You can't use `{name0}` because you're currently trying to define it. Use a different variable here instead."
2313        );
2314    }
2315    KclError::UndefinedValue { details, name }
2316}
2317
2318impl Node<SketchBlock> {
2319    pub(super) async fn finish_arguments_after_eval(
2320        &self,
2321        labeled: IndexMap<String, Arg>,
2322        exec_state: &mut ExecState,
2323        ctx: &ExecutorContext,
2324    ) -> Result<(ObjectId, SketchSurface), EarlyReturn> {
2325        let range = SourceRange::from(self);
2326        let mut args = Args::new_no_args(
2327            range,
2328            self.node_path.clone(),
2329            ctx.clone(),
2330            Some(SketchBlock::CALLEE_NAME.to_owned()),
2331        );
2332        args.labeled = labeled;
2333
2334        // Report any arguments that aren't valid sketch block parameters.
2335        // This is non-fatal so that the rest of the block still executes,
2336        // matching how unexpected keyword arguments are handled for
2337        // function calls.
2338        //
2339        // Checking arguments should be done after evaluating them, the same
2340        // order as if we were calling a function.
2341        self.check_for_unexpected_arguments(&args, exec_state)?;
2342
2343        let arg_on_value: KclValue =
2344            args.get_kw_arg(SKETCH_BLOCK_PARAM_ON, &RuntimeType::sketch_or_surface(), exec_state)?;
2345
2346        let Some(arg_on) = SketchOrSurface::from_kcl_val(&arg_on_value) else {
2347            let message = "The `on` argument to a sketch block must be convertible to a sketch or surface.".to_owned();
2348            debug_assert!(false, "{message}");
2349            return Err(KclError::new_semantic(KclErrorDetails::new(message, vec![range])).into());
2350        };
2351        let mut sketch_surface = arg_on.into_sketch_surface();
2352
2353        // Ensure that the plane has an ObjectId. Always create an Object so
2354        // that we're consistent with IDs.
2355        match &mut sketch_surface {
2356            SketchSurface::Plane(plane) => {
2357                // Ensure that it's been created in the engine.
2358                ensure_sketch_plane_in_engine(plane, exec_state, ctx, range, self.node_path.clone()).await?;
2359            }
2360            SketchSurface::Face(_) => {
2361                // All faces should already be created in the engine.
2362            }
2363        }
2364
2365        // Generate an ID for the sketch block. This must be done after
2366        // arguments so that we get the same result when the arguments are
2367        // cached. This must be done before the sketch block body so that no
2368        // matter how many IDs are generated due to objects in the body, the
2369        // sketch ID is always stable.
2370        let sketch_id = exec_state.next_object_id();
2371        exec_state.add_placeholder_scene_object(sketch_id, range, self.node_path.clone());
2372        let on_cache_name = sketch_on_cache_name(sketch_id);
2373        // Store in memory so that it's cached.
2374        exec_state.mut_stack().add(on_cache_name, arg_on_value, range)?;
2375
2376        Ok((sketch_id, sketch_surface))
2377    }
2378
2379    /// Sketch-mode path: arguments cannot be re-evaluated, so look the `on`
2380    /// surface up from the execution cache. Flat.
2381    pub(super) fn arguments_from_cache(
2382        &self,
2383        exec_state: &mut ExecState,
2384    ) -> Result<(ObjectId, SketchSurface), EarlyReturn> {
2385        let range = SourceRange::from(self);
2386        {
2387            // In sketch mode, we can't re-evaluate arguments. Instead, look
2388            // them up from cache.
2389
2390            // Generate an ID for the sketch block. This must be done before the
2391            // sketch block body so that no matter how many IDs are generated
2392            // due to objects in the body, the sketch ID is always stable.
2393            let sketch_id = exec_state.next_object_id();
2394            exec_state.add_placeholder_scene_object(sketch_id, range, self.node_path.clone());
2395            let on_cache_name = sketch_on_cache_name(sketch_id);
2396            let arg_on_value = exec_state.stack().get_owned(&on_cache_name, range)?;
2397
2398            let Some(arg_on) = SketchOrSurface::from_kcl_val(&arg_on_value) else {
2399                let message =
2400                    "The `on` argument to a sketch block must be convertible to a sketch or surface.".to_owned();
2401                debug_assert!(false, "{message}");
2402                return Err(KclError::new_semantic(KclErrorDetails::new(message, vec![range])).into());
2403            };
2404            let mut sketch_surface = arg_on.into_sketch_surface();
2405
2406            // Ensure that the plane has an ObjectId. Always create an Object so
2407            // that we're consistent with IDs.
2408            if sketch_surface.object_id().is_none() {
2409                // Look up the last object. Since this is where we would have
2410                // created it in real execution, it will be the last object.
2411                let Some(last_object) = exec_state.mod_local.artifacts.scene_objects.last() else {
2412                    return Err(internal_err(
2413                        "In sketch mode, the `on` plane argument must refer to an existing plane object.",
2414                        range,
2415                    )
2416                    .into());
2417                };
2418                sketch_surface.set_object_id(last_object.id);
2419            }
2420
2421            Ok((sketch_id, sketch_surface))
2422        }
2423    }
2424
2425    /// Create the sketch scene object and sketch-block artifact, and open
2426    /// the feature-tree group. Flat.
2427    pub(super) fn scene_setup(
2428        &self,
2429        sketch_id: ObjectId,
2430        sketch_surface: &SketchSurface,
2431        exec_state: &mut ExecState,
2432    ) -> Result<ArtifactId, KclError> {
2433        let range = SourceRange::from(self);
2434        let on_object_id = if let Some(object_id) = sketch_surface.object_id() {
2435            object_id
2436        } else {
2437            let message = "The `on` argument should have an object after ensure_sketch_plane_in_engine".to_owned();
2438            debug_assert!(false, "{message}");
2439            return Err(internal_err(message, range));
2440        };
2441        let sketch_ctor_on = sketch_on_frontend_plane(&self.arguments, on_object_id);
2442        let sketch_block_artifact_id = {
2443            use kcl_api::Plane;
2444            use kcl_api::SourceRef;
2445
2446            use crate::execution::CodeRef;
2447            use crate::execution::SketchBlock;
2448
2449            let on_object = exec_state.mod_local.artifacts.scene_object_by_id(on_object_id);
2450
2451            // Get the plane artifact ID so that we can do an exclusive borrow.
2452            let plane_artifact_id = on_object.map(|object| object.artifact_id);
2453            let plane_info = match &sketch_surface {
2454                SketchSurface::Plane(plane) => Some(super::artifact::artifact_plane_info(&plane.info)),
2455                SketchSurface::Face(_) => None,
2456            };
2457
2458            let standard_plane = match &sketch_ctor_on {
2459                Plane::Default(plane) => Some(*plane),
2460                Plane::Object(_) | Plane::PrimitiveFace(_) => None,
2461            };
2462
2463            let artifact_id = ArtifactId::from(exec_state.next_uuid());
2464            // Label the sketch with the name of the variable whose right-hand
2465            // side is being evaluated. This function runs before the sketch
2466            // body executes, so for `mySketch = sketch(on = XY) { ... }`,
2467            // `being_declared` still holds `mySketch` rather than a name
2468            // declared inside the body.
2469            //
2470            // The label is the nearest enclosing declaration, which is not
2471            // always the sketch's own name:
2472            // - `part = extrude(sketch(on = XY) { ... }, length = 10)` labels
2473            //   the sketch `part`.
2474            // - A sketch constructed inside a function body and returned takes
2475            //   the name of the declaration in progress at the call site, so
2476            //   two calls to that function can produce the same label.
2477            // - A sketch written as an expression statement has no declaration
2478            //   in progress, so its label is the empty string.
2479            let label = exec_state.mod_local.being_declared.clone().unwrap_or_default();
2480            // Create the sketch scene object and replace its placeholder.
2481            let sketch_scene_object = Object {
2482                id: sketch_id,
2483                kind: ObjectKind::Sketch(kcl_api::Sketch {
2484                    args: kcl_api::SketchCtor { on: sketch_ctor_on },
2485                    plane: on_object_id,
2486                    segments: Default::default(),
2487                    constraints: Default::default(),
2488                }),
2489                label,
2490                comments: Default::default(),
2491                artifact_id,
2492                source: SourceRef::new(self.into(), self.node_path.clone()),
2493            };
2494            exec_state.set_scene_object(sketch_scene_object);
2495
2496            // Create and add the sketch block artifact.
2497            exec_state.add_artifact(Artifact::SketchBlock(SketchBlock {
2498                id: artifact_id,
2499                standard_plane,
2500                plane_id: plane_artifact_id,
2501                plane_info,
2502                // Fill this in later once we create the path. We can't just add
2503                // the artifact later because order relative to constraint
2504                // artifacts is significant.
2505                path_id: None,
2506                code_ref: CodeRef::placeholder(range),
2507                sketch_id,
2508            }));
2509
2510            exec_state.push_op(Operation::GroupBegin {
2511                group: Group::SketchBlock { sketch_id },
2512                node_path: NodePath::placeholder(),
2513                source_range: range,
2514            });
2515            artifact_id
2516        };
2517        Ok(sketch_block_artifact_id)
2518    }
2519
2520    /// Solve constraints, send segments to the engine, update artifacts and
2521    /// scene objects, close the feature-tree group, and build the sketch's
2522    /// result object. Flat (the body has already executed); shared by both
2523    /// executors.
2524    #[allow(clippy::too_many_arguments)]
2525    pub(super) async fn finalize_sketch_block(
2526        &self,
2527        sketch_id: ObjectId,
2528        sketch_surface: &SketchSurface,
2529        sketch_block_artifact_id: ArtifactId,
2530        variables: IndexMap<String, KclValue>,
2531        mut sketch_block_state: SketchBlockState,
2532        exec_state: &mut ExecState,
2533        ctx: &ExecutorContext,
2534    ) -> Result<KclValue, KclError> {
2535        let range = SourceRange::from(self);
2536        // Translate sketch variables and constraints to solver input.
2537        let constraints = sketch_block_state
2538            .solver_constraints
2539            .iter()
2540            .cloned()
2541            .map(ezpz::ConstraintRequest::highest_priority)
2542            .chain(
2543                // Optional constraints have a lower priority.
2544                sketch_block_state
2545                    .solver_optional_constraints
2546                    .iter()
2547                    .cloned()
2548                    .map(|c| ezpz::ConstraintRequest::new(c, 1)),
2549            )
2550            .collect::<Vec<_>>();
2551        let initial_guesses = sketch_block_state
2552            .sketch_vars
2553            .iter()
2554            .map(|v| {
2555                let Some(sketch_var) = v.as_sketch_var() else {
2556                    return Err(internal_err("Expected sketch variable", self));
2557                };
2558                let constraint_id = sketch_var.id.to_constraint_id(range)?;
2559                // Normalize units.
2560                let number_value = KclValue::Number {
2561                    value: sketch_var.initial_value,
2562                    ty: sketch_var.ty,
2563                    meta: sketch_var.meta.clone(),
2564                };
2565                let initial_guess_value = normalize_to_solver_distance_unit(
2566                    &number_value,
2567                    v.into(),
2568                    exec_state,
2569                    "sketch variable initial value",
2570                )?;
2571                let initial_guess = if let Some(n) = initial_guess_value.as_ty_f64() {
2572                    n.n
2573                } else {
2574                    let message = format!(
2575                        "Expected number after coercion, but found {}",
2576                        initial_guess_value.human_friendly_type()
2577                    );
2578                    debug_assert!(false, "{}", &message);
2579                    return Err(internal_err(message, self));
2580                };
2581                Ok((constraint_id, initial_guess))
2582            })
2583            .collect::<Result<Vec<_>, KclError>>()?;
2584        // Solve constraints.
2585        let config = ezpz::Config::default()
2586            .with_max_iterations(50)
2587            .with_convergence_tolerance(solver_convergence_tolerance(exec_state));
2588        let solve_result = if exec_state.mod_local.freedom_analysis {
2589            ezpz::solve_analysis(&constraints, initial_guesses.clone(), config).map(|outcome| {
2590                let freedom_analysis = FreedomAnalysis::from_ezpz_analysis(outcome.analysis, constraints.len());
2591                (outcome.outcome, Some(freedom_analysis))
2592            })
2593        } else {
2594            ezpz::solve(&constraints, initial_guesses.clone(), config).map(|outcome| (outcome, None))
2595        };
2596        // Build a combined list of all constraints (regular + optional) for conflict detection
2597        let num_required_constraints = sketch_block_state.solver_constraints.len();
2598        let all_constraints: Vec<ezpz::Constraint> = sketch_block_state
2599            .solver_constraints
2600            .iter()
2601            .cloned()
2602            .chain(sketch_block_state.solver_optional_constraints.iter().cloned())
2603            .collect();
2604
2605        let (solve_outcome, solve_analysis) = match solve_result {
2606            Ok((solved, freedom)) => {
2607                if solved
2608                    .final_values()
2609                    .iter()
2610                    .any(|number| number.is_infinite() || number.is_nan())
2611                {
2612                    return Err(KclError::new_internal(KclErrorDetails::new(
2613                        "KCL's 2D constraint solver returned an invalid number".to_owned(),
2614                        vec![SourceRange::from(self)],
2615                    )));
2616                }
2617                let outcome = Solved::from_ezpz_outcome(solved, &all_constraints, num_required_constraints);
2618                if !outcome.converged {
2619                    exec_state.warn(
2620                        CompilationIssue::err(range, "Constraint solver failed to find a solution".to_owned()),
2621                        annotations::WARN_SOLVER,
2622                    );
2623                }
2624                (outcome, freedom)
2625            }
2626            Err(failure) => {
2627                match &failure.error {
2628                    NonLinearSystemError::FaerMatrix { .. }
2629                    | NonLinearSystemError::Faer { .. }
2630                    | NonLinearSystemError::FaerSolve { .. }
2631                    | NonLinearSystemError::FaerSvd(..) => {
2632                        // Constraint solver failed to find a solution. Build a
2633                        // solution that is the initial guesses.
2634                        exec_state.warn(
2635                            CompilationIssue::err(range, "Internal error in constraint solver".to_owned()),
2636                            annotations::WARN_SOLVER,
2637                        );
2638                        let final_values = initial_guesses.iter().map(|(_, v)| *v).collect::<Vec<_>>();
2639                        (
2640                            Solved {
2641                                final_values,
2642                                iterations: Default::default(),
2643                                warnings: failure.warnings,
2644                                priority_solved: Default::default(),
2645                                variables_in_conflicts: Default::default(),
2646                                unsatisfied_directional_constraints: Default::default(),
2647                                converged: false,
2648                            },
2649                            None,
2650                        )
2651                    }
2652                    NonLinearSystemError::EmptySystemNotAllowed
2653                    | NonLinearSystemError::WrongNumberGuesses { .. }
2654                    | NonLinearSystemError::MissingGuess { .. }
2655                    | NonLinearSystemError::NotFound(..) => {
2656                        // These indicate something's gone wrong in KCL or ezpz,
2657                        // it's not a user error. We should investigate this.
2658                        #[cfg(target_arch = "wasm32")]
2659                        web_sys::console::error_1(
2660                            &format!("Internal error from constraint solver: {}", failure.error).into(),
2661                        );
2662                        return Err(internal_err(
2663                            format!("Internal error from constraint solver: {}", failure.error),
2664                            self,
2665                        ));
2666                    }
2667                    _ => {
2668                        // Catch all error case so that it's not a breaking change to publish new errors.
2669                        return Err(internal_err(
2670                            format!("Error from constraint solver: {}", failure.error),
2671                            self,
2672                        ));
2673                    }
2674                }
2675            }
2676        };
2677        // Propagate warnings.
2678        for warning in &solve_outcome.warnings {
2679            let message = if let Some(index) = warning.about_constraint.as_ref() {
2680                format!("{}; constraint index {}", warning.content, index)
2681            } else {
2682                format!("{}", warning.content)
2683            };
2684            exec_state.warn(CompilationIssue::err(range, message), annotations::WARN_SOLVER);
2685        }
2686        if solve_outcome.converged {
2687            exec_state.mod_local.artifacts.refactor_metadata.extend(
2688                sketch_block_state
2689                    .pending_legacy_angle_refactor_metadata
2690                    .iter()
2691                    .filter_map(|pending| {
2692                        finalize_legacy_angle_refactor_meta(pending, &solve_outcome.final_values)
2693                            .map(RefactorMetadata::LegacyAngle)
2694                    }),
2695            );
2696        }
2697        // Substitute solutions back into sketch variables.
2698        let sketch_engine_id = exec_state.next_uuid();
2699        let solution_ty = solver_numeric_type(exec_state);
2700        let mut solved_segments = Vec::with_capacity(sketch_block_state.needed_by_engine.len());
2701        for unsolved_segment in &sketch_block_state.needed_by_engine {
2702            solved_segments.push(substitute_sketch_var_in_segment(
2703                unsolved_segment.clone(),
2704                sketch_surface,
2705                sketch_engine_id,
2706                None,
2707                &solve_outcome,
2708                solver_numeric_type(exec_state),
2709                solve_analysis.as_ref(),
2710            )?);
2711        }
2712        // Store variable solutions so that the sketch refactoring API can
2713        // write them back to the source. When editing a sketch block, we
2714        // exit early so that the sketch block that we're editing is always
2715        // the last one. Therefore, we should overwrite any previous
2716        // solutions.
2717        exec_state.mod_local.artifacts.var_solutions =
2718            sketch_block_state.var_solutions(&solve_outcome, solution_ty, SourceRange::from(self))?;
2719
2720        // Create scene objects after unknowns are solved.
2721        let scene_objects = create_segment_scene_objects(&solved_segments, range, exec_state)?;
2722
2723        // Build the sketch and send everything to the engine.
2724        let sketch = create_segments_in_engine(
2725            sketch_surface,
2726            sketch_engine_id,
2727            &mut solved_segments,
2728            &sketch_block_state.segment_tags,
2729            ctx,
2730            exec_state,
2731            range,
2732        )
2733        .await?;
2734
2735        // We now have enough information to fill in the path.
2736        if let Some(sketch_artifact_id) = sketch.as_ref().map(|s| s.artifact_id) {
2737            if let Some(Artifact::SketchBlock(sketch_block_artifact)) =
2738                exec_state.artifact_mut(sketch_block_artifact_id)
2739            {
2740                sketch_block_artifact.path_id = Some(sketch_artifact_id);
2741            } else {
2742                let message = "Sketch block artifact not found, so path couldn't be linked to it".to_owned();
2743                debug_assert!(false, "{message}");
2744                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
2745            }
2746        }
2747
2748        // Substitute solutions back into sketch variables. This time, collect
2749        // all the variables in the sketch block. The set of variables may have
2750        // overlap with the objects sent to the engine, but it isn't necessarily
2751        // the same.
2752        let variables = substitute_sketch_vars(
2753            variables,
2754            sketch_surface,
2755            sketch_engine_id,
2756            sketch.as_ref(),
2757            &solve_outcome,
2758            solution_ty,
2759            solve_analysis.as_ref(),
2760        )?;
2761
2762        let mut segment_object_ids = Vec::with_capacity(scene_objects.len());
2763        for scene_object in scene_objects {
2764            segment_object_ids.push(scene_object.id);
2765            // Fill in placeholder scene objects.
2766            exec_state.set_scene_object(scene_object);
2767        }
2768        // Update the sketch scene object with the segments.
2769        let Some(sketch_object) = exec_state.mod_local.artifacts.scene_object_by_id_mut(sketch_id) else {
2770            let message = format!("Sketch object not found after it was just created; id={:?}", sketch_id);
2771            debug_assert!(false, "{}", &message);
2772            return Err(internal_err(message, range));
2773        };
2774        let ObjectKind::Sketch(front_sketch) = &mut sketch_object.kind else {
2775            let message = format!(
2776                "Expected Sketch object after it was just created to be a sketch kind; id={:?}, actual={:?}",
2777                sketch_id, sketch_object
2778            );
2779            debug_assert!(
2780                false,
2781                "{}; scene_objects={:#?}",
2782                message, exec_state.mod_local.artifacts.scene_objects
2783            );
2784            return Err(internal_err(message, range));
2785        };
2786        front_sketch.segments.extend(segment_object_ids);
2787        // Update the sketch scene object with constraints.
2788        front_sketch
2789            .constraints
2790            .extend(std::mem::take(&mut sketch_block_state.sketch_constraints));
2791
2792        // Close the sketch block operation group.
2793        exec_state.push_op(Operation::GroupEnd);
2794
2795        // Warn if the sketch has conflicting constraints. Skip this when
2796        // freedom analysis didn't run (e.g., during dragging), because the
2797        // freedom values on points are stale defaults in that case.
2798        if exec_state.mod_local.freedom_analysis {
2799            let status = {
2800                let scene_objects = &exec_state.mod_local.artifacts.scene_objects;
2801                scene_objects
2802                    .get(sketch_id.0)
2803                    .and_then(|obj| sketch_constraint_status_for_sketch(scene_objects, obj))
2804            };
2805            if let Some(status) = status
2806                && status.status == ConstraintKind::OverConstrained
2807            {
2808                let description = if status.conflict_count == 1 {
2809                    "segment has"
2810                } else {
2811                    "segments have"
2812                };
2813                let message = format!(
2814                    "Sketch is over-constrained: {} {description} conflicting constraints.{}",
2815                    status.conflict_count,
2816                    signed_distance_conflict_hint(&solve_outcome),
2817                );
2818                exec_state.warn(
2819                    CompilationIssue::err(range, message),
2820                    annotations::WARN_OVER_CONSTRAINED_SKETCH,
2821                );
2822            }
2823        }
2824
2825        let properties = self.sketch_properties(sketch, variables);
2826        let metadata = Metadata {
2827            source_range: SourceRange::from(self),
2828        };
2829        let return_value = KclValue::Object {
2830            value: properties,
2831            constrainable: Default::default(),
2832            object_kind: KclObjectKind::Default,
2833            meta: vec![metadata],
2834        };
2835        Ok(return_value)
2836    }
2837
2838    /// Report a non-fatal error for each argument that isn't a valid sketch
2839    /// block parameter. Currently, the only valid parameter is `on`.
2840    fn check_for_unexpected_arguments(&self, args: &Args, exec_state: &mut ExecState) -> Result<(), KclError> {
2841        if !args.unlabeled.is_empty() {
2842            let message = "Sketch block doesn't support unlabeled arguments; argument shorthand should have already been desugared";
2843            debug_assert!(false, "{message}");
2844            return Err(KclError::new_internal(KclErrorDetails::new(
2845                message.to_owned(),
2846                vec![args.source_range],
2847            )));
2848        }
2849        for (label, arg) in &args.labeled {
2850            if label == SKETCH_BLOCK_PARAM_ON {
2851                continue;
2852            }
2853            exec_state.err(CompilationIssue::err(
2854                arg.source_range,
2855                unexpected_kw_arg_message(label, Some(SketchBlock::CALLEE_NAME)),
2856            ));
2857        }
2858        Ok(())
2859    }
2860
2861    pub(super) async fn load_sketch2_into_current_scope(
2862        &self,
2863        exec_state: &mut ExecState,
2864        ctx: &ExecutorContext,
2865        source_range: SourceRange,
2866    ) -> Result<(), KclError> {
2867        let path = vec!["std".to_owned(), "solver".to_owned()];
2868        let resolved_path = ModulePath::from_std_import_path(&path)?;
2869        let module_id = ctx
2870            .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
2871            .await?;
2872        let ModuleItems {
2873            environment: env_ref,
2874            exports,
2875            ..
2876        } = ctx.exec_module_for_items(module_id, exec_state, source_range).await?;
2877
2878        for name in exports {
2879            let value = exec_state
2880                .stack()
2881                .memory
2882                .get_from_owned(&name, env_ref, source_range, 0)?;
2883            exec_state.mut_stack().add(name, value, source_range)?;
2884        }
2885        Ok(())
2886    }
2887
2888    /// Augment the variables in the sketch block with properties that should be
2889    /// accessible on the returned sketch object. This includes metadata like
2890    /// the sketch so that the engine ID and surface can be accessed.
2891    pub(crate) fn sketch_properties(
2892        &self,
2893        sketch: Option<Sketch>,
2894        variables: HashMap<String, KclValue>,
2895    ) -> HashMap<String, KclValue> {
2896        let Some(sketch) = sketch else {
2897            // The sketch block did not produce a Sketch, so we cannot provide
2898            // it.
2899            return variables;
2900        };
2901
2902        let mut properties = variables;
2903
2904        let sketch_value = KclValue::Sketch {
2905            value: Box::new(sketch),
2906        };
2907        let mut meta_map = HashMap::with_capacity(1);
2908        meta_map.insert(SKETCH_OBJECT_META_SKETCH.to_owned(), sketch_value);
2909        let meta_value = KclValue::Object {
2910            value: meta_map,
2911            constrainable: false,
2912            object_kind: KclObjectKind::Default,
2913            meta: vec![Metadata {
2914                source_range: SourceRange::from(self),
2915            }],
2916        };
2917
2918        properties.insert(SKETCH_OBJECT_META.to_owned(), meta_value);
2919
2920        properties
2921    }
2922}
2923
2924impl SketchBlock {
2925    pub(super) fn prep_mem(&self, parent: EnvironmentRef, exec_state: &mut ExecState) -> Result<(), KclError> {
2926        exec_state.mut_stack().push_new_env_for_call(parent)
2927    }
2928}
2929
2930impl Node<SketchVar> {
2931    pub async fn get_result(&self, exec_state: &mut ExecState, _ctx: &ExecutorContext) -> Result<KclValue, KclError> {
2932        let Some(sketch_block_state) = &exec_state.mod_local.sketch_block else {
2933            return Err(KclError::new_semantic(KclErrorDetails::new(
2934                "Cannot use a sketch variable outside of a sketch block".to_owned(),
2935                vec![SourceRange::from(self)],
2936            )));
2937        };
2938        let id = sketch_block_state.next_sketch_var_id();
2939        let sketch_var = if let Some(initial) = &self.initial {
2940            KclValue::from_sketch_var_literal(initial, id, self.node_path.clone(), exec_state)
2941        } else {
2942            let metadata = Metadata {
2943                source_range: SourceRange::from(self),
2944            };
2945
2946            KclValue::SketchVar {
2947                value: Box::new(super::SketchVar {
2948                    id,
2949                    initial_value: 0.0,
2950                    ty: NumericType::default(),
2951                    node_path: self.node_path.clone(),
2952                    meta: vec![metadata],
2953                }),
2954            }
2955        };
2956
2957        let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
2958            return Err(KclError::new_semantic(KclErrorDetails::new(
2959                "Cannot use a sketch variable outside of a sketch block".to_owned(),
2960                vec![SourceRange::from(self)],
2961            )));
2962        };
2963        sketch_block_state.sketch_vars.push(sketch_var.clone());
2964
2965        Ok(sketch_var)
2966    }
2967}
2968
2969pub(super) async fn apply_ascription(
2970    value: &KclValue,
2971    ty: &Node<Type>,
2972    exec_state: &mut ExecState,
2973    ctx: &ExecutorContext,
2974    source_range: SourceRange,
2975) -> Result<KclValue, KclError> {
2976    let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, ctx, value.into(), false, false).await?;
2977
2978    if matches!(&ty, &RuntimeType::Primitive(PrimitiveType::Number(..))) {
2979        exec_state.clear_units_warnings(&source_range);
2980    }
2981
2982    value.coerce(&ty, CoercionMode::explicit(), exec_state).map_err(|e| {
2983        // A coercion that knows why it failed says so; the generic wording below
2984        // would replace that with a worse description of the same failure.
2985        if let Some(message) = e.message {
2986            return KclError::new_semantic(KclErrorDetails::new(message, vec![source_range]));
2987        }
2988
2989        let suggestion = if ty == RuntimeType::length() {
2990            ", you might try coercing to a fully specified numeric type such as `mm`"
2991        } else if ty == RuntimeType::angle() {
2992            ", you might try coercing to a fully specified numeric type such as `deg`"
2993        } else {
2994            ""
2995        };
2996        let ty_str = if let Some(ty) = value.principal_type() {
2997            format!("(with type `{ty}`) ")
2998        } else {
2999            String::new()
3000        };
3001        KclError::new_semantic(KclErrorDetails::new(
3002            format!(
3003                "could not coerce {} {ty_str}to type `{ty}`{suggestion}",
3004                value.human_friendly_type()
3005            ),
3006            vec![source_range],
3007        ))
3008    })
3009}
3010
3011impl Node<Name> {
3012    pub(super) async fn get_result(
3013        &self,
3014        exec_state: &mut ExecState,
3015        ctx: &ExecutorContext,
3016    ) -> Result<KclValue, KclError> {
3017        // Await first so being_declared is read only on the error path,
3018        // instead of cloned before every lookup.
3019        let result = self.get_result_inner(exec_state, ctx).await;
3020        result.map_err(|e| var_in_own_ref_err(e, &exec_state.mod_local.being_declared))
3021    }
3022
3023    async fn get_result_inner(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
3024        if self.abs_path {
3025            return Err(KclError::new_semantic(KclErrorDetails::new(
3026                ABSOLUTE_PATHS_NOT_SUPPORTED.to_owned(),
3027                self.as_source_ranges(),
3028            )));
3029        }
3030
3031        if self.path.is_empty() {
3032            if let Ok(item_value) = exec_state.stack().get(&self.name.name, self.into()) {
3033                return Ok(item_value);
3034            }
3035
3036            let mod_name = format!("{}{}", memory::MODULE_PREFIX, self.name.name);
3037            let not_defined = match exec_state.stack().get(&mod_name, self.into()) {
3038                Ok(module) => return Ok(module),
3039                Err(err) => err,
3040            };
3041
3042            // No value and no module of this name exists. If a type does, report
3043            // that instead: "is not defined" would point away from the mistake.
3044            // Failing that, the name may be a declaration skipped as not yet added.
3045            return Err(type_used_as_value(exec_state, &self.name)
3046                .unwrap_or_else(|| exec_state.with_not_yet_added_hint(&[&self.name.name, &mod_name], not_defined)));
3047        }
3048
3049        let mut mem_spec: Option<ModuleItems> = None;
3050        for (index, p) in self.path.iter().enumerate() {
3051            // Only the last segment can name an enum, because what follows an
3052            // enum is a variant rather than something to traverse into.
3053            let non_enum_type = match enum_named_by_segment(exec_state, p, mem_spec.as_ref()) {
3054                Some(EnumPathHead::Enum {
3055                    def,
3056                    binding_experimental,
3057                }) => {
3058                    if let Some(next) = self.path.get(index + 1) {
3059                        return Err(KclError::new_semantic(KclErrorDetails::new(
3060                            format!(
3061                                "`{}` is an enum, so only a variant name can follow it. There is nothing to reach through `{}::{}`.",
3062                                p.name, p.name, next.name
3063                            ),
3064                            p.as_source_ranges(),
3065                        )));
3066                    }
3067
3068                    return enum_variant_value(def, binding_experimental, &self.name, exec_state);
3069                }
3070                Some(EnumPathHead::NonEnumType) => true,
3071                None => false,
3072            };
3073
3074            let value = match &mem_spec {
3075                Some(items) => {
3076                    if !items.exports.contains(&p.name) {
3077                        if non_enum_type {
3078                            return Err(non_enum_type_in_path(p));
3079                        }
3080                        return Err(KclError::new_semantic(KclErrorDetails::new(
3081                            format!("Item {} not found in module's exported items", p.name),
3082                            p.as_source_ranges(),
3083                        )));
3084                    }
3085
3086                    exec_state
3087                        .stack()
3088                        .memory
3089                        .get_from_owned(&p.name, items.environment, p.as_source_range(), 0)?
3090                }
3091                None => {
3092                    let module_key = format!("{}{}", memory::MODULE_PREFIX, p.name);
3093                    match exec_state.stack().get(&module_key, self.into()) {
3094                        Ok(value) => value,
3095                        Err(_) if non_enum_type => return Err(non_enum_type_in_path(p)),
3096                        Err(err) => return Err(exec_state.with_not_yet_added_hint(&[&module_key], err)),
3097                    }
3098                }
3099            };
3100
3101            let module_id = match value {
3102                KclValue::Module { value, .. } => value,
3103                value => {
3104                    return Err(KclError::new_semantic(KclErrorDetails::new(
3105                        format!(
3106                            "Identifier in path must refer to a module, found {}",
3107                            value.human_friendly_type()
3108                        ),
3109                        p.as_source_ranges(),
3110                    )));
3111                }
3112            };
3113
3114            mem_spec = Some(
3115                ctx.exec_module_for_items(module_id, exec_state, p.as_source_range())
3116                    .await?,
3117            );
3118        }
3119
3120        let ModuleItems {
3121            environment: env,
3122            exports,
3123            not_yet_added,
3124        } = mem_spec.unwrap();
3125
3126        let item_exported = exports.contains(&self.name.name);
3127        let item_value = exec_state
3128            .stack()
3129            .memory
3130            .get_from_owned(&self.name.name, env, self.name.as_source_range(), 0);
3131
3132        // Item is defined and exported.
3133        if item_exported && item_value.is_ok() {
3134            return item_value;
3135        }
3136
3137        let mod_name = format!("{}{}", memory::MODULE_PREFIX, self.name.name);
3138        let mod_exported = exports.contains(&mod_name);
3139        let mod_value = exec_state
3140            .stack()
3141            .memory
3142            .get_from_owned(&mod_name, env, self.name.as_source_range(), 0);
3143
3144        // Module is defined and exported.
3145        if mod_exported && mod_value.is_ok() {
3146            return mod_value;
3147        }
3148
3149        // Neither item or module is defined. The module may have skipped a
3150        // declaration of this name as not yet added.
3151        if item_value.is_err() && mod_value.is_err() {
3152            return item_value
3153                .map_err(|err| exec_state.with_not_yet_added_hint_from(&not_yet_added, &[&self.name.name], err));
3154        }
3155
3156        // Either item or module is defined, but not exported.
3157        debug_assert!((item_value.is_ok() && !item_exported) || (mod_value.is_ok() && !mod_exported));
3158        Err(KclError::new_semantic(KclErrorDetails::new(
3159            format!("Item {} not found in module's exported items", self.name.name),
3160            self.name.as_source_ranges(),
3161        )))
3162    }
3163}
3164
3165fn mock_array_may_have_engine_dependent_cardinality(ty: &RuntimeType) -> bool {
3166    match ty {
3167        RuntimeType::Primitive(
3168            PrimitiveType::Sketch
3169            | PrimitiveType::Solid
3170            | PrimitiveType::Face
3171            | PrimitiveType::Edge
3172            | PrimitiveType::BoundedEdge
3173            | PrimitiveType::ImportedGeometry,
3174        ) => true,
3175        RuntimeType::Union(types) => types.iter().any(mock_array_may_have_engine_dependent_cardinality),
3176        _ => false,
3177    }
3178}
3179
3180impl Node<MemberExpression> {
3181    /// Apply property access or indexing to an already-evaluated object: the
3182    /// evaluation-free second half of member-expression execution.
3183    pub(super) async fn apply_member(
3184        &self,
3185        object: KclValue,
3186        property: Property,
3187        exec_state: &mut ExecState,
3188        ctx: &ExecutorContext,
3189    ) -> Result<KclValueControlFlow, KclError> {
3190        // Result values of the member access itself carry the whole
3191        // expression's metadata.
3192        let meta = Metadata {
3193            source_range: SourceRange::from(self),
3194        };
3195
3196        // Check the property and object match -- e.g. ints for arrays, strs for objects.
3197        match (object, property, self.computed) {
3198            (KclValue::Segment { value: segment }, Property::String(property), false) => match property.as_str() {
3199                "at" => match &segment.repr {
3200                    SegmentRepr::Unsolved { segment } => {
3201                        match &segment.kind {
3202                            UnsolvedSegmentKind::Point { position, .. } => {
3203                                // TODO: assert that types of all elements are the same.
3204                                Ok(KclValue::HomArray {
3205                                    value: vec![
3206                                        KclValue::from_unsolved_expr(position[0].clone(), segment.meta.clone()),
3207                                        KclValue::from_unsolved_expr(position[1].clone(), segment.meta.clone()),
3208                                    ],
3209                                    ty: RuntimeType::any(),
3210                                }
3211                                .normal())
3212                            }
3213                            _ => Err(KclError::new_undefined_value(
3214                                KclErrorDetails::new(
3215                                    format!("Property '{property}' not found in segment"),
3216                                    vec![self.clone().into()],
3217                                ),
3218                                None,
3219                            )),
3220                        }
3221                    }
3222                    SegmentRepr::Solved { segment } => {
3223                        match &segment.kind {
3224                            SegmentKind::Point { position, .. } => {
3225                                // TODO: assert that types of all elements are the same.
3226                                Ok(KclValue::array_from_point2d(
3227                                    [position[0].n, position[1].n],
3228                                    position[0].ty,
3229                                    segment.meta.clone(),
3230                                )
3231                                .normal())
3232                            }
3233                            _ => Err(KclError::new_undefined_value(
3234                                KclErrorDetails::new(
3235                                    format!("Property '{property}' not found in segment"),
3236                                    vec![self.clone().into()],
3237                                ),
3238                                None,
3239                            )),
3240                        }
3241                    }
3242                },
3243                "start" => match &segment.repr {
3244                    SegmentRepr::Unsolved { segment } => match &segment.kind {
3245                        UnsolvedSegmentKind::Point { .. } => Err(KclError::new_undefined_value(
3246                            KclErrorDetails::new(
3247                                format!("Property '{property}' not found in point segment"),
3248                                vec![self.clone().into()],
3249                            ),
3250                            None,
3251                        )),
3252                        UnsolvedSegmentKind::Line {
3253                            start,
3254                            ctor,
3255                            start_object_id,
3256                            ..
3257                        } => Ok(KclValue::Segment {
3258                            value: Box::new(AbstractSegment {
3259                                repr: SegmentRepr::Unsolved {
3260                                    segment: Box::new(UnsolvedSegment {
3261                                        id: segment.id,
3262                                        object_id: *start_object_id,
3263                                        kind: UnsolvedSegmentKind::Point {
3264                                            position: start.clone(),
3265                                            ctor: Box::new(PointCtor {
3266                                                position: ctor.start.clone(),
3267                                            }),
3268                                        },
3269                                        tag: segment.tag.clone(),
3270                                        node_path: segment.node_path.clone(),
3271                                        meta: segment.meta.clone(),
3272                                    }),
3273                                },
3274                                meta: segment.meta.clone(),
3275                            }),
3276                        }
3277                        .normal()),
3278                        UnsolvedSegmentKind::Arc {
3279                            start,
3280                            ctor,
3281                            start_object_id,
3282                            ..
3283                        } => Ok(KclValue::Segment {
3284                            value: Box::new(AbstractSegment {
3285                                repr: SegmentRepr::Unsolved {
3286                                    segment: Box::new(UnsolvedSegment {
3287                                        id: segment.id,
3288                                        object_id: *start_object_id,
3289                                        kind: UnsolvedSegmentKind::Point {
3290                                            position: start.clone(),
3291                                            ctor: Box::new(PointCtor {
3292                                                position: ctor.start.clone(),
3293                                            }),
3294                                        },
3295                                        tag: segment.tag.clone(),
3296                                        node_path: segment.node_path.clone(),
3297                                        meta: segment.meta.clone(),
3298                                    }),
3299                                },
3300                                meta: segment.meta.clone(),
3301                            }),
3302                        }
3303                        .normal()),
3304                        UnsolvedSegmentKind::Circle {
3305                            start,
3306                            ctor,
3307                            start_object_id,
3308                            ..
3309                        } => Ok(KclValue::Segment {
3310                            value: Box::new(AbstractSegment {
3311                                repr: SegmentRepr::Unsolved {
3312                                    segment: Box::new(UnsolvedSegment {
3313                                        id: segment.id,
3314                                        object_id: *start_object_id,
3315                                        kind: UnsolvedSegmentKind::Point {
3316                                            position: start.clone(),
3317                                            ctor: Box::new(PointCtor {
3318                                                position: ctor.start.clone(),
3319                                            }),
3320                                        },
3321                                        tag: segment.tag.clone(),
3322                                        node_path: segment.node_path.clone(),
3323                                        meta: segment.meta.clone(),
3324                                    }),
3325                                },
3326                                meta: segment.meta.clone(),
3327                            }),
3328                        }
3329                        .normal()),
3330                        UnsolvedSegmentKind::ControlPointSpline { .. } => Err(KclError::new_undefined_value(
3331                            KclErrorDetails::new(
3332                                format!("Property '{property}' not found in segment"),
3333                                vec![self.clone().into()],
3334                            ),
3335                            None,
3336                        )),
3337                    },
3338                    SegmentRepr::Solved { segment } => match &segment.kind {
3339                        SegmentKind::Point { .. } => Err(KclError::new_undefined_value(
3340                            KclErrorDetails::new(
3341                                format!("Property '{property}' not found in point segment"),
3342                                vec![self.clone().into()],
3343                            ),
3344                            None,
3345                        )),
3346                        SegmentKind::Line {
3347                            start,
3348                            ctor,
3349                            start_object_id,
3350                            start_freedom,
3351                            ..
3352                        } => Ok(KclValue::Segment {
3353                            value: Box::new(AbstractSegment {
3354                                repr: SegmentRepr::Solved {
3355                                    segment: Box::new(Segment {
3356                                        id: segment.id,
3357                                        object_id: *start_object_id,
3358                                        kind: SegmentKind::Point {
3359                                            position: start.clone(),
3360                                            ctor: Box::new(PointCtor {
3361                                                position: ctor.start.clone(),
3362                                            }),
3363                                            freedom: *start_freedom,
3364                                        },
3365                                        surface: segment.surface.clone(),
3366                                        sketch_id: segment.sketch_id,
3367                                        sketch: segment.sketch.clone(),
3368                                        tag: segment.tag.clone(),
3369                                        node_path: segment.node_path.clone(),
3370                                        meta: segment.meta.clone(),
3371                                    }),
3372                                },
3373                                meta: segment.meta.clone(),
3374                            }),
3375                        }
3376                        .normal()),
3377                        SegmentKind::Arc {
3378                            start,
3379                            ctor,
3380                            start_object_id,
3381                            start_freedom,
3382                            ..
3383                        } => Ok(KclValue::Segment {
3384                            value: Box::new(AbstractSegment {
3385                                repr: SegmentRepr::Solved {
3386                                    segment: Box::new(Segment {
3387                                        id: segment.id,
3388                                        object_id: *start_object_id,
3389                                        kind: SegmentKind::Point {
3390                                            position: start.clone(),
3391                                            ctor: Box::new(PointCtor {
3392                                                position: ctor.start.clone(),
3393                                            }),
3394                                            freedom: *start_freedom,
3395                                        },
3396                                        surface: segment.surface.clone(),
3397                                        sketch_id: segment.sketch_id,
3398                                        sketch: segment.sketch.clone(),
3399                                        tag: segment.tag.clone(),
3400                                        node_path: segment.node_path.clone(),
3401                                        meta: segment.meta.clone(),
3402                                    }),
3403                                },
3404                                meta: segment.meta.clone(),
3405                            }),
3406                        }
3407                        .normal()),
3408                        SegmentKind::Circle {
3409                            start,
3410                            ctor,
3411                            start_object_id,
3412                            start_freedom,
3413                            ..
3414                        } => Ok(KclValue::Segment {
3415                            value: Box::new(AbstractSegment {
3416                                repr: SegmentRepr::Solved {
3417                                    segment: Box::new(Segment {
3418                                        id: segment.id,
3419                                        object_id: *start_object_id,
3420                                        kind: SegmentKind::Point {
3421                                            position: start.clone(),
3422                                            ctor: Box::new(PointCtor {
3423                                                position: ctor.start.clone(),
3424                                            }),
3425                                            freedom: *start_freedom,
3426                                        },
3427                                        surface: segment.surface.clone(),
3428                                        sketch_id: segment.sketch_id,
3429                                        sketch: segment.sketch.clone(),
3430                                        tag: segment.tag.clone(),
3431                                        node_path: segment.node_path.clone(),
3432                                        meta: segment.meta.clone(),
3433                                    }),
3434                                },
3435                                meta: segment.meta.clone(),
3436                            }),
3437                        }
3438                        .normal()),
3439                        SegmentKind::ControlPointSpline { .. } => Err(KclError::new_undefined_value(
3440                            KclErrorDetails::new(
3441                                format!("Property '{property}' not found in segment"),
3442                                vec![self.clone().into()],
3443                            ),
3444                            None,
3445                        )),
3446                    },
3447                },
3448                "end" => match &segment.repr {
3449                    SegmentRepr::Unsolved { segment } => match &segment.kind {
3450                        UnsolvedSegmentKind::Point { .. } => Err(KclError::new_undefined_value(
3451                            KclErrorDetails::new(
3452                                format!("Property '{property}' not found in point segment"),
3453                                vec![self.clone().into()],
3454                            ),
3455                            None,
3456                        )),
3457                        UnsolvedSegmentKind::Line {
3458                            end,
3459                            ctor,
3460                            end_object_id,
3461                            ..
3462                        } => Ok(KclValue::Segment {
3463                            value: Box::new(AbstractSegment {
3464                                repr: SegmentRepr::Unsolved {
3465                                    segment: Box::new(UnsolvedSegment {
3466                                        id: segment.id,
3467                                        object_id: *end_object_id,
3468                                        kind: UnsolvedSegmentKind::Point {
3469                                            position: end.clone(),
3470                                            ctor: Box::new(PointCtor {
3471                                                position: ctor.end.clone(),
3472                                            }),
3473                                        },
3474                                        tag: segment.tag.clone(),
3475                                        node_path: segment.node_path.clone(),
3476                                        meta: segment.meta.clone(),
3477                                    }),
3478                                },
3479                                meta: segment.meta.clone(),
3480                            }),
3481                        }
3482                        .normal()),
3483                        UnsolvedSegmentKind::Arc {
3484                            end,
3485                            ctor,
3486                            end_object_id,
3487                            ..
3488                        } => Ok(KclValue::Segment {
3489                            value: Box::new(AbstractSegment {
3490                                repr: SegmentRepr::Unsolved {
3491                                    segment: Box::new(UnsolvedSegment {
3492                                        id: segment.id,
3493                                        object_id: *end_object_id,
3494                                        kind: UnsolvedSegmentKind::Point {
3495                                            position: end.clone(),
3496                                            ctor: Box::new(PointCtor {
3497                                                position: ctor.end.clone(),
3498                                            }),
3499                                        },
3500                                        tag: segment.tag.clone(),
3501                                        node_path: segment.node_path.clone(),
3502                                        meta: segment.meta.clone(),
3503                                    }),
3504                                },
3505                                meta: segment.meta.clone(),
3506                            }),
3507                        }
3508                        .normal()),
3509                        UnsolvedSegmentKind::Circle { .. } => Err(KclError::new_undefined_value(
3510                            KclErrorDetails::new(
3511                                format!("Property '{property}' not found in segment"),
3512                                vec![self.into()],
3513                            ),
3514                            None,
3515                        )),
3516                        UnsolvedSegmentKind::ControlPointSpline { .. } => Err(KclError::new_undefined_value(
3517                            KclErrorDetails::new(
3518                                format!("Property '{property}' not found in segment"),
3519                                vec![self.clone().into()],
3520                            ),
3521                            None,
3522                        )),
3523                    },
3524                    SegmentRepr::Solved { segment } => match &segment.kind {
3525                        SegmentKind::Point { .. } => Err(KclError::new_undefined_value(
3526                            KclErrorDetails::new(
3527                                format!("Property '{property}' not found in point segment"),
3528                                vec![self.clone().into()],
3529                            ),
3530                            None,
3531                        )),
3532                        SegmentKind::Line {
3533                            end,
3534                            ctor,
3535                            end_object_id,
3536                            end_freedom,
3537                            ..
3538                        } => Ok(KclValue::Segment {
3539                            value: Box::new(AbstractSegment {
3540                                repr: SegmentRepr::Solved {
3541                                    segment: Box::new(Segment {
3542                                        id: segment.id,
3543                                        object_id: *end_object_id,
3544                                        kind: SegmentKind::Point {
3545                                            position: end.clone(),
3546                                            ctor: Box::new(PointCtor {
3547                                                position: ctor.end.clone(),
3548                                            }),
3549                                            freedom: *end_freedom,
3550                                        },
3551                                        surface: segment.surface.clone(),
3552                                        sketch_id: segment.sketch_id,
3553                                        sketch: segment.sketch.clone(),
3554                                        tag: segment.tag.clone(),
3555                                        node_path: segment.node_path.clone(),
3556                                        meta: segment.meta.clone(),
3557                                    }),
3558                                },
3559                                meta: segment.meta.clone(),
3560                            }),
3561                        }
3562                        .normal()),
3563                        SegmentKind::Arc {
3564                            end,
3565                            ctor,
3566                            end_object_id,
3567                            end_freedom,
3568                            ..
3569                        } => Ok(KclValue::Segment {
3570                            value: Box::new(AbstractSegment {
3571                                repr: SegmentRepr::Solved {
3572                                    segment: Box::new(Segment {
3573                                        id: segment.id,
3574                                        object_id: *end_object_id,
3575                                        kind: SegmentKind::Point {
3576                                            position: end.clone(),
3577                                            ctor: Box::new(PointCtor {
3578                                                position: ctor.end.clone(),
3579                                            }),
3580                                            freedom: *end_freedom,
3581                                        },
3582                                        surface: segment.surface.clone(),
3583                                        sketch_id: segment.sketch_id,
3584                                        sketch: segment.sketch.clone(),
3585                                        tag: segment.tag.clone(),
3586                                        node_path: segment.node_path.clone(),
3587                                        meta: segment.meta.clone(),
3588                                    }),
3589                                },
3590                                meta: segment.meta.clone(),
3591                            }),
3592                        }
3593                        .normal()),
3594                        SegmentKind::Circle { .. } => Err(KclError::new_undefined_value(
3595                            KclErrorDetails::new(
3596                                format!("Property '{property}' not found in segment"),
3597                                vec![self.into()],
3598                            ),
3599                            None,
3600                        )),
3601                        SegmentKind::ControlPointSpline { .. } => Err(KclError::new_undefined_value(
3602                            KclErrorDetails::new(
3603                                format!("Property '{property}' not found in segment"),
3604                                vec![self.clone().into()],
3605                            ),
3606                            None,
3607                        )),
3608                    },
3609                },
3610                "center" => match &segment.repr {
3611                    SegmentRepr::Unsolved { segment } => match &segment.kind {
3612                        UnsolvedSegmentKind::Arc {
3613                            center,
3614                            ctor,
3615                            center_object_id,
3616                            ..
3617                        } => Ok(KclValue::Segment {
3618                            value: Box::new(AbstractSegment {
3619                                repr: SegmentRepr::Unsolved {
3620                                    segment: Box::new(UnsolvedSegment {
3621                                        id: segment.id,
3622                                        object_id: *center_object_id,
3623                                        kind: UnsolvedSegmentKind::Point {
3624                                            position: center.clone(),
3625                                            ctor: Box::new(PointCtor {
3626                                                position: ctor.center.clone(),
3627                                            }),
3628                                        },
3629                                        tag: segment.tag.clone(),
3630                                        node_path: segment.node_path.clone(),
3631                                        meta: segment.meta.clone(),
3632                                    }),
3633                                },
3634                                meta: segment.meta.clone(),
3635                            }),
3636                        }
3637                        .normal()),
3638                        UnsolvedSegmentKind::Circle {
3639                            center,
3640                            ctor,
3641                            center_object_id,
3642                            ..
3643                        } => Ok(KclValue::Segment {
3644                            value: Box::new(AbstractSegment {
3645                                repr: SegmentRepr::Unsolved {
3646                                    segment: Box::new(UnsolvedSegment {
3647                                        id: segment.id,
3648                                        object_id: *center_object_id,
3649                                        kind: UnsolvedSegmentKind::Point {
3650                                            position: center.clone(),
3651                                            ctor: Box::new(PointCtor {
3652                                                position: ctor.center.clone(),
3653                                            }),
3654                                        },
3655                                        tag: segment.tag.clone(),
3656                                        node_path: segment.node_path.clone(),
3657                                        meta: segment.meta.clone(),
3658                                    }),
3659                                },
3660                                meta: segment.meta.clone(),
3661                            }),
3662                        }
3663                        .normal()),
3664                        _ => Err(KclError::new_undefined_value(
3665                            KclErrorDetails::new(
3666                                format!("Property '{property}' not found in segment"),
3667                                vec![self.clone().into()],
3668                            ),
3669                            None,
3670                        )),
3671                    },
3672                    SegmentRepr::Solved { segment } => match &segment.kind {
3673                        SegmentKind::Arc {
3674                            center,
3675                            ctor,
3676                            center_object_id,
3677                            center_freedom,
3678                            ..
3679                        } => Ok(KclValue::Segment {
3680                            value: Box::new(AbstractSegment {
3681                                repr: SegmentRepr::Solved {
3682                                    segment: Box::new(Segment {
3683                                        id: segment.id,
3684                                        object_id: *center_object_id,
3685                                        kind: SegmentKind::Point {
3686                                            position: center.clone(),
3687                                            ctor: Box::new(PointCtor {
3688                                                position: ctor.center.clone(),
3689                                            }),
3690                                            freedom: *center_freedom,
3691                                        },
3692                                        surface: segment.surface.clone(),
3693                                        sketch_id: segment.sketch_id,
3694                                        sketch: segment.sketch.clone(),
3695                                        tag: segment.tag.clone(),
3696                                        node_path: segment.node_path.clone(),
3697                                        meta: segment.meta.clone(),
3698                                    }),
3699                                },
3700                                meta: segment.meta.clone(),
3701                            }),
3702                        }
3703                        .normal()),
3704                        SegmentKind::Circle {
3705                            center,
3706                            ctor,
3707                            center_object_id,
3708                            center_freedom,
3709                            ..
3710                        } => Ok(KclValue::Segment {
3711                            value: Box::new(AbstractSegment {
3712                                repr: SegmentRepr::Solved {
3713                                    segment: Box::new(Segment {
3714                                        id: segment.id,
3715                                        object_id: *center_object_id,
3716                                        kind: SegmentKind::Point {
3717                                            position: center.clone(),
3718                                            ctor: Box::new(PointCtor {
3719                                                position: ctor.center.clone(),
3720                                            }),
3721                                            freedom: *center_freedom,
3722                                        },
3723                                        surface: segment.surface.clone(),
3724                                        sketch_id: segment.sketch_id,
3725                                        sketch: segment.sketch.clone(),
3726                                        tag: segment.tag.clone(),
3727                                        node_path: segment.node_path.clone(),
3728                                        meta: segment.meta.clone(),
3729                                    }),
3730                                },
3731                                meta: segment.meta.clone(),
3732                            }),
3733                        }
3734                        .normal()),
3735                        _ => Err(KclError::new_undefined_value(
3736                            KclErrorDetails::new(
3737                                format!("Property '{property}' not found in segment"),
3738                                vec![self.clone().into()],
3739                            ),
3740                            None,
3741                        )),
3742                    },
3743                },
3744                "controls" => match &segment.repr {
3745                    SegmentRepr::Unsolved { segment } => match &segment.kind {
3746                        UnsolvedSegmentKind::ControlPointSpline {
3747                            controls,
3748                            ctor,
3749                            control_object_ids,
3750                            ..
3751                        } => Ok(KclValue::HomArray {
3752                            value: controls
3753                                .iter()
3754                                .zip(control_object_ids.iter())
3755                                .zip(ctor.points.iter())
3756                                .map(|((position, object_id), ctor_point)| KclValue::Segment {
3757                                    value: Box::new(AbstractSegment {
3758                                        repr: SegmentRepr::Unsolved {
3759                                            segment: Box::new(UnsolvedSegment {
3760                                                id: segment.id,
3761                                                object_id: *object_id,
3762                                                kind: UnsolvedSegmentKind::Point {
3763                                                    position: position.clone(),
3764                                                    ctor: Box::new(PointCtor {
3765                                                        position: ctor_point.clone(),
3766                                                    }),
3767                                                },
3768                                                tag: segment.tag.clone(),
3769                                                node_path: segment.node_path.clone(),
3770                                                meta: segment.meta.clone(),
3771                                            }),
3772                                        },
3773                                        meta: segment.meta.clone(),
3774                                    }),
3775                                })
3776                                .collect(),
3777                            ty: RuntimeType::segment(),
3778                        }
3779                        .normal()),
3780                        _ => Err(KclError::new_undefined_value(
3781                            KclErrorDetails::new(
3782                                format!("Property '{property}' not found in segment"),
3783                                vec![self.clone().into()],
3784                            ),
3785                            None,
3786                        )),
3787                    },
3788                    SegmentRepr::Solved { segment } => match &segment.kind {
3789                        SegmentKind::ControlPointSpline {
3790                            controls,
3791                            ctor,
3792                            control_object_ids,
3793                            control_freedoms,
3794                            ..
3795                        } => Ok(KclValue::HomArray {
3796                            value: controls
3797                                .iter()
3798                                .zip(control_object_ids.iter())
3799                                .zip(control_freedoms.iter())
3800                                .zip(ctor.points.iter())
3801                                .map(|(((position, object_id), freedom), ctor_point)| KclValue::Segment {
3802                                    value: Box::new(AbstractSegment {
3803                                        repr: SegmentRepr::Solved {
3804                                            segment: Box::new(Segment {
3805                                                id: segment.id,
3806                                                object_id: *object_id,
3807                                                kind: SegmentKind::Point {
3808                                                    position: position.clone(),
3809                                                    ctor: Box::new(PointCtor {
3810                                                        position: ctor_point.clone(),
3811                                                    }),
3812                                                    freedom: *freedom,
3813                                                },
3814                                                surface: segment.surface.clone(),
3815                                                sketch_id: segment.sketch_id,
3816                                                sketch: segment.sketch.clone(),
3817                                                tag: segment.tag.clone(),
3818                                                node_path: segment.node_path.clone(),
3819                                                meta: segment.meta.clone(),
3820                                            }),
3821                                        },
3822                                        meta: segment.meta.clone(),
3823                                    }),
3824                                })
3825                                .collect(),
3826                            ty: RuntimeType::segment(),
3827                        }
3828                        .normal()),
3829                        _ => Err(KclError::new_undefined_value(
3830                            KclErrorDetails::new(
3831                                format!("Property '{property}' not found in segment"),
3832                                vec![self.clone().into()],
3833                            ),
3834                            None,
3835                        )),
3836                    },
3837                },
3838                "edges" => match &segment.repr {
3839                    SegmentRepr::Unsolved { segment } => match &segment.kind {
3840                        UnsolvedSegmentKind::ControlPointSpline {
3841                            controls,
3842                            ctor,
3843                            control_object_ids,
3844                            control_polygon_edge_object_ids,
3845                            construction,
3846                            ..
3847                        } => Ok(KclValue::HomArray {
3848                            value: control_polygon_edge_object_ids
3849                                .iter()
3850                                .enumerate()
3851                                .map(|(index, object_id)| KclValue::Segment {
3852                                    value: Box::new(AbstractSegment {
3853                                        repr: SegmentRepr::Unsolved {
3854                                            segment: Box::new(UnsolvedSegment {
3855                                                id: segment.id,
3856                                                object_id: *object_id,
3857                                                kind: UnsolvedSegmentKind::Line {
3858                                                    start: controls[index].clone(),
3859                                                    end: controls[index + 1].clone(),
3860                                                    ctor: Box::new(LineCtor {
3861                                                        start: ctor.points[index].clone(),
3862                                                        end: ctor.points[index + 1].clone(),
3863                                                        construction: Some(*construction),
3864                                                    }),
3865                                                    start_object_id: control_object_ids[index],
3866                                                    end_object_id: control_object_ids[index + 1],
3867                                                    construction: *construction,
3868                                                },
3869                                                tag: segment.tag.clone(),
3870                                                node_path: segment.node_path.clone(),
3871                                                meta: segment.meta.clone(),
3872                                            }),
3873                                        },
3874                                        meta: segment.meta.clone(),
3875                                    }),
3876                                })
3877                                .collect(),
3878                            ty: RuntimeType::segment(),
3879                        }
3880                        .normal()),
3881                        _ => Err(KclError::new_undefined_value(
3882                            KclErrorDetails::new(
3883                                format!("Property '{property}' not found in segment"),
3884                                vec![self.clone().into()],
3885                            ),
3886                            None,
3887                        )),
3888                    },
3889                    SegmentRepr::Solved { segment } => match &segment.kind {
3890                        SegmentKind::ControlPointSpline {
3891                            controls,
3892                            ctor,
3893                            control_object_ids,
3894                            control_polygon_edge_object_ids,
3895                            control_freedoms,
3896                            construction,
3897                            ..
3898                        } => Ok(KclValue::HomArray {
3899                            value: control_polygon_edge_object_ids
3900                                .iter()
3901                                .enumerate()
3902                                .map(|(index, object_id)| KclValue::Segment {
3903                                    value: Box::new(AbstractSegment {
3904                                        repr: SegmentRepr::Solved {
3905                                            segment: Box::new(Segment {
3906                                                id: segment.id,
3907                                                object_id: *object_id,
3908                                                kind: SegmentKind::Line {
3909                                                    start: controls[index].clone(),
3910                                                    end: controls[index + 1].clone(),
3911                                                    ctor: Box::new(LineCtor {
3912                                                        start: ctor.points[index].clone(),
3913                                                        end: ctor.points[index + 1].clone(),
3914                                                        construction: Some(*construction),
3915                                                    }),
3916                                                    start_object_id: control_object_ids[index],
3917                                                    end_object_id: control_object_ids[index + 1],
3918                                                    start_freedom: control_freedoms[index],
3919                                                    end_freedom: control_freedoms[index + 1],
3920                                                    construction: *construction,
3921                                                },
3922                                                surface: segment.surface.clone(),
3923                                                sketch_id: segment.sketch_id,
3924                                                sketch: segment.sketch.clone(),
3925                                                tag: segment.tag.clone(),
3926                                                node_path: segment.node_path.clone(),
3927                                                meta: segment.meta.clone(),
3928                                            }),
3929                                        },
3930                                        meta: segment.meta.clone(),
3931                                    }),
3932                                })
3933                                .collect(),
3934                            ty: RuntimeType::segment(),
3935                        }
3936                        .normal()),
3937                        _ => Err(KclError::new_undefined_value(
3938                            KclErrorDetails::new(
3939                                format!("Property '{property}' not found in segment"),
3940                                vec![self.clone().into()],
3941                            ),
3942                            None,
3943                        )),
3944                    },
3945                },
3946                other => Err(KclError::new_undefined_value(
3947                    KclErrorDetails::new(
3948                        format!("Property '{other}' not found in segment"),
3949                        vec![self.clone().into()],
3950                    ),
3951                    None,
3952                )),
3953            },
3954            (KclValue::Plane { value: plane }, Property::String(property), false) => match property.as_str() {
3955                "zAxis" => {
3956                    let (p, u) = plane.info.z_axis.as_3_dims();
3957                    Ok(KclValue::array_from_point3d(p, NumericType::optional_length(u), vec![meta]).normal())
3958                }
3959                "yAxis" => {
3960                    let (p, u) = plane.info.y_axis.as_3_dims();
3961                    Ok(KclValue::array_from_point3d(p, NumericType::optional_length(u), vec![meta]).normal())
3962                }
3963                "xAxis" => {
3964                    let (p, u) = plane.info.x_axis.as_3_dims();
3965                    Ok(KclValue::array_from_point3d(p, NumericType::optional_length(u), vec![meta]).normal())
3966                }
3967                "origin" => {
3968                    let (p, u) = plane.info.origin.as_3_dims();
3969                    Ok(KclValue::array_from_point3d(p, NumericType::optional_length(u), vec![meta]).normal())
3970                }
3971                other => Err(KclError::new_undefined_value(
3972                    KclErrorDetails::new(
3973                        format!("Property '{other}' not found in plane"),
3974                        vec![self.clone().into()],
3975                    ),
3976                    None,
3977                )),
3978            },
3979            (
3980                KclValue::Object {
3981                    value: map,
3982                    object_kind,
3983                    ..
3984                },
3985                Property::String(property),
3986                false,
3987            ) => {
3988                if let Some(value) = map.get(&property) {
3989                    if object_kind
3990                        .deprecated_solid_tag_names()
3991                        .iter()
3992                        .any(|tag_name| tag_name == &property)
3993                    {
3994                        exec_state.warn(
3995                            CompilationIssue::err(
3996                                SourceRange::from(self),
3997                                format!(
3998                                    "Accessing solid-created face `{property}` through sketch tags is deprecated. Use the body's faces instead, e.g. `body.faces.{property}`."
3999                                ),
4000                            ),
4001                            annotations::WARN_DEPRECATED,
4002                        );
4003                    }
4004                    Ok(value.to_owned().normal())
4005                } else {
4006                    Err(KclError::new_undefined_value(
4007                        KclErrorDetails::new(
4008                            format!("Property '{property}' not found in object"),
4009                            vec![self.clone().into()],
4010                        ),
4011                        None,
4012                    ))
4013                }
4014            }
4015            (KclValue::Object { .. }, Property::String(property), true) => {
4016                Err(KclError::new_semantic(KclErrorDetails::new(
4017                    format!("Cannot index object with string; use dot notation instead, e.g. `obj.{property}`"),
4018                    vec![self.clone().into()],
4019                )))
4020            }
4021            (KclValue::Object { value: map, .. }, p @ Property::UInt(i), _) => {
4022                if i == 0
4023                    && let Some(value) = map.get("x")
4024                {
4025                    return Ok(value.to_owned().normal());
4026                }
4027                if i == 1
4028                    && let Some(value) = map.get("y")
4029                {
4030                    return Ok(value.to_owned().normal());
4031                }
4032                if i == 2
4033                    && let Some(value) = map.get("z")
4034                {
4035                    return Ok(value.to_owned().normal());
4036                }
4037                let t = p.type_name();
4038                let article = article_for(t);
4039                Err(KclError::new_semantic(KclErrorDetails::new(
4040                    format!("Only strings can be used as the property of an object, but you're using {article} {t}",),
4041                    vec![self.clone().into()],
4042                )))
4043            }
4044            (KclValue::HomArray { value: arr, ty }, Property::UInt(index), _) => {
4045                let value_of_arr = arr.get(index);
4046                // Out-of-bounds error.
4047                let oob_error = KclError::new_undefined_value(
4048                    KclErrorDetails::new(
4049                        format!("The array doesn't have any item at index {index}"),
4050                        vec![self.clone().into()],
4051                    ),
4052                    None,
4053                );
4054                if let Some(value) = value_of_arr {
4055                    // Indexing into the array was successful.
4056                    Ok(value.to_owned().normal())
4057                } else if ctx.no_engine_commands().await
4058                    && !exec_state.is_sketch_mode_execution()
4059                    && mock_array_may_have_engine_dependent_cardinality(&ty)
4060                {
4061                    // In mock execution, we handle OOB errors
4062                    // by trying to get index 0 only for geometry-handle arrays. Those
4063                    // values may have come from the engine, so their actual length is
4064                    // not known during mock execution runtime. Frontend-only arrays
4065                    // have exact cardinality and must preserve their OOB errors.
4066                    //
4067                    // We don't do this in sketch mode execution since it's
4068                    // forbidden from contacting the engine, meaning array
4069                    // lengths are always accurate, and the OOB error is real.
4070                    let value = arr.first();
4071                    value.map(|value| value.to_owned().normal()).ok_or(oob_error)
4072                } else {
4073                    Err(oob_error)
4074                }
4075            }
4076            // Singletons and single-element arrays should be interchangeable, but only indexing by 0 should work.
4077            // This is kind of a silly property, but it's possible it occurs in generic code or something.
4078            (obj, Property::UInt(0), _) => Ok(obj.normal()),
4079            (KclValue::HomArray { .. }, p, _) => {
4080                let t = p.type_name();
4081                let article = article_for(t);
4082                Err(KclError::new_semantic(KclErrorDetails::new(
4083                    format!("Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",),
4084                    vec![self.clone().into()],
4085                )))
4086            }
4087            (KclValue::Solid { value }, Property::String(prop), false) if prop == "sketch" => {
4088                let Some(sketch) = value.sketch() else {
4089                    return Err(KclError::new_semantic(KclErrorDetails::new(
4090                        "This solid was created without a sketch, so `solid.sketch` is unavailable.".to_owned(),
4091                        vec![self.clone().into()],
4092                    )));
4093                };
4094                Ok(KclValue::Sketch {
4095                    value: Box::new(sketch.clone()),
4096                }
4097                .normal())
4098            }
4099            (KclValue::Solid { value: solid }, Property::String(prop), false) if prop == "faces" => {
4100                Ok(KclValue::Object {
4101                    meta: vec![Metadata {
4102                        source_range: SourceRange::from(self.clone()),
4103                    }],
4104                    value: solid
4105                        .faces
4106                        .iter()
4107                        .map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
4108                        .collect(),
4109                    constrainable: false,
4110                    object_kind: KclObjectKind::Default,
4111                }
4112                .normal())
4113            }
4114            (geometry @ KclValue::Solid { .. }, Property::String(prop), false) if prop == "tags" => {
4115                // This is a common mistake.
4116                Err(KclError::new_semantic(KclErrorDetails::new(
4117                    format!(
4118                        "Property `{prop}` not found on {}. You can get a solid's faces through `exampleSolid.faces`, or its sketch tags through `exampleSolid.sketch.tags`.",
4119                        geometry.human_friendly_type()
4120                    ),
4121                    vec![self.clone().into()],
4122                )))
4123            }
4124            (KclValue::Sketch { value: sk }, Property::String(prop), false) if prop == "tags" => Ok(KclValue::Object {
4125                meta: vec![Metadata {
4126                    source_range: SourceRange::from(self.clone()),
4127                }],
4128                value: sk
4129                    .tags
4130                    .iter()
4131                    .map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
4132                    .collect(),
4133                constrainable: false,
4134                object_kind: KclObjectKind::SketchTags {
4135                    deprecated_solid_tag_names: sk
4136                        .tags
4137                        .iter()
4138                        .filter(|(_, tag)| tag.is_body_created_tag())
4139                        .map(|(name, _)| name.to_owned())
4140                        .collect(),
4141                },
4142            }
4143            .normal()),
4144            (geometry @ (KclValue::Sketch { .. } | KclValue::Solid { .. }), Property::String(property), false) => {
4145                Err(KclError::new_semantic(KclErrorDetails::new(
4146                    format!("Property `{property}` not found on {}", geometry.human_friendly_type()),
4147                    vec![self.clone().into()],
4148                )))
4149            }
4150            (being_indexed, _, false) => Err(KclError::new_semantic(KclErrorDetails::new(
4151                format!(
4152                    "Only objects can have members accessed with dot notation, but you're trying to access {}",
4153                    being_indexed.human_friendly_type()
4154                ),
4155                vec![self.clone().into()],
4156            ))),
4157            (being_indexed, _, true) => Err(KclError::new_semantic(KclErrorDetails::new(
4158                format!(
4159                    "Only arrays can be indexed, but you're trying to index {}",
4160                    being_indexed.human_friendly_type()
4161                ),
4162                vec![self.clone().into()],
4163            ))),
4164        }
4165    }
4166}
4167
4168impl Node<BinaryExpression> {
4169    pub(super) async fn apply_operator(
4170        &self,
4171        exec_state: &mut ExecState,
4172        ctx: &ExecutorContext,
4173        left_value: KclValue,
4174        right_value: KclValue,
4175    ) -> Result<KclValue, KclError> {
4176        let mut meta = left_value.metadata();
4177        meta.extend(right_value.metadata());
4178        // Repeated arithmetic must not multiply copies of the same source range.
4179        let mut seen = HashSet::new();
4180        meta.retain(|metadata| seen.insert(metadata.source_range));
4181
4182        // First check if we are doing string concatenation.
4183        if self.operator == BinaryOperator::Add
4184            && let (KclValue::String { value: left, .. }, KclValue::String { value: right, .. }) =
4185                (&left_value, &right_value)
4186        {
4187            return Ok(KclValue::String {
4188                value: format!("{left}{right}"),
4189                meta,
4190            });
4191        }
4192
4193        // Then check if we have solids.
4194        if self.operator == BinaryOperator::Add || self.operator == BinaryOperator::Or {
4195            if let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value) {
4196                let args = Args::new_no_args(
4197                    self.into(),
4198                    self.node_path.clone(),
4199                    ctx.clone(),
4200                    Some("union".to_owned()),
4201                );
4202                let result = crate::std::csg::inner_union(
4203                    vec![*left.clone(), *right.clone()],
4204                    Default::default(),
4205                    crate::std::csg::CsgAlgorithm::Latest,
4206                    exec_state,
4207                    args,
4208                )
4209                .await?;
4210                return Ok(result.into());
4211            }
4212        } else if self.operator == BinaryOperator::Sub {
4213            // Check if we have solids.
4214            if let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value) {
4215                let args = Args::new_no_args(
4216                    self.into(),
4217                    self.node_path.clone(),
4218                    ctx.clone(),
4219                    Some("subtract".to_owned()),
4220                );
4221                let result = crate::std::csg::inner_subtract(
4222                    vec![*left.clone()],
4223                    vec![*right.clone()],
4224                    Default::default(),
4225                    crate::std::csg::CsgAlgorithm::Latest,
4226                    exec_state,
4227                    args,
4228                )
4229                .await?;
4230                return Ok(result.into());
4231            }
4232        } else if self.operator == BinaryOperator::And
4233            && let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value)
4234        {
4235            // Check if we have solids.
4236            let args = Args::new_no_args(
4237                self.into(),
4238                self.node_path.clone(),
4239                ctx.clone(),
4240                Some("intersect".to_owned()),
4241            );
4242            let result = crate::std::csg::inner_intersect(
4243                vec![*left.clone(), *right.clone()],
4244                Default::default(),
4245                crate::std::csg::CsgAlgorithm::Latest,
4246                exec_state,
4247                args,
4248            )
4249            .await?;
4250            return Ok(result.into());
4251        }
4252
4253        // Check if we are doing logical operations on booleans.
4254        if self.operator == BinaryOperator::Or || self.operator == BinaryOperator::And {
4255            let KclValue::Bool { value: left_value, .. } = left_value else {
4256                return Err(KclError::new_semantic(KclErrorDetails::new(
4257                    format!(
4258                        "Cannot apply logical operator to non-boolean value: {}",
4259                        left_value.human_friendly_type()
4260                    ),
4261                    vec![self.left.clone().into()],
4262                )));
4263            };
4264            let KclValue::Bool { value: right_value, .. } = right_value else {
4265                return Err(KclError::new_semantic(KclErrorDetails::new(
4266                    format!(
4267                        "Cannot apply logical operator to non-boolean value: {}",
4268                        right_value.human_friendly_type()
4269                    ),
4270                    vec![self.right.clone().into()],
4271                )));
4272            };
4273            let raw_value = match self.operator {
4274                BinaryOperator::Or => left_value || right_value,
4275                BinaryOperator::And => left_value && right_value,
4276                _ => unreachable!(),
4277            };
4278            return Ok(KclValue::Bool { value: raw_value, meta });
4279        }
4280
4281        // Check if we're doing equivalence in sketch mode.
4282        if self.operator == BinaryOperator::Eq && exec_state.mod_local.sketch_block.is_some() {
4283            match (&left_value, &right_value) {
4284                // Same sketch variables.
4285                (KclValue::SketchVar { value: left_value, .. }, KclValue::SketchVar { value: right_value, .. })
4286                    if left_value.id == right_value.id =>
4287                {
4288                    return Ok(KclValue::none());
4289                }
4290                // Different sketch variables.
4291                (KclValue::SketchVar { value: var0 }, KclValue::SketchVar { value: var1, .. }) => {
4292                    let constraint = Constraint::ScalarEqual(
4293                        var0.id.to_constraint_id(self.as_source_range())?,
4294                        var1.id.to_constraint_id(self.as_source_range())?,
4295                    );
4296                    let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
4297                        let message = "Being inside a sketch block should have already been checked above".to_owned();
4298                        debug_assert!(false, "{}", &message);
4299                        return Err(internal_err(message, self));
4300                    };
4301                    sketch_block_state.solver_constraints.push(constraint);
4302                    return Ok(KclValue::none());
4303                }
4304                // One sketch variable, one number.
4305                (KclValue::SketchVar { value: var, .. }, input_number @ KclValue::Number { .. })
4306                | (input_number @ KclValue::Number { .. }, KclValue::SketchVar { value: var, .. }) => {
4307                    let number_value = normalize_to_solver_distance_unit(
4308                        input_number,
4309                        input_number.into(),
4310                        exec_state,
4311                        "fixed constraint value",
4312                    )?;
4313                    let Some(n) = number_value.as_ty_f64() else {
4314                        let message = format!(
4315                            "Expected number after coercion, but found {}",
4316                            number_value.human_friendly_type()
4317                        );
4318                        debug_assert!(false, "{}", &message);
4319                        return Err(internal_err(message, self));
4320                    };
4321                    let constraint = Constraint::Fixed(var.id.to_constraint_id(self.as_source_range())?, n.n);
4322                    let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
4323                        let message = "Being inside a sketch block should have already been checked above".to_owned();
4324                        debug_assert!(false, "{}", &message);
4325                        return Err(internal_err(message, self));
4326                    };
4327                    sketch_block_state.solver_constraints.push(constraint);
4328                    exec_state.warn_experimental("scalar fixed constraint", self.as_source_range());
4329                    return Ok(KclValue::none());
4330                }
4331                // One sketch constraint, one number.
4332                (KclValue::SketchConstraint { value: constraint }, input_number @ KclValue::Number { .. })
4333                | (input_number @ KclValue::Number { .. }, KclValue::SketchConstraint { value: constraint }) => {
4334                    let number_value = match constraint.kind {
4335                        // These constraint kinds expect the RHS to be an angle.
4336                        SketchConstraintKind::Angle { .. } => normalize_to_solver_angle_unit(
4337                            input_number,
4338                            input_number.into(),
4339                            exec_state,
4340                            "fixed constraint value",
4341                        )?,
4342                        // These constraint kinds expect the RHS to be a distance.
4343                        SketchConstraintKind::Distance { .. }
4344                        | SketchConstraintKind::PointLineDistance { .. }
4345                        | SketchConstraintKind::LineLineDistance { .. }
4346                        | SketchConstraintKind::PointCircularDistance { .. }
4347                        | SketchConstraintKind::LineCircularDistance { .. }
4348                        | SketchConstraintKind::CircularCircularDistance { .. }
4349                        | SketchConstraintKind::Radius { .. }
4350                        | SketchConstraintKind::Diameter { .. }
4351                        | SketchConstraintKind::HorizontalDistance { .. }
4352                        | SketchConstraintKind::VerticalDistance { .. } => normalize_to_solver_distance_unit(
4353                            input_number,
4354                            input_number.into(),
4355                            exec_state,
4356                            "fixed constraint value",
4357                        )?,
4358                    };
4359                    let Some(n) = number_value.as_ty_f64() else {
4360                        let message = format!(
4361                            "Expected number after coercion, but found {}",
4362                            number_value.human_friendly_type()
4363                        );
4364                        debug_assert!(false, "{}", &message);
4365                        return Err(internal_err(message, self));
4366                    };
4367                    // Recast the number side of == to get the source expression text.
4368                    let number_binary_part = if matches!(&left_value, KclValue::SketchConstraint { .. }) {
4369                        &self.right
4370                    } else {
4371                        &self.left
4372                    };
4373                    let source = {
4374                        use crate::unparser::ExprContext;
4375                        let mut buf = String::new();
4376                        number_binary_part.recast(&mut buf, &Default::default(), 0, ExprContext::Other);
4377                        kcl_api::ConstraintSource {
4378                            expr: buf,
4379                            is_literal: matches!(number_binary_part, BinaryPart::Literal(_)),
4380                        }
4381                    };
4382
4383                    match &constraint.kind {
4384                        SketchConstraintKind::Angle {
4385                            line0,
4386                            line1,
4387                            mode,
4388                            label_position,
4389                        } => {
4390                            let range = self.as_source_range();
4391                            let desired_angle = match n.ty {
4392                                NumericType::Known(crate::exec::UnitType::Angle(crate::exec::UnitAngle::Degrees))
4393                                | NumericType::Default {
4394                                    len: _,
4395                                    angle: UnitAngle::Degrees,
4396                                } => ezpz::datatypes::Angle::from_degrees(n.n),
4397                                NumericType::Known(crate::exec::UnitType::Angle(crate::exec::UnitAngle::Radians))
4398                                | NumericType::Default {
4399                                    len: _,
4400                                    angle: UnitAngle::Radians,
4401                                } => ezpz::datatypes::Angle::from_radians(n.n),
4402                                NumericType::Known(crate::exec::UnitType::Count)
4403                                | NumericType::Known(crate::exec::UnitType::GenericLength)
4404                                | NumericType::Known(crate::exec::UnitType::GenericAngle)
4405                                | NumericType::Known(crate::exec::UnitType::Length(_))
4406                                | NumericType::Unknown
4407                                | NumericType::Any => {
4408                                    let message = format!("Expected angle but found {:?}", n);
4409                                    debug_assert!(false, "{}", &message);
4410                                    return Err(internal_err(message, self));
4411                                }
4412                            };
4413                            let angle_lowering = match *mode {
4414                                AngleConstraintMode::LinesAtAngle => {
4415                                    AngleConstraintLowering::LinesAtAngle(Box::new(PendingLegacyAngleRefactorMeta {
4416                                        source_range: constraint
4417                                            .meta
4418                                            .first()
4419                                            .map(|meta| meta.source_range)
4420                                            .unwrap_or(range),
4421                                        lines: [line0.clone(), line1.clone()],
4422                                        desired_angle_radians: desired_angle.to_radians(),
4423                                    }))
4424                                }
4425                                AngleConstraintMode::PointsAtAngle { sector, inverse } => {
4426                                    let sketch_vars = exec_state
4427                                        .mod_local
4428                                        .sketch_block
4429                                        .as_ref()
4430                                        .ok_or_else(|| {
4431                                            internal_err(
4432                                                "Being inside a sketch block should have already been checked above",
4433                                                self,
4434                                            )
4435                                        })?
4436                                        .sketch_vars
4437                                        .clone();
4438                                    let initial_line0 = constrainable_line_initial_positions(
4439                                        &sketch_vars,
4440                                        line0,
4441                                        exec_state,
4442                                        range,
4443                                        "angle line0",
4444                                    )?;
4445                                    let initial_line1 = constrainable_line_initial_positions(
4446                                        &sketch_vars,
4447                                        line1,
4448                                        exec_state,
4449                                        range,
4450                                        "angle line1",
4451                                    )?;
4452                                    let Some(initial_vertex) = intersect_lines_2d(initial_line0, initial_line1) else {
4453                                        return Err(KclError::new_semantic(KclErrorDetails::new(
4454                                            "angleDimension(lines = ..., sector = ...) requires non-parallel lines"
4455                                                .to_owned(),
4456                                            vec![range],
4457                                        )));
4458                                    };
4459                                    let (line0_representative, line0_direction) =
4460                                        representative_angle_endpoint(line0, initial_line0, initial_vertex, range)?;
4461                                    let (line1_representative, line1_direction) =
4462                                        representative_angle_endpoint(line1, initial_line1, initial_vertex, range)?;
4463                                    let sector_rays = angle_sector_rays(sector, inverse);
4464                                    let angle_kind =
4465                                        ezpz::datatypes::AngleKind::Other(remap_angle_for_representative_rays(
4466                                            sector_rays,
4467                                            [line0_direction, line1_direction],
4468                                            desired_angle,
4469                                        ));
4470                                    AngleConstraintLowering::PointsAtAngle(PointsAtAngleLineData {
4471                                        initial_vertex,
4472                                        representative_points: [line0_representative, line1_representative],
4473                                        angle_kind,
4474                                    })
4475                                }
4476                            };
4477                            let sketch_var_ty = solver_numeric_type(exec_state);
4478                            let constraint_id = exec_state.next_object_id();
4479                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
4480                                let message =
4481                                    "Being inside a sketch block should have already been checked above".to_owned();
4482                                debug_assert!(false, "{}", &message);
4483                                return Err(internal_err(message, self));
4484                            };
4485                            match angle_lowering {
4486                                AngleConstraintLowering::LinesAtAngle(refactor_meta) => {
4487                                    sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
4488                                        datum_line_from_constrainable(line0, range)?,
4489                                        datum_line_from_constrainable(line1, range)?,
4490                                        ezpz::datatypes::AngleKind::Other(desired_angle),
4491                                    ));
4492                                    sketch_block_state
4493                                        .pending_legacy_angle_refactor_metadata
4494                                        .push(*refactor_meta);
4495                                }
4496                                AngleConstraintLowering::PointsAtAngle(points_at_angle_data) => {
4497                                    push_points_at_angle_for_lines(
4498                                        sketch_block_state,
4499                                        sketch_var_ty,
4500                                        [line0, line1],
4501                                        points_at_angle_data,
4502                                        range,
4503                                    )?
4504                                }
4505                            }
4506                            use kcl_api::Angle;
4507                            use kcl_api::SourceRef;
4508
4509                            use crate::execution::Artifact;
4510                            use crate::execution::CodeRef;
4511                            use crate::execution::SketchBlockConstraint;
4512
4513                            let Some(sketch_id) = sketch_block_state.sketch_id else {
4514                                let message = "Sketch id missing for constraint artifact".to_owned();
4515                                debug_assert!(false, "{}", &message);
4516                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
4517                            };
4518                            let (sector, inverse) = match *mode {
4519                                AngleConstraintMode::LinesAtAngle => (None, None),
4520                                AngleConstraintMode::PointsAtAngle { sector, inverse } => {
4521                                    (Some(front_angle_sector(sector)), Some(inverse))
4522                                }
4523                            };
4524                            let sketch_constraint = kcl_api::Constraint::Angle(Angle {
4525                                lines: vec![line0.object_id, line1.object_id],
4526                                angle: n.try_into().map_err(|_| {
4527                                    internal_err("Failed to convert angle units numeric suffix:", range)
4528                                })?,
4529                                sector,
4530                                inverse,
4531                                label_position: label_position.clone(),
4532                                source,
4533                            });
4534                            sketch_block_state.sketch_constraints.push(constraint_id);
4535                            let artifact_id = exec_state.next_artifact_id();
4536                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
4537                                id: artifact_id,
4538                                sketch_id,
4539                                constraint_id,
4540                                constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
4541                                code_ref: CodeRef::placeholder(range),
4542                            }));
4543                            exec_state.add_scene_object(
4544                                Object {
4545                                    id: constraint_id,
4546                                    kind: ObjectKind::Constraint {
4547                                        constraint: sketch_constraint,
4548                                    },
4549                                    label: Default::default(),
4550                                    comments: Default::default(),
4551                                    artifact_id,
4552                                    source: SourceRef::new(range, self.node_path.clone()),
4553                                },
4554                                range,
4555                            );
4556                        }
4557                        SketchConstraintKind::Distance { points, label_position } => {
4558                            let range = self.as_source_range();
4559                            let p0 = &points[0];
4560                            let p1 = &points[1];
4561                            let sketch_var_ty = solver_numeric_type(exec_state);
4562                            let constraint_id = exec_state.next_object_id();
4563                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
4564                                let message =
4565                                    "Being inside a sketch block should have already been checked above".to_owned();
4566                                debug_assert!(false, "{}", &message);
4567                                return Err(internal_err(message, self));
4568                            };
4569                            match (p0, p1) {
4570                                (
4571                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(p0),
4572                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(p1),
4573                                ) => {
4574                                    let solver_pt0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
4575                                        p0.vars.x.to_constraint_id(range)?,
4576                                        p0.vars.y.to_constraint_id(range)?,
4577                                    );
4578                                    let solver_pt1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
4579                                        p1.vars.x.to_constraint_id(range)?,
4580                                        p1.vars.y.to_constraint_id(range)?,
4581                                    );
4582                                    sketch_block_state
4583                                        .solver_constraints
4584                                        .push(Constraint::Distance(solver_pt0, solver_pt1, n.n));
4585                                }
4586                                (
4587                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
4588                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
4589                                )
4590                                | (
4591                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
4592                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
4593                                ) => {
4594                                    let origin_x_id = sketch_block_state.next_sketch_var_id();
4595                                    sketch_block_state.sketch_vars.push(KclValue::SketchVar {
4596                                        value: Box::new(crate::execution::SketchVar {
4597                                            id: origin_x_id,
4598                                            initial_value: 0.0,
4599                                            ty: sketch_var_ty,
4600                                            // Synthesized origin coord for distance(); not source-backed.
4601                                            node_path: None,
4602                                            meta: vec![],
4603                                        }),
4604                                    });
4605                                    let origin_y_id = sketch_block_state.next_sketch_var_id();
4606                                    sketch_block_state.sketch_vars.push(KclValue::SketchVar {
4607                                        value: Box::new(crate::execution::SketchVar {
4608                                            id: origin_y_id,
4609                                            initial_value: 0.0,
4610                                            ty: sketch_var_ty,
4611                                            // Synthesized origin coord for distance(); not source-backed.
4612                                            node_path: None,
4613                                            meta: vec![],
4614                                        }),
4615                                    });
4616                                    let origin_x = origin_x_id.to_constraint_id(range)?;
4617                                    let origin_y = origin_y_id.to_constraint_id(range)?;
4618                                    sketch_block_state
4619                                        .solver_constraints
4620                                        .push(Constraint::Fixed(origin_x, 0.0));
4621                                    sketch_block_state
4622                                        .solver_constraints
4623                                        .push(Constraint::Fixed(origin_y, 0.0));
4624                                    let solver_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
4625                                        point.vars.x.to_constraint_id(range)?,
4626                                        point.vars.y.to_constraint_id(range)?,
4627                                    );
4628                                    let origin_point = ezpz::datatypes::inputs::DatumPoint::new_xy(origin_x, origin_y);
4629                                    sketch_block_state.solver_constraints.push(Constraint::Distance(
4630                                        solver_point,
4631                                        origin_point,
4632                                        n.n,
4633                                    ));
4634                                }
4635                                (
4636                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
4637                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
4638                                ) => {
4639                                    return Err(internal_err(
4640                                        "distance() cannot constrain ORIGIN against ORIGIN".to_owned(),
4641                                        range,
4642                                    ));
4643                                }
4644                            }
4645                            use kcl_api::ConstraintSegment;
4646                            use kcl_api::Distance;
4647                            use kcl_api::SourceRef;
4648
4649                            use crate::execution::Artifact;
4650                            use crate::execution::CodeRef;
4651                            use crate::execution::SketchBlockConstraint;
4652
4653                            let Some(sketch_id) = sketch_block_state.sketch_id else {
4654                                let message = "Sketch id missing for constraint artifact".to_owned();
4655                                debug_assert!(false, "{}", &message);
4656                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
4657                            };
4658                            let sketch_constraint = kcl_api::Constraint::Distance(Distance {
4659                                segments: vec![
4660                                    match p0 {
4661                                        crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
4662                                            ConstraintSegment::from(point.object_id)
4663                                        }
4664                                        crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
4665                                            ConstraintSegment::ORIGIN
4666                                        }
4667                                    },
4668                                    match p1 {
4669                                        crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
4670                                            ConstraintSegment::from(point.object_id)
4671                                        }
4672                                        crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
4673                                            ConstraintSegment::ORIGIN
4674                                        }
4675                                    },
4676                                ],
4677                                distance: n.try_into().map_err(|_| {
4678                                    internal_err("Failed to convert distance units numeric suffix:", range)
4679                                })?,
4680                                label_position: label_position.clone(),
4681                                source,
4682                            });
4683                            sketch_block_state.sketch_constraints.push(constraint_id);
4684                            let artifact_id = exec_state.next_artifact_id();
4685                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
4686                                id: artifact_id,
4687                                sketch_id,
4688                                constraint_id,
4689                                constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
4690                                code_ref: CodeRef::placeholder(range),
4691                            }));
4692                            exec_state.add_scene_object(
4693                                Object {
4694                                    id: constraint_id,
4695                                    kind: ObjectKind::Constraint {
4696                                        constraint: sketch_constraint,
4697                                    },
4698                                    label: Default::default(),
4699                                    comments: Default::default(),
4700                                    artifact_id,
4701                                    source: SourceRef::new(range, self.node_path.clone()),
4702                                },
4703                                range,
4704                            );
4705                        }
4706                        SketchConstraintKind::PointLineDistance {
4707                            point,
4708                            line,
4709                            input_object_ids,
4710                            label_position,
4711                        } => {
4712                            let range = self.as_source_range();
4713                            let sketch_var_ty = solver_numeric_type(exec_state);
4714                            let sketch_vars = exec_state
4715                                .mod_local
4716                                .sketch_block
4717                                .as_ref()
4718                                .ok_or_else(|| {
4719                                    internal_err(
4720                                        "Being inside a sketch block should have already been checked above",
4721                                        self,
4722                                    )
4723                                })?
4724                                .sketch_vars
4725                                .clone();
4726                            let support_initial =
4727                                projected_point_on_line_initial_position(&sketch_vars, point, line, exec_state, range)?;
4728                            let solver_line = datum_line_from_constrainable(line, range)?;
4729
4730                            let constraint_id = exec_state.next_object_id();
4731                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
4732                                let message =
4733                                    "Being inside a sketch block should have already been checked above".to_owned();
4734                                debug_assert!(false, "{}", &message);
4735                                return Err(internal_err(message, self));
4736                            };
4737
4738                            // Lower point-line distance by adding a hidden
4739                            // support point on the line, then constrain the
4740                            // selected point-to-support segment to be
4741                            // perpendicular and equal to the requested
4742                            // distance.
4743                            let solver_point = datum_point_from_constrainable_or_origin(
4744                                sketch_block_state,
4745                                sketch_var_ty,
4746                                point,
4747                                range,
4748                            )?;
4749                            let support_x_id = sketch_block_state.next_sketch_var_id();
4750                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
4751                                value: Box::new(crate::execution::SketchVar {
4752                                    id: support_x_id,
4753                                    initial_value: support_initial[0],
4754                                    ty: sketch_var_ty,
4755                                    // Synthesized support point coord for distance lowering; not source-backed.
4756                                    node_path: None,
4757                                    meta: vec![],
4758                                }),
4759                            });
4760                            let support_y_id = sketch_block_state.next_sketch_var_id();
4761                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
4762                                value: Box::new(crate::execution::SketchVar {
4763                                    id: support_y_id,
4764                                    initial_value: support_initial[1],
4765                                    ty: sketch_var_ty,
4766                                    // Synthesized support point coord for distance lowering; not source-backed.
4767                                    node_path: None,
4768                                    meta: vec![],
4769                                }),
4770                            });
4771                            let support_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
4772                                support_x_id.to_constraint_id(range)?,
4773                                support_y_id.to_constraint_id(range)?,
4774                            );
4775                            let support_line =
4776                                ezpz::datatypes::inputs::DatumLineSegment::new(solver_point, support_point);
4777
4778                            sketch_block_state
4779                                .solver_constraints
4780                                .push(Constraint::PointLineDistance(support_point, solver_line, 0.0));
4781                            sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
4782                                support_line,
4783                                solver_line,
4784                                ezpz::datatypes::AngleKind::Perpendicular,
4785                            ));
4786                            sketch_block_state.solver_constraints.push(Constraint::Distance(
4787                                solver_point,
4788                                support_point,
4789                                n.n,
4790                            ));
4791
4792                            use kcl_api::ConstraintSegment;
4793                            use kcl_api::Distance;
4794                            use kcl_api::SourceRef;
4795
4796                            use crate::execution::Artifact;
4797                            use crate::execution::CodeRef;
4798                            use crate::execution::SketchBlockConstraint;
4799
4800                            let Some(sketch_id) = sketch_block_state.sketch_id else {
4801                                let message = "Sketch id missing for constraint artifact".to_owned();
4802                                debug_assert!(false, "{}", &message);
4803                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
4804                            };
4805                            let sketch_constraint = kcl_api::Constraint::Distance(Distance {
4806                                segments: input_object_ids
4807                                    .iter()
4808                                    .copied()
4809                                    .map(|id| id.map_or(ConstraintSegment::ORIGIN, ConstraintSegment::from))
4810                                    .collect(),
4811                                distance: n.try_into().map_err(|_| {
4812                                    internal_err("Failed to convert distance units numeric suffix:", range)
4813                                })?,
4814                                label_position: label_position.clone(),
4815                                source,
4816                            });
4817                            sketch_block_state.sketch_constraints.push(constraint_id);
4818                            let artifact_id = exec_state.next_artifact_id();
4819                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
4820                                id: artifact_id,
4821                                sketch_id,
4822                                constraint_id,
4823                                constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
4824                                code_ref: CodeRef::placeholder(range),
4825                            }));
4826                            exec_state.add_scene_object(
4827                                Object {
4828                                    id: constraint_id,
4829                                    kind: ObjectKind::Constraint {
4830                                        constraint: sketch_constraint,
4831                                    },
4832                                    label: Default::default(),
4833                                    comments: Default::default(),
4834                                    artifact_id,
4835                                    source: SourceRef::new(range, self.node_path.clone()),
4836                                },
4837                                range,
4838                            );
4839                        }
4840                        SketchConstraintKind::LineLineDistance {
4841                            line0,
4842                            line1,
4843                            input_object_ids,
4844                            label_position,
4845                        } => {
4846                            let range = self.as_source_range();
4847                            let reference_point = crate::execution::ConstrainablePoint2d {
4848                                vars: line0.vars[0].clone(),
4849                                object_id: line0.object_id,
4850                            };
4851                            let sketch_var_ty = solver_numeric_type(exec_state);
4852                            let sketch_vars = exec_state
4853                                .mod_local
4854                                .sketch_block
4855                                .as_ref()
4856                                .ok_or_else(|| {
4857                                    internal_err(
4858                                        "Being inside a sketch block should have already been checked above",
4859                                        self,
4860                                    )
4861                                })?
4862                                .sketch_vars
4863                                .clone();
4864                            let support_initial = projected_point_on_line_initial_position(
4865                                &sketch_vars,
4866                                &crate::execution::ConstrainablePoint2dOrOrigin::Point(reference_point.clone()),
4867                                line1,
4868                                exec_state,
4869                                range,
4870                            )?;
4871                            let solver_point = datum_point_from_constrainable(&reference_point, range)?;
4872                            let solver_line0 = datum_line_from_constrainable(line0, range)?;
4873                            let solver_line1 = datum_line_from_constrainable(line1, range)?;
4874
4875                            let constraint_id = exec_state.next_object_id();
4876                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
4877                                let message =
4878                                    "Being inside a sketch block should have already been checked above".to_owned();
4879                                debug_assert!(false, "{}", &message);
4880                                return Err(internal_err(message, self));
4881                            };
4882
4883                            // Lower line-line distance to the point-line
4884                            // construction above by choosing one endpoint on
4885                            // line0 as the reference point, forcing the lines
4886                            // parallel, and measuring perpendicularly to
4887                            // line1.
4888                            let support_x_id = sketch_block_state.next_sketch_var_id();
4889                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
4890                                value: Box::new(crate::execution::SketchVar {
4891                                    id: support_x_id,
4892                                    initial_value: support_initial[0],
4893                                    ty: sketch_var_ty,
4894                                    // Synthesized support point coord for distance lowering; not source-backed.
4895                                    node_path: None,
4896                                    meta: vec![],
4897                                }),
4898                            });
4899                            let support_y_id = sketch_block_state.next_sketch_var_id();
4900                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
4901                                value: Box::new(crate::execution::SketchVar {
4902                                    id: support_y_id,
4903                                    initial_value: support_initial[1],
4904                                    ty: sketch_var_ty,
4905                                    // Synthesized support point coord for distance lowering; not source-backed.
4906                                    node_path: None,
4907                                    meta: vec![],
4908                                }),
4909                            });
4910                            let support_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
4911                                support_x_id.to_constraint_id(range)?,
4912                                support_y_id.to_constraint_id(range)?,
4913                            );
4914                            let support_line =
4915                                ezpz::datatypes::inputs::DatumLineSegment::new(solver_point, support_point);
4916
4917                            sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
4918                                solver_line0,
4919                                solver_line1,
4920                                ezpz::datatypes::AngleKind::Parallel,
4921                            ));
4922                            sketch_block_state
4923                                .solver_constraints
4924                                .push(Constraint::PointLineDistance(support_point, solver_line1, 0.0));
4925                            sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
4926                                support_line,
4927                                solver_line1,
4928                                ezpz::datatypes::AngleKind::Perpendicular,
4929                            ));
4930                            sketch_block_state.solver_constraints.push(Constraint::Distance(
4931                                solver_point,
4932                                support_point,
4933                                n.n,
4934                            ));
4935
4936                            use kcl_api::ConstraintSegment;
4937                            use kcl_api::Distance;
4938                            use kcl_api::SourceRef;
4939
4940                            use crate::execution::Artifact;
4941                            use crate::execution::CodeRef;
4942                            use crate::execution::SketchBlockConstraint;
4943
4944                            let Some(sketch_id) = sketch_block_state.sketch_id else {
4945                                let message = "Sketch id missing for constraint artifact".to_owned();
4946                                debug_assert!(false, "{}", &message);
4947                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
4948                            };
4949                            let sketch_constraint = kcl_api::Constraint::Distance(Distance {
4950                                segments: input_object_ids.iter().copied().map(ConstraintSegment::from).collect(),
4951                                distance: n.try_into().map_err(|_| {
4952                                    internal_err("Failed to convert distance units numeric suffix:", range)
4953                                })?,
4954                                label_position: label_position.clone(),
4955                                source,
4956                            });
4957                            sketch_block_state.sketch_constraints.push(constraint_id);
4958                            let artifact_id = exec_state.next_artifact_id();
4959                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
4960                                id: artifact_id,
4961                                sketch_id,
4962                                constraint_id,
4963                                constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
4964                                code_ref: CodeRef::placeholder(range),
4965                            }));
4966                            exec_state.add_scene_object(
4967                                Object {
4968                                    id: constraint_id,
4969                                    kind: ObjectKind::Constraint {
4970                                        constraint: sketch_constraint,
4971                                    },
4972                                    label: Default::default(),
4973                                    comments: Default::default(),
4974                                    artifact_id,
4975                                    source: SourceRef::new(range, self.node_path.clone()),
4976                                },
4977                                range,
4978                            );
4979                        }
4980                        SketchConstraintKind::PointCircularDistance {
4981                            point,
4982                            center,
4983                            start,
4984                            end,
4985                            input_object_ids,
4986                            label_position,
4987                        } => {
4988                            let range = self.as_source_range();
4989                            let sketch_var_ty = solver_numeric_type(exec_state);
4990                            let sketch_vars = exec_state
4991                                .mod_local
4992                                .sketch_block
4993                                .as_ref()
4994                                .ok_or_else(|| {
4995                                    internal_err(
4996                                        "Being inside a sketch block should have already been checked above",
4997                                        self,
4998                                    )
4999                                })?
5000                                .sketch_vars
5001                                .clone();
5002                            let circular =
5003                                circular_distance_datums(&sketch_vars, center, start, end.as_ref(), exec_state, range)?;
5004
5005                            let constraint_id = exec_state.next_object_id();
5006                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
5007                                let message =
5008                                    "Being inside a sketch block should have already been checked above".to_owned();
5009                                debug_assert!(false, "{}", &message);
5010                                return Err(internal_err(message, self));
5011                            };
5012
5013                            // Lower point-circular distance to exterior
5014                            // circle tangency: a hidden circle centered on the
5015                            // point has radius equal to the requested distance
5016                            // and is tangent to the target arc/circle.
5017                            let target_point = datum_point_from_constrainable_or_origin(
5018                                sketch_block_state,
5019                                sketch_var_ty,
5020                                point,
5021                                range,
5022                            )?;
5023                            push_circular_distance_constraints(
5024                                sketch_block_state,
5025                                sketch_var_ty,
5026                                target_point,
5027                                circular,
5028                                n.n,
5029                                range,
5030                            )?;
5031
5032                            use kcl_api::ConstraintSegment;
5033                            use kcl_api::Distance;
5034                            use kcl_api::SourceRef;
5035
5036                            use crate::execution::Artifact;
5037                            use crate::execution::CodeRef;
5038                            use crate::execution::SketchBlockConstraint;
5039
5040                            let Some(sketch_id) = sketch_block_state.sketch_id else {
5041                                let message = "Sketch id missing for constraint artifact".to_owned();
5042                                debug_assert!(false, "{}", &message);
5043                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
5044                            };
5045                            let sketch_constraint = kcl_api::Constraint::Distance(Distance {
5046                                segments: input_object_ids
5047                                    .iter()
5048                                    .copied()
5049                                    .map(|id| id.map_or(ConstraintSegment::ORIGIN, ConstraintSegment::from))
5050                                    .collect(),
5051                                distance: n.try_into().map_err(|_| {
5052                                    internal_err("Failed to convert distance units numeric suffix:", range)
5053                                })?,
5054                                label_position: label_position.clone(),
5055                                source,
5056                            });
5057                            sketch_block_state.sketch_constraints.push(constraint_id);
5058                            let artifact_id = exec_state.next_artifact_id();
5059                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
5060                                id: artifact_id,
5061                                sketch_id,
5062                                constraint_id,
5063                                constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
5064                                code_ref: CodeRef::placeholder(range),
5065                            }));
5066                            exec_state.add_scene_object(
5067                                Object {
5068                                    id: constraint_id,
5069                                    kind: ObjectKind::Constraint {
5070                                        constraint: sketch_constraint,
5071                                    },
5072                                    label: Default::default(),
5073                                    comments: Default::default(),
5074                                    artifact_id,
5075                                    source: SourceRef::new(range, self.node_path.clone()),
5076                                },
5077                                range,
5078                            );
5079                        }
5080                        SketchConstraintKind::LineCircularDistance {
5081                            line,
5082                            center,
5083                            start,
5084                            end,
5085                            input_object_ids,
5086                            label_position,
5087                        } => {
5088                            let range = self.as_source_range();
5089                            let sketch_var_ty = solver_numeric_type(exec_state);
5090                            let sketch_vars = exec_state
5091                                .mod_local
5092                                .sketch_block
5093                                .as_ref()
5094                                .ok_or_else(|| {
5095                                    internal_err(
5096                                        "Being inside a sketch block should have already been checked above",
5097                                        self,
5098                                    )
5099                                })?
5100                                .sketch_vars
5101                                .clone();
5102                            let support_initial = projected_point_on_line_initial_position(
5103                                &sketch_vars,
5104                                &crate::execution::ConstrainablePoint2dOrOrigin::Point(center.clone()),
5105                                line,
5106                                exec_state,
5107                                range,
5108                            )?;
5109                            let solver_line = datum_line_from_constrainable(line, range)?;
5110                            let circular =
5111                                circular_distance_datums(&sketch_vars, center, start, end.as_ref(), exec_state, range)?;
5112
5113                            let constraint_id = exec_state.next_object_id();
5114                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
5115                                let message =
5116                                    "Being inside a sketch block should have already been checked above".to_owned();
5117                                debug_assert!(false, "{}", &message);
5118                                return Err(internal_err(message, self));
5119                            };
5120
5121                            // Lower line-circular distance by first projecting
5122                            // the circular center onto the target line with a
5123                            // hidden support point. The circular distance is
5124                            // then the point-circular construction from that
5125                            // support point.
5126                            let support_x_id = sketch_block_state.next_sketch_var_id();
5127                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5128                                value: Box::new(crate::execution::SketchVar {
5129                                    id: support_x_id,
5130                                    initial_value: support_initial[0],
5131                                    ty: sketch_var_ty,
5132                                    // Synthesized support point coord for distance lowering; not source-backed.
5133                                    node_path: None,
5134                                    meta: vec![],
5135                                }),
5136                            });
5137                            let support_y_id = sketch_block_state.next_sketch_var_id();
5138                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5139                                value: Box::new(crate::execution::SketchVar {
5140                                    id: support_y_id,
5141                                    initial_value: support_initial[1],
5142                                    ty: sketch_var_ty,
5143                                    // Synthesized support point coord for distance lowering; not source-backed.
5144                                    node_path: None,
5145                                    meta: vec![],
5146                                }),
5147                            });
5148                            let support_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
5149                                support_x_id.to_constraint_id(range)?,
5150                                support_y_id.to_constraint_id(range)?,
5151                            );
5152                            let support_line =
5153                                ezpz::datatypes::inputs::DatumLineSegment::new(circular.center, support_point);
5154
5155                            sketch_block_state
5156                                .solver_constraints
5157                                .push(Constraint::PointLineDistance(support_point, solver_line, 0.0));
5158                            sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
5159                                support_line,
5160                                solver_line,
5161                                ezpz::datatypes::AngleKind::Perpendicular,
5162                            ));
5163                            push_circular_distance_constraints(
5164                                sketch_block_state,
5165                                sketch_var_ty,
5166                                support_point,
5167                                circular,
5168                                n.n,
5169                                range,
5170                            )?;
5171
5172                            use kcl_api::ConstraintSegment;
5173                            use kcl_api::Distance;
5174                            use kcl_api::SourceRef;
5175
5176                            use crate::execution::Artifact;
5177                            use crate::execution::CodeRef;
5178                            use crate::execution::SketchBlockConstraint;
5179
5180                            let Some(sketch_id) = sketch_block_state.sketch_id else {
5181                                let message = "Sketch id missing for constraint artifact".to_owned();
5182                                debug_assert!(false, "{}", &message);
5183                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
5184                            };
5185                            let sketch_constraint = kcl_api::Constraint::Distance(Distance {
5186                                segments: input_object_ids.iter().copied().map(ConstraintSegment::from).collect(),
5187                                distance: n.try_into().map_err(|_| {
5188                                    internal_err("Failed to convert distance units numeric suffix:", range)
5189                                })?,
5190                                label_position: label_position.clone(),
5191                                source,
5192                            });
5193                            sketch_block_state.sketch_constraints.push(constraint_id);
5194                            let artifact_id = exec_state.next_artifact_id();
5195                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
5196                                id: artifact_id,
5197                                sketch_id,
5198                                constraint_id,
5199                                constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
5200                                code_ref: CodeRef::placeholder(range),
5201                            }));
5202                            exec_state.add_scene_object(
5203                                Object {
5204                                    id: constraint_id,
5205                                    kind: ObjectKind::Constraint {
5206                                        constraint: sketch_constraint,
5207                                    },
5208                                    label: Default::default(),
5209                                    comments: Default::default(),
5210                                    artifact_id,
5211                                    source: SourceRef::new(range, self.node_path.clone()),
5212                                },
5213                                range,
5214                            );
5215                        }
5216                        SketchConstraintKind::CircularCircularDistance {
5217                            center0,
5218                            start0,
5219                            end0,
5220                            center1,
5221                            start1,
5222                            end1,
5223                            input_object_ids,
5224                            label_position,
5225                        } => {
5226                            let range = self.as_source_range();
5227                            let sketch_var_ty = solver_numeric_type(exec_state);
5228                            let sketch_vars = exec_state
5229                                .mod_local
5230                                .sketch_block
5231                                .as_ref()
5232                                .ok_or_else(|| {
5233                                    internal_err(
5234                                        "Being inside a sketch block should have already been checked above",
5235                                        self,
5236                                    )
5237                                })?
5238                                .sketch_vars
5239                                .clone();
5240                            let circular0 = circular_distance_datums(
5241                                &sketch_vars,
5242                                center0,
5243                                start0,
5244                                end0.as_ref(),
5245                                exec_state,
5246                                range,
5247                            )?;
5248                            let circular1 = circular_distance_datums(
5249                                &sketch_vars,
5250                                center1,
5251                                start1,
5252                                end1.as_ref(),
5253                                exec_state,
5254                                range,
5255                            )?;
5256                            let support_initial = circular_circular_support_initial_position(
5257                                &sketch_vars,
5258                                center0,
5259                                center1,
5260                                circular0.radius_initial_value,
5261                                n.n,
5262                                exec_state,
5263                                range,
5264                            )?;
5265
5266                            let constraint_id = exec_state.next_object_id();
5267                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
5268                                let message =
5269                                    "Being inside a sketch block should have already been checked above".to_owned();
5270                                debug_assert!(false, "{}", &message);
5271                                return Err(internal_err(message, self));
5272                            };
5273
5274                            // Lower circular-circular distance with a hidden
5275                            // spacer circle of radius d/2. Constraining its
5276                            // center onto the line between target centers and
5277                            // making it exterior-tangent to both targets gives
5278                            // center distance r0 + d + r1.
5279                            let circular_target0 =
5280                                push_circular_radius_constraints(sketch_block_state, sketch_var_ty, circular0, range)?;
5281                            let circular_target1 =
5282                                push_circular_radius_constraints(sketch_block_state, sketch_var_ty, circular1, range)?;
5283
5284                            let support_x_id = sketch_block_state.next_sketch_var_id();
5285                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5286                                value: Box::new(crate::execution::SketchVar {
5287                                    id: support_x_id,
5288                                    initial_value: support_initial[0],
5289                                    ty: sketch_var_ty,
5290                                    // Synthesized support point coord for distance lowering; not source-backed.
5291                                    node_path: None,
5292                                    meta: vec![],
5293                                }),
5294                            });
5295                            let support_y_id = sketch_block_state.next_sketch_var_id();
5296                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5297                                value: Box::new(crate::execution::SketchVar {
5298                                    id: support_y_id,
5299                                    initial_value: support_initial[1],
5300                                    ty: sketch_var_ty,
5301                                    // Synthesized support point coord for distance lowering; not source-backed.
5302                                    node_path: None,
5303                                    meta: vec![],
5304                                }),
5305                            });
5306                            let support_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
5307                                support_x_id.to_constraint_id(range)?,
5308                                support_y_id.to_constraint_id(range)?,
5309                            );
5310
5311                            let support_radius_id = sketch_block_state.next_sketch_var_id();
5312                            let support_radius_value = n.n / 2.0;
5313                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5314                                value: Box::new(crate::execution::SketchVar {
5315                                    id: support_radius_id,
5316                                    initial_value: support_radius_value,
5317                                    ty: sketch_var_ty,
5318                                    // Synthesized hidden support radius for circular-circular distance; not source-backed.
5319                                    node_path: None,
5320                                    meta: vec![],
5321                                }),
5322                            });
5323                            let support_radius =
5324                                ezpz::datatypes::inputs::DatumDistance::new(support_radius_id.to_constraint_id(range)?);
5325                            let support_circle = ezpz::datatypes::inputs::DatumCircle {
5326                                center: support_point,
5327                                radius: support_radius,
5328                            };
5329                            let center_line = ezpz::datatypes::inputs::DatumLineSegment::new(
5330                                circular_target0.center,
5331                                circular_target1.center,
5332                            );
5333
5334                            sketch_block_state
5335                                .solver_constraints
5336                                .push(Constraint::Fixed(support_radius.id, support_radius_value));
5337                            sketch_block_state
5338                                .solver_constraints
5339                                .push(Constraint::PointLineDistance(support_point, center_line, 0.0));
5340                            sketch_block_state
5341                                .solver_constraints
5342                                .push(Constraint::CircleTangentToCircle(
5343                                    circular_target0,
5344                                    support_circle,
5345                                    ezpz::CircleSide::Exterior,
5346                                ));
5347                            sketch_block_state
5348                                .solver_constraints
5349                                .push(Constraint::CircleTangentToCircle(
5350                                    support_circle,
5351                                    circular_target1,
5352                                    ezpz::CircleSide::Exterior,
5353                                ));
5354
5355                            use kcl_api::ConstraintSegment;
5356                            use kcl_api::Distance;
5357                            use kcl_api::SourceRef;
5358
5359                            use crate::execution::Artifact;
5360                            use crate::execution::CodeRef;
5361                            use crate::execution::SketchBlockConstraint;
5362
5363                            let Some(sketch_id) = sketch_block_state.sketch_id else {
5364                                let message = "Sketch id missing for constraint artifact".to_owned();
5365                                debug_assert!(false, "{}", &message);
5366                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
5367                            };
5368                            let sketch_constraint = kcl_api::Constraint::Distance(Distance {
5369                                segments: input_object_ids.iter().copied().map(ConstraintSegment::from).collect(),
5370                                distance: n.try_into().map_err(|_| {
5371                                    internal_err("Failed to convert distance units numeric suffix:", range)
5372                                })?,
5373                                label_position: label_position.clone(),
5374                                source,
5375                            });
5376                            sketch_block_state.sketch_constraints.push(constraint_id);
5377                            let artifact_id = exec_state.next_artifact_id();
5378                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
5379                                id: artifact_id,
5380                                sketch_id,
5381                                constraint_id,
5382                                constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
5383                                code_ref: CodeRef::placeholder(range),
5384                            }));
5385                            exec_state.add_scene_object(
5386                                Object {
5387                                    id: constraint_id,
5388                                    kind: ObjectKind::Constraint {
5389                                        constraint: sketch_constraint,
5390                                    },
5391                                    label: Default::default(),
5392                                    comments: Default::default(),
5393                                    artifact_id,
5394                                    source: SourceRef::new(range, self.node_path.clone()),
5395                                },
5396                                range,
5397                            );
5398                        }
5399                        SketchConstraintKind::Radius { .. } | SketchConstraintKind::Diameter { .. } => {
5400                            #[derive(Clone, Copy)]
5401                            enum CircularSegmentConstraintTarget {
5402                                Arc {
5403                                    object_id: ObjectId,
5404                                    end: [crate::execution::SketchVarId; 2],
5405                                    direction: ArcDirection,
5406                                },
5407                                Circle {
5408                                    object_id: ObjectId,
5409                                },
5410                            }
5411
5412                            fn sketch_var_initial_value(
5413                                sketch_vars: &[KclValue],
5414                                id: crate::execution::SketchVarId,
5415                                exec_state: &mut ExecState,
5416                                range: SourceRange,
5417                            ) -> Result<f64, KclError> {
5418                                sketch_vars
5419                                    .get(id.0)
5420                                    .and_then(KclValue::as_sketch_var)
5421                                    .map(|sketch_var| {
5422                                        sketch_var
5423                                            .initial_value_to_solver_units(
5424                                                exec_state,
5425                                                range,
5426                                                "circle radius initial value",
5427                                            )
5428                                            .map(|value| value.n)
5429                                    })
5430                                    .transpose()?
5431                                    .ok_or_else(|| {
5432                                        internal_err(
5433                                            format!("Missing sketch variable initial value for id {}", id.0),
5434                                            range,
5435                                        )
5436                                    })
5437                            }
5438
5439                            let (points, label_position) = match &constraint.kind {
5440                                SketchConstraintKind::Radius { points, label_position } => {
5441                                    (points, label_position.clone())
5442                                }
5443                                SketchConstraintKind::Diameter { points, label_position } => {
5444                                    (points, label_position.clone())
5445                                }
5446                                _ => unreachable!(),
5447                            };
5448                            let range = self.as_source_range();
5449                            let center = &points[0];
5450                            let start = &points[1];
5451                            let Some(sketch_block_state) = &exec_state.mod_local.sketch_block else {
5452                                return Err(internal_err(
5453                                    "Being inside a sketch block should have already been checked above",
5454                                    self,
5455                                ));
5456                            };
5457                            let (constraint_name, is_diameter) = match &constraint.kind {
5458                                SketchConstraintKind::Radius { .. } => ("radius", false),
5459                                SketchConstraintKind::Diameter { .. } => ("diameter", true),
5460                                _ => unreachable!(),
5461                            };
5462                            let sketch_vars = sketch_block_state.sketch_vars.clone();
5463                            let target_segment = sketch_block_state
5464                                .needed_by_engine
5465                                .iter()
5466                                .find_map(|seg| match &seg.kind {
5467                                    UnsolvedSegmentKind::Arc {
5468                                        center_object_id,
5469                                        start_object_id,
5470                                        end,
5471                                        direction,
5472                                        ..
5473                                    } if *center_object_id == center.object_id
5474                                        && *start_object_id == start.object_id =>
5475                                    {
5476                                        let (end_x_var, end_y_var) = match (&end[0], &end[1]) {
5477                                            (UnsolvedExpr::Unknown(end_x), UnsolvedExpr::Unknown(end_y)) => {
5478                                                (*end_x, *end_y)
5479                                            }
5480                                            _ => return None,
5481                                        };
5482                                        Some(CircularSegmentConstraintTarget::Arc {
5483                                            object_id: seg.object_id,
5484                                            end: [end_x_var, end_y_var],
5485                                            direction: *direction,
5486                                        })
5487                                    }
5488                                    UnsolvedSegmentKind::Circle {
5489                                        center_object_id,
5490                                        start_object_id,
5491                                        ..
5492                                    } if *center_object_id == center.object_id
5493                                        && *start_object_id == start.object_id =>
5494                                    {
5495                                        Some(CircularSegmentConstraintTarget::Circle {
5496                                            object_id: seg.object_id,
5497                                        })
5498                                    }
5499                                    _ => None,
5500                                })
5501                                .ok_or_else(|| {
5502                                    internal_err(
5503                                        format!("Could not find circular segment for {} constraint", constraint_name),
5504                                        range,
5505                                    )
5506                                })?;
5507                            let radius_value = if is_diameter { n.n / 2.0 } else { n.n };
5508                            let center_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
5509                                center.vars.x.to_constraint_id(range)?,
5510                                center.vars.y.to_constraint_id(range)?,
5511                            );
5512                            let start_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
5513                                start.vars.x.to_constraint_id(range)?,
5514                                start.vars.y.to_constraint_id(range)?,
5515                            );
5516                            let solver_constraint = match target_segment {
5517                                CircularSegmentConstraintTarget::Arc { end, direction, .. } => {
5518                                    let solver_arc = SolverArc::new(
5519                                        [center.vars.x, center.vars.y],
5520                                        [start.vars.x, start.vars.y],
5521                                        end,
5522                                        direction,
5523                                        range,
5524                                    )?;
5525                                    solver_arc.radius_constraint(radius_value)
5526                                }
5527                                CircularSegmentConstraintTarget::Circle { .. } => {
5528                                    let sketch_var_ty = solver_numeric_type(exec_state);
5529                                    let start_x =
5530                                        sketch_var_initial_value(&sketch_vars, start.vars.x, exec_state, range)?;
5531                                    let start_y =
5532                                        sketch_var_initial_value(&sketch_vars, start.vars.y, exec_state, range)?;
5533                                    let center_x =
5534                                        sketch_var_initial_value(&sketch_vars, center.vars.x, exec_state, range)?;
5535                                    let center_y =
5536                                        sketch_var_initial_value(&sketch_vars, center.vars.y, exec_state, range)?;
5537
5538                                    // Get the hypotenuse between the two points, the radius
5539                                    let radius_initial_value = libm::hypot(start_x - center_x, start_y - center_y);
5540
5541                                    let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
5542                                        let message =
5543                                            "Being inside a sketch block should have already been checked above"
5544                                                .to_owned();
5545                                        debug_assert!(false, "{}", &message);
5546                                        return Err(internal_err(message, self));
5547                                    };
5548                                    let radius_id = sketch_block_state.next_sketch_var_id();
5549                                    sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5550                                        value: Box::new(crate::execution::SketchVar {
5551                                            id: radius_id,
5552                                            initial_value: radius_initial_value,
5553                                            ty: sketch_var_ty,
5554                                            // Synthesized hidden radius for circle constraint; not source-backed.
5555                                            node_path: None,
5556                                            meta: vec![],
5557                                        }),
5558                                    });
5559                                    let radius =
5560                                        ezpz::datatypes::inputs::DatumDistance::new(radius_id.to_constraint_id(range)?);
5561                                    let solver_circle = ezpz::datatypes::inputs::DatumCircle {
5562                                        center: center_point,
5563                                        radius,
5564                                    };
5565                                    sketch_block_state.solver_constraints.push(Constraint::DistanceVar(
5566                                        start_point,
5567                                        center_point,
5568                                        radius,
5569                                    ));
5570                                    Constraint::CircleRadius(solver_circle, radius_value)
5571                                }
5572                            };
5573
5574                            let constraint_id = exec_state.next_object_id();
5575                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
5576                                let message =
5577                                    "Being inside a sketch block should have already been checked above".to_owned();
5578                                debug_assert!(false, "{}", &message);
5579                                return Err(internal_err(message, self));
5580                            };
5581                            sketch_block_state.solver_constraints.push(solver_constraint);
5582                            use kcl_api::SourceRef;
5583
5584                            use crate::execution::Artifact;
5585                            use crate::execution::CodeRef;
5586                            use crate::execution::SketchBlockConstraint;
5587                            let segment_object_id = match target_segment {
5588                                CircularSegmentConstraintTarget::Arc { object_id, .. }
5589                                | CircularSegmentConstraintTarget::Circle { object_id } => object_id,
5590                            };
5591
5592                            let constraint = if is_diameter {
5593                                use kcl_api::Diameter;
5594                                kcl_api::Constraint::Diameter(Diameter {
5595                                    arc: segment_object_id,
5596                                    diameter: n.try_into().map_err(|_| {
5597                                        internal_err("Failed to convert diameter units numeric suffix:", range)
5598                                    })?,
5599                                    label_position,
5600                                    source,
5601                                })
5602                            } else {
5603                                use kcl_api::Radius;
5604                                kcl_api::Constraint::Radius(Radius {
5605                                    arc: segment_object_id,
5606                                    radius: n.try_into().map_err(|_| {
5607                                        internal_err("Failed to convert radius units numeric suffix:", range)
5608                                    })?,
5609                                    label_position,
5610                                    source,
5611                                })
5612                            };
5613                            sketch_block_state.sketch_constraints.push(constraint_id);
5614                            let Some(sketch_id) = sketch_block_state.sketch_id else {
5615                                let message = "Sketch id missing for constraint artifact".to_owned();
5616                                debug_assert!(false, "{}", &message);
5617                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
5618                            };
5619                            let artifact_id = exec_state.next_artifact_id();
5620                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
5621                                id: artifact_id,
5622                                sketch_id,
5623                                constraint_id,
5624                                constraint_type: super::artifact::sketch_block_constraint_type(&constraint),
5625                                code_ref: CodeRef::placeholder(range),
5626                            }));
5627                            exec_state.add_scene_object(
5628                                Object {
5629                                    id: constraint_id,
5630                                    kind: ObjectKind::Constraint { constraint },
5631                                    label: Default::default(),
5632                                    comments: Default::default(),
5633                                    artifact_id,
5634                                    source: SourceRef::new(range, self.node_path.clone()),
5635                                },
5636                                range,
5637                            );
5638                        }
5639                        SketchConstraintKind::HorizontalDistance { points, label_position } => {
5640                            let range = self.as_source_range();
5641                            let p0 = &points[0];
5642                            let p1 = &points[1];
5643                            let constraint_id = exec_state.next_object_id();
5644                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
5645                                let message =
5646                                    "Being inside a sketch block should have already been checked above".to_owned();
5647                                debug_assert!(false, "{}", &message);
5648                                return Err(internal_err(message, self));
5649                            };
5650                            match (p0, p1) {
5651                                (
5652                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(p0),
5653                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(p1),
5654                                ) => {
5655                                    let solver_pt0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
5656                                        p0.vars.x.to_constraint_id(range)?,
5657                                        p0.vars.y.to_constraint_id(range)?,
5658                                    );
5659                                    let solver_pt1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
5660                                        p1.vars.x.to_constraint_id(range)?,
5661                                        p1.vars.y.to_constraint_id(range)?,
5662                                    );
5663                                    sketch_block_state
5664                                        .solver_constraints
5665                                        .push(ezpz::Constraint::HorizontalDistance(solver_pt1, solver_pt0, n.n));
5666                                }
5667                                (
5668                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
5669                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
5670                                ) => {
5671                                    // horizontalDistance([point, ORIGIN]) == n means 0 - point.x = n, so point.x = -n.
5672                                    sketch_block_state
5673                                        .solver_constraints
5674                                        .push(ezpz::Constraint::Fixed(point.vars.x.to_constraint_id(range)?, -n.n));
5675                                }
5676                                (
5677                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
5678                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
5679                                ) => {
5680                                    // horizontalDistance([ORIGIN, point]) == n means point.x - 0 = n, so point.x = n.
5681                                    sketch_block_state
5682                                        .solver_constraints
5683                                        .push(ezpz::Constraint::Fixed(point.vars.x.to_constraint_id(range)?, n.n));
5684                                }
5685                                (
5686                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
5687                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
5688                                ) => {
5689                                    return Err(internal_err(
5690                                        "horizontalDistance() cannot constrain ORIGIN against ORIGIN".to_owned(),
5691                                        range,
5692                                    ));
5693                                }
5694                            }
5695                            use kcl_api::ConstraintSegment;
5696                            use kcl_api::Distance;
5697                            use kcl_api::SourceRef;
5698
5699                            use crate::execution::Artifact;
5700                            use crate::execution::CodeRef;
5701                            use crate::execution::SketchBlockConstraint;
5702
5703                            let constraint = kcl_api::Constraint::HorizontalDistance(Distance {
5704                                segments: vec![
5705                                    match p0 {
5706                                        crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
5707                                            ConstraintSegment::from(point.object_id)
5708                                        }
5709                                        crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
5710                                            ConstraintSegment::ORIGIN
5711                                        }
5712                                    },
5713                                    match p1 {
5714                                        crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
5715                                            ConstraintSegment::from(point.object_id)
5716                                        }
5717                                        crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
5718                                            ConstraintSegment::ORIGIN
5719                                        }
5720                                    },
5721                                ],
5722                                distance: n.try_into().map_err(|_| {
5723                                    internal_err("Failed to convert distance units numeric suffix:", range)
5724                                })?,
5725                                label_position: label_position.clone(),
5726                                source,
5727                            });
5728                            sketch_block_state.sketch_constraints.push(constraint_id);
5729                            let Some(sketch_id) = sketch_block_state.sketch_id else {
5730                                let message = "Sketch id missing for constraint artifact".to_owned();
5731                                debug_assert!(false, "{}", &message);
5732                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
5733                            };
5734                            let artifact_id = exec_state.next_artifact_id();
5735                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
5736                                id: artifact_id,
5737                                sketch_id,
5738                                constraint_id,
5739                                constraint_type: super::artifact::sketch_block_constraint_type(&constraint),
5740                                code_ref: CodeRef::placeholder(range),
5741                            }));
5742                            exec_state.add_scene_object(
5743                                Object {
5744                                    id: constraint_id,
5745                                    kind: ObjectKind::Constraint { constraint },
5746                                    label: Default::default(),
5747                                    comments: Default::default(),
5748                                    artifact_id,
5749                                    source: SourceRef::new(range, self.node_path.clone()),
5750                                },
5751                                range,
5752                            );
5753                        }
5754                        SketchConstraintKind::VerticalDistance { points, label_position } => {
5755                            let range = self.as_source_range();
5756                            let p0 = &points[0];
5757                            let p1 = &points[1];
5758                            let constraint_id = exec_state.next_object_id();
5759                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
5760                                let message =
5761                                    "Being inside a sketch block should have already been checked above".to_owned();
5762                                debug_assert!(false, "{}", &message);
5763                                return Err(internal_err(message, self));
5764                            };
5765                            match (p0, p1) {
5766                                (
5767                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(p0),
5768                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(p1),
5769                                ) => {
5770                                    let solver_pt0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
5771                                        p0.vars.x.to_constraint_id(range)?,
5772                                        p0.vars.y.to_constraint_id(range)?,
5773                                    );
5774                                    let solver_pt1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
5775                                        p1.vars.x.to_constraint_id(range)?,
5776                                        p1.vars.y.to_constraint_id(range)?,
5777                                    );
5778                                    sketch_block_state
5779                                        .solver_constraints
5780                                        .push(ezpz::Constraint::VerticalDistance(solver_pt1, solver_pt0, n.n));
5781                                }
5782                                (
5783                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
5784                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
5785                                ) => {
5786                                    sketch_block_state
5787                                        .solver_constraints
5788                                        .push(ezpz::Constraint::Fixed(point.vars.y.to_constraint_id(range)?, -n.n));
5789                                }
5790                                (
5791                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
5792                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
5793                                ) => {
5794                                    sketch_block_state
5795                                        .solver_constraints
5796                                        .push(ezpz::Constraint::Fixed(point.vars.y.to_constraint_id(range)?, n.n));
5797                                }
5798                                (
5799                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
5800                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
5801                                ) => {
5802                                    return Err(internal_err(
5803                                        "verticalDistance() cannot constrain ORIGIN against ORIGIN".to_owned(),
5804                                        range,
5805                                    ));
5806                                }
5807                            }
5808                            use kcl_api::ConstraintSegment;
5809                            use kcl_api::Distance;
5810                            use kcl_api::SourceRef;
5811
5812                            use crate::execution::Artifact;
5813                            use crate::execution::CodeRef;
5814                            use crate::execution::SketchBlockConstraint;
5815
5816                            let constraint = kcl_api::Constraint::VerticalDistance(Distance {
5817                                segments: vec![
5818                                    match p0 {
5819                                        crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
5820                                            ConstraintSegment::from(point.object_id)
5821                                        }
5822                                        crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
5823                                            ConstraintSegment::ORIGIN
5824                                        }
5825                                    },
5826                                    match p1 {
5827                                        crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
5828                                            ConstraintSegment::from(point.object_id)
5829                                        }
5830                                        crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
5831                                            ConstraintSegment::ORIGIN
5832                                        }
5833                                    },
5834                                ],
5835                                distance: n.try_into().map_err(|_| {
5836                                    internal_err("Failed to convert distance units numeric suffix:", range)
5837                                })?,
5838                                label_position: label_position.clone(),
5839                                source,
5840                            });
5841                            sketch_block_state.sketch_constraints.push(constraint_id);
5842                            let Some(sketch_id) = sketch_block_state.sketch_id else {
5843                                let message = "Sketch id missing for constraint artifact".to_owned();
5844                                debug_assert!(false, "{}", &message);
5845                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
5846                            };
5847                            let artifact_id = exec_state.next_artifact_id();
5848                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
5849                                id: artifact_id,
5850                                sketch_id,
5851                                constraint_id,
5852                                constraint_type: super::artifact::sketch_block_constraint_type(&constraint),
5853                                code_ref: CodeRef::placeholder(range),
5854                            }));
5855                            exec_state.add_scene_object(
5856                                Object {
5857                                    id: constraint_id,
5858                                    kind: ObjectKind::Constraint { constraint },
5859                                    label: Default::default(),
5860                                    comments: Default::default(),
5861                                    artifact_id,
5862                                    source: SourceRef::new(range, self.node_path.clone()),
5863                                },
5864                                range,
5865                            );
5866                        }
5867                    }
5868                    return Ok(KclValue::none());
5869                }
5870                _ => {
5871                    return Err(KclError::new_semantic(KclErrorDetails::new(
5872                        format!(
5873                            "Cannot create an equivalence constraint between values of these types: {} and {}",
5874                            left_value.human_friendly_type(),
5875                            right_value.human_friendly_type()
5876                        ),
5877                        vec![self.into()],
5878                    )));
5879                }
5880            }
5881        }
5882
5883        // Inside sketch blocks, `==` is reserved for equivalence constraints
5884        // and has already been handled above.
5885        if matches!(self.operator, BinaryOperator::Eq | BinaryOperator::Neq)
5886            && let (KclValue::String { value: left, .. }, KclValue::String { value: right, .. }) =
5887                (&left_value, &right_value)
5888        {
5889            let is_equal = left == right;
5890            let value = if self.operator == BinaryOperator::Eq {
5891                is_equal
5892            } else {
5893                !is_equal
5894            };
5895            return Ok(KclValue::Bool { value, meta });
5896        }
5897
5898        // Enums compare by declaration and variant. This has to precede
5899        // `number_as_f64` below, which would otherwise reject an enum with
5900        // "expected a number" and describe the wrong problem.
5901        if matches!(self.operator, BinaryOperator::Eq | BinaryOperator::Neq) {
5902            match (&left_value, &right_value) {
5903                (KclValue::Enum { value: left }, KclValue::Enum { value: right }) => {
5904                    if left.enum_id() != right.enum_id() {
5905                        return Err(different_enums_err(left, right, self.as_source_range()));
5906                    }
5907
5908                    let is_equal = left.variant() == right.variant();
5909                    let value = if self.operator == BinaryOperator::Eq {
5910                        is_equal
5911                    } else {
5912                        !is_equal
5913                    };
5914                    return Ok(KclValue::Bool { value, meta });
5915                }
5916                (KclValue::Enum { value }, other) | (other, KclValue::Enum { value }) => {
5917                    return Err(KclError::new_semantic(KclErrorDetails::new(
5918                        format!(
5919                            "Cannot compare enum `{}` with {}.",
5920                            value.qualified_name(),
5921                            other.human_friendly_type()
5922                        ),
5923                        vec![self.as_source_range()],
5924                    )));
5925                }
5926                _ => {}
5927            }
5928        }
5929
5930        let left = number_as_f64(&left_value, self.left.clone().into())?;
5931        let right = number_as_f64(&right_value, self.right.clone().into())?;
5932
5933        let value = match self.operator {
5934            BinaryOperator::Add => {
5935                let (l, r, ty) = NumericType::combine_eq_coerce(left, right, None);
5936                self.warn_on_unknown(&ty, "Adding", exec_state);
5937                KclValue::Number { value: l + r, meta, ty }
5938            }
5939            BinaryOperator::Sub => {
5940                let (l, r, ty) = NumericType::combine_eq_coerce(left, right, None);
5941                self.warn_on_unknown(&ty, "Subtracting", exec_state);
5942                KclValue::Number { value: l - r, meta, ty }
5943            }
5944            BinaryOperator::Mul => {
5945                let (l, r, ty) = NumericType::combine_mul(left, right);
5946                self.warn_on_unknown(&ty, "Multiplying", exec_state);
5947                KclValue::Number { value: l * r, meta, ty }
5948            }
5949            BinaryOperator::Div => {
5950                let (l, r, ty) = NumericType::combine_div(left, right);
5951                self.warn_on_unknown(&ty, "Dividing", exec_state);
5952                KclValue::Number { value: l / r, meta, ty }
5953            }
5954            BinaryOperator::Mod => {
5955                let (l, r, ty) = NumericType::combine_mod(left, right);
5956                self.warn_on_unknown(&ty, "Modulo of", exec_state);
5957                KclValue::Number { value: l % r, meta, ty }
5958            }
5959            BinaryOperator::Pow => KclValue::Number {
5960                value: libm::pow(left.n, right.n),
5961                meta,
5962                ty: exec_state.current_default_units(),
5963            },
5964            BinaryOperator::Neq => {
5965                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
5966                self.warn_on_unknown(&ty, "Comparing", exec_state);
5967                KclValue::Bool { value: l != r, meta }
5968            }
5969            BinaryOperator::Gt => {
5970                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
5971                self.warn_on_unknown(&ty, "Comparing", exec_state);
5972                KclValue::Bool { value: l > r, meta }
5973            }
5974            BinaryOperator::Gte => {
5975                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
5976                self.warn_on_unknown(&ty, "Comparing", exec_state);
5977                KclValue::Bool { value: l >= r, meta }
5978            }
5979            BinaryOperator::Lt => {
5980                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
5981                self.warn_on_unknown(&ty, "Comparing", exec_state);
5982                KclValue::Bool { value: l < r, meta }
5983            }
5984            BinaryOperator::Lte => {
5985                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
5986                self.warn_on_unknown(&ty, "Comparing", exec_state);
5987                KclValue::Bool { value: l <= r, meta }
5988            }
5989            BinaryOperator::Eq => {
5990                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
5991                self.warn_on_unknown(&ty, "Comparing", exec_state);
5992                KclValue::Bool { value: l == r, meta }
5993            }
5994            BinaryOperator::And | BinaryOperator::Or => unreachable!(),
5995        };
5996
5997        Ok(value)
5998    }
5999
6000    fn warn_on_unknown(&self, ty: &NumericType, verb: &str, exec_state: &mut ExecState) {
6001        if ty == &NumericType::Unknown {
6002            let sr = self.as_source_range();
6003            exec_state.clear_units_warnings(&sr);
6004            let mut err = CompilationIssue::err(
6005                sr,
6006                format!(
6007                    "{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)`."
6008                ),
6009            );
6010            err.tag = crate::errors::Tag::UnknownNumericUnits;
6011            exec_state.warn(err, annotations::WARN_UNKNOWN_UNITS);
6012        }
6013    }
6014}
6015
6016impl Node<UnaryExpression> {
6017    pub(super) fn apply_unary(&self, value: KclValue, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
6018        match self.operator {
6019            UnaryOperator::Not => {
6020                let KclValue::Bool {
6021                    value: bool_value,
6022                    meta: _,
6023                } = value
6024                else {
6025                    return Err(KclError::new_semantic(KclErrorDetails::new(
6026                        format!(
6027                            "Cannot apply unary operator ! to non-boolean value: {}",
6028                            value.human_friendly_type()
6029                        ),
6030                        vec![self.into()],
6031                    )));
6032                };
6033                let meta = vec![Metadata {
6034                    source_range: self.into(),
6035                }];
6036                let negated = KclValue::Bool {
6037                    value: !bool_value,
6038                    meta,
6039                };
6040
6041                Ok(negated)
6042            }
6043            UnaryOperator::Neg => {
6044                let err = || {
6045                    KclError::new_semantic(KclErrorDetails::new(
6046                        format!(
6047                            "You can only negate numbers, planes, or lines, but this is a {}",
6048                            value.human_friendly_type()
6049                        ),
6050                        vec![self.into()],
6051                    ))
6052                };
6053                match &value {
6054                    KclValue::Number { value, ty, .. } => {
6055                        let meta = vec![Metadata {
6056                            source_range: self.into(),
6057                        }];
6058                        Ok(KclValue::Number {
6059                            value: -value,
6060                            meta,
6061                            ty: *ty,
6062                        })
6063                    }
6064                    KclValue::Plane { value } => {
6065                        let mut plane = value.clone();
6066                        if plane.info.x_axis.x != 0.0 {
6067                            plane.info.x_axis.x *= -1.0;
6068                        }
6069                        if plane.info.x_axis.y != 0.0 {
6070                            plane.info.x_axis.y *= -1.0;
6071                        }
6072                        if plane.info.x_axis.z != 0.0 {
6073                            plane.info.x_axis.z *= -1.0;
6074                        }
6075                        plane.info.z_axis = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
6076                        plane.info.z_axis.canonicalize_signed_zero();
6077
6078                        plane.id = exec_state.next_uuid();
6079                        plane.object_id = None;
6080                        Ok(KclValue::Plane { value: plane })
6081                    }
6082                    KclValue::Object {
6083                        value: values, meta, ..
6084                    } => {
6085                        // Special-case for negating line-like objects.
6086                        let Some(direction) = values.get("direction") else {
6087                            return Err(err());
6088                        };
6089
6090                        let direction = match direction {
6091                            KclValue::Tuple { value: values, meta } => {
6092                                let values = values
6093                                    .iter()
6094                                    .map(|v| match v {
6095                                        KclValue::Number { value, ty, meta } => Ok(KclValue::Number {
6096                                            value: *value * -1.0,
6097                                            ty: *ty,
6098                                            meta: meta.clone(),
6099                                        }),
6100                                        _ => Err(err()),
6101                                    })
6102                                    .collect::<Result<Vec<_>, _>>()?;
6103
6104                                KclValue::Tuple {
6105                                    value: values,
6106                                    meta: meta.clone(),
6107                                }
6108                            }
6109                            KclValue::HomArray {
6110                                value: values,
6111                                ty: ty @ RuntimeType::Primitive(PrimitiveType::Number(_)),
6112                            } => {
6113                                let values = values
6114                                    .iter()
6115                                    .map(|v| match v {
6116                                        KclValue::Number { value, ty, meta } => Ok(KclValue::Number {
6117                                            value: *value * -1.0,
6118                                            ty: *ty,
6119                                            meta: meta.clone(),
6120                                        }),
6121                                        _ => Err(err()),
6122                                    })
6123                                    .collect::<Result<Vec<_>, _>>()?;
6124
6125                                KclValue::HomArray {
6126                                    value: values,
6127                                    ty: ty.clone(),
6128                                }
6129                            }
6130                            _ => return Err(err()),
6131                        };
6132
6133                        let mut value = values.clone();
6134                        value.insert("direction".to_owned(), direction);
6135                        Ok(KclValue::Object {
6136                            value,
6137                            meta: meta.clone(),
6138                            constrainable: false,
6139                            object_kind: KclObjectKind::Default,
6140                        })
6141                    }
6142                    _ => Err(err()),
6143                }
6144            }
6145            UnaryOperator::Plus => match value {
6146                KclValue::Number { .. } | KclValue::Plane { .. } => Ok(value),
6147                _ => Err(KclError::new_semantic(KclErrorDetails::new(
6148                    format!(
6149                        "You can only apply unary + to numbers or planes, but this is a {}",
6150                        value.human_friendly_type()
6151                    ),
6152                    vec![self.into()],
6153                ))),
6154            },
6155        }
6156    }
6157}
6158
6159impl Node<TagDeclarator> {
6160    pub async fn execute(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
6161        let memory_item = KclValue::TagIdentifier(Box::new(TagIdentifier {
6162            value: self.name.clone(),
6163            info: Vec::new(),
6164            meta: vec![Metadata {
6165                source_range: self.into(),
6166            }],
6167        }));
6168
6169        exec_state
6170            .mut_stack()
6171            .add(self.name.clone(), memory_item, self.into())?;
6172
6173        Ok(self.into())
6174    }
6175}
6176
6177impl Node<ArrayRangeExpression> {
6178    pub(super) fn validate_range_start(&self, start_val: &KclValue) -> Result<(), KclError> {
6179        if start_val.as_ty_f64().is_none() {
6180            return Err(KclError::new_semantic(KclErrorDetails::new(
6181                format!(
6182                    "Expected number for range start but found {}",
6183                    start_val.human_friendly_type()
6184                ),
6185                vec![self.into()],
6186            )));
6187        }
6188        Ok(())
6189    }
6190
6191    /// Build the range value from evaluated endpoints. The evaluation-free
6192    /// second half of range execution.
6193    pub(super) fn build_range(
6194        &self,
6195        start_val: KclValue,
6196        end_val: KclValue,
6197        exec_state: &mut ExecState,
6198    ) -> Result<KclValue, KclError> {
6199        let start = start_val
6200            .as_ty_f64()
6201            .ok_or(KclError::new_semantic(KclErrorDetails::new(
6202                format!(
6203                    "Expected number for range start but found {}",
6204                    start_val.human_friendly_type()
6205                ),
6206                vec![self.into()],
6207            )))?;
6208        let end = end_val.as_ty_f64().ok_or(KclError::new_semantic(KclErrorDetails::new(
6209            format!(
6210                "Expected number for range end but found {}",
6211                end_val.human_friendly_type()
6212            ),
6213            vec![self.into()],
6214        )))?;
6215
6216        let (start, end, ty) = NumericType::combine_range(start, end, exec_state, self.as_source_range())?;
6217        let Some(start) = crate::try_f64_to_i64(start) else {
6218            return Err(KclError::new_semantic(KclErrorDetails::new(
6219                format!("Range start must be an integer, but found {start}"),
6220                vec![self.into()],
6221            )));
6222        };
6223        let Some(end) = crate::try_f64_to_i64(end) else {
6224            return Err(KclError::new_semantic(KclErrorDetails::new(
6225                format!("Range end must be an integer, but found {end}"),
6226                vec![self.into()],
6227            )));
6228        };
6229
6230        if end < start {
6231            return Err(KclError::new_semantic(KclErrorDetails::new(
6232                format!("Range start is greater than range end: {start} .. {end}"),
6233                vec![self.into()],
6234            )));
6235        }
6236
6237        let range: Vec<_> = if self.end_inclusive {
6238            (start..=end).collect()
6239        } else {
6240            (start..end).collect()
6241        };
6242
6243        let meta = vec![Metadata {
6244            source_range: self.into(),
6245        }];
6246
6247        Ok(KclValue::HomArray {
6248            value: range
6249                .into_iter()
6250                .map(|num| KclValue::Number {
6251                    value: num as f64,
6252                    ty,
6253                    meta: meta.clone(),
6254                })
6255                .collect(),
6256            ty: RuntimeType::Primitive(PrimitiveType::Number(ty)),
6257        })
6258    }
6259}
6260
6261fn article_for<S: AsRef<str>>(s: S) -> &'static str {
6262    // '[' is included since it's an array.
6263    if s.as_ref().starts_with(['a', 'e', 'i', 'o', 'u', '[']) {
6264        "an"
6265    } else {
6266        "a"
6267    }
6268}
6269
6270fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<TyF64, KclError> {
6271    v.as_ty_f64().ok_or_else(|| {
6272        let actual_type = v.human_friendly_type();
6273        KclError::new_semantic(KclErrorDetails::new(
6274            format!("Expected a number, but found {actual_type}",),
6275            vec![source_range],
6276        ))
6277    })
6278}
6279
6280/// Begin an if-arm scope. Under a KCL 3.0 entry point, each
6281/// if/else-if/else arm body gets its own scope: bindings made inside the arm
6282/// are visible from their declaration to the arm's closing brace, never
6283/// outside it, and may shadow bindings from enclosing scopes. Under older
6284/// entry points the arm shares the enclosing environment (bindings leak out
6285/// and shadowing is a redefinition error). Returns whether a scope
6286/// environment was pushed; the caller must pop it on every path.
6287pub(super) fn if_arm_scope_begin(exec_state: &mut ExecState) -> Result<bool, KclError> {
6288    if !exec_state.entry_point_version_is_v3_or_higher() {
6289        return Ok(false);
6290    }
6291    exec_state.mut_stack().push_new_env_for_block()?;
6292    Ok(true)
6293}
6294
6295#[derive(Debug)]
6296pub(super) enum Property {
6297    UInt(usize),
6298    String(String),
6299}
6300
6301impl Property {
6302    /// Convert a non-computed property (the identifier in `obj.property`)
6303    /// into a Property. Nothing is evaluated.
6304    pub(super) fn from_static_name(property: &Expr, sr: SourceRange) -> Result<Self, KclError> {
6305        let Expr::Name(identifier) = property else {
6306            // Should actually be impossible because the parser would reject it.
6307            return Err(KclError::new_semantic(KclErrorDetails::new(
6308                "Object expressions like `obj.property` must use simple identifier names, not complex expressions"
6309                    .to_owned(),
6310                vec![sr],
6311            )));
6312        };
6313        Ok(Property::String(identifier.to_string()))
6314    }
6315
6316    /// Convert an already-evaluated computed-property value (the expression
6317    /// inside array brackets) into a Property.
6318    pub(super) fn from_value(prop_value: KclValue, sr: SourceRange) -> Result<Self, KclError> {
6319        let property_sr = vec![sr];
6320        match prop_value {
6321            KclValue::Number { value, ty, meta: _ } => {
6322                if !matches!(
6323                    ty,
6324                    NumericType::Unknown
6325                        | NumericType::Default { .. }
6326                        | NumericType::Known(crate::exec::UnitType::Count)
6327                ) {
6328                    return Err(KclError::new_semantic(KclErrorDetails::new(
6329                        format!(
6330                            "{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"
6331                        ),
6332                        property_sr,
6333                    )));
6334                }
6335                if let Some(x) = crate::try_f64_to_usize(value) {
6336                    Ok(Property::UInt(x))
6337                } else {
6338                    Err(KclError::new_semantic(KclErrorDetails::new(
6339                        format!("{value} is not a valid index, indices must be whole numbers >= 0"),
6340                        property_sr,
6341                    )))
6342                }
6343            }
6344            _ => Err(KclError::new_semantic(KclErrorDetails::new(
6345                "Only numbers (>= 0) can be indexes".to_owned(),
6346                vec![sr],
6347            ))),
6348        }
6349    }
6350}
6351
6352impl Property {
6353    fn type_name(&self) -> &'static str {
6354        match self {
6355            Property::UInt(_) => "number",
6356            Property::String(_) => "string",
6357        }
6358    }
6359}
6360
6361#[cfg(test)]
6362mod test {
6363    use std::sync::Arc;
6364
6365    use kcl_api::UnitLength;
6366    use tokio::io::AsyncWriteExt;
6367
6368    use super::*;
6369    use crate::ExecutorSettings;
6370    use crate::engine::engine_manager;
6371    use crate::errors::Severity;
6372    use crate::exec::UnitType;
6373    use crate::execution::ContextType;
6374    use crate::execution::parse_execute;
6375
6376    fn assert_angle_degrees(actual: ezpz::datatypes::Angle, expected: f64) {
6377        assert!(
6378            (actual.to_degrees() - expected).abs() < 1e-9,
6379            "expected {expected}deg, got {}deg",
6380            actual.to_degrees()
6381        );
6382    }
6383
6384    #[test]
6385    fn remaps_sector_angles_to_existing_representative_endpoint_rays() {
6386        let representative_directions = [AngleRayDirection::Forward, AngleRayDirection::Forward];
6387
6388        assert_angle_degrees(
6389            remap_angle_for_representative_rays(
6390                angle_sector_rays(AngleSector::One, false),
6391                representative_directions,
6392                ezpz::datatypes::Angle::from_degrees(60.0),
6393            ),
6394            60.0,
6395        );
6396        assert_angle_degrees(
6397            remap_angle_for_representative_rays(
6398                angle_sector_rays(AngleSector::Two, false),
6399                representative_directions,
6400                ezpz::datatypes::Angle::from_degrees(120.0),
6401            ),
6402            60.0,
6403        );
6404        assert_angle_degrees(
6405            remap_angle_for_representative_rays(
6406                angle_sector_rays(AngleSector::Three, false),
6407                representative_directions,
6408                ezpz::datatypes::Angle::from_degrees(60.0),
6409            ),
6410            60.0,
6411        );
6412        assert_angle_degrees(
6413            remap_angle_for_representative_rays(
6414                angle_sector_rays(AngleSector::Four, false),
6415                representative_directions,
6416                ezpz::datatypes::Angle::from_degrees(120.0),
6417            ),
6418            60.0,
6419        );
6420        assert_angle_degrees(
6421            remap_angle_for_representative_rays(
6422                angle_sector_rays(AngleSector::One, true),
6423                representative_directions,
6424                ezpz::datatypes::Angle::from_degrees(300.0),
6425            ),
6426            60.0,
6427        );
6428    }
6429
6430    #[test]
6431    fn remaps_sector_angles_when_representative_endpoint_is_on_reverse_ray() {
6432        assert_angle_degrees(
6433            remap_angle_for_representative_rays(
6434                angle_sector_rays(AngleSector::One, false),
6435                [AngleRayDirection::Forward, AngleRayDirection::Reverse],
6436                ezpz::datatypes::Angle::from_degrees(60.0),
6437            ),
6438            240.0,
6439        );
6440    }
6441
6442    #[tokio::test(flavor = "multi_thread")]
6443    async fn angle_unlabeled_keeps_legacy_lines_at_angle() {
6444        let code = r#"
6445sketch(on = XY) {
6446  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
6447  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
6448  lines = [line1, line2]
6449  angle(lines) == 60deg
6450}
6451"#;
6452        let result = parse_execute(code).await.unwrap();
6453
6454        let metadata = result
6455            .exec_state
6456            .global
6457            .root_module_artifacts
6458            .legacy_angle_refactor_metadata();
6459        assert_eq!(metadata.len(), 1);
6460        assert_eq!(metadata[0].sector, 1);
6461        assert!(!metadata[0].inverse);
6462        let program = crate::Program::parse_no_errs(code).unwrap();
6463        let findings = program.lint(crate::lint::checks::lint_legacy_angle).unwrap();
6464        assert_eq!(metadata[0].source_range, findings[0].pos);
6465    }
6466
6467    #[tokio::test(flavor = "multi_thread")]
6468    async fn legacy_angle_refactor_metadata_matches_the_default_label_side() {
6469        let result = parse_execute(
6470            r#"
6471sketch(on = XY) {
6472  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
6473  line2 = line(start = [var 0mm, var 0mm], end = [var -2mm, var -3.464mm])
6474  angle([line1, line2]) == 60deg
6475}
6476"#,
6477        )
6478        .await
6479        .unwrap();
6480
6481        let metadata = result
6482            .exec_state
6483            .global
6484            .root_module_artifacts
6485            .legacy_angle_refactor_metadata();
6486        assert_eq!(metadata.len(), 1);
6487        assert_eq!(metadata[0].sector, 4);
6488        assert!(metadata[0].inverse);
6489    }
6490
6491    #[tokio::test(flavor = "multi_thread")]
6492    async fn legacy_angle_refactor_metadata_uses_reverse_segment_rays() {
6493        let result = parse_execute(
6494            r#"
6495sketch(on = XY) {
6496  line1 = line(start = [var -4mm, var 0mm], end = [var 0mm, var 0mm])
6497  line2 = line(start = [var -2mm, var -3.464mm], end = [var 0mm, var 0mm])
6498  angle([line1, line2]) == 60deg
6499}
6500"#,
6501        )
6502        .await
6503        .unwrap();
6504
6505        let metadata = result
6506            .exec_state
6507            .global
6508            .root_module_artifacts
6509            .legacy_angle_refactor_metadata();
6510        assert_eq!(metadata.len(), 1);
6511        assert_eq!(metadata[0].sector, 3);
6512        assert!(!metadata[0].inverse);
6513    }
6514
6515    #[tokio::test(flavor = "multi_thread")]
6516    async fn legacy_angle_label_position_does_not_change_the_sector() {
6517        let result = parse_execute(
6518            r#"
6519sketch(on = XY) {
6520  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
6521  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
6522  angle([line1, line2], labelPosition = [-3mm, -1.7mm]) == 60deg
6523}
6524"#,
6525        )
6526        .await
6527        .unwrap();
6528
6529        let metadata = result
6530            .exec_state
6531            .global
6532            .root_module_artifacts
6533            .legacy_angle_refactor_metadata();
6534        assert_eq!(metadata.len(), 1);
6535        assert_eq!(metadata[0].sector, 1);
6536        assert!(!metadata[0].inverse);
6537    }
6538
6539    #[tokio::test(flavor = "multi_thread")]
6540    async fn parallel_legacy_angle_has_no_refactor_metadata() {
6541        let result = parse_execute(
6542            r#"
6543sketch(on = XY) {
6544  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
6545  line2 = line(start = [var 0mm, var 1mm], end = [var 4mm, var 1mm])
6546  angle([line1, line2]) == 0deg
6547}
6548"#,
6549        )
6550        .await
6551        .unwrap();
6552
6553        assert!(
6554            result
6555                .exec_state
6556                .global
6557                .root_module_artifacts
6558                .legacy_angle_refactor_metadata()
6559                .is_empty()
6560        );
6561    }
6562
6563    #[tokio::test(flavor = "multi_thread")]
6564    async fn angle_dimension_with_sector_uses_named_lines() {
6565        parse_execute(
6566            r#"
6567sketch(on = XY) {
6568  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
6569  line2 = line(start = [var 0mm, var 1mm], end = [var 2mm, var 3mm])
6570  angleDimension(lines = [line1, line2], sector = 2) == 60deg
6571}
6572"#,
6573        )
6574        .await
6575        .unwrap();
6576    }
6577
6578    #[tokio::test(flavor = "multi_thread")]
6579    async fn angle_dimension_requires_sector() {
6580        let err = parse_execute(
6581            r#"
6582sketch(on = XY) {
6583  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
6584  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
6585  angleDimension(lines = [line1, line2]) == 60deg
6586}
6587"#,
6588        )
6589        .await
6590        .unwrap_err();
6591
6592        assert!(
6593            err.to_string()
6594                .contains("The `angleDimension` function requires a keyword argument `sector`"),
6595            "unexpected error: {err:?}"
6596        );
6597    }
6598
6599    #[tokio::test(flavor = "multi_thread")]
6600    async fn angle_dimension_accepts_label_position() {
6601        let result = parse_execute(
6602            r#"
6603sketch(on = XY) {
6604  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
6605  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
6606  angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm]) == 60deg
6607}
6608"#,
6609        )
6610        .await
6611        .unwrap();
6612        let angle = result
6613            .exec_state
6614            .global
6615            .root_module_artifacts
6616            .scene_objects
6617            .iter()
6618            .find_map(|object| match &object.kind {
6619                ObjectKind::Constraint {
6620                    constraint: kcl_api::Constraint::Angle(angle),
6621                } => Some(angle),
6622                _ => None,
6623            })
6624            .unwrap();
6625        let label_position = angle.label_position.as_ref().unwrap();
6626        assert_eq!(label_position.x.value, 10.0);
6627        assert_eq!(label_position.y.value, 11.0);
6628    }
6629
6630    #[tokio::test(flavor = "multi_thread")]
6631    async fn angle_dimension_accepts_all_four_sectors() {
6632        parse_execute(
6633            r#"
6634sketch(on = XY) {
6635  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
6636  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
6637  angleDimension(lines = [line1, line2], sector = 1) == 60deg
6638  angleDimension(lines = [line1, line2], sector = 2) == 120deg
6639  angleDimension(lines = [line1, line2], sector = 3) == 60deg
6640  angleDimension(lines = [line1, line2], sector = 4) == 120deg
6641}
6642"#,
6643        )
6644        .await
6645        .unwrap();
6646    }
6647
6648    #[tokio::test(flavor = "multi_thread")]
6649    async fn angle_dimension_accepts_inverse_angle_for_sector() {
6650        let result = parse_execute(
6651            r#"
6652sketch(on = XY) {
6653  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
6654  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
6655  angleDimension(lines = [line1, line2], sector = 1, inverse = true) == 360deg - 60deg
6656}
6657"#,
6658        )
6659        .await
6660        .unwrap();
6661        let angle = result
6662            .exec_state
6663            .global
6664            .root_module_artifacts
6665            .scene_objects
6666            .iter()
6667            .find_map(|object| match &object.kind {
6668                ObjectKind::Constraint {
6669                    constraint: kcl_api::Constraint::Angle(angle),
6670                } => Some(angle),
6671                _ => None,
6672            })
6673            .unwrap();
6674        assert_eq!(angle.sector, Some(1));
6675        assert_eq!(angle.inverse, Some(true));
6676    }
6677
6678    #[tokio::test(flavor = "multi_thread")]
6679    async fn angle_dimension_rejects_invalid_sector() {
6680        let err = parse_execute(
6681            r#"
6682sketch(on = XY) {
6683  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
6684  line2 = line(start = [var 0mm, var 1mm], end = [var 2mm, var 3mm])
6685  angleDimension(lines = [line1, line2], sector = 5) == 60deg
6686}
6687"#,
6688        )
6689        .await
6690        .unwrap_err();
6691
6692        assert!(
6693            err.to_string()
6694                .contains("angleDimension() sector must be 1, 2, 3, or 4"),
6695            "unexpected error: {err:?}"
6696        );
6697    }
6698
6699    #[tokio::test(flavor = "multi_thread")]
6700    async fn angle_dimension_rejects_parallel_lines() {
6701        let err = parse_execute(
6702            r#"
6703sketch(on = XY) {
6704  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
6705  line2 = line(start = [var 0mm, var 1mm], end = [var 4mm, var 1mm])
6706  angleDimension(lines = [line1, line2], sector = 2) == 60deg
6707}
6708"#,
6709        )
6710        .await
6711        .unwrap_err();
6712
6713        assert!(
6714            err.to_string()
6715                .contains("angleDimension(lines = ..., sector = ...) requires non-parallel lines"),
6716            "unexpected error: {err:?}"
6717        );
6718    }
6719
6720    #[tokio::test(flavor = "multi_thread")]
6721    async fn angle_accepts_label_position() {
6722        let result = parse_execute(
6723            r#"
6724sketch(on = XY) {
6725  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
6726  line2 = line(start = [var 0mm, var 1mm], end = [var 2mm, var 3mm])
6727  angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
6728}
6729"#,
6730        )
6731        .await
6732        .unwrap();
6733        let angle = result
6734            .exec_state
6735            .global
6736            .root_module_artifacts
6737            .scene_objects
6738            .iter()
6739            .find_map(|object| match &object.kind {
6740                ObjectKind::Constraint {
6741                    constraint: kcl_api::Constraint::Angle(angle),
6742                } => Some(angle),
6743                _ => None,
6744            })
6745            .unwrap();
6746        let label_position = angle.label_position.as_ref().unwrap();
6747        assert_eq!(label_position.x.value, 10.0);
6748        assert_eq!(label_position.y.value, 11.0);
6749    }
6750
6751    #[tokio::test(flavor = "multi_thread")]
6752    async fn angle_requires_unlabeled_lines() {
6753        parse_execute(
6754            r#"
6755sketch(on = XY) {
6756  angle() == 60deg
6757}
6758"#,
6759        )
6760        .await
6761        .unwrap_err();
6762    }
6763
6764    #[tokio::test(flavor = "multi_thread")]
6765    async fn ascription() {
6766        let program = r#"
6767a = 42: number
6768b = a: number
6769p = {
6770  origin = { x = 0, y = 0, z = 0 },
6771  xAxis = { x = 1, y = 0, z = 0 },
6772  yAxis = { x = 0, y = 1, z = 0 },
6773  zAxis = { x = 0, y = 0, z = 1 }
6774}: Plane
6775arr1 = [42]: [number(cm)]
6776"#;
6777
6778        let result = parse_execute(program).await.unwrap();
6779        let mem = result.exec_state.stack();
6780        assert!(matches!(
6781            mem.memory
6782                .get_from_owned("p", result.mem_env, SourceRange::default(), 0)
6783                .unwrap(),
6784            KclValue::Plane { .. }
6785        ));
6786        let arr1 = mem
6787            .memory
6788            .get_from_owned("arr1", result.mem_env, SourceRange::default(), 0)
6789            .unwrap();
6790        if let KclValue::HomArray { value, ty } = arr1 {
6791            assert_eq!(value.len(), 1, "Expected Vec with specific length: found {value:?}");
6792            assert_eq!(ty, RuntimeType::known_length(UnitLength::Centimeters));
6793            // Compare, ignoring meta.
6794            if let KclValue::Number { value, ty, .. } = &value[0] {
6795                // It should not convert units.
6796                assert_eq!(*value, 42.0);
6797                assert_eq!(*ty, NumericType::Known(UnitType::Length(UnitLength::Centimeters)));
6798            } else {
6799                panic!("Expected a number; found {:?}", value[0]);
6800            }
6801        } else {
6802            panic!("Expected HomArray; found {arr1:?}");
6803        }
6804
6805        let program = r#"
6806a = 42: string
6807"#;
6808        let result = parse_execute(program).await;
6809        let err = result.unwrap_err();
6810        assert!(
6811            err.to_string()
6812                .contains("could not coerce a number (with type `number`) to type `string`"),
6813            "Expected error but found {err:?}"
6814        );
6815
6816        let program = r#"
6817a = 42: Plane
6818"#;
6819        let result = parse_execute(program).await;
6820        let err = result.unwrap_err();
6821        assert!(
6822            err.to_string()
6823                .contains("could not coerce a number (with type `number`) to type `Plane`"),
6824            "Expected error but found {err:?}"
6825        );
6826
6827        let program = r#"
6828arr = [0]: [string]
6829"#;
6830        let result = parse_execute(program).await;
6831        let err = result.unwrap_err();
6832        assert!(
6833            err.to_string().contains(
6834                "could not coerce an array of `number` with 1 value (with type `[any; 1]`) to type `[string]`"
6835            ),
6836            "Expected error but found {err:?}"
6837        );
6838
6839        let program = r#"
6840mixedArr = [0, "a"]: [number(mm)]
6841"#;
6842        let result = parse_execute(program).await;
6843        let err = result.unwrap_err();
6844        assert!(
6845            err.to_string().contains(
6846                "could not coerce an array of `number`, `string` (with type `[any; 2]`) to type `[number(mm)]`"
6847            ),
6848            "Expected error but found {err:?}"
6849        );
6850
6851        let program = r#"
6852mixedArr = [0, "a"]: [mm]
6853"#;
6854        let result = parse_execute(program).await;
6855        let err = result.unwrap_err();
6856        assert!(
6857            err.to_string().contains(
6858                "could not coerce an array of `number`, `string` (with type `[any; 2]`) to type `[number(mm)]`"
6859            ),
6860            "Expected error but found {err:?}"
6861        );
6862    }
6863
6864    #[tokio::test(flavor = "multi_thread")]
6865    async fn neg_plane() {
6866        let program = r#"
6867p = {
6868  origin = { x = 0, y = 0, z = 0 },
6869  xAxis = { x = 1, y = 0, z = 0 },
6870  yAxis = { x = 0, y = 1, z = 0 },
6871}: Plane
6872p2 = -p
6873"#;
6874
6875        let result = parse_execute(program).await.unwrap();
6876        let mem = result.exec_state.stack();
6877        match mem
6878            .memory
6879            .get_from_owned("p2", result.mem_env, SourceRange::default(), 0)
6880            .unwrap()
6881        {
6882            KclValue::Plane { value } => {
6883                assert_eq!(value.info.x_axis.x, -1.0);
6884                assert_eq!(value.info.x_axis.y, 0.0);
6885                assert_eq!(value.info.x_axis.z, 0.0);
6886            }
6887            _ => unreachable!(),
6888        }
6889    }
6890
6891    #[tokio::test(flavor = "multi_thread")]
6892    async fn multiple_returns() {
6893        let program = r#"fn foo() {
6894  return 0
6895  return 42
6896}
6897
6898a = foo()
6899"#;
6900
6901        let result = parse_execute(program).await;
6902        assert!(result.unwrap_err().to_string().contains("return"));
6903    }
6904
6905    #[tokio::test(flavor = "multi_thread")]
6906    async fn load_all_modules() {
6907        // program a.kcl
6908        let program_a_kcl = r#"
6909export a = 1
6910"#;
6911        // program b.kcl
6912        let program_b_kcl = r#"
6913import a from 'a.kcl'
6914
6915export b = a + 1
6916"#;
6917        // program c.kcl
6918        let program_c_kcl = r#"
6919import a from 'a.kcl'
6920
6921export c = a + 2
6922"#;
6923
6924        // program main.kcl
6925        let main_kcl = r#"
6926import b from 'b.kcl'
6927import c from 'c.kcl'
6928
6929d = b + c
6930"#;
6931
6932        let main = crate::parsing::parse_str(main_kcl, ModuleId::default())
6933            .parse_errs_as_err()
6934            .unwrap();
6935
6936        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_load_all_modules").unwrap();
6937
6938        tokio::fs::File::create(tmpdir.path().join("main.kcl"))
6939            .await
6940            .unwrap()
6941            .write_all(main_kcl.as_bytes())
6942            .await
6943            .unwrap();
6944
6945        tokio::fs::File::create(tmpdir.path().join("a.kcl"))
6946            .await
6947            .unwrap()
6948            .write_all(program_a_kcl.as_bytes())
6949            .await
6950            .unwrap();
6951
6952        tokio::fs::File::create(tmpdir.path().join("b.kcl"))
6953            .await
6954            .unwrap()
6955            .write_all(program_b_kcl.as_bytes())
6956            .await
6957            .unwrap();
6958
6959        tokio::fs::File::create(tmpdir.path().join("c.kcl"))
6960            .await
6961            .unwrap()
6962            .write_all(program_c_kcl.as_bytes())
6963            .await
6964            .unwrap();
6965
6966        let exec_ctxt = ExecutorContext {
6967            engine: Arc::new(engine_manager::EngineManager::new_mock()),
6968            cache: crate::Cache::default(),
6969            engine_batch: crate::engine::EngineBatchContext::default(),
6970            fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
6971            settings: ExecutorSettings {
6972                project_directory: Some(crate::TypedPath(tmpdir.path().into())),
6973                ..Default::default()
6974            },
6975            context_type: ContextType::Mock,
6976            execution_callbacks: Default::default(),
6977            machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
6978            configure_engine_render: true,
6979            needs_oit: Default::default(),
6980        };
6981        let mut exec_state = ExecState::new(&exec_ctxt);
6982
6983        exec_ctxt
6984            .run(
6985                &crate::Program {
6986                    kcl_version: main.kcl_version,
6987                    ast: main.ast.clone(),
6988                    original_file_contents: "".to_owned(),
6989                },
6990                &mut exec_state,
6991            )
6992            .await
6993            .unwrap();
6994    }
6995
6996    #[tokio::test(flavor = "multi_thread")]
6997    async fn user_coercion() {
6998        let program = r#"fn foo(x: Axis2d) {
6999  return 0
7000}
7001
7002foo(x = { direction = [0, 0], origin = [0, 0]})
7003"#;
7004
7005        parse_execute(program).await.unwrap();
7006
7007        let program = r#"fn foo(x: Axis3d) {
7008  return 0
7009}
7010
7011foo(x = { direction = [0, 0], origin = [0, 0]})
7012"#;
7013
7014        parse_execute(program).await.unwrap_err();
7015    }
7016
7017    #[tokio::test(flavor = "multi_thread")]
7018    async fn coerce_return() {
7019        let program = r#"fn foo(): number(mm) {
7020  return 42
7021}
7022
7023a = foo()
7024"#;
7025
7026        parse_execute(program).await.unwrap();
7027
7028        let program = r#"fn foo(): mm {
7029  return 42
7030}
7031
7032a = foo()
7033"#;
7034
7035        parse_execute(program).await.unwrap();
7036
7037        let program = r#"fn foo(): number(mm) {
7038  return { bar: 42 }
7039}
7040
7041a = foo()
7042"#;
7043
7044        parse_execute(program).await.unwrap_err();
7045
7046        let program = r#"fn foo(): mm {
7047  return { bar: 42 }
7048}
7049
7050a = foo()
7051"#;
7052
7053        parse_execute(program).await.unwrap_err();
7054    }
7055
7056    #[tokio::test(flavor = "multi_thread")]
7057    async fn test_sensible_error_when_missing_equals_in_kwarg() {
7058        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)"]
7059            .into_iter()
7060            .enumerate()
7061        {
7062            let program = format!(
7063                "fn foo() {{ return 0 }}
7064z = 0
7065fn f(x, y, z) {{ return 0 }}
7066{call}"
7067            );
7068            let err = parse_execute(&program).await.unwrap_err();
7069            let msg = err.message();
7070            assert!(
7071                msg.contains("This argument needs a label, but it doesn't have one"),
7072                "failed test {i}: {msg}"
7073            );
7074            assert!(msg.contains("`y`"), "failed test {i}, missing `y`: {msg}");
7075            if i == 0 {
7076                assert!(msg.contains("`z`"), "failed test {i}, missing `z`: {msg}");
7077            }
7078        }
7079    }
7080
7081    #[tokio::test(flavor = "multi_thread")]
7082    async fn default_param_for_unlabeled() {
7083        // Tests that the input param for myExtrude is taken from the pipeline value and same-name
7084        // keyword args.
7085        let ast = r#"fn myExtrude(@sk, length) {
7086  return extrude(sk, length)
7087}
7088sketch001 = startSketchOn(XY)
7089  |> circle(center = [0, 0], radius = 93.75)
7090  |> myExtrude(length = 40)
7091"#;
7092
7093        parse_execute(ast).await.unwrap();
7094    }
7095
7096    #[tokio::test(flavor = "multi_thread")]
7097    async fn dont_use_unlabelled_as_input() {
7098        // `length` should be used as the `length` argument to extrude, not the unlabelled input
7099        let ast = r#"length = 10
7100startSketchOn(XY)
7101  |> circle(center = [0, 0], radius = 93.75)
7102  |> extrude(length)
7103"#;
7104
7105        parse_execute(ast).await.unwrap();
7106    }
7107
7108    #[tokio::test(flavor = "multi_thread")]
7109    async fn ascription_in_binop() {
7110        let ast = r#"foo = tan(0): number(rad) - 4deg"#;
7111        parse_execute(ast).await.unwrap();
7112
7113        let ast = r#"foo = tan(0): rad - 4deg"#;
7114        parse_execute(ast).await.unwrap();
7115    }
7116
7117    #[tokio::test(flavor = "multi_thread")]
7118    async fn neg_sqrt() {
7119        let ast = r#"bad = sqrt(-2)"#;
7120
7121        let e = parse_execute(ast).await.unwrap_err();
7122        // Make sure we get a useful error message and not an engine error.
7123        assert!(e.message().contains("sqrt"), "Error message: '{}'", e.message());
7124    }
7125
7126    #[tokio::test(flavor = "multi_thread")]
7127    async fn non_array_fns() {
7128        let ast = r#"push(1, item = 2)
7129pop(1)
7130map(1, f = fn(@x) { return x + 1 })
7131reduce(1, f = fn(@x, accum) { return accum + x}, initial = 0)"#;
7132
7133        parse_execute(ast).await.unwrap();
7134    }
7135
7136    #[tokio::test(flavor = "multi_thread")]
7137    async fn non_array_indexing() {
7138        let good = r#"a = 42
7139good = a[0]
7140"#;
7141        let result = parse_execute(good).await.unwrap();
7142        let mem = result.exec_state.stack();
7143        let num = mem
7144            .memory
7145            .get_from_owned("good", result.mem_env, SourceRange::default(), 0)
7146            .unwrap()
7147            .as_ty_f64()
7148            .unwrap();
7149        assert_eq!(num.n, 42.0);
7150
7151        let bad = r#"a = 42
7152bad = a[1]
7153"#;
7154
7155        parse_execute(bad).await.unwrap_err();
7156    }
7157
7158    #[tokio::test(flavor = "multi_thread")]
7159    async fn coerce_unknown_to_length() {
7160        let ast = r#"x = 2mm * 2mm
7161y = x: number(Length)"#;
7162        let e = parse_execute(ast).await.unwrap_err();
7163        assert!(
7164            e.message().contains("could not coerce"),
7165            "Error message: '{}'",
7166            e.message()
7167        );
7168
7169        let ast = r#"x = 2mm
7170y = x: number(Length)"#;
7171        let result = parse_execute(ast).await.unwrap();
7172        let mem = result.exec_state.stack();
7173        let num = mem
7174            .memory
7175            .get_from_owned("y", result.mem_env, SourceRange::default(), 0)
7176            .unwrap()
7177            .as_ty_f64()
7178            .unwrap();
7179        assert_eq!(num.n, 2.0);
7180        assert_eq!(num.ty, NumericType::mm());
7181    }
7182
7183    #[tokio::test(flavor = "multi_thread")]
7184    async fn one_warning_unknown() {
7185        let ast = r#"
7186// Should warn once
7187a = PI * 2
7188// Should warn once
7189b = (PI * 2) / 3
7190// Should not warn
7191c = ((PI * 2) / 3): number(deg)
7192"#;
7193
7194        let result = parse_execute(ast).await.unwrap();
7195        assert_eq!(result.exec_state.issues().len(), 2);
7196    }
7197
7198    #[tokio::test(flavor = "multi_thread")]
7199    async fn non_count_indexing() {
7200        let ast = r#"x = [0, 0]
7201y = x[1mm]
7202"#;
7203        parse_execute(ast).await.unwrap_err();
7204
7205        let ast = r#"x = [0, 0]
7206y = 1deg
7207z = x[y]
7208"#;
7209        parse_execute(ast).await.unwrap_err();
7210
7211        let ast = r#"x = [0, 0]
7212y = x[0mm + 1]
7213"#;
7214        parse_execute(ast).await.unwrap_err();
7215    }
7216
7217    #[tokio::test(flavor = "multi_thread")]
7218    async fn getting_property_of_plane() {
7219        let ast = std::fs::read_to_string("tests/inputs/planestuff.kcl").unwrap();
7220        parse_execute(&ast).await.unwrap();
7221    }
7222
7223    #[tokio::test(flavor = "multi_thread")]
7224    async fn no_artifacts_from_within_hole_call() {
7225        // Test that executing stdlib KCL, like the `hole` function
7226        // (which is actually implemented in KCL not Rust)
7227        // does not generate artifacts from within the stdlib code,
7228        // only from the user code.
7229        let ast = std::fs::read_to_string("tests/inputs/sample_hole.kcl").unwrap();
7230        let out = parse_execute(&ast).await.unwrap();
7231
7232        // Get all the operations that occurred.
7233        let actual_operations = out.exec_state.global.root_module_artifacts.operations;
7234
7235        // There should be 5, for sketching the cube and applying the hole.
7236        // If the stdlib internal calls are being tracked, that's a bug,
7237        // and the actual number of operations will be something like 35.
7238        let expected = 5;
7239        assert_eq!(
7240            actual_operations.len(),
7241            expected,
7242            "expected {expected} operations, received {}:\n{actual_operations:#?}",
7243            actual_operations.len(),
7244        );
7245    }
7246
7247    #[tokio::test(flavor = "multi_thread")]
7248    async fn feature_tree_annotation_on_user_defined_kcl() {
7249        // The call to foo() should not generate an operation,
7250        // because its 'feature_tree' attribute has been set to false.
7251        let ast = std::fs::read_to_string("tests/inputs/feature_tree_annotation_on_user_defined_kcl.kcl").unwrap();
7252        let out = parse_execute(&ast).await.unwrap();
7253
7254        // Get all the operations that occurred.
7255        let actual_operations = out.exec_state.global.root_module_artifacts.operations;
7256
7257        let expected = 0;
7258        assert_eq!(
7259            actual_operations.len(),
7260            expected,
7261            "expected {expected} operations, received {}:\n{actual_operations:#?}",
7262            actual_operations.len(),
7263        );
7264    }
7265
7266    #[tokio::test(flavor = "multi_thread")]
7267    async fn no_feature_tree_annotation_on_user_defined_kcl() {
7268        // The call to foo() should generate an operation,
7269        // because @(feature_tree) defaults to true.
7270        let ast = std::fs::read_to_string("tests/inputs/no_feature_tree_annotation_on_user_defined_kcl.kcl").unwrap();
7271        let out = parse_execute(&ast).await.unwrap();
7272
7273        // Get all the operations that occurred.
7274        let actual_operations = out.exec_state.global.root_module_artifacts.operations;
7275
7276        let expected = 2;
7277        assert_eq!(
7278            actual_operations.len(),
7279            expected,
7280            "expected {expected} operations, received {}:\n{actual_operations:#?}",
7281            actual_operations.len(),
7282        );
7283        assert!(matches!(actual_operations[0], Operation::GroupBegin { .. }));
7284        assert!(matches!(actual_operations[1], Operation::GroupEnd));
7285    }
7286
7287    #[tokio::test(flavor = "multi_thread")]
7288    async fn custom_warning() {
7289        let warn = r#"
7290a = PI * 2
7291"#;
7292        let result = parse_execute(warn).await.unwrap();
7293        assert_eq!(result.exec_state.issues().len(), 1);
7294        assert_eq!(result.exec_state.issues()[0].severity, Severity::Warning);
7295
7296        let allow = r#"
7297@warnings(allow = unknownUnits)
7298a = PI * 2
7299"#;
7300        let result = parse_execute(allow).await.unwrap();
7301        assert_eq!(result.exec_state.issues().len(), 0);
7302
7303        let deny = r#"
7304@warnings(deny = [unknownUnits])
7305a = PI * 2
7306"#;
7307        let result = parse_execute(deny).await.unwrap();
7308        assert_eq!(result.exec_state.issues().len(), 1);
7309        assert_eq!(result.exec_state.issues()[0].severity, Severity::Error);
7310    }
7311
7312    /// KCL 3.0 renamed `@warnings` to `@diagnostics`. Under KCL 3.0-preview
7313    /// or later, `@diagnostics` customizes diagnostics the way `@warnings`
7314    /// does in earlier versions.
7315    #[tokio::test(flavor = "multi_thread")]
7316    async fn diagnostics_attribute_in_v3() {
7317        let warn = "@settings(kclVersion = \"3.0-preview\")\na = PI * 2\n";
7318        let result = parse_execute(warn).await.unwrap();
7319        let issues = result.exec_state.issues();
7320        assert_eq!(issues.len(), 1, "{issues:#?}");
7321        assert_eq!(issues[0].severity, Severity::Warning);
7322        assert_eq!(issues[0].tag, crate::errors::Tag::UnknownNumericUnits);
7323
7324        let allow = "@settings(kclVersion = \"3.0-preview\")\n@diagnostics(allow = unknownUnits)\na = PI * 2\n";
7325        let result = parse_execute(allow).await.unwrap();
7326        assert!(
7327            result.exec_state.issues().is_empty(),
7328            "{:#?}",
7329            result.exec_state.issues()
7330        );
7331
7332        let deny = "@settings(kclVersion = \"3.0-preview\")\n@diagnostics(deny = [unknownUnits])\na = PI * 2\n";
7333        let result = parse_execute(deny).await.unwrap();
7334        let issues = result.exec_state.issues();
7335        assert_eq!(issues.len(), 1, "{issues:#?}");
7336        assert_eq!(issues[0].severity, Severity::Error);
7337        assert_eq!(issues[0].tag, crate::errors::Tag::UnknownNumericUnits);
7338    }
7339
7340    /// Before KCL 3.0, `@diagnostics` is not an attribute: it gets the usual
7341    /// unknown-annotation warning and has no effect, while `@warnings` keeps
7342    /// working.
7343    #[tokio::test(flavor = "multi_thread")]
7344    async fn diagnostics_attribute_is_unknown_before_v3() {
7345        for version in ["1.0", "2.0"] {
7346            let code = format!("@settings(kclVersion = {version})\n@diagnostics(allow = unknownUnits)\na = PI * 2\n");
7347            let result = parse_execute(&code).await.unwrap();
7348            let issues = result.exec_state.issues();
7349            assert_eq!(issues.len(), 2, "code={code}, issues={issues:#?}");
7350            assert_eq!(issues[0].severity, Severity::Warning);
7351            assert_eq!(issues[0].message, "Unknown annotation");
7352            assert_eq!(
7353                &code[issues[0].source_range.start()..issues[0].source_range.end()],
7354                "@diagnostics(allow = unknownUnits)"
7355            );
7356            assert_eq!(issues[1].severity, Severity::Warning);
7357            assert_eq!(issues[1].tag, crate::errors::Tag::UnknownNumericUnits);
7358
7359            let code = format!("@settings(kclVersion = {version})\n@warnings(allow = unknownUnits)\na = PI * 2\n");
7360            let result = parse_execute(&code).await.unwrap();
7361            assert!(
7362                result.exec_state.issues().is_empty(),
7363                "code={code}, issues={:#?}",
7364                result.exec_state.issues()
7365            );
7366        }
7367    }
7368
7369    /// Errors for a malformed attribute name the attribute that was written:
7370    /// `warnings` before KCL 3.0 and `diagnostics` in KCL 3.0-preview or
7371    /// later.
7372    #[tokio::test(flavor = "multi_thread")]
7373    async fn diagnostics_attribute_errors_use_the_attribute_name() {
7374        for (version, attr, noun) in [
7375            ("1.0", "warnings", "warning"),
7376            ("2.0", "warnings", "warning"),
7377            ("\"3.0-preview\"", "diagnostics", "diagnostic"),
7378        ] {
7379            let settings = format!("@settings(kclVersion = {version})\n");
7380
7381            let code = format!("{settings}@{attr}\n");
7382            let error = parse_execute(&code).await.unwrap_err();
7383            assert_eq!(error.message(), format!("Empty `{attr}` annotation"), "code={code}");
7384
7385            let code = format!("{settings}@{attr}(warn = unknownUnits)\n");
7386            let error = parse_execute(&code).await.unwrap_err();
7387            assert_eq!(
7388                error.message(),
7389                format!("Unexpected {attr} key: `warn`; expected one of `allow`, `deny`"),
7390                "code={code}"
7391            );
7392
7393            let code = format!("{settings}@{attr}(allow = 1)\n");
7394            let error = parse_execute(&code).await.unwrap_err();
7395            assert_eq!(
7396                error.message(),
7397                format!(
7398                    "Unexpected {attr} value, expected a name or array of names, e.g., `unknownUnits` or `[unknownUnits, deprecated]`"
7399                ),
7400                "code={code}"
7401            );
7402
7403            let code = format!("{settings}@{attr}(allow = bogus)\n");
7404            let error = parse_execute(&code).await.unwrap_err();
7405            let expected_prefix = format!("Unexpected {noun} value: `bogus`; accepted values: unknownUnits, ");
7406            assert!(
7407                error.message().starts_with(&expected_prefix),
7408                "code={code}, message={}",
7409                error.message()
7410            );
7411        }
7412    }
7413
7414    /// KCL 3.0: using the old `@warnings` name is a non-fatal error that
7415    /// explains the rename and offers the fix. The attribute is ignored, so
7416    /// the warning it tried to allow is still reported.
7417    #[tokio::test(flavor = "multi_thread")]
7418    async fn warnings_attribute_is_renamed_in_v3() {
7419        let code = "@settings(kclVersion = \"3.0-preview\")\n@warnings(allow = unknownUnits)\na = PI * 2\n";
7420        let result = parse_execute(code).await.unwrap();
7421        let issues = result.exec_state.issues();
7422        assert_eq!(issues.len(), 2, "{issues:#?}");
7423
7424        let renamed = &issues[0];
7425        assert_eq!(renamed.severity, Severity::Error);
7426        assert_eq!(
7427            renamed.message,
7428            "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."
7429        );
7430        assert_eq!(
7431            &code[renamed.source_range.start()..renamed.source_range.end()],
7432            "@warnings(allow = unknownUnits)"
7433        );
7434        let suggestion = renamed.suggestion.as_ref().unwrap();
7435        assert_eq!(suggestion.title, "Rename to `@diagnostics`");
7436        assert_eq!(
7437            suggestion.apply(code),
7438            "@settings(kclVersion = \"3.0-preview\")\n@diagnostics(allow = unknownUnits)\na = PI * 2\n"
7439        );
7440
7441        assert_eq!(issues[1].severity, Severity::Warning);
7442        assert_eq!(issues[1].tag, crate::errors::Tag::UnknownNumericUnits);
7443
7444        // Since the attribute is ignored, its properties aren't checked.
7445        let code = "@settings(kclVersion = \"3.0-preview\")\n@warnings(allow = bogus)\n";
7446        let result = parse_execute(code).await.unwrap();
7447        let issues = result.exec_state.issues();
7448        assert_eq!(issues.len(), 1, "{issues:#?}");
7449        assert_eq!(issues[0].severity, Severity::Error);
7450        assert!(issues[0].message.starts_with("The `@warnings` attribute was renamed"));
7451    }
7452
7453    #[tokio::test(flavor = "multi_thread")]
7454    async fn sketch_block_unqualified_functions_use_sketch2() {
7455        let ast = r#"
7456s = sketch(on = XY) {
7457  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
7458  line2 = line(start = [var 1mm, var 0mm], end = [var 1mm, var 1mm])
7459  coincident([line1.end, line2.start])
7460}
7461"#;
7462        let result = parse_execute(ast).await.unwrap();
7463        let mem = result.exec_state.stack();
7464        let sketch_value = mem
7465            .memory
7466            .get_from_owned("s", result.mem_env, SourceRange::default(), 0)
7467            .unwrap();
7468
7469        let KclValue::Object { value, .. } = sketch_value else {
7470            panic!("Expected sketch block to return an object, got {sketch_value:?}");
7471        };
7472
7473        assert!(value.contains_key("line1"));
7474        assert!(value.contains_key("line2"));
7475        // Ensure sketch2 aliases used during execution are not returned as
7476        // sketch block fields.
7477        assert!(!value.contains_key("line"));
7478        assert!(!value.contains_key("coincident"));
7479    }
7480
7481    #[tokio::test(flavor = "multi_thread")]
7482    async fn solver_module_is_not_available_outside_sketch_blocks() {
7483        let err = parse_execute("a = solver::ORIGIN").await.unwrap_err();
7484        assert!(err.message().contains("solver"), "Error message: '{}'", err.message());
7485
7486        let err = parse_execute(
7487            r#"@settings(experimentalFeatures = allow)
7488
7489import "std::solver""#,
7490        )
7491        .await
7492        .unwrap_err();
7493        assert!(
7494            err.message().contains("only available inside sketch blocks"),
7495            "Error message: '{}'",
7496            err.message()
7497        );
7498    }
7499
7500    #[tokio::test(flavor = "multi_thread")]
7501    async fn cannot_solid_extrude_an_open_profile() {
7502        // This should fail during mock execution, because KCL should catch
7503        // that the profile is not closed.
7504        let code = std::fs::read_to_string("tests/inputs/cannot_solid_extrude_an_open_profile.kcl").unwrap();
7505        let program = crate::Program::parse_no_errs(&code).expect("should parse");
7506        let exec_ctxt = ExecutorContext::new_mock(None).await;
7507        let mut exec_state = ExecState::new(&exec_ctxt);
7508
7509        let err = exec_ctxt.run(&program, &mut exec_state).await.unwrap_err().error;
7510        assert!(matches!(err, KclError::Semantic { .. }));
7511        exec_ctxt.close().await;
7512    }
7513}