Skip to main content

kcl_lib/execution/
exec_ast.rs

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