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