Skip to main content

kcl_lib/execution/
exec_ast.rs

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