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