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::Group;
11use kcl_api::NumericType;
12use kcl_api::Operation;
13use kcl_api::UnitAngle;
14
15use crate::CompilationIssue;
16use crate::NodePath;
17use crate::NodePathExt;
18use crate::SourceRange;
19use crate::errors::KclError;
20use crate::errors::KclErrorDetails;
21use crate::exec::Sketch;
22use crate::execution::AbstractSegment;
23use crate::execution::AngleConstraintMode;
24use crate::execution::AngleRayDirection;
25use crate::execution::AngleSector;
26use crate::execution::Artifact;
27use crate::execution::ArtifactId;
28use crate::execution::BodyType;
29use crate::execution::ConstrainableLine2d;
30use crate::execution::ConstraintKind;
31use crate::execution::EarlyReturn;
32use crate::execution::EnvironmentRef;
33use crate::execution::ExecState;
34use crate::execution::ExecutorContext;
35use crate::execution::KclValue;
36use crate::execution::KclValueControlFlow;
37use crate::execution::KclVersion;
38use crate::execution::LegacyAngleRefactorMeta;
39use crate::execution::Metadata;
40use crate::execution::ModelingCmdMeta;
41use crate::execution::ModuleArtifactState;
42use crate::execution::PendingLegacyAngleRefactorMeta;
43use crate::execution::PreserveMem;
44use crate::execution::RefactorMetadata;
45use crate::execution::SKETCH_BLOCK_PARAM_ON;
46use crate::execution::SKETCH_OBJECT_META;
47use crate::execution::SKETCH_OBJECT_META_SKETCH;
48use crate::execution::Segment;
49use crate::execution::SegmentKind;
50use crate::execution::SegmentRepr;
51use crate::execution::SketchConstraintKind;
52use crate::execution::SketchSurface;
53use crate::execution::SolverArc;
54use crate::execution::StatementKind;
55use crate::execution::TagIdentifier;
56use crate::execution::UnsolvedExpr;
57use crate::execution::UnsolvedSegment;
58use crate::execution::UnsolvedSegmentKind;
59use crate::execution::annotations;
60use crate::execution::annotations::FnAttrs;
61use crate::execution::cad_op::op_from_kcl_value;
62use crate::execution::control_continue;
63use crate::execution::early_return;
64use crate::execution::fn_call::Arg;
65use crate::execution::fn_call::Args;
66use crate::execution::fn_call::unexpected_kw_arg_message;
67use crate::execution::kcl_value::EnumTypeDef;
68use crate::execution::kcl_value::EnumTypeId;
69use crate::execution::kcl_value::EnumValue;
70use crate::execution::kcl_value::FunctionSource;
71use crate::execution::kcl_value::KclFunctionSourceParams;
72use crate::execution::kcl_value::KclObjectKind;
73use crate::execution::kcl_value::TypeDef;
74use crate::execution::memory::SKETCH_PREFIX;
75use crate::execution::memory::{self};
76use crate::execution::sketch_constraint_status_for_sketch;
77use crate::execution::sketch_solve::FreedomAnalysis;
78use crate::execution::sketch_solve::Solved;
79use crate::execution::sketch_solve::UnsatisfiedDirectionalConstraint;
80use crate::execution::sketch_solve::create_segment_scene_objects;
81use crate::execution::sketch_solve::normalize_to_solver_angle_unit;
82use crate::execution::sketch_solve::normalize_to_solver_distance_unit;
83use crate::execution::sketch_solve::solver_numeric_type;
84use crate::execution::sketch_solve::substitute_sketch_var_in_segment;
85use crate::execution::sketch_solve::substitute_sketch_vars;
86use crate::execution::state::ModuleState;
87use crate::execution::state::SketchBlockState;
88use crate::execution::types::CoercionMode;
89use crate::execution::types::NumericTypeExt;
90use crate::execution::types::PrimitiveType;
91use crate::execution::types::RuntimeType;
92use crate::execution::types::resolve_named_type_def;
93use crate::execution::types::type_value_named_by_segment;
94use crate::front::ArcDirection;
95use crate::front::LineCtor;
96use crate::front::Object;
97use crate::front::ObjectId;
98use crate::front::ObjectKind;
99use crate::front::PointCtor;
100use crate::modules::ModuleExecutionOutcome;
101use crate::modules::ModuleId;
102use crate::modules::ModuleItems;
103use crate::modules::ModulePath;
104use crate::modules::ModuleRepr;
105use crate::parsing::ast::types::ABSOLUTE_PATHS_NOT_SUPPORTED;
106use crate::parsing::ast::types::Annotation;
107use crate::parsing::ast::types::ArrayExpression;
108use crate::parsing::ast::types::ArrayRangeExpression;
109use crate::parsing::ast::types::AscribedExpression;
110use crate::parsing::ast::types::BinaryExpression;
111use crate::parsing::ast::types::BinaryOperator;
112use crate::parsing::ast::types::BinaryPart;
113use crate::parsing::ast::types::BodyItem;
114use crate::parsing::ast::types::CodeBlock;
115use crate::parsing::ast::types::Expr;
116use crate::parsing::ast::types::FunctionExpression;
117use crate::parsing::ast::types::Identifier;
118use crate::parsing::ast::types::IfExpression;
119use crate::parsing::ast::types::ImportPath;
120use crate::parsing::ast::types::ImportSelector;
121use crate::parsing::ast::types::ImportStatement;
122use crate::parsing::ast::types::ItemVisibility;
123use crate::parsing::ast::types::MemberExpression;
124use crate::parsing::ast::types::Name;
125use crate::parsing::ast::types::Node;
126use crate::parsing::ast::types::ObjectExpression;
127use crate::parsing::ast::types::PipeExpression;
128use crate::parsing::ast::types::Program;
129use crate::parsing::ast::types::ReturnStatement;
130use crate::parsing::ast::types::SketchBlock;
131use crate::parsing::ast::types::SketchVar;
132use crate::parsing::ast::types::TagDeclarator;
133use crate::parsing::ast::types::Type;
134use crate::parsing::ast::types::TypeDeclaration;
135use crate::parsing::ast::types::TypeDeclarationDefinition;
136use crate::parsing::ast::types::UnaryExpression;
137use crate::parsing::ast::types::UnaryOperator;
138use crate::parsing::ast::types::VariableDeclaration;
139use crate::std::StdFnProps;
140use crate::std::args::FromKclValue;
141use crate::std::args::TyF64;
142use crate::std::shapes::SketchOrSurface;
143use crate::std::sketch::ensure_sketch_plane_in_engine;
144use crate::std::solver::SOLVER_CONVERGENCE_TOLERANCE;
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: crate::front::ObjectId,
904) -> crate::front::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 crate::front::Plane::Default(name);
914        }
915        break;
916    }
917
918    crate::front::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) = crate::parsing::parse_str_syntax(&source.source, id).unwrap();
1930                crate::parsing::validate_never_type_ranges(
1931                    &never_type_ranges,
1932                    crate::parsing::SyntaxSource::BundledStdlib,
1933                )
1934                .unwrap();
1935                exec_state.add_module(id, resolved_path.clone(), ModuleRepr::Kcl(parsed, None));
1936                Ok(id)
1937            }
1938        }
1939    }
1940
1941    pub(super) async fn exec_module_for_items(
1942        &self,
1943        module_id: ModuleId,
1944        exec_state: &mut ExecState,
1945        source_range: SourceRange,
1946    ) -> Result<ModuleItems, KclError> {
1947        let path = exec_state.global.module_infos[&module_id].path.clone();
1948        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1949        // DON'T EARLY RETURN! We need to restore the module repr
1950
1951        let result = match &mut repr {
1952            ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
1953            ModuleRepr::Kcl(_, Some(outcome)) => Ok(outcome.items()),
1954            ModuleRepr::Kcl(program, cache) => self
1955                .exec_module_from_ast(program, module_id, &path, exec_state, source_range, PreserveMem::Normal)
1956                .await
1957                .map(|outcome| {
1958                    let items = outcome.items();
1959                    *cache = Some(outcome);
1960                    items
1961                }),
1962            ModuleRepr::Foreign(geom, _) => Err(KclError::new_semantic(KclErrorDetails::new(
1963                "Cannot import items from foreign modules".to_owned(),
1964                vec![geom.source_range],
1965            ))),
1966            ModuleRepr::Dummy => unreachable!("Looking up {}, but it is still being interpreted", path),
1967        };
1968
1969        exec_state.global.module_infos[&module_id].restore_repr(repr);
1970        result
1971    }
1972
1973    async fn exec_module_for_result(
1974        &self,
1975        module_id: ModuleId,
1976        exec_state: &mut ExecState,
1977        source_range: SourceRange,
1978    ) -> Result<Option<KclValue>, KclError> {
1979        let path = exec_state.global.module_infos[&module_id].path.clone();
1980        let mut repr = exec_state.global.module_infos[&module_id].take_repr();
1981        // DON'T EARLY RETURN! We need to restore the module repr
1982
1983        let result = match &mut repr {
1984            ModuleRepr::Root => Err(exec_state.circular_import_error(&path, source_range)),
1985            ModuleRepr::Kcl(_, Some(outcome)) => Ok(outcome.last_expr.clone()),
1986            ModuleRepr::Kcl(program, cached_items) => {
1987                let result = self
1988                    .exec_module_from_ast(program, module_id, &path, exec_state, source_range, PreserveMem::Normal)
1989                    .await;
1990                match result {
1991                    Ok(outcome) => {
1992                        let value = outcome.last_expr.clone();
1993                        *cached_items = Some(outcome);
1994                        Ok(value)
1995                    }
1996                    Err(e) => Err(e),
1997                }
1998            }
1999            ModuleRepr::Foreign(_, Some((imported, _))) => Ok(imported.clone()),
2000            ModuleRepr::Foreign(geom, cached) => {
2001                let caller_artifacts = std::mem::take(&mut exec_state.mod_local.artifacts);
2002                let result = super::import::send_to_engine(geom.clone(), exec_state, self)
2003                    .await
2004                    .map(|geom| Some(KclValue::ImportedGeometry(geom)));
2005                let module_artifacts = std::mem::replace(&mut exec_state.mod_local.artifacts, caller_artifacts);
2006
2007                match result {
2008                    Ok(val) => {
2009                        *cached = Some((val.clone(), module_artifacts));
2010                        Ok(val)
2011                    }
2012                    Err(e) => {
2013                        // Preserve the failed command in the caller's error artifacts, matching
2014                        // the behavior before foreign module artifact states were isolated.
2015                        exec_state.mod_local.artifacts.extend(module_artifacts);
2016                        // Label the failure with the import so the backtrace
2017                        // names the foreign file, like KCL module failures do.
2018                        Err(e.add_import_location(&path.import_name(), source_range))
2019                    }
2020                }
2021            }
2022            ModuleRepr::Dummy => unreachable!(),
2023        };
2024
2025        exec_state.global.module_infos[&module_id].restore_repr(repr);
2026
2027        result
2028    }
2029
2030    pub async fn exec_module_from_ast(
2031        &self,
2032        program: &Node<Program>,
2033        module_id: ModuleId,
2034        path: &ModulePath,
2035        exec_state: &mut ExecState,
2036        source_range: SourceRange,
2037        preserve_mem: PreserveMem,
2038    ) -> Result<ModuleExecutionOutcome, KclError> {
2039        exec_state.global.mod_loader.enter_module(path);
2040        let result = self
2041            .exec_module_body(program, exec_state, preserve_mem, module_id, path)
2042            .await;
2043        exec_state.global.mod_loader.leave_module(path, source_range)?;
2044
2045        // TODO: ModuleArtifactState is getting dropped here when there's an
2046        // error.  Should we propagate it for non-root modules?
2047        result.map_err(|(err, _, _)| {
2048            match err {
2049                KclError::ImportCycle { .. } => {
2050                    // It was an import cycle.  Keep the original message.
2051                    err.override_source_ranges(vec![source_range])
2052                }
2053                // The module loaded successfully, so preserve execution errors
2054                // exactly as they occurred inside it. Rewrapping them here loses
2055                // the error kind, structured fields, and imported-file location.
2056                _ => err.add_import_location(&path.import_name(), source_range),
2057            }
2058        })
2059    }
2060
2061    /// Resolve a name to its value, running the module body first when the name
2062    /// refers to a module. Flat (module execution happens in its own fresh
2063    /// root); shared by both executors.
2064    pub(super) async fn resolve_name_for_eval(
2065        &self,
2066        name: &Node<Name>,
2067        metadata: &Metadata,
2068        exec_state: &mut ExecState,
2069    ) -> Result<KclValue, KclError> {
2070        let value = name.get_result(exec_state, self).await?;
2071        if let KclValue::Module { value: module_id, meta } = value {
2072            Ok(self
2073                .exec_module_for_result(module_id, exec_state, metadata.source_range)
2074                .await?
2075                .unwrap_or_else(|| {
2076                    exec_state.warn(
2077                        CompilationIssue::err(
2078                            metadata.source_range,
2079                            "Imported module has no return value. The last statement of the module must be an expression, usually the Solid.",
2080                        ),
2081                        annotations::WARN_MOD_RETURN_VALUE,
2082                    );
2083
2084                    let mut new_meta = vec![metadata.to_owned()];
2085                    new_meta.extend(meta);
2086                    KclValue::KclNone {
2087                        value: Default::default(),
2088                        meta: new_meta,
2089                    }
2090                }))
2091        } else {
2092            Ok(value)
2093        }
2094    }
2095
2096    #[async_recursion]
2097    pub(crate) async fn execute_expr<'a: 'async_recursion>(
2098        &self,
2099        init: &Expr,
2100        exec_state: &mut ExecState,
2101        metadata: &Metadata,
2102        annotations: &[Node<Annotation>],
2103        statement_kind: StatementKind<'a>,
2104    ) -> Result<KclValueControlFlow, KclError> {
2105        let item = match init {
2106            Expr::None(none) => KclValue::from(none).continue_(),
2107            Expr::Literal(literal) => KclValue::from_literal((**literal).clone(), exec_state).continue_(),
2108            Expr::TagDeclarator(tag) => tag.execute(exec_state).await?.continue_(),
2109            Expr::Name(name) => self
2110                .resolve_name_for_eval(name, metadata, exec_state)
2111                .await?
2112                .continue_(),
2113            Expr::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, self).await?,
2114            Expr::FunctionExpression(function_expression) => self
2115                .create_function_closure(function_expression, annotations, metadata, statement_kind, exec_state)
2116                .await?
2117                .continue_(),
2118            Expr::CallExpressionKw(call_expression) => call_expression.execute(exec_state, self).await?,
2119            Expr::PipeExpression(pipe_expression) => pipe_expression.get_result(exec_state, self).await?,
2120            Expr::PipeSubstitution(pipe_substitution) => match statement_kind {
2121                StatementKind::Declaration { name } => {
2122                    let message = format!(
2123                        "you cannot declare variable {name} as %, because % can only be used in function calls"
2124                    );
2125
2126                    return Err(KclError::new_semantic(KclErrorDetails::new(
2127                        message,
2128                        vec![pipe_substitution.into()],
2129                    )));
2130                }
2131                StatementKind::Expression => match exec_state.mod_local.pipe_value.clone() {
2132                    Some(x) => x.continue_(),
2133                    None => {
2134                        return Err(KclError::new_semantic(KclErrorDetails::new(
2135                            "cannot use % outside a pipe expression".to_owned(),
2136                            vec![pipe_substitution.into()],
2137                        )));
2138                    }
2139                },
2140            },
2141            Expr::ArrayExpression(array_expression) => array_expression.execute(exec_state, self).await?,
2142            Expr::ArrayRangeExpression(range_expression) => range_expression.execute(exec_state, self).await?,
2143            Expr::ObjectExpression(object_expression) => object_expression.execute(exec_state, self).await?,
2144            Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state, self).await?,
2145            Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
2146            Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?,
2147            Expr::LabelledExpression(expr) => {
2148                let value_cf = self
2149                    .execute_expr(&expr.expr, exec_state, metadata, &[], statement_kind)
2150                    .await?;
2151                let value = control_continue!(value_cf);
2152                exec_state
2153                    .mut_stack()
2154                    .add(expr.label.name.clone(), value.clone(), init.into())?;
2155                // TODO this lets us use the label as a variable name, but not as a tag in most cases
2156                value.continue_()
2157            }
2158            Expr::AscribedExpression(expr) => expr.get_result(exec_state, self).await?,
2159            Expr::SketchBlock(expr) => expr.get_result(exec_state, self).await?,
2160            Expr::SketchVar(expr) => expr.get_result(exec_state, self).await?.continue_(),
2161        };
2162        Ok(item)
2163    }
2164
2165    /// Evaluate a single expression on whichever executor is active, as a
2166    /// fresh root. For bounded internal evaluations (e.g. GD&T's constant
2167    /// plane lookup).
2168    pub(crate) async fn eval_expr_fresh_root(
2169        &self,
2170        expr: &Expr,
2171        exec_state: &mut ExecState,
2172        metadata: &Metadata,
2173    ) -> Result<KclValueControlFlow, KclError> {
2174        if self.is_machine_executor() {
2175            return crate::execution::machine::run_expr(self, expr, exec_state, metadata).await;
2176        }
2177        self.execute_expr(expr, exec_state, metadata, &[], StatementKind::Expression)
2178            .await
2179    }
2180
2181    /// Create the closure value for a function expression, including the
2182    /// recursive-closure placeholder fixup and binding a named `fn name() {}`
2183    /// in the current scope. Flat; shared by both executors.
2184    pub(super) async fn create_function_closure(
2185        &self,
2186        function_expression: &crate::parsing::ast::types::BoxNode<FunctionExpression>,
2187        annotations: &[Node<Annotation>],
2188        metadata: &Metadata,
2189        statement_kind: StatementKind<'_>,
2190        exec_state: &mut ExecState,
2191    ) -> Result<KclValue, KclError> {
2192        let attrs = annotations::get_fn_attrs(annotations, metadata.source_range)?;
2193        let experimental = attrs
2194            .as_ref()
2195            .map(|a| a.experimental)
2196            // Use the default for the field, not the bool type.
2197            .unwrap_or_else(|| FnAttrs::default().experimental);
2198
2199        // Check the KCL @(feature_tree = ) annotation.
2200        let include_in_feature_tree = attrs
2201            .as_ref()
2202            .map(|a| a.include_in_feature_tree)
2203            // Use the default for the field, not the bool type.
2204            .unwrap_or_else(|| FnAttrs::default().include_in_feature_tree);
2205        let (mut closure, placeholder_env_ref) = if let Some(attrs) = attrs
2206            && (attrs.impl_ == annotations::Impl::Rust
2207                || attrs.impl_ == annotations::Impl::RustConstrainable
2208                || attrs.impl_ == annotations::Impl::RustConstraint)
2209        {
2210            if let ModulePath::Std { value: std_path } = &exec_state.mod_local.path {
2211                let (func, props) = crate::std::std_fn(std_path, statement_kind.expect_name());
2212                (
2213                    KclValue::Function {
2214                        value: Box::new(FunctionSource::rust(func, function_expression.clone(), props, attrs)),
2215                        meta: vec![metadata.to_owned()],
2216                    },
2217                    None,
2218                )
2219            } else {
2220                return Err(KclError::new_semantic(KclErrorDetails::new(
2221                    "Rust implementation of functions is restricted to the standard library".to_owned(),
2222                    vec![metadata.source_range],
2223                )));
2224            }
2225        } else {
2226            let std_props = function_expression
2227                .name_str()
2228                .and_then(|name| exec_state.mod_local.path.build_std_fully_qualified_name(name))
2229                .map(|name| StdFnProps::default(&name));
2230            // Snapshotting memory here is crucial for semantics so that we close
2231            // over variables. Variables defined lexically later shouldn't
2232            // be available to the function body.
2233            let (env_ref, placeholder_env_ref) = if function_expression.name.is_some() {
2234                // Recursive function needs a snapshot that includes
2235                // itself.
2236                let dummy = EnvironmentRef::dummy();
2237                (dummy, Some(dummy))
2238            } else {
2239                (exec_state.mut_stack().snapshot()?, None)
2240            };
2241            (
2242                KclValue::Function {
2243                    value: Box::new(FunctionSource::kcl(
2244                        function_expression.clone(),
2245                        env_ref,
2246                        KclFunctionSourceParams {
2247                            std_props,
2248                            experimental,
2249                            include_in_feature_tree,
2250                        },
2251                    )),
2252                    meta: vec![metadata.to_owned()],
2253                },
2254                placeholder_env_ref,
2255            )
2256        };
2257
2258        // Resolve the signature's type names now, in the scope where
2259        // the declaration is written. Call sites consume the stored
2260        // resolutions and never look type names up themselves.
2261        if let KclValue::Function { value, .. } = &mut closure {
2262            value.resolve_signature_types(exec_state, self).await?;
2263        }
2264
2265        // If the function expression has a name, i.e. `fn name() {}`,
2266        // bind it in the current scope.
2267        if let Some(fn_name) = &function_expression.name {
2268            // If we used a placeholder env ref for recursion, fix it up
2269            // with the name recursively bound so that it's available in
2270            // the function body.
2271            if let Some(placeholder_env_ref) = placeholder_env_ref {
2272                closure = exec_state.mut_stack().add_recursive_closure(
2273                    fn_name.name.to_owned(),
2274                    closure,
2275                    placeholder_env_ref,
2276                    metadata.source_range,
2277                )?;
2278            } else {
2279                // Regular non-recursive binding.
2280                exec_state
2281                    .mut_stack()
2282                    .add(fn_name.name.clone(), closure.clone(), metadata.source_range)?;
2283            }
2284        }
2285
2286        Ok(closure)
2287    }
2288}
2289
2290/// Skip a declaration whose `added_in` the program predates, recording it under
2291/// `key` so a failed lookup can explain why. Shared by both executors. `key` is
2292/// only computed on a skip, so declarations without attributes cost nothing.
2293pub(super) fn skip_if_not_yet_added(
2294    annotations: &[Node<Annotation>],
2295    key: impl FnOnce() -> String,
2296    exported: bool,
2297    source_range: SourceRange,
2298    exec_state: &mut ExecState,
2299) -> Result<bool, KclError> {
2300    let Some(added_in) = annotations::added_in_version(annotations, source_range)? else {
2301        return Ok(false);
2302    };
2303    if annotations::version_ge(exec_state.entry_point_kcl_version().as_str(), &added_in) {
2304        return Ok(false);
2305    }
2306    exec_state.record_not_yet_added(key(), added_in, exported);
2307    Ok(true)
2308}
2309
2310/// The head of a `Color::Red` path is looked up both as a module and as an enum,
2311/// so one scope must not bind a module and an enum under the same name. Reporting
2312/// the clash where the second name is introduced keeps every `X::y` use site
2313/// unambiguous, so no check is needed at the use site.
2314///
2315/// Type aliases that resolve to enums participate in this rule because they can
2316/// head a `::` path. Other aliases and bare types may continue to share a name
2317/// with a module.
2318fn module_enum_clash(name: &str, source_range: SourceRange) -> KclError {
2319    KclError::new_semantic(KclErrorDetails::new(
2320        format!(
2321            "An enum and a module cannot share the name `{name}` in the same scope, because `{name}::x` would be ambiguous. Rename one of them."
2322        ),
2323        vec![source_range],
2324    ))
2325}
2326
2327/// Call before binding an enum under `name`.
2328fn reject_enum_clashing_with_module(
2329    exec_state: &ExecState,
2330    name: &str,
2331    source_range: SourceRange,
2332) -> Result<(), KclError> {
2333    let key = format!("{}{}", memory::MODULE_PREFIX, name);
2334    if !exec_state.stack().cur_frame_contains(&key)? {
2335        return Ok(());
2336    }
2337
2338    Err(module_enum_clash(name, source_range))
2339}
2340
2341/// Call before binding a module under `name`. The mirror of
2342/// [`reject_enum_clashing_with_module`].
2343fn reject_module_clashing_with_enum(
2344    exec_state: &ExecState,
2345    name: &str,
2346    source_range: SourceRange,
2347) -> Result<(), KclError> {
2348    let key = format!("{}{}", memory::TYPE_PREFIX, name);
2349    if !exec_state.stack().cur_frame_contains(&key)? {
2350        return Ok(());
2351    }
2352
2353    let Ok(KclValue::Type {
2354        value: TypeDef::Enum(_),
2355        ..
2356    }) = exec_state.stack().get(&key, source_range)
2357    else {
2358        return Ok(());
2359    };
2360
2361    Err(module_enum_clash(name, source_range))
2362}
2363
2364/// Builds the error for comparing values of two different enum types.
2365fn different_enums_err(left: &EnumValue, right: &EnumValue, source_range: SourceRange) -> KclError {
2366    let left_name = left.enum_id().declared_name();
2367    let right_name = right.enum_id().declared_name();
2368
2369    let message = if left_name == right_name {
2370        // Identity is the declaration, so two enums can share a name and still be
2371        // different types. Naming both would read as a mistake in the message.
2372        format!(
2373            "Cannot compare two different enums that are both named `{left_name}`. They come from separate declarations."
2374        )
2375    } else {
2376        format!("Cannot compare enum `{left_name}` with enum `{right_name}`. They are different types.")
2377    };
2378
2379    KclError::new_semantic(KclErrorDetails::new(message, vec![source_range]))
2380}
2381
2382/// Builds the error for a name that resolves to a type where a value is needed,
2383/// such as `x = Color`.
2384///
2385/// Returns `None` when no type of that name is in scope, which leaves the
2386/// caller's "is not defined" error in place.
2387fn type_used_as_value(exec_state: &ExecState, name: &Node<Identifier>) -> Option<KclError> {
2388    let key = format!("{}{}", memory::TYPE_PREFIX, name.name);
2389    let KclValue::Type { value: def, .. } = exec_state.stack().get(&key, name.as_source_range()).ok()? else {
2390        return None;
2391    };
2392
2393    // The suggestion uses the name as written, which may be an import alias, so
2394    // that it can be pasted into the file that produced the error.
2395    let suggestion = match &def {
2396        TypeDef::Enum(def) => def
2397            .variants()
2398            .first()
2399            .map(|variant| format!(" Use one of its variants, such as `{}::{variant}`.", name.name))
2400            .unwrap_or_default(),
2401        _ => String::new(),
2402    };
2403
2404    Some(KclError::new_semantic(KclErrorDetails::new(
2405        format!("`{}` is a type, not a value.{suggestion}", name.name),
2406        name.as_source_ranges(),
2407    )))
2408}
2409
2410enum EnumPathHead {
2411    Enum {
2412        def: Arc<EnumTypeDef>,
2413        binding_experimental: bool,
2414    },
2415    NonEnumType,
2416}
2417
2418/// Classifies the type named by a `::` path segment: `Color` in both
2419/// `Color::Red` and `colors::Color::Red`.
2420///
2421/// A non-enum type is retained until module lookup has also failed because a
2422/// type alias and a module may share a name. The module remains the valid path
2423/// head in that case.
2424fn enum_named_by_segment(
2425    exec_state: &ExecState,
2426    segment: &Node<Identifier>,
2427    within: Option<&ModuleItems>,
2428) -> Option<EnumPathHead> {
2429    match type_value_named_by_segment(exec_state, segment, within)? {
2430        KclValue::Type {
2431            value: TypeDef::Enum(def),
2432            experimental,
2433            ..
2434        } => Some(EnumPathHead::Enum {
2435            def,
2436            binding_experimental: experimental,
2437        }),
2438        KclValue::Type { .. } => Some(EnumPathHead::NonEnumType),
2439        _ => None,
2440    }
2441}
2442
2443fn non_enum_type_in_path(segment: &Node<Identifier>) -> KclError {
2444    KclError::new_semantic(KclErrorDetails::new(
2445        format!(
2446            "`{}` is a type that does not resolve to an enum, so it cannot be used as the head of a `::` path.",
2447            segment.name
2448        ),
2449        segment.as_source_ranges(),
2450    ))
2451}
2452
2453/// `Red` in `Color::Red`.
2454fn enum_variant_value(
2455    def: Arc<EnumTypeDef>,
2456    binding_experimental: bool,
2457    variant: &Node<Identifier>,
2458    exec_state: &mut ExecState,
2459) -> Result<KclValue, KclError> {
2460    let enum_name = def.id().declared_name();
2461
2462    if !def.has_variant(&variant.name) {
2463        let known = if def.variants().is_empty() {
2464            format!("Enum `{enum_name}` has no variants")
2465        } else {
2466            format!("Its variants are: {}", def.variants().join(", "))
2467        };
2468
2469        return Err(KclError::new_semantic(KclErrorDetails::new(
2470            format!("`{}` is not a variant of enum `{enum_name}`. {known}.", variant.name),
2471            variant.as_source_ranges(),
2472        )));
2473    }
2474
2475    // Older KCL versions report every enum use. In V3, an explicit experimental
2476    // annotation on either the declaration or the binding still applies.
2477    if !exec_state.entry_point_version_is_v3_or_higher() || def.is_experimental() || binding_experimental {
2478        exec_state.warn_experimental(&format!("the enum `{enum_name}`"), variant.as_source_range());
2479    }
2480
2481    // The value holds the declaration itself, so its identity is read off that
2482    // declaration and can never be rebuilt from a name. A later enum v2 adding a
2483    // declaration site to `EnumTypeId` therefore changes nothing here.
2484    Ok(KclValue::Enum {
2485        value: Box::new(EnumValue::new(
2486            def,
2487            variant.name.clone(),
2488            vec![Metadata {
2489                source_range: variant.as_source_range(),
2490            }],
2491        )),
2492    })
2493}
2494
2495/// Glob imports copy exported keys verbatim, prefix included, so which namespace
2496/// an incoming key lands in has to be read back off the key itself.
2497fn reject_glob_import_clash(
2498    exec_state: &ExecState,
2499    key: &str,
2500    item: &KclValue,
2501    source_range: SourceRange,
2502) -> Result<(), KclError> {
2503    if let Some(name) = key.strip_prefix(memory::MODULE_PREFIX) {
2504        return reject_module_clashing_with_enum(exec_state, name, source_range);
2505    }
2506
2507    if let Some(name) = key.strip_prefix(memory::TYPE_PREFIX)
2508        && matches!(
2509            item,
2510            KclValue::Type {
2511                value: TypeDef::Enum(_),
2512                ..
2513            }
2514        )
2515    {
2516        return reject_enum_clashing_with_module(exec_state, name, source_range);
2517    }
2518
2519    Ok(())
2520}
2521
2522/// When executing in sketch mode, whether we should skip executing this
2523/// expression.
2524pub(super) fn sketch_mode_should_skip(expr: &Expr) -> bool {
2525    fn contains_edited_sketch_block(node: crate::walk::Node<'_>) -> bool {
2526        if let crate::walk::Node::SketchBlock(sketch_block) = node {
2527            return sketch_block.is_being_edited;
2528        }
2529        node.children().into_iter().any(contains_edited_sketch_block)
2530    }
2531
2532    !contains_edited_sketch_block(expr.into())
2533}
2534
2535/// If the error is about an undefined name, and that name matches the name being defined,
2536/// make the error message more specific.
2537fn var_in_own_ref_err(e: KclError, being_declared: &Option<String>) -> KclError {
2538    let KclError::UndefinedValue { name, mut details } = e else {
2539        return e;
2540    };
2541    // TODO after June 26th: replace this with a let-chain,
2542    // which will be available in Rust 1.88
2543    // https://rust-lang.github.io/rfcs/2497-if-let-chains.html
2544    if let (Some(name0), Some(name1)) = (&being_declared, &name)
2545        && name0 == name1
2546    {
2547        details.message = format!(
2548            "You can't use `{name0}` because you're currently trying to define it. Use a different variable here instead."
2549        );
2550    }
2551    KclError::UndefinedValue { details, name }
2552}
2553
2554impl Node<AscribedExpression> {
2555    #[async_recursion]
2556    pub(super) async fn get_result(
2557        &self,
2558        exec_state: &mut ExecState,
2559        ctx: &ExecutorContext,
2560    ) -> Result<KclValueControlFlow, KclError> {
2561        let metadata = Metadata {
2562            source_range: SourceRange::from(self),
2563        };
2564        let result = ctx
2565            .execute_expr(&self.expr, exec_state, &metadata, &[], StatementKind::Expression)
2566            .await?;
2567        let result = control_continue!(result);
2568        apply_ascription(&result, &self.ty, exec_state, ctx, self.into())
2569            .await
2570            .map(KclValue::continue_)
2571    }
2572}
2573
2574impl Node<SketchBlock> {
2575    pub(super) async fn get_result(
2576        &self,
2577        exec_state: &mut ExecState,
2578        ctx: &ExecutorContext,
2579    ) -> Result<KclValueControlFlow, KclError> {
2580        if exec_state.mod_local.sketch_block.is_some() {
2581            // Disallow nested sketch blocks for now.
2582            return Err(KclError::new_semantic(KclErrorDetails::new(
2583                "Cannot execute a sketch block from within another sketch block".to_owned(),
2584                vec![SourceRange::from(self)],
2585            )));
2586        }
2587
2588        let range = SourceRange::from(self);
2589
2590        // Evaluate arguments.
2591        let (sketch_id, sketch_surface) = match self.exec_arguments(exec_state, ctx).await {
2592            Ok(x) => x,
2593            Err(cf_error) => match cf_error {
2594                // Control flow needs to return early.
2595                EarlyReturn::Value(cf_value) => return Ok(cf_value),
2596                EarlyReturn::Error(err) => return Err(err),
2597            },
2598        };
2599        let sketch_block_artifact_id = self.scene_setup(sketch_id, &sketch_surface, exec_state)?;
2600
2601        let (return_result, variables, sketch_block_state) = {
2602            // Don't early return until the stack frame is popped!
2603            self.prep_mem(exec_state.mut_stack().snapshot()?, exec_state)?;
2604
2605            // Track that we're executing a sketch block.
2606            let initial_sketch_block_state = {
2607                SketchBlockState {
2608                    sketch_id: Some(sketch_id),
2609                    ..Default::default()
2610                }
2611            };
2612
2613            let original_value = exec_state.mod_local.sketch_block.replace(initial_sketch_block_state);
2614
2615            // When executing the body of the sketch block, we no longer want to
2616            // skip any code.
2617            let original_sketch_mode = std::mem::replace(&mut exec_state.mod_local.sketch_mode, false);
2618
2619            // Load `sketch2::*` into the sketch block's parent scope, so calls
2620            // like `line(...)` resolve to sketch2 functions. Then execute the
2621            // user body in a child scope, so these aliases aren't included in
2622            // the returned sketch object.
2623            let (result, block_variables) = match self.load_sketch2_into_current_scope(exec_state, ctx, range).await {
2624                Ok(()) => {
2625                    let parent = exec_state.mut_stack().snapshot()?;
2626                    exec_state.mut_stack().push_new_env_for_call(parent)?;
2627                    let result = ctx.exec_block(&self.body, exec_state, BodyType::Block).await;
2628                    let (result, block_variables) = match exec_state.stack().find_all_in_current_env() {
2629                        Ok(block_variables) => (result, block_variables.into_iter().collect::<IndexMap<_, _>>()),
2630                        Err(err) => (Err(err), IndexMap::new()),
2631                    };
2632                    let result = match exec_state.mut_stack().pop_env() {
2633                        Ok(_) => result,
2634                        Err(err) => Err(err),
2635                    };
2636                    (result, block_variables)
2637                }
2638                Err(err) => (Err(err), IndexMap::new()),
2639            };
2640
2641            exec_state.mod_local.sketch_mode = original_sketch_mode;
2642
2643            let sketch_block_state = std::mem::replace(&mut exec_state.mod_local.sketch_block, original_value);
2644
2645            // Pop the scope used for sketch2 aliases.
2646            let result = match exec_state.mut_stack().pop_env() {
2647                Ok(_) => result,
2648                Err(err) => Err(err),
2649            };
2650
2651            (result, block_variables, sketch_block_state)
2652        };
2653
2654        // Propagate errors.
2655        let return_control_flow = return_result?;
2656        // If the sketch block body exited early (e.g. via `exit()`), propagate
2657        // the exit so that the rest of the program terminates instead of only
2658        // ending this sketch block. Without this, execution would fall through,
2659        // solve the partial sketch, and continue on to later statements.
2660        if let Some(control_flow) = return_control_flow
2661            && control_flow.is_some_return()
2662        {
2663            // Balance the GroupBegin operation pushed above so the feature tree
2664            // stays well-formed.
2665            exec_state.push_op(Operation::GroupEnd);
2666            return Ok(control_flow);
2667        }
2668        let Some(sketch_block_state) = sketch_block_state else {
2669            debug_assert!(false, "Sketch block state should still be set to Some from just above");
2670            return Err(internal_err(
2671                "Sketch block state should still be set to Some from just above",
2672                self,
2673            ));
2674        };
2675        let return_value = self
2676            .finalize_sketch_block(
2677                sketch_id,
2678                &sketch_surface,
2679                sketch_block_artifact_id,
2680                variables,
2681                sketch_block_state,
2682                exec_state,
2683                ctx,
2684            )
2685            .await?;
2686        Ok(if self.is_being_edited {
2687            // When the sketch block is being edited, we exit the program
2688            // immediately.
2689            return_value.exit()
2690        } else {
2691            return_value.continue_()
2692        })
2693    }
2694
2695    /// Executes the arguments of the sketch block and returns the sketch ID and
2696    /// surface. The surface is the `on` argument, which is basically a Plane or
2697    /// Face.
2698    ///
2699    /// In sketch mode, the execution cache is used to look up the sketch
2700    /// surface.
2701    ///
2702    /// The sketch ID is generated in either case so that it's stable. But only
2703    /// a placeholder scene object is created for it.
2704    async fn exec_arguments(
2705        &self,
2706        exec_state: &mut ExecState,
2707        ctx: &ExecutorContext,
2708    ) -> Result<(ObjectId, SketchSurface), EarlyReturn> {
2709        if !exec_state.sketch_mode() {
2710            // Evaluate arguments.
2711            //
2712            // Sketch mode only executes the sketch block body. Arguments must
2713            // be evaluated in engine execution so that things like Planes and
2714            // Faces can be created in the engine.
2715            let mut labeled = IndexMap::new();
2716            for labeled_arg in &self.arguments {
2717                let source_range = SourceRange::from(labeled_arg.arg.clone());
2718                let metadata = Metadata { source_range };
2719                let value_cf = ctx
2720                    .execute_expr(&labeled_arg.arg, exec_state, &metadata, &[], StatementKind::Expression)
2721                    .await?;
2722                let value = early_return!(value_cf);
2723                let arg = Arg::new(value, source_range);
2724                match &labeled_arg.label {
2725                    Some(label) => {
2726                        labeled.insert(label.name.clone(), arg);
2727                    }
2728                    None => {
2729                        let name = labeled_arg.arg.ident_name();
2730                        if let Some(name) = name {
2731                            labeled.insert(name.to_owned(), arg);
2732                        } else {
2733                            return Err(KclError::new_semantic(KclErrorDetails::new(
2734                                "Arguments to sketch blocks must be either labeled or simple identifiers".to_owned(),
2735                                vec![SourceRange::from(&labeled_arg.arg)],
2736                            ))
2737                            .into());
2738                        }
2739                    }
2740                }
2741            }
2742            self.finish_arguments_after_eval(labeled, exec_state, ctx).await
2743        } else {
2744            self.arguments_from_cache(exec_state)
2745        }
2746    }
2747
2748    /// The evaluation-free tail of sketch-block argument handling: validate
2749    /// the arguments, resolve the `on` surface, ensure it exists in the
2750    /// engine, and mint the stable sketch id. Shared by both executors.
2751    pub(super) async fn finish_arguments_after_eval(
2752        &self,
2753        labeled: IndexMap<String, Arg>,
2754        exec_state: &mut ExecState,
2755        ctx: &ExecutorContext,
2756    ) -> Result<(ObjectId, SketchSurface), EarlyReturn> {
2757        let range = SourceRange::from(self);
2758        let mut args = Args::new_no_args(
2759            range,
2760            self.node_path.clone(),
2761            ctx.clone(),
2762            Some(SketchBlock::CALLEE_NAME.to_owned()),
2763        );
2764        args.labeled = labeled;
2765
2766        // Report any arguments that aren't valid sketch block parameters.
2767        // This is non-fatal so that the rest of the block still executes,
2768        // matching how unexpected keyword arguments are handled for
2769        // function calls.
2770        //
2771        // Checking arguments should be done after evaluating them, the same
2772        // order as if we were calling a function.
2773        self.check_for_unexpected_arguments(&args, exec_state)?;
2774
2775        let arg_on_value: KclValue =
2776            args.get_kw_arg(SKETCH_BLOCK_PARAM_ON, &RuntimeType::sketch_or_surface(), exec_state)?;
2777
2778        let Some(arg_on) = SketchOrSurface::from_kcl_val(&arg_on_value) else {
2779            let message = "The `on` argument to a sketch block must be convertible to a sketch or surface.".to_owned();
2780            debug_assert!(false, "{message}");
2781            return Err(KclError::new_semantic(KclErrorDetails::new(message, vec![range])).into());
2782        };
2783        let mut sketch_surface = arg_on.into_sketch_surface();
2784
2785        // Ensure that the plane has an ObjectId. Always create an Object so
2786        // that we're consistent with IDs.
2787        match &mut sketch_surface {
2788            SketchSurface::Plane(plane) => {
2789                // Ensure that it's been created in the engine.
2790                ensure_sketch_plane_in_engine(plane, exec_state, ctx, range, self.node_path.clone()).await?;
2791            }
2792            SketchSurface::Face(_) => {
2793                // All faces should already be created in the engine.
2794            }
2795        }
2796
2797        // Generate an ID for the sketch block. This must be done after
2798        // arguments so that we get the same result when the arguments are
2799        // cached. This must be done before the sketch block body so that no
2800        // matter how many IDs are generated due to objects in the body, the
2801        // sketch ID is always stable.
2802        let sketch_id = exec_state.next_object_id();
2803        exec_state.add_placeholder_scene_object(sketch_id, range, self.node_path.clone());
2804        let on_cache_name = sketch_on_cache_name(sketch_id);
2805        // Store in memory so that it's cached.
2806        exec_state.mut_stack().add(on_cache_name, arg_on_value, range)?;
2807
2808        Ok((sketch_id, sketch_surface))
2809    }
2810
2811    /// Sketch-mode path: arguments cannot be re-evaluated, so look the `on`
2812    /// surface up from the execution cache. Flat; shared by both executors.
2813    pub(super) fn arguments_from_cache(
2814        &self,
2815        exec_state: &mut ExecState,
2816    ) -> Result<(ObjectId, SketchSurface), EarlyReturn> {
2817        let range = SourceRange::from(self);
2818        {
2819            // In sketch mode, we can't re-evaluate arguments. Instead, look
2820            // them up from cache.
2821
2822            // Generate an ID for the sketch block. This must be done before the
2823            // sketch block body so that no matter how many IDs are generated
2824            // due to objects in the body, the sketch ID is always stable.
2825            let sketch_id = exec_state.next_object_id();
2826            exec_state.add_placeholder_scene_object(sketch_id, range, self.node_path.clone());
2827            let on_cache_name = sketch_on_cache_name(sketch_id);
2828            let arg_on_value = exec_state.stack().get_owned(&on_cache_name, range)?;
2829
2830            let Some(arg_on) = SketchOrSurface::from_kcl_val(&arg_on_value) else {
2831                let message =
2832                    "The `on` argument to a sketch block must be convertible to a sketch or surface.".to_owned();
2833                debug_assert!(false, "{message}");
2834                return Err(KclError::new_semantic(KclErrorDetails::new(message, vec![range])).into());
2835            };
2836            let mut sketch_surface = arg_on.into_sketch_surface();
2837
2838            // Ensure that the plane has an ObjectId. Always create an Object so
2839            // that we're consistent with IDs.
2840            if sketch_surface.object_id().is_none() {
2841                // Look up the last object. Since this is where we would have
2842                // created it in real execution, it will be the last object.
2843                let Some(last_object) = exec_state.mod_local.artifacts.scene_objects.last() else {
2844                    return Err(internal_err(
2845                        "In sketch mode, the `on` plane argument must refer to an existing plane object.",
2846                        range,
2847                    )
2848                    .into());
2849                };
2850                sketch_surface.set_object_id(last_object.id);
2851            }
2852
2853            Ok((sketch_id, sketch_surface))
2854        }
2855    }
2856
2857    /// Create the sketch scene object and sketch-block artifact, and open
2858    /// the feature-tree group. Flat; shared by both executors.
2859    pub(super) fn scene_setup(
2860        &self,
2861        sketch_id: ObjectId,
2862        sketch_surface: &SketchSurface,
2863        exec_state: &mut ExecState,
2864    ) -> Result<ArtifactId, KclError> {
2865        let range = SourceRange::from(self);
2866        let on_object_id = if let Some(object_id) = sketch_surface.object_id() {
2867            object_id
2868        } else {
2869            let message = "The `on` argument should have an object after ensure_sketch_plane_in_engine".to_owned();
2870            debug_assert!(false, "{message}");
2871            return Err(internal_err(message, range));
2872        };
2873        let sketch_ctor_on = sketch_on_frontend_plane(&self.arguments, on_object_id);
2874        let sketch_block_artifact_id = {
2875            use crate::execution::CodeRef;
2876            use crate::execution::SketchBlock;
2877            use crate::front::Plane;
2878            use crate::front::SourceRef;
2879
2880            let on_object = exec_state.mod_local.artifacts.scene_object_by_id(on_object_id);
2881
2882            // Get the plane artifact ID so that we can do an exclusive borrow.
2883            let plane_artifact_id = on_object.map(|object| object.artifact_id);
2884            let plane_info = match &sketch_surface {
2885                SketchSurface::Plane(plane) => Some(super::artifact::artifact_plane_info(&plane.info)),
2886                SketchSurface::Face(_) => None,
2887            };
2888
2889            let standard_plane = match &sketch_ctor_on {
2890                Plane::Default(plane) => Some(*plane),
2891                Plane::Object(_) | Plane::PrimitiveFace(_) => None,
2892            };
2893
2894            let artifact_id = ArtifactId::from(exec_state.next_uuid());
2895            // Label the sketch with the name of the variable whose right-hand
2896            // side is being evaluated. This function runs before the sketch
2897            // body executes, so for `mySketch = sketch(on = XY) { ... }`,
2898            // `being_declared` still holds `mySketch` rather than a name
2899            // declared inside the body.
2900            //
2901            // The label is the nearest enclosing declaration, which is not
2902            // always the sketch's own name:
2903            // - `part = extrude(sketch(on = XY) { ... }, length = 10)` labels
2904            //   the sketch `part`.
2905            // - A sketch constructed inside a function body and returned takes
2906            //   the name of the declaration in progress at the call site, so
2907            //   two calls to that function can produce the same label.
2908            // - A sketch written as an expression statement has no declaration
2909            //   in progress, so its label is the empty string.
2910            let label = exec_state.mod_local.being_declared.clone().unwrap_or_default();
2911            // Create the sketch scene object and replace its placeholder.
2912            let sketch_scene_object = Object {
2913                id: sketch_id,
2914                kind: ObjectKind::Sketch(crate::frontend::sketch::Sketch {
2915                    args: crate::front::SketchCtor { on: sketch_ctor_on },
2916                    plane: on_object_id,
2917                    segments: Default::default(),
2918                    constraints: Default::default(),
2919                }),
2920                label,
2921                comments: Default::default(),
2922                artifact_id,
2923                source: SourceRef::new(self.into(), self.node_path.clone()),
2924            };
2925            exec_state.set_scene_object(sketch_scene_object);
2926
2927            // Create and add the sketch block artifact.
2928            exec_state.add_artifact(Artifact::SketchBlock(SketchBlock {
2929                id: artifact_id,
2930                standard_plane,
2931                plane_id: plane_artifact_id,
2932                plane_info,
2933                // Fill this in later once we create the path. We can't just add
2934                // the artifact later because order relative to constraint
2935                // artifacts is significant.
2936                path_id: None,
2937                code_ref: CodeRef::placeholder(range),
2938                sketch_id,
2939            }));
2940
2941            exec_state.push_op(Operation::GroupBegin {
2942                group: Group::SketchBlock { sketch_id },
2943                node_path: NodePath::placeholder(),
2944                source_range: range,
2945            });
2946            artifact_id
2947        };
2948        Ok(sketch_block_artifact_id)
2949    }
2950
2951    /// Solve constraints, send segments to the engine, update artifacts and
2952    /// scene objects, close the feature-tree group, and build the sketch's
2953    /// result object. Flat (the body has already executed); shared by both
2954    /// executors.
2955    #[allow(clippy::too_many_arguments)]
2956    pub(super) async fn finalize_sketch_block(
2957        &self,
2958        sketch_id: ObjectId,
2959        sketch_surface: &SketchSurface,
2960        sketch_block_artifact_id: ArtifactId,
2961        variables: IndexMap<String, KclValue>,
2962        mut sketch_block_state: SketchBlockState,
2963        exec_state: &mut ExecState,
2964        ctx: &ExecutorContext,
2965    ) -> Result<KclValue, KclError> {
2966        let range = SourceRange::from(self);
2967        // Translate sketch variables and constraints to solver input.
2968        let constraints = sketch_block_state
2969            .solver_constraints
2970            .iter()
2971            .cloned()
2972            .map(ezpz::ConstraintRequest::highest_priority)
2973            .chain(
2974                // Optional constraints have a lower priority.
2975                sketch_block_state
2976                    .solver_optional_constraints
2977                    .iter()
2978                    .cloned()
2979                    .map(|c| ezpz::ConstraintRequest::new(c, 1)),
2980            )
2981            .collect::<Vec<_>>();
2982        let initial_guesses = sketch_block_state
2983            .sketch_vars
2984            .iter()
2985            .map(|v| {
2986                let Some(sketch_var) = v.as_sketch_var() else {
2987                    return Err(internal_err("Expected sketch variable", self));
2988                };
2989                let constraint_id = sketch_var.id.to_constraint_id(range)?;
2990                // Normalize units.
2991                let number_value = KclValue::Number {
2992                    value: sketch_var.initial_value,
2993                    ty: sketch_var.ty,
2994                    meta: sketch_var.meta.clone(),
2995                };
2996                let initial_guess_value = normalize_to_solver_distance_unit(
2997                    &number_value,
2998                    v.into(),
2999                    exec_state,
3000                    "sketch variable initial value",
3001                )?;
3002                let initial_guess = if let Some(n) = initial_guess_value.as_ty_f64() {
3003                    n.n
3004                } else {
3005                    let message = format!(
3006                        "Expected number after coercion, but found {}",
3007                        initial_guess_value.human_friendly_type()
3008                    );
3009                    debug_assert!(false, "{}", &message);
3010                    return Err(internal_err(message, self));
3011                };
3012                Ok((constraint_id, initial_guess))
3013            })
3014            .collect::<Result<Vec<_>, KclError>>()?;
3015        // Solve constraints.
3016        let config = ezpz::Config::default()
3017            .with_max_iterations(50)
3018            .with_convergence_tolerance(SOLVER_CONVERGENCE_TOLERANCE);
3019        let solve_result = if exec_state.mod_local.freedom_analysis {
3020            ezpz::solve_analysis(&constraints, initial_guesses.clone(), config).map(|outcome| {
3021                let freedom_analysis = FreedomAnalysis::from_ezpz_analysis(outcome.analysis, constraints.len());
3022                (outcome.outcome, Some(freedom_analysis))
3023            })
3024        } else {
3025            ezpz::solve(&constraints, initial_guesses.clone(), config).map(|outcome| (outcome, None))
3026        };
3027        // Build a combined list of all constraints (regular + optional) for conflict detection
3028        let num_required_constraints = sketch_block_state.solver_constraints.len();
3029        let all_constraints: Vec<ezpz::Constraint> = sketch_block_state
3030            .solver_constraints
3031            .iter()
3032            .cloned()
3033            .chain(sketch_block_state.solver_optional_constraints.iter().cloned())
3034            .collect();
3035
3036        let (solve_outcome, solve_analysis) = match solve_result {
3037            Ok((solved, freedom)) => {
3038                if solved
3039                    .final_values()
3040                    .iter()
3041                    .any(|number| number.is_infinite() || number.is_nan())
3042                {
3043                    return Err(KclError::new_internal(KclErrorDetails::new(
3044                        "KCL's 2D constraint solver returned an invalid number".to_owned(),
3045                        vec![SourceRange::from(self)],
3046                    )));
3047                }
3048                let outcome = Solved::from_ezpz_outcome(solved, &all_constraints, num_required_constraints);
3049                if !outcome.converged {
3050                    exec_state.warn(
3051                        CompilationIssue::err(range, "Constraint solver failed to find a solution".to_owned()),
3052                        annotations::WARN_SOLVER,
3053                    );
3054                }
3055                (outcome, freedom)
3056            }
3057            Err(failure) => {
3058                match &failure.error {
3059                    NonLinearSystemError::FaerMatrix { .. }
3060                    | NonLinearSystemError::Faer { .. }
3061                    | NonLinearSystemError::FaerSolve { .. }
3062                    | NonLinearSystemError::FaerSvd(..) => {
3063                        // Constraint solver failed to find a solution. Build a
3064                        // solution that is the initial guesses.
3065                        exec_state.warn(
3066                            CompilationIssue::err(range, "Internal error in constraint solver".to_owned()),
3067                            annotations::WARN_SOLVER,
3068                        );
3069                        let final_values = initial_guesses.iter().map(|(_, v)| *v).collect::<Vec<_>>();
3070                        (
3071                            Solved {
3072                                final_values,
3073                                iterations: Default::default(),
3074                                warnings: failure.warnings,
3075                                priority_solved: Default::default(),
3076                                variables_in_conflicts: Default::default(),
3077                                unsatisfied_directional_constraints: Default::default(),
3078                                converged: false,
3079                            },
3080                            None,
3081                        )
3082                    }
3083                    NonLinearSystemError::EmptySystemNotAllowed
3084                    | NonLinearSystemError::WrongNumberGuesses { .. }
3085                    | NonLinearSystemError::MissingGuess { .. }
3086                    | NonLinearSystemError::NotFound(..) => {
3087                        // These indicate something's gone wrong in KCL or ezpz,
3088                        // it's not a user error. We should investigate this.
3089                        #[cfg(target_arch = "wasm32")]
3090                        web_sys::console::error_1(
3091                            &format!("Internal error from constraint solver: {}", failure.error).into(),
3092                        );
3093                        return Err(internal_err(
3094                            format!("Internal error from constraint solver: {}", failure.error),
3095                            self,
3096                        ));
3097                    }
3098                    _ => {
3099                        // Catch all error case so that it's not a breaking change to publish new errors.
3100                        return Err(internal_err(
3101                            format!("Error from constraint solver: {}", failure.error),
3102                            self,
3103                        ));
3104                    }
3105                }
3106            }
3107        };
3108        // Propagate warnings.
3109        for warning in &solve_outcome.warnings {
3110            let message = if let Some(index) = warning.about_constraint.as_ref() {
3111                format!("{}; constraint index {}", warning.content, index)
3112            } else {
3113                format!("{}", warning.content)
3114            };
3115            exec_state.warn(CompilationIssue::err(range, message), annotations::WARN_SOLVER);
3116        }
3117        if solve_outcome.converged {
3118            exec_state.mod_local.artifacts.refactor_metadata.extend(
3119                sketch_block_state
3120                    .pending_legacy_angle_refactor_metadata
3121                    .iter()
3122                    .filter_map(|pending| {
3123                        finalize_legacy_angle_refactor_meta(pending, &solve_outcome.final_values)
3124                            .map(RefactorMetadata::LegacyAngle)
3125                    }),
3126            );
3127        }
3128        // Substitute solutions back into sketch variables.
3129        let sketch_engine_id = exec_state.next_uuid();
3130        let solution_ty = solver_numeric_type(exec_state);
3131        let mut solved_segments = Vec::with_capacity(sketch_block_state.needed_by_engine.len());
3132        for unsolved_segment in &sketch_block_state.needed_by_engine {
3133            solved_segments.push(substitute_sketch_var_in_segment(
3134                unsolved_segment.clone(),
3135                sketch_surface,
3136                sketch_engine_id,
3137                None,
3138                &solve_outcome,
3139                solver_numeric_type(exec_state),
3140                solve_analysis.as_ref(),
3141            )?);
3142        }
3143        // Store variable solutions so that the sketch refactoring API can
3144        // write them back to the source. When editing a sketch block, we
3145        // exit early so that the sketch block that we're editing is always
3146        // the last one. Therefore, we should overwrite any previous
3147        // solutions.
3148        exec_state.mod_local.artifacts.var_solutions =
3149            sketch_block_state.var_solutions(&solve_outcome, solution_ty, SourceRange::from(self))?;
3150
3151        // Create scene objects after unknowns are solved.
3152        let scene_objects = create_segment_scene_objects(&solved_segments, range, exec_state)?;
3153
3154        // Build the sketch and send everything to the engine.
3155        let sketch = create_segments_in_engine(
3156            sketch_surface,
3157            sketch_engine_id,
3158            &mut solved_segments,
3159            &sketch_block_state.segment_tags,
3160            ctx,
3161            exec_state,
3162            range,
3163        )
3164        .await?;
3165
3166        // We now have enough information to fill in the path.
3167        if let Some(sketch_artifact_id) = sketch.as_ref().map(|s| s.artifact_id) {
3168            if let Some(Artifact::SketchBlock(sketch_block_artifact)) =
3169                exec_state.artifact_mut(sketch_block_artifact_id)
3170            {
3171                sketch_block_artifact.path_id = Some(sketch_artifact_id);
3172            } else {
3173                let message = "Sketch block artifact not found, so path couldn't be linked to it".to_owned();
3174                debug_assert!(false, "{message}");
3175                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
3176            }
3177        }
3178
3179        // Substitute solutions back into sketch variables. This time, collect
3180        // all the variables in the sketch block. The set of variables may have
3181        // overlap with the objects sent to the engine, but it isn't necessarily
3182        // the same.
3183        let variables = substitute_sketch_vars(
3184            variables,
3185            sketch_surface,
3186            sketch_engine_id,
3187            sketch.as_ref(),
3188            &solve_outcome,
3189            solution_ty,
3190            solve_analysis.as_ref(),
3191        )?;
3192
3193        let mut segment_object_ids = Vec::with_capacity(scene_objects.len());
3194        for scene_object in scene_objects {
3195            segment_object_ids.push(scene_object.id);
3196            // Fill in placeholder scene objects.
3197            exec_state.set_scene_object(scene_object);
3198        }
3199        // Update the sketch scene object with the segments.
3200        let Some(sketch_object) = exec_state.mod_local.artifacts.scene_object_by_id_mut(sketch_id) else {
3201            let message = format!("Sketch object not found after it was just created; id={:?}", sketch_id);
3202            debug_assert!(false, "{}", &message);
3203            return Err(internal_err(message, range));
3204        };
3205        let ObjectKind::Sketch(front_sketch) = &mut sketch_object.kind else {
3206            let message = format!(
3207                "Expected Sketch object after it was just created to be a sketch kind; id={:?}, actual={:?}",
3208                sketch_id, sketch_object
3209            );
3210            debug_assert!(
3211                false,
3212                "{}; scene_objects={:#?}",
3213                message, exec_state.mod_local.artifacts.scene_objects
3214            );
3215            return Err(internal_err(message, range));
3216        };
3217        front_sketch.segments.extend(segment_object_ids);
3218        // Update the sketch scene object with constraints.
3219        front_sketch
3220            .constraints
3221            .extend(std::mem::take(&mut sketch_block_state.sketch_constraints));
3222
3223        // Close the sketch block operation group.
3224        exec_state.push_op(Operation::GroupEnd);
3225
3226        // Warn if the sketch has conflicting constraints. Skip this when
3227        // freedom analysis didn't run (e.g., during dragging), because the
3228        // freedom values on points are stale defaults in that case.
3229        if exec_state.mod_local.freedom_analysis {
3230            let status = {
3231                let scene_objects = &exec_state.mod_local.artifacts.scene_objects;
3232                scene_objects
3233                    .get(sketch_id.0)
3234                    .and_then(|obj| sketch_constraint_status_for_sketch(scene_objects, obj))
3235            };
3236            if let Some(status) = status
3237                && status.status == ConstraintKind::OverConstrained
3238            {
3239                let description = if status.conflict_count == 1 {
3240                    "segment has"
3241                } else {
3242                    "segments have"
3243                };
3244                let message = format!(
3245                    "Sketch is over-constrained: {} {description} conflicting constraints.{}",
3246                    status.conflict_count,
3247                    signed_distance_conflict_hint(&solve_outcome),
3248                );
3249                exec_state.warn(
3250                    CompilationIssue::err(range, message),
3251                    annotations::WARN_OVER_CONSTRAINED_SKETCH,
3252                );
3253            }
3254        }
3255
3256        let properties = self.sketch_properties(sketch, variables);
3257        let metadata = Metadata {
3258            source_range: SourceRange::from(self),
3259        };
3260        let return_value = KclValue::Object {
3261            value: properties,
3262            constrainable: Default::default(),
3263            object_kind: KclObjectKind::Default,
3264            meta: vec![metadata],
3265        };
3266        Ok(return_value)
3267    }
3268
3269    /// Report a non-fatal error for each argument that isn't a valid sketch
3270    /// block parameter. Currently, the only valid parameter is `on`.
3271    fn check_for_unexpected_arguments(&self, args: &Args, exec_state: &mut ExecState) -> Result<(), KclError> {
3272        if !args.unlabeled.is_empty() {
3273            let message = "Sketch block doesn't support unlabeled arguments; argument shorthand should have already been desugared";
3274            debug_assert!(false, "{message}");
3275            return Err(KclError::new_internal(KclErrorDetails::new(
3276                message.to_owned(),
3277                vec![args.source_range],
3278            )));
3279        }
3280        for (label, arg) in &args.labeled {
3281            if label == SKETCH_BLOCK_PARAM_ON {
3282                continue;
3283            }
3284            exec_state.err(CompilationIssue::err(
3285                arg.source_range,
3286                unexpected_kw_arg_message(label, Some(SketchBlock::CALLEE_NAME)),
3287            ));
3288        }
3289        Ok(())
3290    }
3291
3292    pub(super) async fn load_sketch2_into_current_scope(
3293        &self,
3294        exec_state: &mut ExecState,
3295        ctx: &ExecutorContext,
3296        source_range: SourceRange,
3297    ) -> Result<(), KclError> {
3298        let path = vec!["std".to_owned(), "solver".to_owned()];
3299        let resolved_path = ModulePath::from_std_import_path(&path)?;
3300        let module_id = ctx
3301            .open_module(&ImportPath::Std { path }, &[], &resolved_path, exec_state, source_range)
3302            .await?;
3303        let ModuleItems {
3304            environment: env_ref,
3305            exports,
3306            ..
3307        } = ctx.exec_module_for_items(module_id, exec_state, source_range).await?;
3308
3309        for name in exports {
3310            let value = exec_state
3311                .stack()
3312                .memory
3313                .get_from_owned(&name, env_ref, source_range, 0)?;
3314            exec_state.mut_stack().add(name, value, source_range)?;
3315        }
3316        Ok(())
3317    }
3318
3319    /// Augment the variables in the sketch block with properties that should be
3320    /// accessible on the returned sketch object. This includes metadata like
3321    /// the sketch so that the engine ID and surface can be accessed.
3322    pub(crate) fn sketch_properties(
3323        &self,
3324        sketch: Option<Sketch>,
3325        variables: HashMap<String, KclValue>,
3326    ) -> HashMap<String, KclValue> {
3327        let Some(sketch) = sketch else {
3328            // The sketch block did not produce a Sketch, so we cannot provide
3329            // it.
3330            return variables;
3331        };
3332
3333        let mut properties = variables;
3334
3335        let sketch_value = KclValue::Sketch {
3336            value: Box::new(sketch),
3337        };
3338        let mut meta_map = HashMap::with_capacity(1);
3339        meta_map.insert(SKETCH_OBJECT_META_SKETCH.to_owned(), sketch_value);
3340        let meta_value = KclValue::Object {
3341            value: meta_map,
3342            constrainable: false,
3343            object_kind: KclObjectKind::Default,
3344            meta: vec![Metadata {
3345                source_range: SourceRange::from(self),
3346            }],
3347        };
3348
3349        properties.insert(SKETCH_OBJECT_META.to_owned(), meta_value);
3350
3351        properties
3352    }
3353}
3354
3355impl SketchBlock {
3356    pub(super) fn prep_mem(&self, parent: EnvironmentRef, exec_state: &mut ExecState) -> Result<(), KclError> {
3357        exec_state.mut_stack().push_new_env_for_call(parent)
3358    }
3359}
3360
3361impl Node<SketchVar> {
3362    pub async fn get_result(&self, exec_state: &mut ExecState, _ctx: &ExecutorContext) -> Result<KclValue, KclError> {
3363        let Some(sketch_block_state) = &exec_state.mod_local.sketch_block else {
3364            return Err(KclError::new_semantic(KclErrorDetails::new(
3365                "Cannot use a sketch variable outside of a sketch block".to_owned(),
3366                vec![SourceRange::from(self)],
3367            )));
3368        };
3369        let id = sketch_block_state.next_sketch_var_id();
3370        let sketch_var = if let Some(initial) = &self.initial {
3371            KclValue::from_sketch_var_literal(initial, id, self.node_path.clone(), exec_state)
3372        } else {
3373            let metadata = Metadata {
3374                source_range: SourceRange::from(self),
3375            };
3376
3377            KclValue::SketchVar {
3378                value: Box::new(super::SketchVar {
3379                    id,
3380                    initial_value: 0.0,
3381                    ty: NumericType::default(),
3382                    node_path: self.node_path.clone(),
3383                    meta: vec![metadata],
3384                }),
3385            }
3386        };
3387
3388        let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
3389            return Err(KclError::new_semantic(KclErrorDetails::new(
3390                "Cannot use a sketch variable outside of a sketch block".to_owned(),
3391                vec![SourceRange::from(self)],
3392            )));
3393        };
3394        sketch_block_state.sketch_vars.push(sketch_var.clone());
3395
3396        Ok(sketch_var)
3397    }
3398}
3399
3400pub(super) async fn apply_ascription(
3401    value: &KclValue,
3402    ty: &Node<Type>,
3403    exec_state: &mut ExecState,
3404    ctx: &ExecutorContext,
3405    source_range: SourceRange,
3406) -> Result<KclValue, KclError> {
3407    let ty = RuntimeType::from_parsed(ty.inner.clone(), exec_state, ctx, value.into(), false, false).await?;
3408
3409    if matches!(&ty, &RuntimeType::Primitive(PrimitiveType::Number(..))) {
3410        exec_state.clear_units_warnings(&source_range);
3411    }
3412
3413    value.coerce(&ty, CoercionMode::explicit(), exec_state).map_err(|e| {
3414        // A coercion that knows why it failed says so; the generic wording below
3415        // would replace that with a worse description of the same failure.
3416        if let Some(message) = e.message {
3417            return KclError::new_semantic(KclErrorDetails::new(message, vec![source_range]));
3418        }
3419
3420        let suggestion = if ty == RuntimeType::length() {
3421            ", you might try coercing to a fully specified numeric type such as `mm`"
3422        } else if ty == RuntimeType::angle() {
3423            ", you might try coercing to a fully specified numeric type such as `deg`"
3424        } else {
3425            ""
3426        };
3427        let ty_str = if let Some(ty) = value.principal_type() {
3428            format!("(with type `{ty}`) ")
3429        } else {
3430            String::new()
3431        };
3432        KclError::new_semantic(KclErrorDetails::new(
3433            format!(
3434                "could not coerce {} {ty_str}to type `{ty}`{suggestion}",
3435                value.human_friendly_type()
3436            ),
3437            vec![source_range],
3438        ))
3439    })
3440}
3441
3442impl BinaryPart {
3443    #[async_recursion]
3444    pub(super) async fn get_result(
3445        &self,
3446        exec_state: &mut ExecState,
3447        ctx: &ExecutorContext,
3448    ) -> Result<KclValueControlFlow, KclError> {
3449        match self {
3450            BinaryPart::Literal(literal) => Ok(KclValue::from_literal((**literal).clone(), exec_state).continue_()),
3451            BinaryPart::Name(name) => {
3452                let metadata = Metadata {
3453                    source_range: SourceRange::from(&**name),
3454                };
3455                ctx.resolve_name_for_eval(name, &metadata, exec_state)
3456                    .await
3457                    .map(KclValue::continue_)
3458            }
3459            BinaryPart::BinaryExpression(binary_expression) => binary_expression.get_result(exec_state, ctx).await,
3460            BinaryPart::CallExpressionKw(call_expression) => call_expression.execute(exec_state, ctx).await,
3461            BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, ctx).await,
3462            BinaryPart::MemberExpression(member_expression) => member_expression.get_result(exec_state, ctx).await,
3463            BinaryPart::ArrayExpression(e) => e.execute(exec_state, ctx).await,
3464            BinaryPart::ArrayRangeExpression(e) => e.execute(exec_state, ctx).await,
3465            BinaryPart::ObjectExpression(e) => e.execute(exec_state, ctx).await,
3466            BinaryPart::IfExpression(e) => e.get_result(exec_state, ctx).await,
3467            BinaryPart::AscribedExpression(e) => e.get_result(exec_state, ctx).await,
3468            BinaryPart::SketchVar(e) => e.get_result(exec_state, ctx).await.map(KclValue::continue_),
3469        }
3470    }
3471}
3472
3473impl Node<Name> {
3474    pub(super) async fn get_result(
3475        &self,
3476        exec_state: &mut ExecState,
3477        ctx: &ExecutorContext,
3478    ) -> Result<KclValue, KclError> {
3479        // Await first so being_declared is read only on the error path,
3480        // instead of cloned before every lookup.
3481        let result = self.get_result_inner(exec_state, ctx).await;
3482        result.map_err(|e| var_in_own_ref_err(e, &exec_state.mod_local.being_declared))
3483    }
3484
3485    async fn get_result_inner(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
3486        if self.abs_path {
3487            return Err(KclError::new_semantic(KclErrorDetails::new(
3488                ABSOLUTE_PATHS_NOT_SUPPORTED.to_owned(),
3489                self.as_source_ranges(),
3490            )));
3491        }
3492
3493        if self.path.is_empty() {
3494            if let Ok(item_value) = exec_state.stack().get(&self.name.name, self.into()) {
3495                return Ok(item_value);
3496            }
3497
3498            let mod_name = format!("{}{}", memory::MODULE_PREFIX, self.name.name);
3499            let not_defined = match exec_state.stack().get(&mod_name, self.into()) {
3500                Ok(module) => return Ok(module),
3501                Err(err) => err,
3502            };
3503
3504            // No value and no module of this name exists. If a type does, report
3505            // that instead: "is not defined" would point away from the mistake.
3506            // Failing that, the name may be a declaration skipped as not yet added.
3507            return Err(type_used_as_value(exec_state, &self.name)
3508                .unwrap_or_else(|| exec_state.with_not_yet_added_hint(&[&self.name.name, &mod_name], not_defined)));
3509        }
3510
3511        let mut mem_spec: Option<ModuleItems> = None;
3512        for (index, p) in self.path.iter().enumerate() {
3513            // Only the last segment can name an enum, because what follows an
3514            // enum is a variant rather than something to traverse into.
3515            let non_enum_type = match enum_named_by_segment(exec_state, p, mem_spec.as_ref()) {
3516                Some(EnumPathHead::Enum {
3517                    def,
3518                    binding_experimental,
3519                }) => {
3520                    if let Some(next) = self.path.get(index + 1) {
3521                        return Err(KclError::new_semantic(KclErrorDetails::new(
3522                            format!(
3523                                "`{}` is an enum, so only a variant name can follow it. There is nothing to reach through `{}::{}`.",
3524                                p.name, p.name, next.name
3525                            ),
3526                            p.as_source_ranges(),
3527                        )));
3528                    }
3529
3530                    return enum_variant_value(def, binding_experimental, &self.name, exec_state);
3531                }
3532                Some(EnumPathHead::NonEnumType) => true,
3533                None => false,
3534            };
3535
3536            let value = match &mem_spec {
3537                Some(items) => {
3538                    if !items.exports.contains(&p.name) {
3539                        if non_enum_type {
3540                            return Err(non_enum_type_in_path(p));
3541                        }
3542                        return Err(KclError::new_semantic(KclErrorDetails::new(
3543                            format!("Item {} not found in module's exported items", p.name),
3544                            p.as_source_ranges(),
3545                        )));
3546                    }
3547
3548                    exec_state
3549                        .stack()
3550                        .memory
3551                        .get_from_owned(&p.name, items.environment, p.as_source_range(), 0)?
3552                }
3553                None => {
3554                    let module_key = format!("{}{}", memory::MODULE_PREFIX, p.name);
3555                    match exec_state.stack().get(&module_key, self.into()) {
3556                        Ok(value) => value,
3557                        Err(_) if non_enum_type => return Err(non_enum_type_in_path(p)),
3558                        Err(err) => return Err(exec_state.with_not_yet_added_hint(&[&module_key], err)),
3559                    }
3560                }
3561            };
3562
3563            let module_id = match value {
3564                KclValue::Module { value, .. } => value,
3565                value => {
3566                    return Err(KclError::new_semantic(KclErrorDetails::new(
3567                        format!(
3568                            "Identifier in path must refer to a module, found {}",
3569                            value.human_friendly_type()
3570                        ),
3571                        p.as_source_ranges(),
3572                    )));
3573                }
3574            };
3575
3576            mem_spec = Some(
3577                ctx.exec_module_for_items(module_id, exec_state, p.as_source_range())
3578                    .await?,
3579            );
3580        }
3581
3582        let ModuleItems {
3583            environment: env,
3584            exports,
3585            not_yet_added,
3586        } = mem_spec.unwrap();
3587
3588        let item_exported = exports.contains(&self.name.name);
3589        let item_value = exec_state
3590            .stack()
3591            .memory
3592            .get_from_owned(&self.name.name, env, self.name.as_source_range(), 0);
3593
3594        // Item is defined and exported.
3595        if item_exported && item_value.is_ok() {
3596            return item_value;
3597        }
3598
3599        let mod_name = format!("{}{}", memory::MODULE_PREFIX, self.name.name);
3600        let mod_exported = exports.contains(&mod_name);
3601        let mod_value = exec_state
3602            .stack()
3603            .memory
3604            .get_from_owned(&mod_name, env, self.name.as_source_range(), 0);
3605
3606        // Module is defined and exported.
3607        if mod_exported && mod_value.is_ok() {
3608            return mod_value;
3609        }
3610
3611        // Neither item or module is defined. The module may have skipped a
3612        // declaration of this name as not yet added.
3613        if item_value.is_err() && mod_value.is_err() {
3614            return item_value
3615                .map_err(|err| exec_state.with_not_yet_added_hint_from(&not_yet_added, &[&self.name.name], err));
3616        }
3617
3618        // Either item or module is defined, but not exported.
3619        debug_assert!((item_value.is_ok() && !item_exported) || (mod_value.is_ok() && !mod_exported));
3620        Err(KclError::new_semantic(KclErrorDetails::new(
3621            format!("Item {} not found in module's exported items", self.name.name),
3622            self.name.as_source_ranges(),
3623        )))
3624    }
3625}
3626
3627fn mock_array_may_have_engine_dependent_cardinality(ty: &RuntimeType) -> bool {
3628    match ty {
3629        RuntimeType::Primitive(
3630            PrimitiveType::Sketch
3631            | PrimitiveType::Solid
3632            | PrimitiveType::Face
3633            | PrimitiveType::Edge
3634            | PrimitiveType::BoundedEdge
3635            | PrimitiveType::ImportedGeometry,
3636        ) => true,
3637        RuntimeType::Union(types) => types.iter().any(mock_array_may_have_engine_dependent_cardinality),
3638        _ => false,
3639    }
3640}
3641
3642impl Node<MemberExpression> {
3643    async fn get_result(
3644        &self,
3645        exec_state: &mut ExecState,
3646        ctx: &ExecutorContext,
3647    ) -> Result<KclValueControlFlow, KclError> {
3648        if exec_state.entry_point_version_is_v3_or_higher() {
3649            // KCL 3.0: evaluate in source order, the object before the
3650            // property.
3651            let object = control_continue!(self.eval_object(exec_state, ctx).await?);
3652            let property = match self.eval_property(exec_state, ctx).await {
3653                Ok(property) => property,
3654                // The property expression exited, e.g. by calling exit().
3655                // Propagate the exit so that it terminates the enclosing module.
3656                Err(EarlyReturn::Value(cf)) => return Ok(cf),
3657                Err(EarlyReturn::Error(err)) => return Err(err),
3658            };
3659            return self.apply_member(object, property, exec_state, ctx).await;
3660        }
3661
3662        // Pre-KCL-3.0: the property is evaluated before the object. This is
3663        // backwards with respect to the source text, but it is preserved
3664        // because ids and engine commands are order-dependent.
3665        let property = match self.eval_property(exec_state, ctx).await {
3666            Ok(property) => property,
3667            // The property expression exited, e.g. by calling exit().
3668            // Propagate the exit so that it terminates the enclosing module.
3669            Err(EarlyReturn::Value(cf)) => return Ok(cf),
3670            Err(EarlyReturn::Error(err)) => return Err(err),
3671        };
3672        let object = control_continue!(self.eval_object(exec_state, ctx).await?);
3673        self.apply_member(object, property, exec_state, ctx).await
3674    }
3675
3676    /// Evaluate the object half of the member expression. It gets its own
3677    /// source range so that diagnostics raised while evaluating it (module
3678    /// errors, missing-return warnings) point at the object instead of the
3679    /// whole member expression.
3680    async fn eval_object(
3681        &self,
3682        exec_state: &mut ExecState,
3683        ctx: &ExecutorContext,
3684    ) -> Result<KclValueControlFlow, KclError> {
3685        let object_meta = Metadata {
3686            source_range: SourceRange::from(&self.object),
3687        };
3688        ctx.execute_expr(&self.object, exec_state, &object_meta, &[], StatementKind::Expression)
3689            .await
3690    }
3691
3692    /// Evaluate the property half of the member expression: a computed index
3693    /// is executed, while `obj.name` just reads the identifier. Likewise
3694    /// gets its own source range.
3695    async fn eval_property(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<Property, EarlyReturn> {
3696        let property_meta = Metadata {
3697            source_range: SourceRange::from(&self.property),
3698        };
3699        Property::try_from(
3700            self.computed,
3701            self.property.clone(),
3702            exec_state,
3703            self.into(),
3704            ctx,
3705            &property_meta,
3706            &[],
3707            StatementKind::Expression,
3708        )
3709        .await
3710    }
3711
3712    /// Apply property access or indexing to an already-evaluated object: the
3713    /// evaluation-free second half of member-expression execution, shared by
3714    /// both executors.
3715    pub(super) async fn apply_member(
3716        &self,
3717        object: KclValue,
3718        property: Property,
3719        exec_state: &mut ExecState,
3720        ctx: &ExecutorContext,
3721    ) -> Result<KclValueControlFlow, KclError> {
3722        // Result values of the member access itself carry the whole
3723        // expression's metadata.
3724        let meta = Metadata {
3725            source_range: SourceRange::from(self),
3726        };
3727
3728        // Check the property and object match -- e.g. ints for arrays, strs for objects.
3729        match (object, property, self.computed) {
3730            (KclValue::Segment { value: segment }, Property::String(property), false) => match property.as_str() {
3731                "at" => match &segment.repr {
3732                    SegmentRepr::Unsolved { segment } => {
3733                        match &segment.kind {
3734                            UnsolvedSegmentKind::Point { position, .. } => {
3735                                // TODO: assert that types of all elements are the same.
3736                                Ok(KclValue::HomArray {
3737                                    value: vec![
3738                                        KclValue::from_unsolved_expr(position[0].clone(), segment.meta.clone()),
3739                                        KclValue::from_unsolved_expr(position[1].clone(), segment.meta.clone()),
3740                                    ],
3741                                    ty: RuntimeType::any(),
3742                                }
3743                                .continue_())
3744                            }
3745                            _ => Err(KclError::new_undefined_value(
3746                                KclErrorDetails::new(
3747                                    format!("Property '{property}' not found in segment"),
3748                                    vec![self.clone().into()],
3749                                ),
3750                                None,
3751                            )),
3752                        }
3753                    }
3754                    SegmentRepr::Solved { segment } => {
3755                        match &segment.kind {
3756                            SegmentKind::Point { position, .. } => {
3757                                // TODO: assert that types of all elements are the same.
3758                                Ok(KclValue::array_from_point2d(
3759                                    [position[0].n, position[1].n],
3760                                    position[0].ty,
3761                                    segment.meta.clone(),
3762                                )
3763                                .continue_())
3764                            }
3765                            _ => Err(KclError::new_undefined_value(
3766                                KclErrorDetails::new(
3767                                    format!("Property '{property}' not found in segment"),
3768                                    vec![self.clone().into()],
3769                                ),
3770                                None,
3771                            )),
3772                        }
3773                    }
3774                },
3775                "start" => match &segment.repr {
3776                    SegmentRepr::Unsolved { segment } => match &segment.kind {
3777                        UnsolvedSegmentKind::Point { .. } => Err(KclError::new_undefined_value(
3778                            KclErrorDetails::new(
3779                                format!("Property '{property}' not found in point segment"),
3780                                vec![self.clone().into()],
3781                            ),
3782                            None,
3783                        )),
3784                        UnsolvedSegmentKind::Line {
3785                            start,
3786                            ctor,
3787                            start_object_id,
3788                            ..
3789                        } => Ok(KclValue::Segment {
3790                            value: Box::new(AbstractSegment {
3791                                repr: SegmentRepr::Unsolved {
3792                                    segment: Box::new(UnsolvedSegment {
3793                                        id: segment.id,
3794                                        object_id: *start_object_id,
3795                                        kind: UnsolvedSegmentKind::Point {
3796                                            position: start.clone(),
3797                                            ctor: Box::new(PointCtor {
3798                                                position: ctor.start.clone(),
3799                                            }),
3800                                        },
3801                                        tag: segment.tag.clone(),
3802                                        node_path: segment.node_path.clone(),
3803                                        meta: segment.meta.clone(),
3804                                    }),
3805                                },
3806                                meta: segment.meta.clone(),
3807                            }),
3808                        }
3809                        .continue_()),
3810                        UnsolvedSegmentKind::Arc {
3811                            start,
3812                            ctor,
3813                            start_object_id,
3814                            ..
3815                        } => Ok(KclValue::Segment {
3816                            value: Box::new(AbstractSegment {
3817                                repr: SegmentRepr::Unsolved {
3818                                    segment: Box::new(UnsolvedSegment {
3819                                        id: segment.id,
3820                                        object_id: *start_object_id,
3821                                        kind: UnsolvedSegmentKind::Point {
3822                                            position: start.clone(),
3823                                            ctor: Box::new(PointCtor {
3824                                                position: ctor.start.clone(),
3825                                            }),
3826                                        },
3827                                        tag: segment.tag.clone(),
3828                                        node_path: segment.node_path.clone(),
3829                                        meta: segment.meta.clone(),
3830                                    }),
3831                                },
3832                                meta: segment.meta.clone(),
3833                            }),
3834                        }
3835                        .continue_()),
3836                        UnsolvedSegmentKind::Circle {
3837                            start,
3838                            ctor,
3839                            start_object_id,
3840                            ..
3841                        } => Ok(KclValue::Segment {
3842                            value: Box::new(AbstractSegment {
3843                                repr: SegmentRepr::Unsolved {
3844                                    segment: Box::new(UnsolvedSegment {
3845                                        id: segment.id,
3846                                        object_id: *start_object_id,
3847                                        kind: UnsolvedSegmentKind::Point {
3848                                            position: start.clone(),
3849                                            ctor: Box::new(PointCtor {
3850                                                position: ctor.start.clone(),
3851                                            }),
3852                                        },
3853                                        tag: segment.tag.clone(),
3854                                        node_path: segment.node_path.clone(),
3855                                        meta: segment.meta.clone(),
3856                                    }),
3857                                },
3858                                meta: segment.meta.clone(),
3859                            }),
3860                        }
3861                        .continue_()),
3862                        UnsolvedSegmentKind::ControlPointSpline { .. } => Err(KclError::new_undefined_value(
3863                            KclErrorDetails::new(
3864                                format!("Property '{property}' not found in segment"),
3865                                vec![self.clone().into()],
3866                            ),
3867                            None,
3868                        )),
3869                    },
3870                    SegmentRepr::Solved { segment } => match &segment.kind {
3871                        SegmentKind::Point { .. } => Err(KclError::new_undefined_value(
3872                            KclErrorDetails::new(
3873                                format!("Property '{property}' not found in point segment"),
3874                                vec![self.clone().into()],
3875                            ),
3876                            None,
3877                        )),
3878                        SegmentKind::Line {
3879                            start,
3880                            ctor,
3881                            start_object_id,
3882                            start_freedom,
3883                            ..
3884                        } => Ok(KclValue::Segment {
3885                            value: Box::new(AbstractSegment {
3886                                repr: SegmentRepr::Solved {
3887                                    segment: Box::new(Segment {
3888                                        id: segment.id,
3889                                        object_id: *start_object_id,
3890                                        kind: SegmentKind::Point {
3891                                            position: start.clone(),
3892                                            ctor: Box::new(PointCtor {
3893                                                position: ctor.start.clone(),
3894                                            }),
3895                                            freedom: *start_freedom,
3896                                        },
3897                                        surface: segment.surface.clone(),
3898                                        sketch_id: segment.sketch_id,
3899                                        sketch: segment.sketch.clone(),
3900                                        tag: segment.tag.clone(),
3901                                        node_path: segment.node_path.clone(),
3902                                        meta: segment.meta.clone(),
3903                                    }),
3904                                },
3905                                meta: segment.meta.clone(),
3906                            }),
3907                        }
3908                        .continue_()),
3909                        SegmentKind::Arc {
3910                            start,
3911                            ctor,
3912                            start_object_id,
3913                            start_freedom,
3914                            ..
3915                        } => Ok(KclValue::Segment {
3916                            value: Box::new(AbstractSegment {
3917                                repr: SegmentRepr::Solved {
3918                                    segment: Box::new(Segment {
3919                                        id: segment.id,
3920                                        object_id: *start_object_id,
3921                                        kind: SegmentKind::Point {
3922                                            position: start.clone(),
3923                                            ctor: Box::new(PointCtor {
3924                                                position: ctor.start.clone(),
3925                                            }),
3926                                            freedom: *start_freedom,
3927                                        },
3928                                        surface: segment.surface.clone(),
3929                                        sketch_id: segment.sketch_id,
3930                                        sketch: segment.sketch.clone(),
3931                                        tag: segment.tag.clone(),
3932                                        node_path: segment.node_path.clone(),
3933                                        meta: segment.meta.clone(),
3934                                    }),
3935                                },
3936                                meta: segment.meta.clone(),
3937                            }),
3938                        }
3939                        .continue_()),
3940                        SegmentKind::Circle {
3941                            start,
3942                            ctor,
3943                            start_object_id,
3944                            start_freedom,
3945                            ..
3946                        } => Ok(KclValue::Segment {
3947                            value: Box::new(AbstractSegment {
3948                                repr: SegmentRepr::Solved {
3949                                    segment: Box::new(Segment {
3950                                        id: segment.id,
3951                                        object_id: *start_object_id,
3952                                        kind: SegmentKind::Point {
3953                                            position: start.clone(),
3954                                            ctor: Box::new(PointCtor {
3955                                                position: ctor.start.clone(),
3956                                            }),
3957                                            freedom: *start_freedom,
3958                                        },
3959                                        surface: segment.surface.clone(),
3960                                        sketch_id: segment.sketch_id,
3961                                        sketch: segment.sketch.clone(),
3962                                        tag: segment.tag.clone(),
3963                                        node_path: segment.node_path.clone(),
3964                                        meta: segment.meta.clone(),
3965                                    }),
3966                                },
3967                                meta: segment.meta.clone(),
3968                            }),
3969                        }
3970                        .continue_()),
3971                        SegmentKind::ControlPointSpline { .. } => Err(KclError::new_undefined_value(
3972                            KclErrorDetails::new(
3973                                format!("Property '{property}' not found in segment"),
3974                                vec![self.clone().into()],
3975                            ),
3976                            None,
3977                        )),
3978                    },
3979                },
3980                "end" => match &segment.repr {
3981                    SegmentRepr::Unsolved { segment } => match &segment.kind {
3982                        UnsolvedSegmentKind::Point { .. } => Err(KclError::new_undefined_value(
3983                            KclErrorDetails::new(
3984                                format!("Property '{property}' not found in point segment"),
3985                                vec![self.clone().into()],
3986                            ),
3987                            None,
3988                        )),
3989                        UnsolvedSegmentKind::Line {
3990                            end,
3991                            ctor,
3992                            end_object_id,
3993                            ..
3994                        } => Ok(KclValue::Segment {
3995                            value: Box::new(AbstractSegment {
3996                                repr: SegmentRepr::Unsolved {
3997                                    segment: Box::new(UnsolvedSegment {
3998                                        id: segment.id,
3999                                        object_id: *end_object_id,
4000                                        kind: UnsolvedSegmentKind::Point {
4001                                            position: end.clone(),
4002                                            ctor: Box::new(PointCtor {
4003                                                position: ctor.end.clone(),
4004                                            }),
4005                                        },
4006                                        tag: segment.tag.clone(),
4007                                        node_path: segment.node_path.clone(),
4008                                        meta: segment.meta.clone(),
4009                                    }),
4010                                },
4011                                meta: segment.meta.clone(),
4012                            }),
4013                        }
4014                        .continue_()),
4015                        UnsolvedSegmentKind::Arc {
4016                            end,
4017                            ctor,
4018                            end_object_id,
4019                            ..
4020                        } => Ok(KclValue::Segment {
4021                            value: Box::new(AbstractSegment {
4022                                repr: SegmentRepr::Unsolved {
4023                                    segment: Box::new(UnsolvedSegment {
4024                                        id: segment.id,
4025                                        object_id: *end_object_id,
4026                                        kind: UnsolvedSegmentKind::Point {
4027                                            position: end.clone(),
4028                                            ctor: Box::new(PointCtor {
4029                                                position: ctor.end.clone(),
4030                                            }),
4031                                        },
4032                                        tag: segment.tag.clone(),
4033                                        node_path: segment.node_path.clone(),
4034                                        meta: segment.meta.clone(),
4035                                    }),
4036                                },
4037                                meta: segment.meta.clone(),
4038                            }),
4039                        }
4040                        .continue_()),
4041                        UnsolvedSegmentKind::Circle { .. } => Err(KclError::new_undefined_value(
4042                            KclErrorDetails::new(
4043                                format!("Property '{property}' not found in segment"),
4044                                vec![self.into()],
4045                            ),
4046                            None,
4047                        )),
4048                        UnsolvedSegmentKind::ControlPointSpline { .. } => Err(KclError::new_undefined_value(
4049                            KclErrorDetails::new(
4050                                format!("Property '{property}' not found in segment"),
4051                                vec![self.clone().into()],
4052                            ),
4053                            None,
4054                        )),
4055                    },
4056                    SegmentRepr::Solved { segment } => match &segment.kind {
4057                        SegmentKind::Point { .. } => Err(KclError::new_undefined_value(
4058                            KclErrorDetails::new(
4059                                format!("Property '{property}' not found in point segment"),
4060                                vec![self.clone().into()],
4061                            ),
4062                            None,
4063                        )),
4064                        SegmentKind::Line {
4065                            end,
4066                            ctor,
4067                            end_object_id,
4068                            end_freedom,
4069                            ..
4070                        } => Ok(KclValue::Segment {
4071                            value: Box::new(AbstractSegment {
4072                                repr: SegmentRepr::Solved {
4073                                    segment: Box::new(Segment {
4074                                        id: segment.id,
4075                                        object_id: *end_object_id,
4076                                        kind: SegmentKind::Point {
4077                                            position: end.clone(),
4078                                            ctor: Box::new(PointCtor {
4079                                                position: ctor.end.clone(),
4080                                            }),
4081                                            freedom: *end_freedom,
4082                                        },
4083                                        surface: segment.surface.clone(),
4084                                        sketch_id: segment.sketch_id,
4085                                        sketch: segment.sketch.clone(),
4086                                        tag: segment.tag.clone(),
4087                                        node_path: segment.node_path.clone(),
4088                                        meta: segment.meta.clone(),
4089                                    }),
4090                                },
4091                                meta: segment.meta.clone(),
4092                            }),
4093                        }
4094                        .continue_()),
4095                        SegmentKind::Arc {
4096                            end,
4097                            ctor,
4098                            end_object_id,
4099                            end_freedom,
4100                            ..
4101                        } => Ok(KclValue::Segment {
4102                            value: Box::new(AbstractSegment {
4103                                repr: SegmentRepr::Solved {
4104                                    segment: Box::new(Segment {
4105                                        id: segment.id,
4106                                        object_id: *end_object_id,
4107                                        kind: SegmentKind::Point {
4108                                            position: end.clone(),
4109                                            ctor: Box::new(PointCtor {
4110                                                position: ctor.end.clone(),
4111                                            }),
4112                                            freedom: *end_freedom,
4113                                        },
4114                                        surface: segment.surface.clone(),
4115                                        sketch_id: segment.sketch_id,
4116                                        sketch: segment.sketch.clone(),
4117                                        tag: segment.tag.clone(),
4118                                        node_path: segment.node_path.clone(),
4119                                        meta: segment.meta.clone(),
4120                                    }),
4121                                },
4122                                meta: segment.meta.clone(),
4123                            }),
4124                        }
4125                        .continue_()),
4126                        SegmentKind::Circle { .. } => Err(KclError::new_undefined_value(
4127                            KclErrorDetails::new(
4128                                format!("Property '{property}' not found in segment"),
4129                                vec![self.into()],
4130                            ),
4131                            None,
4132                        )),
4133                        SegmentKind::ControlPointSpline { .. } => Err(KclError::new_undefined_value(
4134                            KclErrorDetails::new(
4135                                format!("Property '{property}' not found in segment"),
4136                                vec![self.clone().into()],
4137                            ),
4138                            None,
4139                        )),
4140                    },
4141                },
4142                "center" => match &segment.repr {
4143                    SegmentRepr::Unsolved { segment } => match &segment.kind {
4144                        UnsolvedSegmentKind::Arc {
4145                            center,
4146                            ctor,
4147                            center_object_id,
4148                            ..
4149                        } => Ok(KclValue::Segment {
4150                            value: Box::new(AbstractSegment {
4151                                repr: SegmentRepr::Unsolved {
4152                                    segment: Box::new(UnsolvedSegment {
4153                                        id: segment.id,
4154                                        object_id: *center_object_id,
4155                                        kind: UnsolvedSegmentKind::Point {
4156                                            position: center.clone(),
4157                                            ctor: Box::new(PointCtor {
4158                                                position: ctor.center.clone(),
4159                                            }),
4160                                        },
4161                                        tag: segment.tag.clone(),
4162                                        node_path: segment.node_path.clone(),
4163                                        meta: segment.meta.clone(),
4164                                    }),
4165                                },
4166                                meta: segment.meta.clone(),
4167                            }),
4168                        }
4169                        .continue_()),
4170                        UnsolvedSegmentKind::Circle {
4171                            center,
4172                            ctor,
4173                            center_object_id,
4174                            ..
4175                        } => Ok(KclValue::Segment {
4176                            value: Box::new(AbstractSegment {
4177                                repr: SegmentRepr::Unsolved {
4178                                    segment: Box::new(UnsolvedSegment {
4179                                        id: segment.id,
4180                                        object_id: *center_object_id,
4181                                        kind: UnsolvedSegmentKind::Point {
4182                                            position: center.clone(),
4183                                            ctor: Box::new(PointCtor {
4184                                                position: ctor.center.clone(),
4185                                            }),
4186                                        },
4187                                        tag: segment.tag.clone(),
4188                                        node_path: segment.node_path.clone(),
4189                                        meta: segment.meta.clone(),
4190                                    }),
4191                                },
4192                                meta: segment.meta.clone(),
4193                            }),
4194                        }
4195                        .continue_()),
4196                        _ => Err(KclError::new_undefined_value(
4197                            KclErrorDetails::new(
4198                                format!("Property '{property}' not found in segment"),
4199                                vec![self.clone().into()],
4200                            ),
4201                            None,
4202                        )),
4203                    },
4204                    SegmentRepr::Solved { segment } => match &segment.kind {
4205                        SegmentKind::Arc {
4206                            center,
4207                            ctor,
4208                            center_object_id,
4209                            center_freedom,
4210                            ..
4211                        } => Ok(KclValue::Segment {
4212                            value: Box::new(AbstractSegment {
4213                                repr: SegmentRepr::Solved {
4214                                    segment: Box::new(Segment {
4215                                        id: segment.id,
4216                                        object_id: *center_object_id,
4217                                        kind: SegmentKind::Point {
4218                                            position: center.clone(),
4219                                            ctor: Box::new(PointCtor {
4220                                                position: ctor.center.clone(),
4221                                            }),
4222                                            freedom: *center_freedom,
4223                                        },
4224                                        surface: segment.surface.clone(),
4225                                        sketch_id: segment.sketch_id,
4226                                        sketch: segment.sketch.clone(),
4227                                        tag: segment.tag.clone(),
4228                                        node_path: segment.node_path.clone(),
4229                                        meta: segment.meta.clone(),
4230                                    }),
4231                                },
4232                                meta: segment.meta.clone(),
4233                            }),
4234                        }
4235                        .continue_()),
4236                        SegmentKind::Circle {
4237                            center,
4238                            ctor,
4239                            center_object_id,
4240                            center_freedom,
4241                            ..
4242                        } => Ok(KclValue::Segment {
4243                            value: Box::new(AbstractSegment {
4244                                repr: SegmentRepr::Solved {
4245                                    segment: Box::new(Segment {
4246                                        id: segment.id,
4247                                        object_id: *center_object_id,
4248                                        kind: SegmentKind::Point {
4249                                            position: center.clone(),
4250                                            ctor: Box::new(PointCtor {
4251                                                position: ctor.center.clone(),
4252                                            }),
4253                                            freedom: *center_freedom,
4254                                        },
4255                                        surface: segment.surface.clone(),
4256                                        sketch_id: segment.sketch_id,
4257                                        sketch: segment.sketch.clone(),
4258                                        tag: segment.tag.clone(),
4259                                        node_path: segment.node_path.clone(),
4260                                        meta: segment.meta.clone(),
4261                                    }),
4262                                },
4263                                meta: segment.meta.clone(),
4264                            }),
4265                        }
4266                        .continue_()),
4267                        _ => Err(KclError::new_undefined_value(
4268                            KclErrorDetails::new(
4269                                format!("Property '{property}' not found in segment"),
4270                                vec![self.clone().into()],
4271                            ),
4272                            None,
4273                        )),
4274                    },
4275                },
4276                "controls" => match &segment.repr {
4277                    SegmentRepr::Unsolved { segment } => match &segment.kind {
4278                        UnsolvedSegmentKind::ControlPointSpline {
4279                            controls,
4280                            ctor,
4281                            control_object_ids,
4282                            ..
4283                        } => Ok(KclValue::HomArray {
4284                            value: controls
4285                                .iter()
4286                                .zip(control_object_ids.iter())
4287                                .zip(ctor.points.iter())
4288                                .map(|((position, object_id), ctor_point)| KclValue::Segment {
4289                                    value: Box::new(AbstractSegment {
4290                                        repr: SegmentRepr::Unsolved {
4291                                            segment: Box::new(UnsolvedSegment {
4292                                                id: segment.id,
4293                                                object_id: *object_id,
4294                                                kind: UnsolvedSegmentKind::Point {
4295                                                    position: position.clone(),
4296                                                    ctor: Box::new(PointCtor {
4297                                                        position: ctor_point.clone(),
4298                                                    }),
4299                                                },
4300                                                tag: segment.tag.clone(),
4301                                                node_path: segment.node_path.clone(),
4302                                                meta: segment.meta.clone(),
4303                                            }),
4304                                        },
4305                                        meta: segment.meta.clone(),
4306                                    }),
4307                                })
4308                                .collect(),
4309                            ty: RuntimeType::segment(),
4310                        }
4311                        .continue_()),
4312                        _ => Err(KclError::new_undefined_value(
4313                            KclErrorDetails::new(
4314                                format!("Property '{property}' not found in segment"),
4315                                vec![self.clone().into()],
4316                            ),
4317                            None,
4318                        )),
4319                    },
4320                    SegmentRepr::Solved { segment } => match &segment.kind {
4321                        SegmentKind::ControlPointSpline {
4322                            controls,
4323                            ctor,
4324                            control_object_ids,
4325                            control_freedoms,
4326                            ..
4327                        } => Ok(KclValue::HomArray {
4328                            value: controls
4329                                .iter()
4330                                .zip(control_object_ids.iter())
4331                                .zip(control_freedoms.iter())
4332                                .zip(ctor.points.iter())
4333                                .map(|(((position, object_id), freedom), ctor_point)| KclValue::Segment {
4334                                    value: Box::new(AbstractSegment {
4335                                        repr: SegmentRepr::Solved {
4336                                            segment: Box::new(Segment {
4337                                                id: segment.id,
4338                                                object_id: *object_id,
4339                                                kind: SegmentKind::Point {
4340                                                    position: position.clone(),
4341                                                    ctor: Box::new(PointCtor {
4342                                                        position: ctor_point.clone(),
4343                                                    }),
4344                                                    freedom: *freedom,
4345                                                },
4346                                                surface: segment.surface.clone(),
4347                                                sketch_id: segment.sketch_id,
4348                                                sketch: segment.sketch.clone(),
4349                                                tag: segment.tag.clone(),
4350                                                node_path: segment.node_path.clone(),
4351                                                meta: segment.meta.clone(),
4352                                            }),
4353                                        },
4354                                        meta: segment.meta.clone(),
4355                                    }),
4356                                })
4357                                .collect(),
4358                            ty: RuntimeType::segment(),
4359                        }
4360                        .continue_()),
4361                        _ => Err(KclError::new_undefined_value(
4362                            KclErrorDetails::new(
4363                                format!("Property '{property}' not found in segment"),
4364                                vec![self.clone().into()],
4365                            ),
4366                            None,
4367                        )),
4368                    },
4369                },
4370                "edges" => match &segment.repr {
4371                    SegmentRepr::Unsolved { segment } => match &segment.kind {
4372                        UnsolvedSegmentKind::ControlPointSpline {
4373                            controls,
4374                            ctor,
4375                            control_object_ids,
4376                            control_polygon_edge_object_ids,
4377                            construction,
4378                            ..
4379                        } => Ok(KclValue::HomArray {
4380                            value: control_polygon_edge_object_ids
4381                                .iter()
4382                                .enumerate()
4383                                .map(|(index, object_id)| KclValue::Segment {
4384                                    value: Box::new(AbstractSegment {
4385                                        repr: SegmentRepr::Unsolved {
4386                                            segment: Box::new(UnsolvedSegment {
4387                                                id: segment.id,
4388                                                object_id: *object_id,
4389                                                kind: UnsolvedSegmentKind::Line {
4390                                                    start: controls[index].clone(),
4391                                                    end: controls[index + 1].clone(),
4392                                                    ctor: Box::new(LineCtor {
4393                                                        start: ctor.points[index].clone(),
4394                                                        end: ctor.points[index + 1].clone(),
4395                                                        construction: Some(*construction),
4396                                                    }),
4397                                                    start_object_id: control_object_ids[index],
4398                                                    end_object_id: control_object_ids[index + 1],
4399                                                    construction: *construction,
4400                                                },
4401                                                tag: segment.tag.clone(),
4402                                                node_path: segment.node_path.clone(),
4403                                                meta: segment.meta.clone(),
4404                                            }),
4405                                        },
4406                                        meta: segment.meta.clone(),
4407                                    }),
4408                                })
4409                                .collect(),
4410                            ty: RuntimeType::segment(),
4411                        }
4412                        .continue_()),
4413                        _ => Err(KclError::new_undefined_value(
4414                            KclErrorDetails::new(
4415                                format!("Property '{property}' not found in segment"),
4416                                vec![self.clone().into()],
4417                            ),
4418                            None,
4419                        )),
4420                    },
4421                    SegmentRepr::Solved { segment } => match &segment.kind {
4422                        SegmentKind::ControlPointSpline {
4423                            controls,
4424                            ctor,
4425                            control_object_ids,
4426                            control_polygon_edge_object_ids,
4427                            control_freedoms,
4428                            construction,
4429                            ..
4430                        } => Ok(KclValue::HomArray {
4431                            value: control_polygon_edge_object_ids
4432                                .iter()
4433                                .enumerate()
4434                                .map(|(index, object_id)| KclValue::Segment {
4435                                    value: Box::new(AbstractSegment {
4436                                        repr: SegmentRepr::Solved {
4437                                            segment: Box::new(Segment {
4438                                                id: segment.id,
4439                                                object_id: *object_id,
4440                                                kind: SegmentKind::Line {
4441                                                    start: controls[index].clone(),
4442                                                    end: controls[index + 1].clone(),
4443                                                    ctor: Box::new(LineCtor {
4444                                                        start: ctor.points[index].clone(),
4445                                                        end: ctor.points[index + 1].clone(),
4446                                                        construction: Some(*construction),
4447                                                    }),
4448                                                    start_object_id: control_object_ids[index],
4449                                                    end_object_id: control_object_ids[index + 1],
4450                                                    start_freedom: control_freedoms[index],
4451                                                    end_freedom: control_freedoms[index + 1],
4452                                                    construction: *construction,
4453                                                },
4454                                                surface: segment.surface.clone(),
4455                                                sketch_id: segment.sketch_id,
4456                                                sketch: segment.sketch.clone(),
4457                                                tag: segment.tag.clone(),
4458                                                node_path: segment.node_path.clone(),
4459                                                meta: segment.meta.clone(),
4460                                            }),
4461                                        },
4462                                        meta: segment.meta.clone(),
4463                                    }),
4464                                })
4465                                .collect(),
4466                            ty: RuntimeType::segment(),
4467                        }
4468                        .continue_()),
4469                        _ => Err(KclError::new_undefined_value(
4470                            KclErrorDetails::new(
4471                                format!("Property '{property}' not found in segment"),
4472                                vec![self.clone().into()],
4473                            ),
4474                            None,
4475                        )),
4476                    },
4477                },
4478                other => Err(KclError::new_undefined_value(
4479                    KclErrorDetails::new(
4480                        format!("Property '{other}' not found in segment"),
4481                        vec![self.clone().into()],
4482                    ),
4483                    None,
4484                )),
4485            },
4486            (KclValue::Plane { value: plane }, Property::String(property), false) => match property.as_str() {
4487                "zAxis" => {
4488                    let (p, u) = plane.info.z_axis.as_3_dims();
4489                    Ok(KclValue::array_from_point3d(p, NumericType::optional_length(u), vec![meta]).continue_())
4490                }
4491                "yAxis" => {
4492                    let (p, u) = plane.info.y_axis.as_3_dims();
4493                    Ok(KclValue::array_from_point3d(p, NumericType::optional_length(u), vec![meta]).continue_())
4494                }
4495                "xAxis" => {
4496                    let (p, u) = plane.info.x_axis.as_3_dims();
4497                    Ok(KclValue::array_from_point3d(p, NumericType::optional_length(u), vec![meta]).continue_())
4498                }
4499                "origin" => {
4500                    let (p, u) = plane.info.origin.as_3_dims();
4501                    Ok(KclValue::array_from_point3d(p, NumericType::optional_length(u), vec![meta]).continue_())
4502                }
4503                other => Err(KclError::new_undefined_value(
4504                    KclErrorDetails::new(
4505                        format!("Property '{other}' not found in plane"),
4506                        vec![self.clone().into()],
4507                    ),
4508                    None,
4509                )),
4510            },
4511            (
4512                KclValue::Object {
4513                    value: map,
4514                    object_kind,
4515                    ..
4516                },
4517                Property::String(property),
4518                false,
4519            ) => {
4520                if let Some(value) = map.get(&property) {
4521                    if object_kind
4522                        .deprecated_solid_tag_names()
4523                        .iter()
4524                        .any(|tag_name| tag_name == &property)
4525                    {
4526                        exec_state.warn(
4527                            CompilationIssue::err(
4528                                SourceRange::from(self),
4529                                format!(
4530                                    "Accessing solid-created face `{property}` through sketch tags is deprecated. Use the body's faces instead, e.g. `body.faces.{property}`."
4531                                ),
4532                            ),
4533                            annotations::WARN_DEPRECATED,
4534                        );
4535                    }
4536                    Ok(value.to_owned().continue_())
4537                } else {
4538                    Err(KclError::new_undefined_value(
4539                        KclErrorDetails::new(
4540                            format!("Property '{property}' not found in object"),
4541                            vec![self.clone().into()],
4542                        ),
4543                        None,
4544                    ))
4545                }
4546            }
4547            (KclValue::Object { .. }, Property::String(property), true) => {
4548                Err(KclError::new_semantic(KclErrorDetails::new(
4549                    format!("Cannot index object with string; use dot notation instead, e.g. `obj.{property}`"),
4550                    vec![self.clone().into()],
4551                )))
4552            }
4553            (KclValue::Object { value: map, .. }, p @ Property::UInt(i), _) => {
4554                if i == 0
4555                    && let Some(value) = map.get("x")
4556                {
4557                    return Ok(value.to_owned().continue_());
4558                }
4559                if i == 1
4560                    && let Some(value) = map.get("y")
4561                {
4562                    return Ok(value.to_owned().continue_());
4563                }
4564                if i == 2
4565                    && let Some(value) = map.get("z")
4566                {
4567                    return Ok(value.to_owned().continue_());
4568                }
4569                let t = p.type_name();
4570                let article = article_for(t);
4571                Err(KclError::new_semantic(KclErrorDetails::new(
4572                    format!("Only strings can be used as the property of an object, but you're using {article} {t}",),
4573                    vec![self.clone().into()],
4574                )))
4575            }
4576            (KclValue::HomArray { value: arr, ty }, Property::UInt(index), _) => {
4577                let value_of_arr = arr.get(index);
4578                // Out-of-bounds error.
4579                let oob_error = KclError::new_undefined_value(
4580                    KclErrorDetails::new(
4581                        format!("The array doesn't have any item at index {index}"),
4582                        vec![self.clone().into()],
4583                    ),
4584                    None,
4585                );
4586                if let Some(value) = value_of_arr {
4587                    // Indexing into the array was successful.
4588                    Ok(value.to_owned().continue_())
4589                } else if ctx.no_engine_commands().await
4590                    && !exec_state.is_sketch_mode_execution()
4591                    && mock_array_may_have_engine_dependent_cardinality(&ty)
4592                {
4593                    // In mock execution, we handle OOB errors
4594                    // by trying to get index 0 only for geometry-handle arrays. Those
4595                    // values may have come from the engine, so their actual length is
4596                    // not known during mock execution runtime. Frontend-only arrays
4597                    // have exact cardinality and must preserve their OOB errors.
4598                    //
4599                    // We don't do this in sketch mode execution since it's
4600                    // forbidden from contacting the engine, meaning array
4601                    // lengths are always accurate, and the OOB error is real.
4602                    let value = arr.first();
4603                    value.map(|value| value.to_owned().continue_()).ok_or(oob_error)
4604                } else {
4605                    Err(oob_error)
4606                }
4607            }
4608            // Singletons and single-element arrays should be interchangeable, but only indexing by 0 should work.
4609            // This is kind of a silly property, but it's possible it occurs in generic code or something.
4610            (obj, Property::UInt(0), _) => Ok(obj.continue_()),
4611            (KclValue::HomArray { .. }, p, _) => {
4612                let t = p.type_name();
4613                let article = article_for(t);
4614                Err(KclError::new_semantic(KclErrorDetails::new(
4615                    format!("Only integers >= 0 can be used as the index of an array, but you're using {article} {t}",),
4616                    vec![self.clone().into()],
4617                )))
4618            }
4619            (KclValue::Solid { value }, Property::String(prop), false) if prop == "sketch" => {
4620                let Some(sketch) = value.sketch() else {
4621                    return Err(KclError::new_semantic(KclErrorDetails::new(
4622                        "This solid was created without a sketch, so `solid.sketch` is unavailable.".to_owned(),
4623                        vec![self.clone().into()],
4624                    )));
4625                };
4626                Ok(KclValue::Sketch {
4627                    value: Box::new(sketch.clone()),
4628                }
4629                .continue_())
4630            }
4631            (KclValue::Solid { value: solid }, Property::String(prop), false) if prop == "faces" => {
4632                Ok(KclValue::Object {
4633                    meta: vec![Metadata {
4634                        source_range: SourceRange::from(self.clone()),
4635                    }],
4636                    value: solid
4637                        .faces
4638                        .iter()
4639                        .map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
4640                        .collect(),
4641                    constrainable: false,
4642                    object_kind: KclObjectKind::Default,
4643                }
4644                .continue_())
4645            }
4646            (geometry @ KclValue::Solid { .. }, Property::String(prop), false) if prop == "tags" => {
4647                // This is a common mistake.
4648                Err(KclError::new_semantic(KclErrorDetails::new(
4649                    format!(
4650                        "Property `{prop}` not found on {}. You can get a solid's faces through `exampleSolid.faces`, or its sketch tags through `exampleSolid.sketch.tags`.",
4651                        geometry.human_friendly_type()
4652                    ),
4653                    vec![self.clone().into()],
4654                )))
4655            }
4656            (KclValue::Sketch { value: sk }, Property::String(prop), false) if prop == "tags" => Ok(KclValue::Object {
4657                meta: vec![Metadata {
4658                    source_range: SourceRange::from(self.clone()),
4659                }],
4660                value: sk
4661                    .tags
4662                    .iter()
4663                    .map(|(k, tag)| (k.to_owned(), KclValue::TagIdentifier(Box::new(tag.to_owned()))))
4664                    .collect(),
4665                constrainable: false,
4666                object_kind: KclObjectKind::SketchTags {
4667                    deprecated_solid_tag_names: sk
4668                        .tags
4669                        .iter()
4670                        .filter(|(_, tag)| tag.is_body_created_tag())
4671                        .map(|(name, _)| name.to_owned())
4672                        .collect(),
4673                },
4674            }
4675            .continue_()),
4676            (geometry @ (KclValue::Sketch { .. } | KclValue::Solid { .. }), Property::String(property), false) => {
4677                Err(KclError::new_semantic(KclErrorDetails::new(
4678                    format!("Property `{property}` not found on {}", geometry.human_friendly_type()),
4679                    vec![self.clone().into()],
4680                )))
4681            }
4682            (being_indexed, _, false) => Err(KclError::new_semantic(KclErrorDetails::new(
4683                format!(
4684                    "Only objects can have members accessed with dot notation, but you're trying to access {}",
4685                    being_indexed.human_friendly_type()
4686                ),
4687                vec![self.clone().into()],
4688            ))),
4689            (being_indexed, _, true) => Err(KclError::new_semantic(KclErrorDetails::new(
4690                format!(
4691                    "Only arrays can be indexed, but you're trying to index {}",
4692                    being_indexed.human_friendly_type()
4693                ),
4694                vec![self.clone().into()],
4695            ))),
4696        }
4697    }
4698}
4699
4700impl Node<BinaryExpression> {
4701    pub(super) async fn get_result(
4702        &self,
4703        exec_state: &mut ExecState,
4704        ctx: &ExecutorContext,
4705    ) -> Result<KclValueControlFlow, KclError> {
4706        enum State {
4707            EvaluateLeft(Node<BinaryExpression>),
4708            FromLeft {
4709                node: Node<BinaryExpression>,
4710            },
4711            EvaluateRight {
4712                node: Node<BinaryExpression>,
4713                left: KclValue,
4714            },
4715            FromRight {
4716                node: Node<BinaryExpression>,
4717                left: KclValue,
4718            },
4719        }
4720
4721        let mut stack = vec![State::EvaluateLeft(self.clone())];
4722        let mut last_result: Option<KclValue> = None;
4723
4724        while let Some(state) = stack.pop() {
4725            match state {
4726                State::EvaluateLeft(node) => {
4727                    let left_part = node.left.clone();
4728                    match left_part {
4729                        BinaryPart::BinaryExpression(child) => {
4730                            stack.push(State::FromLeft { node });
4731                            stack.push(State::EvaluateLeft(child.into_node()));
4732                        }
4733                        part => {
4734                            let left_value = part.get_result(exec_state, ctx).await?;
4735                            let left_value = control_continue!(left_value);
4736                            stack.push(State::EvaluateRight { node, left: left_value });
4737                        }
4738                    }
4739                }
4740                State::FromLeft { node } => {
4741                    let Some(left_value) = last_result.take() else {
4742                        return Err(Self::missing_result_error(&node));
4743                    };
4744                    stack.push(State::EvaluateRight { node, left: left_value });
4745                }
4746                State::EvaluateRight { node, left } => {
4747                    let right_part = node.right.clone();
4748                    match right_part {
4749                        BinaryPart::BinaryExpression(child) => {
4750                            stack.push(State::FromRight { node, left });
4751                            stack.push(State::EvaluateLeft(child.into_node()));
4752                        }
4753                        part => {
4754                            let right_value = part.get_result(exec_state, ctx).await?;
4755                            let right_value = control_continue!(right_value);
4756                            let result = node.apply_operator(exec_state, ctx, left, right_value).await?;
4757                            last_result = Some(result);
4758                        }
4759                    }
4760                }
4761                State::FromRight { node, left } => {
4762                    let Some(right_value) = last_result.take() else {
4763                        return Err(Self::missing_result_error(&node));
4764                    };
4765                    let result = node.apply_operator(exec_state, ctx, left, right_value).await?;
4766                    last_result = Some(result);
4767                }
4768            }
4769        }
4770
4771        last_result
4772            .map(KclValue::continue_)
4773            .ok_or_else(|| Self::missing_result_error(self))
4774    }
4775
4776    pub(super) async fn apply_operator(
4777        &self,
4778        exec_state: &mut ExecState,
4779        ctx: &ExecutorContext,
4780        left_value: KclValue,
4781        right_value: KclValue,
4782    ) -> Result<KclValue, KclError> {
4783        let mut meta = left_value.metadata();
4784        meta.extend(right_value.metadata());
4785        // Repeated arithmetic must not multiply copies of the same source range.
4786        let mut seen = HashSet::new();
4787        meta.retain(|metadata| seen.insert(metadata.source_range));
4788
4789        // First check if we are doing string concatenation.
4790        if self.operator == BinaryOperator::Add
4791            && let (KclValue::String { value: left, .. }, KclValue::String { value: right, .. }) =
4792                (&left_value, &right_value)
4793        {
4794            return Ok(KclValue::String {
4795                value: format!("{left}{right}"),
4796                meta,
4797            });
4798        }
4799
4800        // Then check if we have solids.
4801        if self.operator == BinaryOperator::Add || self.operator == BinaryOperator::Or {
4802            if let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value) {
4803                let args = Args::new_no_args(
4804                    self.into(),
4805                    self.node_path.clone(),
4806                    ctx.clone(),
4807                    Some("union".to_owned()),
4808                );
4809                let result = crate::std::csg::inner_union(
4810                    vec![*left.clone(), *right.clone()],
4811                    Default::default(),
4812                    crate::std::csg::CsgAlgorithm::Latest,
4813                    exec_state,
4814                    args,
4815                )
4816                .await?;
4817                return Ok(result.into());
4818            }
4819        } else if self.operator == BinaryOperator::Sub {
4820            // Check if we have solids.
4821            if let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value) {
4822                let args = Args::new_no_args(
4823                    self.into(),
4824                    self.node_path.clone(),
4825                    ctx.clone(),
4826                    Some("subtract".to_owned()),
4827                );
4828                let result = crate::std::csg::inner_subtract(
4829                    vec![*left.clone()],
4830                    vec![*right.clone()],
4831                    Default::default(),
4832                    crate::std::csg::CsgAlgorithm::Latest,
4833                    exec_state,
4834                    args,
4835                )
4836                .await?;
4837                return Ok(result.into());
4838            }
4839        } else if self.operator == BinaryOperator::And
4840            && let (KclValue::Solid { value: left }, KclValue::Solid { value: right }) = (&left_value, &right_value)
4841        {
4842            // Check if we have solids.
4843            let args = Args::new_no_args(
4844                self.into(),
4845                self.node_path.clone(),
4846                ctx.clone(),
4847                Some("intersect".to_owned()),
4848            );
4849            let result = crate::std::csg::inner_intersect(
4850                vec![*left.clone(), *right.clone()],
4851                Default::default(),
4852                crate::std::csg::CsgAlgorithm::Latest,
4853                exec_state,
4854                args,
4855            )
4856            .await?;
4857            return Ok(result.into());
4858        }
4859
4860        // Check if we are doing logical operations on booleans.
4861        if self.operator == BinaryOperator::Or || self.operator == BinaryOperator::And {
4862            let KclValue::Bool { value: left_value, .. } = left_value else {
4863                return Err(KclError::new_semantic(KclErrorDetails::new(
4864                    format!(
4865                        "Cannot apply logical operator to non-boolean value: {}",
4866                        left_value.human_friendly_type()
4867                    ),
4868                    vec![self.left.clone().into()],
4869                )));
4870            };
4871            let KclValue::Bool { value: right_value, .. } = right_value else {
4872                return Err(KclError::new_semantic(KclErrorDetails::new(
4873                    format!(
4874                        "Cannot apply logical operator to non-boolean value: {}",
4875                        right_value.human_friendly_type()
4876                    ),
4877                    vec![self.right.clone().into()],
4878                )));
4879            };
4880            let raw_value = match self.operator {
4881                BinaryOperator::Or => left_value || right_value,
4882                BinaryOperator::And => left_value && right_value,
4883                _ => unreachable!(),
4884            };
4885            return Ok(KclValue::Bool { value: raw_value, meta });
4886        }
4887
4888        // Check if we're doing equivalence in sketch mode.
4889        if self.operator == BinaryOperator::Eq && exec_state.mod_local.sketch_block.is_some() {
4890            match (&left_value, &right_value) {
4891                // Same sketch variables.
4892                (KclValue::SketchVar { value: left_value, .. }, KclValue::SketchVar { value: right_value, .. })
4893                    if left_value.id == right_value.id =>
4894                {
4895                    return Ok(KclValue::none());
4896                }
4897                // Different sketch variables.
4898                (KclValue::SketchVar { value: var0 }, KclValue::SketchVar { value: var1, .. }) => {
4899                    let constraint = Constraint::ScalarEqual(
4900                        var0.id.to_constraint_id(self.as_source_range())?,
4901                        var1.id.to_constraint_id(self.as_source_range())?,
4902                    );
4903                    let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
4904                        let message = "Being inside a sketch block should have already been checked above".to_owned();
4905                        debug_assert!(false, "{}", &message);
4906                        return Err(internal_err(message, self));
4907                    };
4908                    sketch_block_state.solver_constraints.push(constraint);
4909                    return Ok(KclValue::none());
4910                }
4911                // One sketch variable, one number.
4912                (KclValue::SketchVar { value: var, .. }, input_number @ KclValue::Number { .. })
4913                | (input_number @ KclValue::Number { .. }, KclValue::SketchVar { value: var, .. }) => {
4914                    let number_value = normalize_to_solver_distance_unit(
4915                        input_number,
4916                        input_number.into(),
4917                        exec_state,
4918                        "fixed constraint value",
4919                    )?;
4920                    let Some(n) = number_value.as_ty_f64() else {
4921                        let message = format!(
4922                            "Expected number after coercion, but found {}",
4923                            number_value.human_friendly_type()
4924                        );
4925                        debug_assert!(false, "{}", &message);
4926                        return Err(internal_err(message, self));
4927                    };
4928                    let constraint = Constraint::Fixed(var.id.to_constraint_id(self.as_source_range())?, n.n);
4929                    let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
4930                        let message = "Being inside a sketch block should have already been checked above".to_owned();
4931                        debug_assert!(false, "{}", &message);
4932                        return Err(internal_err(message, self));
4933                    };
4934                    sketch_block_state.solver_constraints.push(constraint);
4935                    exec_state.warn_experimental("scalar fixed constraint", self.as_source_range());
4936                    return Ok(KclValue::none());
4937                }
4938                // One sketch constraint, one number.
4939                (KclValue::SketchConstraint { value: constraint }, input_number @ KclValue::Number { .. })
4940                | (input_number @ KclValue::Number { .. }, KclValue::SketchConstraint { value: constraint }) => {
4941                    let number_value = match constraint.kind {
4942                        // These constraint kinds expect the RHS to be an angle.
4943                        SketchConstraintKind::Angle { .. } => normalize_to_solver_angle_unit(
4944                            input_number,
4945                            input_number.into(),
4946                            exec_state,
4947                            "fixed constraint value",
4948                        )?,
4949                        // These constraint kinds expect the RHS to be a distance.
4950                        SketchConstraintKind::Distance { .. }
4951                        | SketchConstraintKind::PointLineDistance { .. }
4952                        | SketchConstraintKind::LineLineDistance { .. }
4953                        | SketchConstraintKind::PointCircularDistance { .. }
4954                        | SketchConstraintKind::LineCircularDistance { .. }
4955                        | SketchConstraintKind::CircularCircularDistance { .. }
4956                        | SketchConstraintKind::Radius { .. }
4957                        | SketchConstraintKind::Diameter { .. }
4958                        | SketchConstraintKind::HorizontalDistance { .. }
4959                        | SketchConstraintKind::VerticalDistance { .. } => normalize_to_solver_distance_unit(
4960                            input_number,
4961                            input_number.into(),
4962                            exec_state,
4963                            "fixed constraint value",
4964                        )?,
4965                    };
4966                    let Some(n) = number_value.as_ty_f64() else {
4967                        let message = format!(
4968                            "Expected number after coercion, but found {}",
4969                            number_value.human_friendly_type()
4970                        );
4971                        debug_assert!(false, "{}", &message);
4972                        return Err(internal_err(message, self));
4973                    };
4974                    // Recast the number side of == to get the source expression text.
4975                    let number_binary_part = if matches!(&left_value, KclValue::SketchConstraint { .. }) {
4976                        &self.right
4977                    } else {
4978                        &self.left
4979                    };
4980                    let source = {
4981                        use crate::unparser::ExprContext;
4982                        let mut buf = String::new();
4983                        number_binary_part.recast(&mut buf, &Default::default(), 0, ExprContext::Other);
4984                        crate::frontend::sketch::ConstraintSource {
4985                            expr: buf,
4986                            is_literal: matches!(number_binary_part, BinaryPart::Literal(_)),
4987                        }
4988                    };
4989
4990                    match &constraint.kind {
4991                        SketchConstraintKind::Angle {
4992                            line0,
4993                            line1,
4994                            mode,
4995                            label_position,
4996                        } => {
4997                            let range = self.as_source_range();
4998                            let desired_angle = match n.ty {
4999                                NumericType::Known(crate::exec::UnitType::Angle(crate::exec::UnitAngle::Degrees))
5000                                | NumericType::Default {
5001                                    len: _,
5002                                    angle: UnitAngle::Degrees,
5003                                } => ezpz::datatypes::Angle::from_degrees(n.n),
5004                                NumericType::Known(crate::exec::UnitType::Angle(crate::exec::UnitAngle::Radians))
5005                                | NumericType::Default {
5006                                    len: _,
5007                                    angle: UnitAngle::Radians,
5008                                } => ezpz::datatypes::Angle::from_radians(n.n),
5009                                NumericType::Known(crate::exec::UnitType::Count)
5010                                | NumericType::Known(crate::exec::UnitType::GenericLength)
5011                                | NumericType::Known(crate::exec::UnitType::GenericAngle)
5012                                | NumericType::Known(crate::exec::UnitType::Length(_))
5013                                | NumericType::Unknown
5014                                | NumericType::Any => {
5015                                    let message = format!("Expected angle but found {:?}", n);
5016                                    debug_assert!(false, "{}", &message);
5017                                    return Err(internal_err(message, self));
5018                                }
5019                            };
5020                            let angle_lowering = match *mode {
5021                                AngleConstraintMode::LinesAtAngle => {
5022                                    AngleConstraintLowering::LinesAtAngle(Box::new(PendingLegacyAngleRefactorMeta {
5023                                        source_range: constraint
5024                                            .meta
5025                                            .first()
5026                                            .map(|meta| meta.source_range)
5027                                            .unwrap_or(range),
5028                                        lines: [line0.clone(), line1.clone()],
5029                                        desired_angle_radians: desired_angle.to_radians(),
5030                                    }))
5031                                }
5032                                AngleConstraintMode::PointsAtAngle { sector, inverse } => {
5033                                    let sketch_vars = exec_state
5034                                        .mod_local
5035                                        .sketch_block
5036                                        .as_ref()
5037                                        .ok_or_else(|| {
5038                                            internal_err(
5039                                                "Being inside a sketch block should have already been checked above",
5040                                                self,
5041                                            )
5042                                        })?
5043                                        .sketch_vars
5044                                        .clone();
5045                                    let initial_line0 = constrainable_line_initial_positions(
5046                                        &sketch_vars,
5047                                        line0,
5048                                        exec_state,
5049                                        range,
5050                                        "angle line0",
5051                                    )?;
5052                                    let initial_line1 = constrainable_line_initial_positions(
5053                                        &sketch_vars,
5054                                        line1,
5055                                        exec_state,
5056                                        range,
5057                                        "angle line1",
5058                                    )?;
5059                                    let Some(initial_vertex) = intersect_lines_2d(initial_line0, initial_line1) else {
5060                                        return Err(KclError::new_semantic(KclErrorDetails::new(
5061                                            "angleDimension(lines = ..., sector = ...) requires non-parallel lines"
5062                                                .to_owned(),
5063                                            vec![range],
5064                                        )));
5065                                    };
5066                                    let (line0_representative, line0_direction) =
5067                                        representative_angle_endpoint(line0, initial_line0, initial_vertex, range)?;
5068                                    let (line1_representative, line1_direction) =
5069                                        representative_angle_endpoint(line1, initial_line1, initial_vertex, range)?;
5070                                    let sector_rays = angle_sector_rays(sector, inverse);
5071                                    let angle_kind =
5072                                        ezpz::datatypes::AngleKind::Other(remap_angle_for_representative_rays(
5073                                            sector_rays,
5074                                            [line0_direction, line1_direction],
5075                                            desired_angle,
5076                                        ));
5077                                    AngleConstraintLowering::PointsAtAngle(PointsAtAngleLineData {
5078                                        initial_vertex,
5079                                        representative_points: [line0_representative, line1_representative],
5080                                        angle_kind,
5081                                    })
5082                                }
5083                            };
5084                            let sketch_var_ty = solver_numeric_type(exec_state);
5085                            let constraint_id = exec_state.next_object_id();
5086                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
5087                                let message =
5088                                    "Being inside a sketch block should have already been checked above".to_owned();
5089                                debug_assert!(false, "{}", &message);
5090                                return Err(internal_err(message, self));
5091                            };
5092                            match angle_lowering {
5093                                AngleConstraintLowering::LinesAtAngle(refactor_meta) => {
5094                                    sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
5095                                        datum_line_from_constrainable(line0, range)?,
5096                                        datum_line_from_constrainable(line1, range)?,
5097                                        ezpz::datatypes::AngleKind::Other(desired_angle),
5098                                    ));
5099                                    sketch_block_state
5100                                        .pending_legacy_angle_refactor_metadata
5101                                        .push(*refactor_meta);
5102                                }
5103                                AngleConstraintLowering::PointsAtAngle(points_at_angle_data) => {
5104                                    push_points_at_angle_for_lines(
5105                                        sketch_block_state,
5106                                        sketch_var_ty,
5107                                        [line0, line1],
5108                                        points_at_angle_data,
5109                                        range,
5110                                    )?
5111                                }
5112                            }
5113                            use crate::execution::Artifact;
5114                            use crate::execution::CodeRef;
5115                            use crate::execution::SketchBlockConstraint;
5116                            use crate::front::Angle;
5117                            use crate::front::SourceRef;
5118
5119                            let Some(sketch_id) = sketch_block_state.sketch_id else {
5120                                let message = "Sketch id missing for constraint artifact".to_owned();
5121                                debug_assert!(false, "{}", &message);
5122                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
5123                            };
5124                            let (sector, inverse) = match *mode {
5125                                AngleConstraintMode::LinesAtAngle => (None, None),
5126                                AngleConstraintMode::PointsAtAngle { sector, inverse } => {
5127                                    (Some(front_angle_sector(sector)), Some(inverse))
5128                                }
5129                            };
5130                            let sketch_constraint = crate::front::Constraint::Angle(Angle {
5131                                lines: vec![line0.object_id, line1.object_id],
5132                                angle: n.try_into().map_err(|_| {
5133                                    internal_err("Failed to convert angle units numeric suffix:", range)
5134                                })?,
5135                                sector,
5136                                inverse,
5137                                label_position: label_position.clone(),
5138                                source,
5139                            });
5140                            sketch_block_state.sketch_constraints.push(constraint_id);
5141                            let artifact_id = exec_state.next_artifact_id();
5142                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
5143                                id: artifact_id,
5144                                sketch_id,
5145                                constraint_id,
5146                                constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
5147                                code_ref: CodeRef::placeholder(range),
5148                            }));
5149                            exec_state.add_scene_object(
5150                                Object {
5151                                    id: constraint_id,
5152                                    kind: ObjectKind::Constraint {
5153                                        constraint: sketch_constraint,
5154                                    },
5155                                    label: Default::default(),
5156                                    comments: Default::default(),
5157                                    artifact_id,
5158                                    source: SourceRef::new(range, self.node_path.clone()),
5159                                },
5160                                range,
5161                            );
5162                        }
5163                        SketchConstraintKind::Distance { points, label_position } => {
5164                            let range = self.as_source_range();
5165                            let p0 = &points[0];
5166                            let p1 = &points[1];
5167                            let sketch_var_ty = solver_numeric_type(exec_state);
5168                            let constraint_id = exec_state.next_object_id();
5169                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
5170                                let message =
5171                                    "Being inside a sketch block should have already been checked above".to_owned();
5172                                debug_assert!(false, "{}", &message);
5173                                return Err(internal_err(message, self));
5174                            };
5175                            match (p0, p1) {
5176                                (
5177                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(p0),
5178                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(p1),
5179                                ) => {
5180                                    let solver_pt0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
5181                                        p0.vars.x.to_constraint_id(range)?,
5182                                        p0.vars.y.to_constraint_id(range)?,
5183                                    );
5184                                    let solver_pt1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
5185                                        p1.vars.x.to_constraint_id(range)?,
5186                                        p1.vars.y.to_constraint_id(range)?,
5187                                    );
5188                                    sketch_block_state
5189                                        .solver_constraints
5190                                        .push(Constraint::Distance(solver_pt0, solver_pt1, n.n));
5191                                }
5192                                (
5193                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
5194                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
5195                                )
5196                                | (
5197                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
5198                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
5199                                ) => {
5200                                    let origin_x_id = sketch_block_state.next_sketch_var_id();
5201                                    sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5202                                        value: Box::new(crate::execution::SketchVar {
5203                                            id: origin_x_id,
5204                                            initial_value: 0.0,
5205                                            ty: sketch_var_ty,
5206                                            // Synthesized origin coord for distance(); not source-backed.
5207                                            node_path: None,
5208                                            meta: vec![],
5209                                        }),
5210                                    });
5211                                    let origin_y_id = sketch_block_state.next_sketch_var_id();
5212                                    sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5213                                        value: Box::new(crate::execution::SketchVar {
5214                                            id: origin_y_id,
5215                                            initial_value: 0.0,
5216                                            ty: sketch_var_ty,
5217                                            // Synthesized origin coord for distance(); not source-backed.
5218                                            node_path: None,
5219                                            meta: vec![],
5220                                        }),
5221                                    });
5222                                    let origin_x = origin_x_id.to_constraint_id(range)?;
5223                                    let origin_y = origin_y_id.to_constraint_id(range)?;
5224                                    sketch_block_state
5225                                        .solver_constraints
5226                                        .push(Constraint::Fixed(origin_x, 0.0));
5227                                    sketch_block_state
5228                                        .solver_constraints
5229                                        .push(Constraint::Fixed(origin_y, 0.0));
5230                                    let solver_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
5231                                        point.vars.x.to_constraint_id(range)?,
5232                                        point.vars.y.to_constraint_id(range)?,
5233                                    );
5234                                    let origin_point = ezpz::datatypes::inputs::DatumPoint::new_xy(origin_x, origin_y);
5235                                    sketch_block_state.solver_constraints.push(Constraint::Distance(
5236                                        solver_point,
5237                                        origin_point,
5238                                        n.n,
5239                                    ));
5240                                }
5241                                (
5242                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
5243                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
5244                                ) => {
5245                                    return Err(internal_err(
5246                                        "distance() cannot constrain ORIGIN against ORIGIN".to_owned(),
5247                                        range,
5248                                    ));
5249                                }
5250                            }
5251                            use crate::execution::Artifact;
5252                            use crate::execution::CodeRef;
5253                            use crate::execution::SketchBlockConstraint;
5254                            use crate::front::Distance;
5255                            use crate::front::SourceRef;
5256                            use crate::frontend::sketch::ConstraintSegment;
5257
5258                            let Some(sketch_id) = sketch_block_state.sketch_id else {
5259                                let message = "Sketch id missing for constraint artifact".to_owned();
5260                                debug_assert!(false, "{}", &message);
5261                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
5262                            };
5263                            let sketch_constraint = crate::front::Constraint::Distance(Distance {
5264                                segments: vec![
5265                                    match p0 {
5266                                        crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
5267                                            ConstraintSegment::from(point.object_id)
5268                                        }
5269                                        crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
5270                                            ConstraintSegment::ORIGIN
5271                                        }
5272                                    },
5273                                    match p1 {
5274                                        crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
5275                                            ConstraintSegment::from(point.object_id)
5276                                        }
5277                                        crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
5278                                            ConstraintSegment::ORIGIN
5279                                        }
5280                                    },
5281                                ],
5282                                distance: n.try_into().map_err(|_| {
5283                                    internal_err("Failed to convert distance units numeric suffix:", range)
5284                                })?,
5285                                label_position: label_position.clone(),
5286                                source,
5287                            });
5288                            sketch_block_state.sketch_constraints.push(constraint_id);
5289                            let artifact_id = exec_state.next_artifact_id();
5290                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
5291                                id: artifact_id,
5292                                sketch_id,
5293                                constraint_id,
5294                                constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
5295                                code_ref: CodeRef::placeholder(range),
5296                            }));
5297                            exec_state.add_scene_object(
5298                                Object {
5299                                    id: constraint_id,
5300                                    kind: ObjectKind::Constraint {
5301                                        constraint: sketch_constraint,
5302                                    },
5303                                    label: Default::default(),
5304                                    comments: Default::default(),
5305                                    artifact_id,
5306                                    source: SourceRef::new(range, self.node_path.clone()),
5307                                },
5308                                range,
5309                            );
5310                        }
5311                        SketchConstraintKind::PointLineDistance {
5312                            point,
5313                            line,
5314                            input_object_ids,
5315                            label_position,
5316                        } => {
5317                            let range = self.as_source_range();
5318                            let sketch_var_ty = solver_numeric_type(exec_state);
5319                            let sketch_vars = exec_state
5320                                .mod_local
5321                                .sketch_block
5322                                .as_ref()
5323                                .ok_or_else(|| {
5324                                    internal_err(
5325                                        "Being inside a sketch block should have already been checked above",
5326                                        self,
5327                                    )
5328                                })?
5329                                .sketch_vars
5330                                .clone();
5331                            let support_initial =
5332                                projected_point_on_line_initial_position(&sketch_vars, point, line, exec_state, range)?;
5333                            let solver_line = datum_line_from_constrainable(line, range)?;
5334
5335                            let constraint_id = exec_state.next_object_id();
5336                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
5337                                let message =
5338                                    "Being inside a sketch block should have already been checked above".to_owned();
5339                                debug_assert!(false, "{}", &message);
5340                                return Err(internal_err(message, self));
5341                            };
5342
5343                            // Lower point-line distance by adding a hidden
5344                            // support point on the line, then constrain the
5345                            // selected point-to-support segment to be
5346                            // perpendicular and equal to the requested
5347                            // distance.
5348                            let solver_point = datum_point_from_constrainable_or_origin(
5349                                sketch_block_state,
5350                                sketch_var_ty,
5351                                point,
5352                                range,
5353                            )?;
5354                            let support_x_id = sketch_block_state.next_sketch_var_id();
5355                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5356                                value: Box::new(crate::execution::SketchVar {
5357                                    id: support_x_id,
5358                                    initial_value: support_initial[0],
5359                                    ty: sketch_var_ty,
5360                                    // Synthesized support point coord for distance lowering; not source-backed.
5361                                    node_path: None,
5362                                    meta: vec![],
5363                                }),
5364                            });
5365                            let support_y_id = sketch_block_state.next_sketch_var_id();
5366                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5367                                value: Box::new(crate::execution::SketchVar {
5368                                    id: support_y_id,
5369                                    initial_value: support_initial[1],
5370                                    ty: sketch_var_ty,
5371                                    // Synthesized support point coord for distance lowering; not source-backed.
5372                                    node_path: None,
5373                                    meta: vec![],
5374                                }),
5375                            });
5376                            let support_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
5377                                support_x_id.to_constraint_id(range)?,
5378                                support_y_id.to_constraint_id(range)?,
5379                            );
5380                            let support_line =
5381                                ezpz::datatypes::inputs::DatumLineSegment::new(solver_point, support_point);
5382
5383                            sketch_block_state
5384                                .solver_constraints
5385                                .push(Constraint::PointLineDistance(support_point, solver_line, 0.0));
5386                            sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
5387                                support_line,
5388                                solver_line,
5389                                ezpz::datatypes::AngleKind::Perpendicular,
5390                            ));
5391                            sketch_block_state.solver_constraints.push(Constraint::Distance(
5392                                solver_point,
5393                                support_point,
5394                                n.n,
5395                            ));
5396
5397                            use crate::execution::Artifact;
5398                            use crate::execution::CodeRef;
5399                            use crate::execution::SketchBlockConstraint;
5400                            use crate::front::Distance;
5401                            use crate::front::SourceRef;
5402                            use crate::frontend::sketch::ConstraintSegment;
5403
5404                            let Some(sketch_id) = sketch_block_state.sketch_id else {
5405                                let message = "Sketch id missing for constraint artifact".to_owned();
5406                                debug_assert!(false, "{}", &message);
5407                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
5408                            };
5409                            let sketch_constraint = crate::front::Constraint::Distance(Distance {
5410                                segments: input_object_ids
5411                                    .iter()
5412                                    .copied()
5413                                    .map(|id| id.map_or(ConstraintSegment::ORIGIN, ConstraintSegment::from))
5414                                    .collect(),
5415                                distance: n.try_into().map_err(|_| {
5416                                    internal_err("Failed to convert distance units numeric suffix:", range)
5417                                })?,
5418                                label_position: label_position.clone(),
5419                                source,
5420                            });
5421                            sketch_block_state.sketch_constraints.push(constraint_id);
5422                            let artifact_id = exec_state.next_artifact_id();
5423                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
5424                                id: artifact_id,
5425                                sketch_id,
5426                                constraint_id,
5427                                constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
5428                                code_ref: CodeRef::placeholder(range),
5429                            }));
5430                            exec_state.add_scene_object(
5431                                Object {
5432                                    id: constraint_id,
5433                                    kind: ObjectKind::Constraint {
5434                                        constraint: sketch_constraint,
5435                                    },
5436                                    label: Default::default(),
5437                                    comments: Default::default(),
5438                                    artifact_id,
5439                                    source: SourceRef::new(range, self.node_path.clone()),
5440                                },
5441                                range,
5442                            );
5443                        }
5444                        SketchConstraintKind::LineLineDistance {
5445                            line0,
5446                            line1,
5447                            input_object_ids,
5448                            label_position,
5449                        } => {
5450                            let range = self.as_source_range();
5451                            let reference_point = crate::execution::ConstrainablePoint2d {
5452                                vars: line0.vars[0].clone(),
5453                                object_id: line0.object_id,
5454                            };
5455                            let sketch_var_ty = solver_numeric_type(exec_state);
5456                            let sketch_vars = exec_state
5457                                .mod_local
5458                                .sketch_block
5459                                .as_ref()
5460                                .ok_or_else(|| {
5461                                    internal_err(
5462                                        "Being inside a sketch block should have already been checked above",
5463                                        self,
5464                                    )
5465                                })?
5466                                .sketch_vars
5467                                .clone();
5468                            let support_initial = projected_point_on_line_initial_position(
5469                                &sketch_vars,
5470                                &crate::execution::ConstrainablePoint2dOrOrigin::Point(reference_point.clone()),
5471                                line1,
5472                                exec_state,
5473                                range,
5474                            )?;
5475                            let solver_point = datum_point_from_constrainable(&reference_point, range)?;
5476                            let solver_line0 = datum_line_from_constrainable(line0, range)?;
5477                            let solver_line1 = datum_line_from_constrainable(line1, range)?;
5478
5479                            let constraint_id = exec_state.next_object_id();
5480                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
5481                                let message =
5482                                    "Being inside a sketch block should have already been checked above".to_owned();
5483                                debug_assert!(false, "{}", &message);
5484                                return Err(internal_err(message, self));
5485                            };
5486
5487                            // Lower line-line distance to the point-line
5488                            // construction above by choosing one endpoint on
5489                            // line0 as the reference point, forcing the lines
5490                            // parallel, and measuring perpendicularly to
5491                            // line1.
5492                            let support_x_id = sketch_block_state.next_sketch_var_id();
5493                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5494                                value: Box::new(crate::execution::SketchVar {
5495                                    id: support_x_id,
5496                                    initial_value: support_initial[0],
5497                                    ty: sketch_var_ty,
5498                                    // Synthesized support point coord for distance lowering; not source-backed.
5499                                    node_path: None,
5500                                    meta: vec![],
5501                                }),
5502                            });
5503                            let support_y_id = sketch_block_state.next_sketch_var_id();
5504                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5505                                value: Box::new(crate::execution::SketchVar {
5506                                    id: support_y_id,
5507                                    initial_value: support_initial[1],
5508                                    ty: sketch_var_ty,
5509                                    // Synthesized support point coord for distance lowering; not source-backed.
5510                                    node_path: None,
5511                                    meta: vec![],
5512                                }),
5513                            });
5514                            let support_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
5515                                support_x_id.to_constraint_id(range)?,
5516                                support_y_id.to_constraint_id(range)?,
5517                            );
5518                            let support_line =
5519                                ezpz::datatypes::inputs::DatumLineSegment::new(solver_point, support_point);
5520
5521                            sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
5522                                solver_line0,
5523                                solver_line1,
5524                                ezpz::datatypes::AngleKind::Parallel,
5525                            ));
5526                            sketch_block_state
5527                                .solver_constraints
5528                                .push(Constraint::PointLineDistance(support_point, solver_line1, 0.0));
5529                            sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
5530                                support_line,
5531                                solver_line1,
5532                                ezpz::datatypes::AngleKind::Perpendicular,
5533                            ));
5534                            sketch_block_state.solver_constraints.push(Constraint::Distance(
5535                                solver_point,
5536                                support_point,
5537                                n.n,
5538                            ));
5539
5540                            use crate::execution::Artifact;
5541                            use crate::execution::CodeRef;
5542                            use crate::execution::SketchBlockConstraint;
5543                            use crate::front::Distance;
5544                            use crate::front::SourceRef;
5545                            use crate::frontend::sketch::ConstraintSegment;
5546
5547                            let Some(sketch_id) = sketch_block_state.sketch_id else {
5548                                let message = "Sketch id missing for constraint artifact".to_owned();
5549                                debug_assert!(false, "{}", &message);
5550                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
5551                            };
5552                            let sketch_constraint = crate::front::Constraint::Distance(Distance {
5553                                segments: input_object_ids.iter().copied().map(ConstraintSegment::from).collect(),
5554                                distance: n.try_into().map_err(|_| {
5555                                    internal_err("Failed to convert distance units numeric suffix:", range)
5556                                })?,
5557                                label_position: label_position.clone(),
5558                                source,
5559                            });
5560                            sketch_block_state.sketch_constraints.push(constraint_id);
5561                            let artifact_id = exec_state.next_artifact_id();
5562                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
5563                                id: artifact_id,
5564                                sketch_id,
5565                                constraint_id,
5566                                constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
5567                                code_ref: CodeRef::placeholder(range),
5568                            }));
5569                            exec_state.add_scene_object(
5570                                Object {
5571                                    id: constraint_id,
5572                                    kind: ObjectKind::Constraint {
5573                                        constraint: sketch_constraint,
5574                                    },
5575                                    label: Default::default(),
5576                                    comments: Default::default(),
5577                                    artifact_id,
5578                                    source: SourceRef::new(range, self.node_path.clone()),
5579                                },
5580                                range,
5581                            );
5582                        }
5583                        SketchConstraintKind::PointCircularDistance {
5584                            point,
5585                            center,
5586                            start,
5587                            end,
5588                            input_object_ids,
5589                            label_position,
5590                        } => {
5591                            let range = self.as_source_range();
5592                            let sketch_var_ty = solver_numeric_type(exec_state);
5593                            let sketch_vars = exec_state
5594                                .mod_local
5595                                .sketch_block
5596                                .as_ref()
5597                                .ok_or_else(|| {
5598                                    internal_err(
5599                                        "Being inside a sketch block should have already been checked above",
5600                                        self,
5601                                    )
5602                                })?
5603                                .sketch_vars
5604                                .clone();
5605                            let circular =
5606                                circular_distance_datums(&sketch_vars, center, start, end.as_ref(), exec_state, range)?;
5607
5608                            let constraint_id = exec_state.next_object_id();
5609                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
5610                                let message =
5611                                    "Being inside a sketch block should have already been checked above".to_owned();
5612                                debug_assert!(false, "{}", &message);
5613                                return Err(internal_err(message, self));
5614                            };
5615
5616                            // Lower point-circular distance to exterior
5617                            // circle tangency: a hidden circle centered on the
5618                            // point has radius equal to the requested distance
5619                            // and is tangent to the target arc/circle.
5620                            let target_point = datum_point_from_constrainable_or_origin(
5621                                sketch_block_state,
5622                                sketch_var_ty,
5623                                point,
5624                                range,
5625                            )?;
5626                            push_circular_distance_constraints(
5627                                sketch_block_state,
5628                                sketch_var_ty,
5629                                target_point,
5630                                circular,
5631                                n.n,
5632                                range,
5633                            )?;
5634
5635                            use crate::execution::Artifact;
5636                            use crate::execution::CodeRef;
5637                            use crate::execution::SketchBlockConstraint;
5638                            use crate::front::Distance;
5639                            use crate::front::SourceRef;
5640                            use crate::frontend::sketch::ConstraintSegment;
5641
5642                            let Some(sketch_id) = sketch_block_state.sketch_id else {
5643                                let message = "Sketch id missing for constraint artifact".to_owned();
5644                                debug_assert!(false, "{}", &message);
5645                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
5646                            };
5647                            let sketch_constraint = crate::front::Constraint::Distance(Distance {
5648                                segments: input_object_ids
5649                                    .iter()
5650                                    .copied()
5651                                    .map(|id| id.map_or(ConstraintSegment::ORIGIN, ConstraintSegment::from))
5652                                    .collect(),
5653                                distance: n.try_into().map_err(|_| {
5654                                    internal_err("Failed to convert distance units numeric suffix:", range)
5655                                })?,
5656                                label_position: label_position.clone(),
5657                                source,
5658                            });
5659                            sketch_block_state.sketch_constraints.push(constraint_id);
5660                            let artifact_id = exec_state.next_artifact_id();
5661                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
5662                                id: artifact_id,
5663                                sketch_id,
5664                                constraint_id,
5665                                constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
5666                                code_ref: CodeRef::placeholder(range),
5667                            }));
5668                            exec_state.add_scene_object(
5669                                Object {
5670                                    id: constraint_id,
5671                                    kind: ObjectKind::Constraint {
5672                                        constraint: sketch_constraint,
5673                                    },
5674                                    label: Default::default(),
5675                                    comments: Default::default(),
5676                                    artifact_id,
5677                                    source: SourceRef::new(range, self.node_path.clone()),
5678                                },
5679                                range,
5680                            );
5681                        }
5682                        SketchConstraintKind::LineCircularDistance {
5683                            line,
5684                            center,
5685                            start,
5686                            end,
5687                            input_object_ids,
5688                            label_position,
5689                        } => {
5690                            let range = self.as_source_range();
5691                            let sketch_var_ty = solver_numeric_type(exec_state);
5692                            let sketch_vars = exec_state
5693                                .mod_local
5694                                .sketch_block
5695                                .as_ref()
5696                                .ok_or_else(|| {
5697                                    internal_err(
5698                                        "Being inside a sketch block should have already been checked above",
5699                                        self,
5700                                    )
5701                                })?
5702                                .sketch_vars
5703                                .clone();
5704                            let support_initial = projected_point_on_line_initial_position(
5705                                &sketch_vars,
5706                                &crate::execution::ConstrainablePoint2dOrOrigin::Point(center.clone()),
5707                                line,
5708                                exec_state,
5709                                range,
5710                            )?;
5711                            let solver_line = datum_line_from_constrainable(line, range)?;
5712                            let circular =
5713                                circular_distance_datums(&sketch_vars, center, start, end.as_ref(), exec_state, range)?;
5714
5715                            let constraint_id = exec_state.next_object_id();
5716                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
5717                                let message =
5718                                    "Being inside a sketch block should have already been checked above".to_owned();
5719                                debug_assert!(false, "{}", &message);
5720                                return Err(internal_err(message, self));
5721                            };
5722
5723                            // Lower line-circular distance by first projecting
5724                            // the circular center onto the target line with a
5725                            // hidden support point. The circular distance is
5726                            // then the point-circular construction from that
5727                            // support point.
5728                            let support_x_id = sketch_block_state.next_sketch_var_id();
5729                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5730                                value: Box::new(crate::execution::SketchVar {
5731                                    id: support_x_id,
5732                                    initial_value: support_initial[0],
5733                                    ty: sketch_var_ty,
5734                                    // Synthesized support point coord for distance lowering; not source-backed.
5735                                    node_path: None,
5736                                    meta: vec![],
5737                                }),
5738                            });
5739                            let support_y_id = sketch_block_state.next_sketch_var_id();
5740                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5741                                value: Box::new(crate::execution::SketchVar {
5742                                    id: support_y_id,
5743                                    initial_value: support_initial[1],
5744                                    ty: sketch_var_ty,
5745                                    // Synthesized support point coord for distance lowering; not source-backed.
5746                                    node_path: None,
5747                                    meta: vec![],
5748                                }),
5749                            });
5750                            let support_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
5751                                support_x_id.to_constraint_id(range)?,
5752                                support_y_id.to_constraint_id(range)?,
5753                            );
5754                            let support_line =
5755                                ezpz::datatypes::inputs::DatumLineSegment::new(circular.center, support_point);
5756
5757                            sketch_block_state
5758                                .solver_constraints
5759                                .push(Constraint::PointLineDistance(support_point, solver_line, 0.0));
5760                            sketch_block_state.solver_constraints.push(Constraint::LinesAtAngle(
5761                                support_line,
5762                                solver_line,
5763                                ezpz::datatypes::AngleKind::Perpendicular,
5764                            ));
5765                            push_circular_distance_constraints(
5766                                sketch_block_state,
5767                                sketch_var_ty,
5768                                support_point,
5769                                circular,
5770                                n.n,
5771                                range,
5772                            )?;
5773
5774                            use crate::execution::Artifact;
5775                            use crate::execution::CodeRef;
5776                            use crate::execution::SketchBlockConstraint;
5777                            use crate::front::Distance;
5778                            use crate::front::SourceRef;
5779                            use crate::frontend::sketch::ConstraintSegment;
5780
5781                            let Some(sketch_id) = sketch_block_state.sketch_id else {
5782                                let message = "Sketch id missing for constraint artifact".to_owned();
5783                                debug_assert!(false, "{}", &message);
5784                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
5785                            };
5786                            let sketch_constraint = crate::front::Constraint::Distance(Distance {
5787                                segments: input_object_ids.iter().copied().map(ConstraintSegment::from).collect(),
5788                                distance: n.try_into().map_err(|_| {
5789                                    internal_err("Failed to convert distance units numeric suffix:", range)
5790                                })?,
5791                                label_position: label_position.clone(),
5792                                source,
5793                            });
5794                            sketch_block_state.sketch_constraints.push(constraint_id);
5795                            let artifact_id = exec_state.next_artifact_id();
5796                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
5797                                id: artifact_id,
5798                                sketch_id,
5799                                constraint_id,
5800                                constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
5801                                code_ref: CodeRef::placeholder(range),
5802                            }));
5803                            exec_state.add_scene_object(
5804                                Object {
5805                                    id: constraint_id,
5806                                    kind: ObjectKind::Constraint {
5807                                        constraint: sketch_constraint,
5808                                    },
5809                                    label: Default::default(),
5810                                    comments: Default::default(),
5811                                    artifact_id,
5812                                    source: SourceRef::new(range, self.node_path.clone()),
5813                                },
5814                                range,
5815                            );
5816                        }
5817                        SketchConstraintKind::CircularCircularDistance {
5818                            center0,
5819                            start0,
5820                            end0,
5821                            center1,
5822                            start1,
5823                            end1,
5824                            input_object_ids,
5825                            label_position,
5826                        } => {
5827                            let range = self.as_source_range();
5828                            let sketch_var_ty = solver_numeric_type(exec_state);
5829                            let sketch_vars = exec_state
5830                                .mod_local
5831                                .sketch_block
5832                                .as_ref()
5833                                .ok_or_else(|| {
5834                                    internal_err(
5835                                        "Being inside a sketch block should have already been checked above",
5836                                        self,
5837                                    )
5838                                })?
5839                                .sketch_vars
5840                                .clone();
5841                            let circular0 = circular_distance_datums(
5842                                &sketch_vars,
5843                                center0,
5844                                start0,
5845                                end0.as_ref(),
5846                                exec_state,
5847                                range,
5848                            )?;
5849                            let circular1 = circular_distance_datums(
5850                                &sketch_vars,
5851                                center1,
5852                                start1,
5853                                end1.as_ref(),
5854                                exec_state,
5855                                range,
5856                            )?;
5857                            let support_initial = circular_circular_support_initial_position(
5858                                &sketch_vars,
5859                                center0,
5860                                center1,
5861                                circular0.radius_initial_value,
5862                                n.n,
5863                                exec_state,
5864                                range,
5865                            )?;
5866
5867                            let constraint_id = exec_state.next_object_id();
5868                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
5869                                let message =
5870                                    "Being inside a sketch block should have already been checked above".to_owned();
5871                                debug_assert!(false, "{}", &message);
5872                                return Err(internal_err(message, self));
5873                            };
5874
5875                            // Lower circular-circular distance with a hidden
5876                            // spacer circle of radius d/2. Constraining its
5877                            // center onto the line between target centers and
5878                            // making it exterior-tangent to both targets gives
5879                            // center distance r0 + d + r1.
5880                            let circular_target0 =
5881                                push_circular_radius_constraints(sketch_block_state, sketch_var_ty, circular0, range)?;
5882                            let circular_target1 =
5883                                push_circular_radius_constraints(sketch_block_state, sketch_var_ty, circular1, range)?;
5884
5885                            let support_x_id = sketch_block_state.next_sketch_var_id();
5886                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5887                                value: Box::new(crate::execution::SketchVar {
5888                                    id: support_x_id,
5889                                    initial_value: support_initial[0],
5890                                    ty: sketch_var_ty,
5891                                    // Synthesized support point coord for distance lowering; not source-backed.
5892                                    node_path: None,
5893                                    meta: vec![],
5894                                }),
5895                            });
5896                            let support_y_id = sketch_block_state.next_sketch_var_id();
5897                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5898                                value: Box::new(crate::execution::SketchVar {
5899                                    id: support_y_id,
5900                                    initial_value: support_initial[1],
5901                                    ty: sketch_var_ty,
5902                                    // Synthesized support point coord for distance lowering; not source-backed.
5903                                    node_path: None,
5904                                    meta: vec![],
5905                                }),
5906                            });
5907                            let support_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
5908                                support_x_id.to_constraint_id(range)?,
5909                                support_y_id.to_constraint_id(range)?,
5910                            );
5911
5912                            let support_radius_id = sketch_block_state.next_sketch_var_id();
5913                            let support_radius_value = n.n / 2.0;
5914                            sketch_block_state.sketch_vars.push(KclValue::SketchVar {
5915                                value: Box::new(crate::execution::SketchVar {
5916                                    id: support_radius_id,
5917                                    initial_value: support_radius_value,
5918                                    ty: sketch_var_ty,
5919                                    // Synthesized hidden support radius for circular-circular distance; not source-backed.
5920                                    node_path: None,
5921                                    meta: vec![],
5922                                }),
5923                            });
5924                            let support_radius =
5925                                ezpz::datatypes::inputs::DatumDistance::new(support_radius_id.to_constraint_id(range)?);
5926                            let support_circle = ezpz::datatypes::inputs::DatumCircle {
5927                                center: support_point,
5928                                radius: support_radius,
5929                            };
5930                            let center_line = ezpz::datatypes::inputs::DatumLineSegment::new(
5931                                circular_target0.center,
5932                                circular_target1.center,
5933                            );
5934
5935                            sketch_block_state
5936                                .solver_constraints
5937                                .push(Constraint::Fixed(support_radius.id, support_radius_value));
5938                            sketch_block_state
5939                                .solver_constraints
5940                                .push(Constraint::PointLineDistance(support_point, center_line, 0.0));
5941                            sketch_block_state
5942                                .solver_constraints
5943                                .push(Constraint::CircleTangentToCircle(
5944                                    circular_target0,
5945                                    support_circle,
5946                                    ezpz::CircleSide::Exterior,
5947                                ));
5948                            sketch_block_state
5949                                .solver_constraints
5950                                .push(Constraint::CircleTangentToCircle(
5951                                    support_circle,
5952                                    circular_target1,
5953                                    ezpz::CircleSide::Exterior,
5954                                ));
5955
5956                            use crate::execution::Artifact;
5957                            use crate::execution::CodeRef;
5958                            use crate::execution::SketchBlockConstraint;
5959                            use crate::front::Distance;
5960                            use crate::front::SourceRef;
5961                            use crate::frontend::sketch::ConstraintSegment;
5962
5963                            let Some(sketch_id) = sketch_block_state.sketch_id else {
5964                                let message = "Sketch id missing for constraint artifact".to_owned();
5965                                debug_assert!(false, "{}", &message);
5966                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
5967                            };
5968                            let sketch_constraint = crate::front::Constraint::Distance(Distance {
5969                                segments: input_object_ids.iter().copied().map(ConstraintSegment::from).collect(),
5970                                distance: n.try_into().map_err(|_| {
5971                                    internal_err("Failed to convert distance units numeric suffix:", range)
5972                                })?,
5973                                label_position: label_position.clone(),
5974                                source,
5975                            });
5976                            sketch_block_state.sketch_constraints.push(constraint_id);
5977                            let artifact_id = exec_state.next_artifact_id();
5978                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
5979                                id: artifact_id,
5980                                sketch_id,
5981                                constraint_id,
5982                                constraint_type: super::artifact::sketch_block_constraint_type(&sketch_constraint),
5983                                code_ref: CodeRef::placeholder(range),
5984                            }));
5985                            exec_state.add_scene_object(
5986                                Object {
5987                                    id: constraint_id,
5988                                    kind: ObjectKind::Constraint {
5989                                        constraint: sketch_constraint,
5990                                    },
5991                                    label: Default::default(),
5992                                    comments: Default::default(),
5993                                    artifact_id,
5994                                    source: SourceRef::new(range, self.node_path.clone()),
5995                                },
5996                                range,
5997                            );
5998                        }
5999                        SketchConstraintKind::Radius { .. } | SketchConstraintKind::Diameter { .. } => {
6000                            #[derive(Clone, Copy)]
6001                            enum CircularSegmentConstraintTarget {
6002                                Arc {
6003                                    object_id: ObjectId,
6004                                    end: [crate::execution::SketchVarId; 2],
6005                                    direction: ArcDirection,
6006                                },
6007                                Circle {
6008                                    object_id: ObjectId,
6009                                },
6010                            }
6011
6012                            fn sketch_var_initial_value(
6013                                sketch_vars: &[KclValue],
6014                                id: crate::execution::SketchVarId,
6015                                exec_state: &mut ExecState,
6016                                range: SourceRange,
6017                            ) -> Result<f64, KclError> {
6018                                sketch_vars
6019                                    .get(id.0)
6020                                    .and_then(KclValue::as_sketch_var)
6021                                    .map(|sketch_var| {
6022                                        sketch_var
6023                                            .initial_value_to_solver_units(
6024                                                exec_state,
6025                                                range,
6026                                                "circle radius initial value",
6027                                            )
6028                                            .map(|value| value.n)
6029                                    })
6030                                    .transpose()?
6031                                    .ok_or_else(|| {
6032                                        internal_err(
6033                                            format!("Missing sketch variable initial value for id {}", id.0),
6034                                            range,
6035                                        )
6036                                    })
6037                            }
6038
6039                            let (points, label_position) = match &constraint.kind {
6040                                SketchConstraintKind::Radius { points, label_position } => {
6041                                    (points, label_position.clone())
6042                                }
6043                                SketchConstraintKind::Diameter { points, label_position } => {
6044                                    (points, label_position.clone())
6045                                }
6046                                _ => unreachable!(),
6047                            };
6048                            let range = self.as_source_range();
6049                            let center = &points[0];
6050                            let start = &points[1];
6051                            let Some(sketch_block_state) = &exec_state.mod_local.sketch_block else {
6052                                return Err(internal_err(
6053                                    "Being inside a sketch block should have already been checked above",
6054                                    self,
6055                                ));
6056                            };
6057                            let (constraint_name, is_diameter) = match &constraint.kind {
6058                                SketchConstraintKind::Radius { .. } => ("radius", false),
6059                                SketchConstraintKind::Diameter { .. } => ("diameter", true),
6060                                _ => unreachable!(),
6061                            };
6062                            let sketch_vars = sketch_block_state.sketch_vars.clone();
6063                            let target_segment = sketch_block_state
6064                                .needed_by_engine
6065                                .iter()
6066                                .find_map(|seg| match &seg.kind {
6067                                    UnsolvedSegmentKind::Arc {
6068                                        center_object_id,
6069                                        start_object_id,
6070                                        end,
6071                                        direction,
6072                                        ..
6073                                    } if *center_object_id == center.object_id
6074                                        && *start_object_id == start.object_id =>
6075                                    {
6076                                        let (end_x_var, end_y_var) = match (&end[0], &end[1]) {
6077                                            (UnsolvedExpr::Unknown(end_x), UnsolvedExpr::Unknown(end_y)) => {
6078                                                (*end_x, *end_y)
6079                                            }
6080                                            _ => return None,
6081                                        };
6082                                        Some(CircularSegmentConstraintTarget::Arc {
6083                                            object_id: seg.object_id,
6084                                            end: [end_x_var, end_y_var],
6085                                            direction: *direction,
6086                                        })
6087                                    }
6088                                    UnsolvedSegmentKind::Circle {
6089                                        center_object_id,
6090                                        start_object_id,
6091                                        ..
6092                                    } if *center_object_id == center.object_id
6093                                        && *start_object_id == start.object_id =>
6094                                    {
6095                                        Some(CircularSegmentConstraintTarget::Circle {
6096                                            object_id: seg.object_id,
6097                                        })
6098                                    }
6099                                    _ => None,
6100                                })
6101                                .ok_or_else(|| {
6102                                    internal_err(
6103                                        format!("Could not find circular segment for {} constraint", constraint_name),
6104                                        range,
6105                                    )
6106                                })?;
6107                            let radius_value = if is_diameter { n.n / 2.0 } else { n.n };
6108                            let center_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
6109                                center.vars.x.to_constraint_id(range)?,
6110                                center.vars.y.to_constraint_id(range)?,
6111                            );
6112                            let start_point = ezpz::datatypes::inputs::DatumPoint::new_xy(
6113                                start.vars.x.to_constraint_id(range)?,
6114                                start.vars.y.to_constraint_id(range)?,
6115                            );
6116                            let solver_constraint = match target_segment {
6117                                CircularSegmentConstraintTarget::Arc { end, direction, .. } => {
6118                                    let solver_arc = SolverArc::new(
6119                                        [center.vars.x, center.vars.y],
6120                                        [start.vars.x, start.vars.y],
6121                                        end,
6122                                        direction,
6123                                        range,
6124                                    )?;
6125                                    solver_arc.radius_constraint(radius_value)
6126                                }
6127                                CircularSegmentConstraintTarget::Circle { .. } => {
6128                                    let sketch_var_ty = solver_numeric_type(exec_state);
6129                                    let start_x =
6130                                        sketch_var_initial_value(&sketch_vars, start.vars.x, exec_state, range)?;
6131                                    let start_y =
6132                                        sketch_var_initial_value(&sketch_vars, start.vars.y, exec_state, range)?;
6133                                    let center_x =
6134                                        sketch_var_initial_value(&sketch_vars, center.vars.x, exec_state, range)?;
6135                                    let center_y =
6136                                        sketch_var_initial_value(&sketch_vars, center.vars.y, exec_state, range)?;
6137
6138                                    // Get the hypotenuse between the two points, the radius
6139                                    let radius_initial_value = libm::hypot(start_x - center_x, start_y - center_y);
6140
6141                                    let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
6142                                        let message =
6143                                            "Being inside a sketch block should have already been checked above"
6144                                                .to_owned();
6145                                        debug_assert!(false, "{}", &message);
6146                                        return Err(internal_err(message, self));
6147                                    };
6148                                    let radius_id = sketch_block_state.next_sketch_var_id();
6149                                    sketch_block_state.sketch_vars.push(KclValue::SketchVar {
6150                                        value: Box::new(crate::execution::SketchVar {
6151                                            id: radius_id,
6152                                            initial_value: radius_initial_value,
6153                                            ty: sketch_var_ty,
6154                                            // Synthesized hidden radius for circle constraint; not source-backed.
6155                                            node_path: None,
6156                                            meta: vec![],
6157                                        }),
6158                                    });
6159                                    let radius =
6160                                        ezpz::datatypes::inputs::DatumDistance::new(radius_id.to_constraint_id(range)?);
6161                                    let solver_circle = ezpz::datatypes::inputs::DatumCircle {
6162                                        center: center_point,
6163                                        radius,
6164                                    };
6165                                    sketch_block_state.solver_constraints.push(Constraint::DistanceVar(
6166                                        start_point,
6167                                        center_point,
6168                                        radius,
6169                                    ));
6170                                    Constraint::CircleRadius(solver_circle, radius_value)
6171                                }
6172                            };
6173
6174                            let constraint_id = exec_state.next_object_id();
6175                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
6176                                let message =
6177                                    "Being inside a sketch block should have already been checked above".to_owned();
6178                                debug_assert!(false, "{}", &message);
6179                                return Err(internal_err(message, self));
6180                            };
6181                            sketch_block_state.solver_constraints.push(solver_constraint);
6182                            use crate::execution::Artifact;
6183                            use crate::execution::CodeRef;
6184                            use crate::execution::SketchBlockConstraint;
6185                            use crate::front::SourceRef;
6186                            let segment_object_id = match target_segment {
6187                                CircularSegmentConstraintTarget::Arc { object_id, .. }
6188                                | CircularSegmentConstraintTarget::Circle { object_id } => object_id,
6189                            };
6190
6191                            let constraint = if is_diameter {
6192                                use crate::frontend::sketch::Diameter;
6193                                crate::front::Constraint::Diameter(Diameter {
6194                                    arc: segment_object_id,
6195                                    diameter: n.try_into().map_err(|_| {
6196                                        internal_err("Failed to convert diameter units numeric suffix:", range)
6197                                    })?,
6198                                    label_position,
6199                                    source,
6200                                })
6201                            } else {
6202                                use crate::frontend::sketch::Radius;
6203                                crate::front::Constraint::Radius(Radius {
6204                                    arc: segment_object_id,
6205                                    radius: n.try_into().map_err(|_| {
6206                                        internal_err("Failed to convert radius units numeric suffix:", range)
6207                                    })?,
6208                                    label_position,
6209                                    source,
6210                                })
6211                            };
6212                            sketch_block_state.sketch_constraints.push(constraint_id);
6213                            let Some(sketch_id) = sketch_block_state.sketch_id else {
6214                                let message = "Sketch id missing for constraint artifact".to_owned();
6215                                debug_assert!(false, "{}", &message);
6216                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
6217                            };
6218                            let artifact_id = exec_state.next_artifact_id();
6219                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
6220                                id: artifact_id,
6221                                sketch_id,
6222                                constraint_id,
6223                                constraint_type: super::artifact::sketch_block_constraint_type(&constraint),
6224                                code_ref: CodeRef::placeholder(range),
6225                            }));
6226                            exec_state.add_scene_object(
6227                                Object {
6228                                    id: constraint_id,
6229                                    kind: ObjectKind::Constraint { constraint },
6230                                    label: Default::default(),
6231                                    comments: Default::default(),
6232                                    artifact_id,
6233                                    source: SourceRef::new(range, self.node_path.clone()),
6234                                },
6235                                range,
6236                            );
6237                        }
6238                        SketchConstraintKind::HorizontalDistance { points, label_position } => {
6239                            let range = self.as_source_range();
6240                            let p0 = &points[0];
6241                            let p1 = &points[1];
6242                            let constraint_id = exec_state.next_object_id();
6243                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
6244                                let message =
6245                                    "Being inside a sketch block should have already been checked above".to_owned();
6246                                debug_assert!(false, "{}", &message);
6247                                return Err(internal_err(message, self));
6248                            };
6249                            match (p0, p1) {
6250                                (
6251                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(p0),
6252                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(p1),
6253                                ) => {
6254                                    let solver_pt0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
6255                                        p0.vars.x.to_constraint_id(range)?,
6256                                        p0.vars.y.to_constraint_id(range)?,
6257                                    );
6258                                    let solver_pt1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
6259                                        p1.vars.x.to_constraint_id(range)?,
6260                                        p1.vars.y.to_constraint_id(range)?,
6261                                    );
6262                                    sketch_block_state
6263                                        .solver_constraints
6264                                        .push(ezpz::Constraint::HorizontalDistance(solver_pt1, solver_pt0, n.n));
6265                                }
6266                                (
6267                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
6268                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
6269                                ) => {
6270                                    // horizontalDistance([point, ORIGIN]) == n means 0 - point.x = n, so point.x = -n.
6271                                    sketch_block_state
6272                                        .solver_constraints
6273                                        .push(ezpz::Constraint::Fixed(point.vars.x.to_constraint_id(range)?, -n.n));
6274                                }
6275                                (
6276                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
6277                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
6278                                ) => {
6279                                    // horizontalDistance([ORIGIN, point]) == n means point.x - 0 = n, so point.x = n.
6280                                    sketch_block_state
6281                                        .solver_constraints
6282                                        .push(ezpz::Constraint::Fixed(point.vars.x.to_constraint_id(range)?, n.n));
6283                                }
6284                                (
6285                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
6286                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
6287                                ) => {
6288                                    return Err(internal_err(
6289                                        "horizontalDistance() cannot constrain ORIGIN against ORIGIN".to_owned(),
6290                                        range,
6291                                    ));
6292                                }
6293                            }
6294                            use crate::execution::Artifact;
6295                            use crate::execution::CodeRef;
6296                            use crate::execution::SketchBlockConstraint;
6297                            use crate::front::Distance;
6298                            use crate::front::SourceRef;
6299                            use crate::frontend::sketch::ConstraintSegment;
6300
6301                            let constraint = crate::front::Constraint::HorizontalDistance(Distance {
6302                                segments: vec![
6303                                    match p0 {
6304                                        crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
6305                                            ConstraintSegment::from(point.object_id)
6306                                        }
6307                                        crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
6308                                            ConstraintSegment::ORIGIN
6309                                        }
6310                                    },
6311                                    match p1 {
6312                                        crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
6313                                            ConstraintSegment::from(point.object_id)
6314                                        }
6315                                        crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
6316                                            ConstraintSegment::ORIGIN
6317                                        }
6318                                    },
6319                                ],
6320                                distance: n.try_into().map_err(|_| {
6321                                    internal_err("Failed to convert distance units numeric suffix:", range)
6322                                })?,
6323                                label_position: label_position.clone(),
6324                                source,
6325                            });
6326                            sketch_block_state.sketch_constraints.push(constraint_id);
6327                            let Some(sketch_id) = sketch_block_state.sketch_id else {
6328                                let message = "Sketch id missing for constraint artifact".to_owned();
6329                                debug_assert!(false, "{}", &message);
6330                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
6331                            };
6332                            let artifact_id = exec_state.next_artifact_id();
6333                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
6334                                id: artifact_id,
6335                                sketch_id,
6336                                constraint_id,
6337                                constraint_type: super::artifact::sketch_block_constraint_type(&constraint),
6338                                code_ref: CodeRef::placeholder(range),
6339                            }));
6340                            exec_state.add_scene_object(
6341                                Object {
6342                                    id: constraint_id,
6343                                    kind: ObjectKind::Constraint { constraint },
6344                                    label: Default::default(),
6345                                    comments: Default::default(),
6346                                    artifact_id,
6347                                    source: SourceRef::new(range, self.node_path.clone()),
6348                                },
6349                                range,
6350                            );
6351                        }
6352                        SketchConstraintKind::VerticalDistance { points, label_position } => {
6353                            let range = self.as_source_range();
6354                            let p0 = &points[0];
6355                            let p1 = &points[1];
6356                            let constraint_id = exec_state.next_object_id();
6357                            let Some(sketch_block_state) = &mut exec_state.mod_local.sketch_block else {
6358                                let message =
6359                                    "Being inside a sketch block should have already been checked above".to_owned();
6360                                debug_assert!(false, "{}", &message);
6361                                return Err(internal_err(message, self));
6362                            };
6363                            match (p0, p1) {
6364                                (
6365                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(p0),
6366                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(p1),
6367                                ) => {
6368                                    let solver_pt0 = ezpz::datatypes::inputs::DatumPoint::new_xy(
6369                                        p0.vars.x.to_constraint_id(range)?,
6370                                        p0.vars.y.to_constraint_id(range)?,
6371                                    );
6372                                    let solver_pt1 = ezpz::datatypes::inputs::DatumPoint::new_xy(
6373                                        p1.vars.x.to_constraint_id(range)?,
6374                                        p1.vars.y.to_constraint_id(range)?,
6375                                    );
6376                                    sketch_block_state
6377                                        .solver_constraints
6378                                        .push(ezpz::Constraint::VerticalDistance(solver_pt1, solver_pt0, n.n));
6379                                }
6380                                (
6381                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
6382                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
6383                                ) => {
6384                                    sketch_block_state
6385                                        .solver_constraints
6386                                        .push(ezpz::Constraint::Fixed(point.vars.y.to_constraint_id(range)?, -n.n));
6387                                }
6388                                (
6389                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
6390                                    crate::execution::ConstrainablePoint2dOrOrigin::Point(point),
6391                                ) => {
6392                                    sketch_block_state
6393                                        .solver_constraints
6394                                        .push(ezpz::Constraint::Fixed(point.vars.y.to_constraint_id(range)?, n.n));
6395                                }
6396                                (
6397                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
6398                                    crate::execution::ConstrainablePoint2dOrOrigin::Origin,
6399                                ) => {
6400                                    return Err(internal_err(
6401                                        "verticalDistance() cannot constrain ORIGIN against ORIGIN".to_owned(),
6402                                        range,
6403                                    ));
6404                                }
6405                            }
6406                            use crate::execution::Artifact;
6407                            use crate::execution::CodeRef;
6408                            use crate::execution::SketchBlockConstraint;
6409                            use crate::front::Distance;
6410                            use crate::front::SourceRef;
6411                            use crate::frontend::sketch::ConstraintSegment;
6412
6413                            let constraint = crate::front::Constraint::VerticalDistance(Distance {
6414                                segments: vec![
6415                                    match p0 {
6416                                        crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
6417                                            ConstraintSegment::from(point.object_id)
6418                                        }
6419                                        crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
6420                                            ConstraintSegment::ORIGIN
6421                                        }
6422                                    },
6423                                    match p1 {
6424                                        crate::execution::ConstrainablePoint2dOrOrigin::Point(point) => {
6425                                            ConstraintSegment::from(point.object_id)
6426                                        }
6427                                        crate::execution::ConstrainablePoint2dOrOrigin::Origin => {
6428                                            ConstraintSegment::ORIGIN
6429                                        }
6430                                    },
6431                                ],
6432                                distance: n.try_into().map_err(|_| {
6433                                    internal_err("Failed to convert distance units numeric suffix:", range)
6434                                })?,
6435                                label_position: label_position.clone(),
6436                                source,
6437                            });
6438                            sketch_block_state.sketch_constraints.push(constraint_id);
6439                            let Some(sketch_id) = sketch_block_state.sketch_id else {
6440                                let message = "Sketch id missing for constraint artifact".to_owned();
6441                                debug_assert!(false, "{}", &message);
6442                                return Err(KclError::new_internal(KclErrorDetails::new(message, vec![range])));
6443                            };
6444                            let artifact_id = exec_state.next_artifact_id();
6445                            exec_state.add_artifact(Artifact::SketchBlockConstraint(SketchBlockConstraint {
6446                                id: artifact_id,
6447                                sketch_id,
6448                                constraint_id,
6449                                constraint_type: super::artifact::sketch_block_constraint_type(&constraint),
6450                                code_ref: CodeRef::placeholder(range),
6451                            }));
6452                            exec_state.add_scene_object(
6453                                Object {
6454                                    id: constraint_id,
6455                                    kind: ObjectKind::Constraint { constraint },
6456                                    label: Default::default(),
6457                                    comments: Default::default(),
6458                                    artifact_id,
6459                                    source: SourceRef::new(range, self.node_path.clone()),
6460                                },
6461                                range,
6462                            );
6463                        }
6464                    }
6465                    return Ok(KclValue::none());
6466                }
6467                _ => {
6468                    return Err(KclError::new_semantic(KclErrorDetails::new(
6469                        format!(
6470                            "Cannot create an equivalence constraint between values of these types: {} and {}",
6471                            left_value.human_friendly_type(),
6472                            right_value.human_friendly_type()
6473                        ),
6474                        vec![self.into()],
6475                    )));
6476                }
6477            }
6478        }
6479
6480        // Inside sketch blocks, `==` is reserved for equivalence constraints
6481        // and has already been handled above.
6482        if matches!(self.operator, BinaryOperator::Eq | BinaryOperator::Neq)
6483            && let (KclValue::String { value: left, .. }, KclValue::String { value: right, .. }) =
6484                (&left_value, &right_value)
6485        {
6486            let is_equal = left == right;
6487            let value = if self.operator == BinaryOperator::Eq {
6488                is_equal
6489            } else {
6490                !is_equal
6491            };
6492            return Ok(KclValue::Bool { value, meta });
6493        }
6494
6495        // Enums compare by declaration and variant. This has to precede
6496        // `number_as_f64` below, which would otherwise reject an enum with
6497        // "expected a number" and describe the wrong problem.
6498        if matches!(self.operator, BinaryOperator::Eq | BinaryOperator::Neq) {
6499            match (&left_value, &right_value) {
6500                (KclValue::Enum { value: left }, KclValue::Enum { value: right }) => {
6501                    if left.enum_id() != right.enum_id() {
6502                        return Err(different_enums_err(left, right, self.as_source_range()));
6503                    }
6504
6505                    let is_equal = left.variant() == right.variant();
6506                    let value = if self.operator == BinaryOperator::Eq {
6507                        is_equal
6508                    } else {
6509                        !is_equal
6510                    };
6511                    return Ok(KclValue::Bool { value, meta });
6512                }
6513                (KclValue::Enum { value }, other) | (other, KclValue::Enum { value }) => {
6514                    return Err(KclError::new_semantic(KclErrorDetails::new(
6515                        format!(
6516                            "Cannot compare enum `{}` with {}.",
6517                            value.qualified_name(),
6518                            other.human_friendly_type()
6519                        ),
6520                        vec![self.as_source_range()],
6521                    )));
6522                }
6523                _ => {}
6524            }
6525        }
6526
6527        let left = number_as_f64(&left_value, self.left.clone().into())?;
6528        let right = number_as_f64(&right_value, self.right.clone().into())?;
6529
6530        let value = match self.operator {
6531            BinaryOperator::Add => {
6532                let (l, r, ty) = NumericType::combine_eq_coerce(left, right, None);
6533                self.warn_on_unknown(&ty, "Adding", exec_state);
6534                KclValue::Number { value: l + r, meta, ty }
6535            }
6536            BinaryOperator::Sub => {
6537                let (l, r, ty) = NumericType::combine_eq_coerce(left, right, None);
6538                self.warn_on_unknown(&ty, "Subtracting", exec_state);
6539                KclValue::Number { value: l - r, meta, ty }
6540            }
6541            BinaryOperator::Mul => {
6542                let (l, r, ty) = NumericType::combine_mul(left, right);
6543                self.warn_on_unknown(&ty, "Multiplying", exec_state);
6544                KclValue::Number { value: l * r, meta, ty }
6545            }
6546            BinaryOperator::Div => {
6547                let (l, r, ty) = NumericType::combine_div(left, right);
6548                self.warn_on_unknown(&ty, "Dividing", exec_state);
6549                KclValue::Number { value: l / r, meta, ty }
6550            }
6551            BinaryOperator::Mod => {
6552                let (l, r, ty) = NumericType::combine_mod(left, right);
6553                self.warn_on_unknown(&ty, "Modulo of", exec_state);
6554                KclValue::Number { value: l % r, meta, ty }
6555            }
6556            BinaryOperator::Pow => KclValue::Number {
6557                value: libm::pow(left.n, right.n),
6558                meta,
6559                ty: exec_state.current_default_units(),
6560            },
6561            BinaryOperator::Neq => {
6562                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
6563                self.warn_on_unknown(&ty, "Comparing", exec_state);
6564                KclValue::Bool { value: l != r, meta }
6565            }
6566            BinaryOperator::Gt => {
6567                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
6568                self.warn_on_unknown(&ty, "Comparing", exec_state);
6569                KclValue::Bool { value: l > r, meta }
6570            }
6571            BinaryOperator::Gte => {
6572                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
6573                self.warn_on_unknown(&ty, "Comparing", exec_state);
6574                KclValue::Bool { value: l >= r, meta }
6575            }
6576            BinaryOperator::Lt => {
6577                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
6578                self.warn_on_unknown(&ty, "Comparing", exec_state);
6579                KclValue::Bool { value: l < r, meta }
6580            }
6581            BinaryOperator::Lte => {
6582                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
6583                self.warn_on_unknown(&ty, "Comparing", exec_state);
6584                KclValue::Bool { value: l <= r, meta }
6585            }
6586            BinaryOperator::Eq => {
6587                let (l, r, ty) = NumericType::combine_eq(left, right, exec_state, self.as_source_range());
6588                self.warn_on_unknown(&ty, "Comparing", exec_state);
6589                KclValue::Bool { value: l == r, meta }
6590            }
6591            BinaryOperator::And | BinaryOperator::Or => unreachable!(),
6592        };
6593
6594        Ok(value)
6595    }
6596
6597    fn missing_result_error(node: &Node<BinaryExpression>) -> KclError {
6598        internal_err("missing result while evaluating binary expression", node)
6599    }
6600
6601    fn warn_on_unknown(&self, ty: &NumericType, verb: &str, exec_state: &mut ExecState) {
6602        if ty == &NumericType::Unknown {
6603            let sr = self.as_source_range();
6604            exec_state.clear_units_warnings(&sr);
6605            let mut err = CompilationIssue::err(
6606                sr,
6607                format!(
6608                    "{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)`."
6609                ),
6610            );
6611            err.tag = crate::errors::Tag::UnknownNumericUnits;
6612            exec_state.warn(err, annotations::WARN_UNKNOWN_UNITS);
6613        }
6614    }
6615}
6616
6617impl Node<UnaryExpression> {
6618    pub(super) async fn get_result(
6619        &self,
6620        exec_state: &mut ExecState,
6621        ctx: &ExecutorContext,
6622    ) -> Result<KclValueControlFlow, KclError> {
6623        let value = self.argument.get_result(exec_state, ctx).await?;
6624        let value = control_continue!(value);
6625        self.apply_unary(value, exec_state).map(KclValue::continue_)
6626    }
6627
6628    /// Apply the unary operator to an already-evaluated operand. Shared by
6629    /// both executors.
6630    pub(super) fn apply_unary(&self, value: KclValue, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
6631        match self.operator {
6632            UnaryOperator::Not => {
6633                let KclValue::Bool {
6634                    value: bool_value,
6635                    meta: _,
6636                } = value
6637                else {
6638                    return Err(KclError::new_semantic(KclErrorDetails::new(
6639                        format!(
6640                            "Cannot apply unary operator ! to non-boolean value: {}",
6641                            value.human_friendly_type()
6642                        ),
6643                        vec![self.into()],
6644                    )));
6645                };
6646                let meta = vec![Metadata {
6647                    source_range: self.into(),
6648                }];
6649                let negated = KclValue::Bool {
6650                    value: !bool_value,
6651                    meta,
6652                };
6653
6654                Ok(negated)
6655            }
6656            UnaryOperator::Neg => {
6657                let err = || {
6658                    KclError::new_semantic(KclErrorDetails::new(
6659                        format!(
6660                            "You can only negate numbers, planes, or lines, but this is a {}",
6661                            value.human_friendly_type()
6662                        ),
6663                        vec![self.into()],
6664                    ))
6665                };
6666                match &value {
6667                    KclValue::Number { value, ty, .. } => {
6668                        let meta = vec![Metadata {
6669                            source_range: self.into(),
6670                        }];
6671                        Ok(KclValue::Number {
6672                            value: -value,
6673                            meta,
6674                            ty: *ty,
6675                        })
6676                    }
6677                    KclValue::Plane { value } => {
6678                        let mut plane = value.clone();
6679                        if plane.info.x_axis.x != 0.0 {
6680                            plane.info.x_axis.x *= -1.0;
6681                        }
6682                        if plane.info.x_axis.y != 0.0 {
6683                            plane.info.x_axis.y *= -1.0;
6684                        }
6685                        if plane.info.x_axis.z != 0.0 {
6686                            plane.info.x_axis.z *= -1.0;
6687                        }
6688                        plane.info.z_axis = plane.info.x_axis.axes_cross_product(&plane.info.y_axis);
6689                        plane.info.z_axis.canonicalize_signed_zero();
6690
6691                        plane.id = exec_state.next_uuid();
6692                        plane.object_id = None;
6693                        Ok(KclValue::Plane { value: plane })
6694                    }
6695                    KclValue::Object {
6696                        value: values, meta, ..
6697                    } => {
6698                        // Special-case for negating line-like objects.
6699                        let Some(direction) = values.get("direction") else {
6700                            return Err(err());
6701                        };
6702
6703                        let direction = match direction {
6704                            KclValue::Tuple { value: values, meta } => {
6705                                let values = values
6706                                    .iter()
6707                                    .map(|v| match v {
6708                                        KclValue::Number { value, ty, meta } => Ok(KclValue::Number {
6709                                            value: *value * -1.0,
6710                                            ty: *ty,
6711                                            meta: meta.clone(),
6712                                        }),
6713                                        _ => Err(err()),
6714                                    })
6715                                    .collect::<Result<Vec<_>, _>>()?;
6716
6717                                KclValue::Tuple {
6718                                    value: values,
6719                                    meta: meta.clone(),
6720                                }
6721                            }
6722                            KclValue::HomArray {
6723                                value: values,
6724                                ty: ty @ RuntimeType::Primitive(PrimitiveType::Number(_)),
6725                            } => {
6726                                let values = values
6727                                    .iter()
6728                                    .map(|v| match v {
6729                                        KclValue::Number { value, ty, meta } => Ok(KclValue::Number {
6730                                            value: *value * -1.0,
6731                                            ty: *ty,
6732                                            meta: meta.clone(),
6733                                        }),
6734                                        _ => Err(err()),
6735                                    })
6736                                    .collect::<Result<Vec<_>, _>>()?;
6737
6738                                KclValue::HomArray {
6739                                    value: values,
6740                                    ty: ty.clone(),
6741                                }
6742                            }
6743                            _ => return Err(err()),
6744                        };
6745
6746                        let mut value = values.clone();
6747                        value.insert("direction".to_owned(), direction);
6748                        Ok(KclValue::Object {
6749                            value,
6750                            meta: meta.clone(),
6751                            constrainable: false,
6752                            object_kind: KclObjectKind::Default,
6753                        })
6754                    }
6755                    _ => Err(err()),
6756                }
6757            }
6758            UnaryOperator::Plus => match value {
6759                KclValue::Number { .. } | KclValue::Plane { .. } => Ok(value),
6760                _ => Err(KclError::new_semantic(KclErrorDetails::new(
6761                    format!(
6762                        "You can only apply unary + to numbers or planes, but this is a {}",
6763                        value.human_friendly_type()
6764                    ),
6765                    vec![self.into()],
6766                ))),
6767            },
6768        }
6769    }
6770}
6771
6772pub(crate) async fn execute_pipe_body(
6773    exec_state: &mut ExecState,
6774    body: &[Expr],
6775    source_range: SourceRange,
6776    ctx: &ExecutorContext,
6777) -> Result<KclValueControlFlow, KclError> {
6778    let Some((first, body)) = body.split_first() else {
6779        return Err(KclError::new_semantic(KclErrorDetails::new(
6780            "Pipe expressions cannot be empty".to_owned(),
6781            vec![source_range],
6782        )));
6783    };
6784    // Evaluate the first element in the pipeline.
6785    // They use the pipe_value from some AST node above this, so that if pipe expression is nested in a larger pipe expression,
6786    // they use the % from the parent. After all, this pipe expression hasn't been executed yet, so it doesn't have any % value
6787    // of its own.
6788    let meta = Metadata {
6789        source_range: SourceRange::from(first),
6790    };
6791    let output = ctx
6792        .execute_expr(first, exec_state, &meta, &[], StatementKind::Expression)
6793        .await?;
6794    let output = control_continue!(output);
6795
6796    // Now that we've evaluated the first child expression in the pipeline, following child expressions
6797    // should use the previous child expression for %.
6798    // This means there's no more need for the previous pipe_value from the parent AST node above this one.
6799    let previous_pipe_value = exec_state.mod_local.pipe_value.replace(output);
6800    // Evaluate remaining elements.
6801    let result = inner_execute_pipe_body(exec_state, body, ctx).await;
6802    // Restore the previous pipe value.
6803    exec_state.mod_local.pipe_value = previous_pipe_value;
6804
6805    result
6806}
6807
6808/// Execute the tail of a pipe expression.  exec_state.pipe_value must be set by
6809/// the caller.
6810#[async_recursion]
6811async fn inner_execute_pipe_body(
6812    exec_state: &mut ExecState,
6813    body: &[Expr],
6814    ctx: &ExecutorContext,
6815) -> Result<KclValueControlFlow, KclError> {
6816    for expression in body {
6817        if let Expr::TagDeclarator(_) = expression {
6818            return Err(KclError::new_semantic(KclErrorDetails::new(
6819                format!("This cannot be in a PipeExpression: {expression:?}"),
6820                vec![expression.into()],
6821            )));
6822        }
6823        let metadata = Metadata {
6824            source_range: SourceRange::from(expression),
6825        };
6826        let output = ctx
6827            .execute_expr(expression, exec_state, &metadata, &[], StatementKind::Expression)
6828            .await?;
6829        let output = control_continue!(output);
6830        exec_state.mod_local.pipe_value = Some(output);
6831    }
6832    // Safe to unwrap here, because pipe_value always has something pushed in when the `match first` executes.
6833    let final_output = exec_state.mod_local.pipe_value.take().unwrap();
6834    Ok(final_output.continue_())
6835}
6836
6837impl Node<TagDeclarator> {
6838    pub async fn execute(&self, exec_state: &mut ExecState) -> Result<KclValue, KclError> {
6839        let memory_item = KclValue::TagIdentifier(Box::new(TagIdentifier {
6840            value: self.name.clone(),
6841            info: Vec::new(),
6842            meta: vec![Metadata {
6843                source_range: self.into(),
6844            }],
6845        }));
6846
6847        exec_state
6848            .mut_stack()
6849            .add(self.name.clone(), memory_item, self.into())?;
6850
6851        Ok(self.into())
6852    }
6853}
6854
6855impl Node<ArrayExpression> {
6856    #[async_recursion]
6857    pub(super) async fn execute(
6858        &self,
6859        exec_state: &mut ExecState,
6860        ctx: &ExecutorContext,
6861    ) -> Result<KclValueControlFlow, KclError> {
6862        let mut results = Vec::with_capacity(self.elements.len());
6863
6864        for element in &self.elements {
6865            let metadata = Metadata::from(element);
6866            // TODO: Carry statement kind here so that we know if we're
6867            // inside a variable declaration.
6868            let value = ctx
6869                .execute_expr(element, exec_state, &metadata, &[], StatementKind::Expression)
6870                .await?;
6871            let value = control_continue!(value);
6872
6873            results.push(value);
6874        }
6875
6876        Ok(KclValue::HomArray {
6877            value: results,
6878            ty: RuntimeType::Primitive(PrimitiveType::Any),
6879        }
6880        .continue_())
6881    }
6882}
6883
6884impl Node<ArrayRangeExpression> {
6885    #[async_recursion]
6886    pub(super) async fn execute(
6887        &self,
6888        exec_state: &mut ExecState,
6889        ctx: &ExecutorContext,
6890    ) -> Result<KclValueControlFlow, KclError> {
6891        let metadata = Metadata::from(&self.start_element);
6892        let start_val = ctx
6893            .execute_expr(
6894                &self.start_element,
6895                exec_state,
6896                &metadata,
6897                &[],
6898                StatementKind::Expression,
6899            )
6900            .await?;
6901        let start_val_for_build = control_continue!(start_val);
6902        self.validate_range_start(&start_val_for_build)?;
6903        let metadata = Metadata::from(&self.end_element);
6904        let end_val = ctx
6905            .execute_expr(&self.end_element, exec_state, &metadata, &[], StatementKind::Expression)
6906            .await?;
6907        let end_val = control_continue!(end_val);
6908        self.build_range(start_val_for_build, end_val, exec_state)
6909            .map(KclValue::continue_)
6910    }
6911
6912    /// Validate the evaluated start endpoint, preserving the original error
6913    /// ordering: a bad start is reported before the end element is ever
6914    /// evaluated. Shared by both executors; `build_range` re-checks it,
6915    /// which is redundant but keeps `build_range` total on its own.
6916    pub(super) fn validate_range_start(&self, start_val: &KclValue) -> Result<(), KclError> {
6917        if start_val.as_ty_f64().is_none() {
6918            return Err(KclError::new_semantic(KclErrorDetails::new(
6919                format!(
6920                    "Expected number for range start but found {}",
6921                    start_val.human_friendly_type()
6922                ),
6923                vec![self.into()],
6924            )));
6925        }
6926        Ok(())
6927    }
6928
6929    /// Build the range value from evaluated endpoints. The evaluation-free
6930    /// second half of range execution, shared by both executors.
6931    pub(super) fn build_range(
6932        &self,
6933        start_val: KclValue,
6934        end_val: KclValue,
6935        exec_state: &mut ExecState,
6936    ) -> Result<KclValue, KclError> {
6937        let start = start_val
6938            .as_ty_f64()
6939            .ok_or(KclError::new_semantic(KclErrorDetails::new(
6940                format!(
6941                    "Expected number for range start but found {}",
6942                    start_val.human_friendly_type()
6943                ),
6944                vec![self.into()],
6945            )))?;
6946        let end = end_val.as_ty_f64().ok_or(KclError::new_semantic(KclErrorDetails::new(
6947            format!(
6948                "Expected number for range end but found {}",
6949                end_val.human_friendly_type()
6950            ),
6951            vec![self.into()],
6952        )))?;
6953
6954        let (start, end, ty) = NumericType::combine_range(start, end, exec_state, self.as_source_range())?;
6955        let Some(start) = crate::try_f64_to_i64(start) else {
6956            return Err(KclError::new_semantic(KclErrorDetails::new(
6957                format!("Range start must be an integer, but found {start}"),
6958                vec![self.into()],
6959            )));
6960        };
6961        let Some(end) = crate::try_f64_to_i64(end) else {
6962            return Err(KclError::new_semantic(KclErrorDetails::new(
6963                format!("Range end must be an integer, but found {end}"),
6964                vec![self.into()],
6965            )));
6966        };
6967
6968        if end < start {
6969            return Err(KclError::new_semantic(KclErrorDetails::new(
6970                format!("Range start is greater than range end: {start} .. {end}"),
6971                vec![self.into()],
6972            )));
6973        }
6974
6975        let range: Vec<_> = if self.end_inclusive {
6976            (start..=end).collect()
6977        } else {
6978            (start..end).collect()
6979        };
6980
6981        let meta = vec![Metadata {
6982            source_range: self.into(),
6983        }];
6984
6985        Ok(KclValue::HomArray {
6986            value: range
6987                .into_iter()
6988                .map(|num| KclValue::Number {
6989                    value: num as f64,
6990                    ty,
6991                    meta: meta.clone(),
6992                })
6993                .collect(),
6994            ty: RuntimeType::Primitive(PrimitiveType::Number(ty)),
6995        })
6996    }
6997}
6998
6999impl Node<ObjectExpression> {
7000    #[async_recursion]
7001    pub(super) async fn execute(
7002        &self,
7003        exec_state: &mut ExecState,
7004        ctx: &ExecutorContext,
7005    ) -> Result<KclValueControlFlow, KclError> {
7006        let mut object = HashMap::with_capacity(self.properties.len());
7007        for property in &self.properties {
7008            let metadata = Metadata::from(&property.value);
7009            let result = ctx
7010                .execute_expr(&property.value, exec_state, &metadata, &[], StatementKind::Expression)
7011                .await?;
7012            let result = control_continue!(result);
7013            object.insert(property.key.name.clone(), result);
7014        }
7015
7016        Ok(KclValue::Object {
7017            value: object,
7018            meta: vec![Metadata {
7019                source_range: self.into(),
7020            }],
7021            constrainable: false,
7022            object_kind: KclObjectKind::Default,
7023        }
7024        .continue_())
7025    }
7026}
7027
7028fn article_for<S: AsRef<str>>(s: S) -> &'static str {
7029    // '[' is included since it's an array.
7030    if s.as_ref().starts_with(['a', 'e', 'i', 'o', 'u', '[']) {
7031        "an"
7032    } else {
7033        "a"
7034    }
7035}
7036
7037fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<TyF64, KclError> {
7038    v.as_ty_f64().ok_or_else(|| {
7039        let actual_type = v.human_friendly_type();
7040        KclError::new_semantic(KclErrorDetails::new(
7041            format!("Expected a number, but found {actual_type}",),
7042            vec![source_range],
7043        ))
7044    })
7045}
7046
7047impl Node<IfExpression> {
7048    #[async_recursion]
7049    pub(super) async fn get_result(
7050        &self,
7051        exec_state: &mut ExecState,
7052        ctx: &ExecutorContext,
7053    ) -> Result<KclValueControlFlow, KclError> {
7054        // Check the `if` branch. Conditions are evaluated in the enclosing
7055        // scope; only arm bodies get their own scope (under KCL 3.0).
7056        let cond_value = ctx
7057            .execute_expr(
7058                &self.cond,
7059                exec_state,
7060                &Metadata::from(self),
7061                &[],
7062                StatementKind::Expression,
7063            )
7064            .await?;
7065        let cond_value = control_continue!(cond_value);
7066        if cond_value.get_bool()? {
7067            return exec_if_arm(ctx, &self.then_val, exec_state).await;
7068        }
7069
7070        // Check any `else if` branches.
7071        for else_if in &self.else_ifs {
7072            let cond_value = ctx
7073                .execute_expr(
7074                    &else_if.cond,
7075                    exec_state,
7076                    &Metadata::from(self),
7077                    &[],
7078                    StatementKind::Expression,
7079                )
7080                .await?;
7081            let cond_value = control_continue!(cond_value);
7082            if cond_value.get_bool()? {
7083                return exec_if_arm(ctx, &else_if.then_val, exec_state).await;
7084            }
7085        }
7086
7087        // Run the final `else` branch.
7088        exec_if_arm(ctx, &self.final_else, exec_state).await
7089    }
7090}
7091
7092/// Begin an if-arm scope. Under a KCL 3.0 entry point, each
7093/// if/else-if/else arm body gets its own scope: bindings made inside the arm
7094/// are visible from their declaration to the arm's closing brace, never
7095/// outside it, and may shadow bindings from enclosing scopes. Under older
7096/// entry points the arm shares the enclosing environment (bindings leak out
7097/// and shadowing is a redefinition error). Returns whether a scope
7098/// environment was pushed; the caller must pop it on every path. Shared by
7099/// both executors.
7100pub(super) fn if_arm_scope_begin(exec_state: &mut ExecState) -> Result<bool, KclError> {
7101    if !exec_state.entry_point_version_is_v3_or_higher() {
7102        return Ok(false);
7103    }
7104    exec_state.mut_stack().push_new_env_for_block()?;
7105    Ok(true)
7106}
7107
7108/// Execute one if/else-if/else arm body in its own scope (under a
7109/// KCL 3.0 entry point). Values escaping the arm -- including closures
7110/// declared in it -- stay valid after the pop because environments that may
7111/// still be referenced are preserved, exactly as for function returns.
7112async fn exec_if_arm(
7113    ctx: &ExecutorContext,
7114    block: &Node<Program>,
7115    exec_state: &mut ExecState,
7116) -> Result<KclValueControlFlow, KclError> {
7117    let scoped = if_arm_scope_begin(exec_state)?;
7118    let result = ctx.exec_block(block, exec_state, BodyType::Block).await;
7119    if scoped {
7120        // Pop on success (including Return/Exit control flow escaping the
7121        // arm) and error alike. A pop failure wins over the block's error,
7122        // matching call_abort_on_arg_binding_failure.
7123        exec_state.mut_stack().pop_env()?;
7124    }
7125    // Block must end in an expression, so this is always Some.
7126    // Enforced by the parser.
7127    // See https://github.com/KittyCAD/modeling-app/issues/4015
7128    let Some(cf) = result? else {
7129        let message = "if-expression arm produced no value";
7130        debug_assert!(false, "{message}");
7131        return Err(KclError::new_internal(KclErrorDetails::new(
7132            message.to_owned(),
7133            vec![block.to_source_range()],
7134        )));
7135    };
7136    Ok(cf)
7137}
7138
7139#[derive(Debug)]
7140pub(super) enum Property {
7141    UInt(usize),
7142    String(String),
7143}
7144
7145impl Property {
7146    #[allow(clippy::too_many_arguments)]
7147    async fn try_from<'a>(
7148        computed: bool,
7149        value: Expr,
7150        exec_state: &mut ExecState,
7151        sr: SourceRange,
7152        ctx: &ExecutorContext,
7153        metadata: &Metadata,
7154        annotations: &[Node<Annotation>],
7155        statement_kind: StatementKind<'a>,
7156    ) -> Result<Self, EarlyReturn> {
7157        if !computed {
7158            return Ok(Self::from_static_name(&value, sr)?);
7159        }
7160
7161        let prop_value = ctx
7162            .execute_expr(&value, exec_state, metadata, annotations, statement_kind)
7163            .await?;
7164        // If the property expression exited, e.g. by calling exit(), propagate
7165        // the exit so that it terminates the enclosing module.
7166        let prop_value = early_return!(prop_value);
7167        Ok(Self::from_value(prop_value, sr)?)
7168    }
7169
7170    /// Convert a non-computed property (the identifier in `obj.property`)
7171    /// into a Property. Nothing is evaluated. Shared by both executors.
7172    pub(super) fn from_static_name(property: &Expr, sr: SourceRange) -> Result<Self, KclError> {
7173        let Expr::Name(identifier) = property else {
7174            // Should actually be impossible because the parser would reject it.
7175            return Err(KclError::new_semantic(KclErrorDetails::new(
7176                "Object expressions like `obj.property` must use simple identifier names, not complex expressions"
7177                    .to_owned(),
7178                vec![sr],
7179            )));
7180        };
7181        Ok(Property::String(identifier.to_string()))
7182    }
7183
7184    /// Convert an already-evaluated computed-property value (the expression
7185    /// inside array brackets) into a Property. Shared by both executors.
7186    pub(super) fn from_value(prop_value: KclValue, sr: SourceRange) -> Result<Self, KclError> {
7187        let property_sr = vec![sr];
7188        match prop_value {
7189            KclValue::Number { value, ty, meta: _ } => {
7190                if !matches!(
7191                    ty,
7192                    NumericType::Unknown
7193                        | NumericType::Default { .. }
7194                        | NumericType::Known(crate::exec::UnitType::Count)
7195                ) {
7196                    return Err(KclError::new_semantic(KclErrorDetails::new(
7197                        format!(
7198                            "{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"
7199                        ),
7200                        property_sr,
7201                    )));
7202                }
7203                if let Some(x) = crate::try_f64_to_usize(value) {
7204                    Ok(Property::UInt(x))
7205                } else {
7206                    Err(KclError::new_semantic(KclErrorDetails::new(
7207                        format!("{value} is not a valid index, indices must be whole numbers >= 0"),
7208                        property_sr,
7209                    )))
7210                }
7211            }
7212            _ => Err(KclError::new_semantic(KclErrorDetails::new(
7213                "Only numbers (>= 0) can be indexes".to_owned(),
7214                vec![sr],
7215            ))),
7216        }
7217    }
7218}
7219
7220impl Property {
7221    fn type_name(&self) -> &'static str {
7222        match self {
7223            Property::UInt(_) => "number",
7224            Property::String(_) => "string",
7225        }
7226    }
7227}
7228
7229impl Node<PipeExpression> {
7230    #[async_recursion]
7231    pub(super) async fn get_result(
7232        &self,
7233        exec_state: &mut ExecState,
7234        ctx: &ExecutorContext,
7235    ) -> Result<KclValueControlFlow, KclError> {
7236        execute_pipe_body(exec_state, &self.body, self.into(), ctx).await
7237    }
7238}
7239
7240#[cfg(test)]
7241mod test {
7242    use std::sync::Arc;
7243
7244    use kcl_api::UnitLength;
7245    use tokio::io::AsyncWriteExt;
7246
7247    use super::*;
7248    use crate::ExecutorSettings;
7249    use crate::engine::engine_manager;
7250    use crate::errors::Severity;
7251    use crate::exec::UnitType;
7252    use crate::execution::ContextType;
7253    use crate::execution::machine::ExecutorKind;
7254    use crate::execution::parse_execute;
7255
7256    fn assert_angle_degrees(actual: ezpz::datatypes::Angle, expected: f64) {
7257        assert!(
7258            (actual.to_degrees() - expected).abs() < 1e-9,
7259            "expected {expected}deg, got {}deg",
7260            actual.to_degrees()
7261        );
7262    }
7263
7264    #[test]
7265    fn remaps_sector_angles_to_existing_representative_endpoint_rays() {
7266        let representative_directions = [AngleRayDirection::Forward, AngleRayDirection::Forward];
7267
7268        assert_angle_degrees(
7269            remap_angle_for_representative_rays(
7270                angle_sector_rays(AngleSector::One, false),
7271                representative_directions,
7272                ezpz::datatypes::Angle::from_degrees(60.0),
7273            ),
7274            60.0,
7275        );
7276        assert_angle_degrees(
7277            remap_angle_for_representative_rays(
7278                angle_sector_rays(AngleSector::Two, false),
7279                representative_directions,
7280                ezpz::datatypes::Angle::from_degrees(120.0),
7281            ),
7282            60.0,
7283        );
7284        assert_angle_degrees(
7285            remap_angle_for_representative_rays(
7286                angle_sector_rays(AngleSector::Three, false),
7287                representative_directions,
7288                ezpz::datatypes::Angle::from_degrees(60.0),
7289            ),
7290            60.0,
7291        );
7292        assert_angle_degrees(
7293            remap_angle_for_representative_rays(
7294                angle_sector_rays(AngleSector::Four, false),
7295                representative_directions,
7296                ezpz::datatypes::Angle::from_degrees(120.0),
7297            ),
7298            60.0,
7299        );
7300        assert_angle_degrees(
7301            remap_angle_for_representative_rays(
7302                angle_sector_rays(AngleSector::One, true),
7303                representative_directions,
7304                ezpz::datatypes::Angle::from_degrees(300.0),
7305            ),
7306            60.0,
7307        );
7308    }
7309
7310    #[test]
7311    fn remaps_sector_angles_when_representative_endpoint_is_on_reverse_ray() {
7312        assert_angle_degrees(
7313            remap_angle_for_representative_rays(
7314                angle_sector_rays(AngleSector::One, false),
7315                [AngleRayDirection::Forward, AngleRayDirection::Reverse],
7316                ezpz::datatypes::Angle::from_degrees(60.0),
7317            ),
7318            240.0,
7319        );
7320    }
7321
7322    #[tokio::test(flavor = "multi_thread")]
7323    async fn angle_unlabeled_keeps_legacy_lines_at_angle() {
7324        let code = r#"
7325sketch(on = XY) {
7326  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
7327  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
7328  lines = [line1, line2]
7329  angle(lines) == 60deg
7330}
7331"#;
7332        let result = parse_execute(code).await.unwrap();
7333
7334        let metadata = result
7335            .exec_state
7336            .global
7337            .root_module_artifacts
7338            .legacy_angle_refactor_metadata();
7339        assert_eq!(metadata.len(), 1);
7340        assert_eq!(metadata[0].sector, 1);
7341        assert!(!metadata[0].inverse);
7342        let program = crate::Program::parse_no_errs(code).unwrap();
7343        let findings = program.lint(crate::lint::checks::lint_legacy_angle).unwrap();
7344        assert_eq!(metadata[0].source_range, findings[0].pos);
7345    }
7346
7347    #[tokio::test(flavor = "multi_thread")]
7348    async fn legacy_angle_refactor_metadata_matches_the_default_label_side() {
7349        let result = parse_execute(
7350            r#"
7351sketch(on = XY) {
7352  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
7353  line2 = line(start = [var 0mm, var 0mm], end = [var -2mm, var -3.464mm])
7354  angle([line1, line2]) == 60deg
7355}
7356"#,
7357        )
7358        .await
7359        .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, 4);
7368        assert!(metadata[0].inverse);
7369    }
7370
7371    #[tokio::test(flavor = "multi_thread")]
7372    async fn legacy_angle_refactor_metadata_uses_reverse_segment_rays() {
7373        let result = parse_execute(
7374            r#"
7375sketch(on = XY) {
7376  line1 = line(start = [var -4mm, var 0mm], end = [var 0mm, var 0mm])
7377  line2 = line(start = [var -2mm, var -3.464mm], end = [var 0mm, var 0mm])
7378  angle([line1, line2]) == 60deg
7379}
7380"#,
7381        )
7382        .await
7383        .unwrap();
7384
7385        let metadata = result
7386            .exec_state
7387            .global
7388            .root_module_artifacts
7389            .legacy_angle_refactor_metadata();
7390        assert_eq!(metadata.len(), 1);
7391        assert_eq!(metadata[0].sector, 3);
7392        assert!(!metadata[0].inverse);
7393    }
7394
7395    #[tokio::test(flavor = "multi_thread")]
7396    async fn legacy_angle_label_position_does_not_change_the_sector() {
7397        let result = parse_execute(
7398            r#"
7399sketch(on = XY) {
7400  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
7401  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
7402  angle([line1, line2], labelPosition = [-3mm, -1.7mm]) == 60deg
7403}
7404"#,
7405        )
7406        .await
7407        .unwrap();
7408
7409        let metadata = result
7410            .exec_state
7411            .global
7412            .root_module_artifacts
7413            .legacy_angle_refactor_metadata();
7414        assert_eq!(metadata.len(), 1);
7415        assert_eq!(metadata[0].sector, 1);
7416        assert!(!metadata[0].inverse);
7417    }
7418
7419    #[tokio::test(flavor = "multi_thread")]
7420    async fn parallel_legacy_angle_has_no_refactor_metadata() {
7421        let result = parse_execute(
7422            r#"
7423sketch(on = XY) {
7424  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
7425  line2 = line(start = [var 0mm, var 1mm], end = [var 4mm, var 1mm])
7426  angle([line1, line2]) == 0deg
7427}
7428"#,
7429        )
7430        .await
7431        .unwrap();
7432
7433        assert!(
7434            result
7435                .exec_state
7436                .global
7437                .root_module_artifacts
7438                .legacy_angle_refactor_metadata()
7439                .is_empty()
7440        );
7441    }
7442
7443    #[tokio::test(flavor = "multi_thread")]
7444    async fn angle_dimension_with_sector_uses_named_lines() {
7445        parse_execute(
7446            r#"
7447sketch(on = XY) {
7448  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
7449  line2 = line(start = [var 0mm, var 1mm], end = [var 2mm, var 3mm])
7450  angleDimension(lines = [line1, line2], sector = 2) == 60deg
7451}
7452"#,
7453        )
7454        .await
7455        .unwrap();
7456    }
7457
7458    #[tokio::test(flavor = "multi_thread")]
7459    async fn angle_dimension_requires_sector() {
7460        let err = parse_execute(
7461            r#"
7462sketch(on = XY) {
7463  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
7464  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
7465  angleDimension(lines = [line1, line2]) == 60deg
7466}
7467"#,
7468        )
7469        .await
7470        .unwrap_err();
7471
7472        assert!(
7473            err.to_string()
7474                .contains("The `angleDimension` function requires a keyword argument `sector`"),
7475            "unexpected error: {err:?}"
7476        );
7477    }
7478
7479    #[tokio::test(flavor = "multi_thread")]
7480    async fn angle_dimension_accepts_label_position() {
7481        let result = parse_execute(
7482            r#"
7483sketch(on = XY) {
7484  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
7485  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
7486  angleDimension(lines = [line1, line2], sector = 1, labelPosition = [10mm, 11mm]) == 60deg
7487}
7488"#,
7489        )
7490        .await
7491        .unwrap();
7492        let angle = result
7493            .exec_state
7494            .global
7495            .root_module_artifacts
7496            .scene_objects
7497            .iter()
7498            .find_map(|object| match &object.kind {
7499                ObjectKind::Constraint {
7500                    constraint: crate::front::Constraint::Angle(angle),
7501                } => Some(angle),
7502                _ => None,
7503            })
7504            .unwrap();
7505        let label_position = angle.label_position.as_ref().unwrap();
7506        assert_eq!(label_position.x.value, 10.0);
7507        assert_eq!(label_position.y.value, 11.0);
7508    }
7509
7510    #[tokio::test(flavor = "multi_thread")]
7511    async fn angle_dimension_accepts_all_four_sectors() {
7512        parse_execute(
7513            r#"
7514sketch(on = XY) {
7515  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
7516  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
7517  angleDimension(lines = [line1, line2], sector = 1) == 60deg
7518  angleDimension(lines = [line1, line2], sector = 2) == 120deg
7519  angleDimension(lines = [line1, line2], sector = 3) == 60deg
7520  angleDimension(lines = [line1, line2], sector = 4) == 120deg
7521}
7522"#,
7523        )
7524        .await
7525        .unwrap();
7526    }
7527
7528    #[tokio::test(flavor = "multi_thread")]
7529    async fn angle_dimension_accepts_inverse_angle_for_sector() {
7530        let result = parse_execute(
7531            r#"
7532sketch(on = XY) {
7533  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
7534  line2 = line(start = [var 0mm, var 0mm], end = [var 2mm, var 3.464mm])
7535  angleDimension(lines = [line1, line2], sector = 1, inverse = true) == 360deg - 60deg
7536}
7537"#,
7538        )
7539        .await
7540        .unwrap();
7541        let angle = result
7542            .exec_state
7543            .global
7544            .root_module_artifacts
7545            .scene_objects
7546            .iter()
7547            .find_map(|object| match &object.kind {
7548                ObjectKind::Constraint {
7549                    constraint: crate::front::Constraint::Angle(angle),
7550                } => Some(angle),
7551                _ => None,
7552            })
7553            .unwrap();
7554        assert_eq!(angle.sector, Some(1));
7555        assert_eq!(angle.inverse, Some(true));
7556    }
7557
7558    #[tokio::test(flavor = "multi_thread")]
7559    async fn angle_dimension_rejects_invalid_sector() {
7560        let err = parse_execute(
7561            r#"
7562sketch(on = XY) {
7563  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
7564  line2 = line(start = [var 0mm, var 1mm], end = [var 2mm, var 3mm])
7565  angleDimension(lines = [line1, line2], sector = 5) == 60deg
7566}
7567"#,
7568        )
7569        .await
7570        .unwrap_err();
7571
7572        assert!(
7573            err.to_string()
7574                .contains("angleDimension() sector must be 1, 2, 3, or 4"),
7575            "unexpected error: {err:?}"
7576        );
7577    }
7578
7579    #[tokio::test(flavor = "multi_thread")]
7580    async fn angle_dimension_rejects_parallel_lines() {
7581        let err = parse_execute(
7582            r#"
7583sketch(on = XY) {
7584  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
7585  line2 = line(start = [var 0mm, var 1mm], end = [var 4mm, var 1mm])
7586  angleDimension(lines = [line1, line2], sector = 2) == 60deg
7587}
7588"#,
7589        )
7590        .await
7591        .unwrap_err();
7592
7593        assert!(
7594            err.to_string()
7595                .contains("angleDimension(lines = ..., sector = ...) requires non-parallel lines"),
7596            "unexpected error: {err:?}"
7597        );
7598    }
7599
7600    #[tokio::test(flavor = "multi_thread")]
7601    async fn angle_accepts_label_position() {
7602        let result = parse_execute(
7603            r#"
7604sketch(on = XY) {
7605  line1 = line(start = [var 0mm, var 0mm], end = [var 4mm, var 0mm])
7606  line2 = line(start = [var 0mm, var 1mm], end = [var 2mm, var 3mm])
7607  angle([line1, line2], labelPosition = [10mm, 11mm]) == 60deg
7608}
7609"#,
7610        )
7611        .await
7612        .unwrap();
7613        let angle = result
7614            .exec_state
7615            .global
7616            .root_module_artifacts
7617            .scene_objects
7618            .iter()
7619            .find_map(|object| match &object.kind {
7620                ObjectKind::Constraint {
7621                    constraint: crate::front::Constraint::Angle(angle),
7622                } => Some(angle),
7623                _ => None,
7624            })
7625            .unwrap();
7626        let label_position = angle.label_position.as_ref().unwrap();
7627        assert_eq!(label_position.x.value, 10.0);
7628        assert_eq!(label_position.y.value, 11.0);
7629    }
7630
7631    #[tokio::test(flavor = "multi_thread")]
7632    async fn angle_requires_unlabeled_lines() {
7633        parse_execute(
7634            r#"
7635sketch(on = XY) {
7636  angle() == 60deg
7637}
7638"#,
7639        )
7640        .await
7641        .unwrap_err();
7642    }
7643
7644    #[tokio::test(flavor = "multi_thread")]
7645    async fn ascription() {
7646        let program = r#"
7647a = 42: number
7648b = a: number
7649p = {
7650  origin = { x = 0, y = 0, z = 0 },
7651  xAxis = { x = 1, y = 0, z = 0 },
7652  yAxis = { x = 0, y = 1, z = 0 },
7653  zAxis = { x = 0, y = 0, z = 1 }
7654}: Plane
7655arr1 = [42]: [number(cm)]
7656"#;
7657
7658        let result = parse_execute(program).await.unwrap();
7659        let mem = result.exec_state.stack();
7660        assert!(matches!(
7661            mem.memory
7662                .get_from_owned("p", result.mem_env, SourceRange::default(), 0)
7663                .unwrap(),
7664            KclValue::Plane { .. }
7665        ));
7666        let arr1 = mem
7667            .memory
7668            .get_from_owned("arr1", result.mem_env, SourceRange::default(), 0)
7669            .unwrap();
7670        if let KclValue::HomArray { value, ty } = arr1 {
7671            assert_eq!(value.len(), 1, "Expected Vec with specific length: found {value:?}");
7672            assert_eq!(ty, RuntimeType::known_length(UnitLength::Centimeters));
7673            // Compare, ignoring meta.
7674            if let KclValue::Number { value, ty, .. } = &value[0] {
7675                // It should not convert units.
7676                assert_eq!(*value, 42.0);
7677                assert_eq!(*ty, NumericType::Known(UnitType::Length(UnitLength::Centimeters)));
7678            } else {
7679                panic!("Expected a number; found {:?}", value[0]);
7680            }
7681        } else {
7682            panic!("Expected HomArray; found {arr1:?}");
7683        }
7684
7685        let program = r#"
7686a = 42: string
7687"#;
7688        let result = parse_execute(program).await;
7689        let err = result.unwrap_err();
7690        assert!(
7691            err.to_string()
7692                .contains("could not coerce a number (with type `number`) to type `string`"),
7693            "Expected error but found {err:?}"
7694        );
7695
7696        let program = r#"
7697a = 42: Plane
7698"#;
7699        let result = parse_execute(program).await;
7700        let err = result.unwrap_err();
7701        assert!(
7702            err.to_string()
7703                .contains("could not coerce a number (with type `number`) to type `Plane`"),
7704            "Expected error but found {err:?}"
7705        );
7706
7707        let program = r#"
7708arr = [0]: [string]
7709"#;
7710        let result = parse_execute(program).await;
7711        let err = result.unwrap_err();
7712        assert!(
7713            err.to_string().contains(
7714                "could not coerce an array of `number` with 1 value (with type `[any; 1]`) to type `[string]`"
7715            ),
7716            "Expected error but found {err:?}"
7717        );
7718
7719        let program = r#"
7720mixedArr = [0, "a"]: [number(mm)]
7721"#;
7722        let result = parse_execute(program).await;
7723        let err = result.unwrap_err();
7724        assert!(
7725            err.to_string().contains(
7726                "could not coerce an array of `number`, `string` (with type `[any; 2]`) to type `[number(mm)]`"
7727            ),
7728            "Expected error but found {err:?}"
7729        );
7730
7731        let program = r#"
7732mixedArr = [0, "a"]: [mm]
7733"#;
7734        let result = parse_execute(program).await;
7735        let err = result.unwrap_err();
7736        assert!(
7737            err.to_string().contains(
7738                "could not coerce an array of `number`, `string` (with type `[any; 2]`) to type `[number(mm)]`"
7739            ),
7740            "Expected error but found {err:?}"
7741        );
7742    }
7743
7744    #[tokio::test(flavor = "multi_thread")]
7745    async fn neg_plane() {
7746        let program = r#"
7747p = {
7748  origin = { x = 0, y = 0, z = 0 },
7749  xAxis = { x = 1, y = 0, z = 0 },
7750  yAxis = { x = 0, y = 1, z = 0 },
7751}: Plane
7752p2 = -p
7753"#;
7754
7755        let result = parse_execute(program).await.unwrap();
7756        let mem = result.exec_state.stack();
7757        match mem
7758            .memory
7759            .get_from_owned("p2", result.mem_env, SourceRange::default(), 0)
7760            .unwrap()
7761        {
7762            KclValue::Plane { value } => {
7763                assert_eq!(value.info.x_axis.x, -1.0);
7764                assert_eq!(value.info.x_axis.y, 0.0);
7765                assert_eq!(value.info.x_axis.z, 0.0);
7766            }
7767            _ => unreachable!(),
7768        }
7769    }
7770
7771    #[tokio::test(flavor = "multi_thread")]
7772    async fn multiple_returns() {
7773        let program = r#"fn foo() {
7774  return 0
7775  return 42
7776}
7777
7778a = foo()
7779"#;
7780
7781        let result = parse_execute(program).await;
7782        assert!(result.unwrap_err().to_string().contains("return"));
7783    }
7784
7785    #[tokio::test(flavor = "multi_thread")]
7786    async fn load_all_modules() {
7787        // program a.kcl
7788        let program_a_kcl = r#"
7789export a = 1
7790"#;
7791        // program b.kcl
7792        let program_b_kcl = r#"
7793import a from 'a.kcl'
7794
7795export b = a + 1
7796"#;
7797        // program c.kcl
7798        let program_c_kcl = r#"
7799import a from 'a.kcl'
7800
7801export c = a + 2
7802"#;
7803
7804        // program main.kcl
7805        let main_kcl = r#"
7806import b from 'b.kcl'
7807import c from 'c.kcl'
7808
7809d = b + c
7810"#;
7811
7812        let main = crate::parsing::parse_str(main_kcl, ModuleId::default())
7813            .parse_errs_as_err()
7814            .unwrap();
7815
7816        let tmpdir = tempfile::TempDir::with_prefix("zma_kcl_load_all_modules").unwrap();
7817
7818        tokio::fs::File::create(tmpdir.path().join("main.kcl"))
7819            .await
7820            .unwrap()
7821            .write_all(main_kcl.as_bytes())
7822            .await
7823            .unwrap();
7824
7825        tokio::fs::File::create(tmpdir.path().join("a.kcl"))
7826            .await
7827            .unwrap()
7828            .write_all(program_a_kcl.as_bytes())
7829            .await
7830            .unwrap();
7831
7832        tokio::fs::File::create(tmpdir.path().join("b.kcl"))
7833            .await
7834            .unwrap()
7835            .write_all(program_b_kcl.as_bytes())
7836            .await
7837            .unwrap();
7838
7839        tokio::fs::File::create(tmpdir.path().join("c.kcl"))
7840            .await
7841            .unwrap()
7842            .write_all(program_c_kcl.as_bytes())
7843            .await
7844            .unwrap();
7845
7846        let exec_ctxt = ExecutorContext {
7847            engine: Arc::new(engine_manager::EngineManager::new_mock()),
7848            engine_batch: crate::engine::EngineBatchContext::default(),
7849            fs: crate::fs::new_file_system_handle(crate::fs::FileManager::new()),
7850            settings: ExecutorSettings {
7851                project_directory: Some(crate::TypedPath(tmpdir.path().into())),
7852                ..Default::default()
7853            },
7854            context_type: ContextType::Mock,
7855            execution_callbacks: Default::default(),
7856            executor_kind: ExecutorKind::resolve(),
7857            machine_call_depth_limit: crate::execution::machine::DEFAULT_MACHINE_CALL_DEPTH_LIMIT,
7858        };
7859        let mut exec_state = ExecState::new(&exec_ctxt);
7860
7861        exec_ctxt
7862            .run(
7863                &crate::Program {
7864                    ast: main.clone(),
7865                    original_file_contents: "".to_owned(),
7866                },
7867                &mut exec_state,
7868            )
7869            .await
7870            .unwrap();
7871    }
7872
7873    #[tokio::test(flavor = "multi_thread")]
7874    async fn user_coercion() {
7875        let program = r#"fn foo(x: Axis2d) {
7876  return 0
7877}
7878
7879foo(x = { direction = [0, 0], origin = [0, 0]})
7880"#;
7881
7882        parse_execute(program).await.unwrap();
7883
7884        let program = r#"fn foo(x: Axis3d) {
7885  return 0
7886}
7887
7888foo(x = { direction = [0, 0], origin = [0, 0]})
7889"#;
7890
7891        parse_execute(program).await.unwrap_err();
7892    }
7893
7894    #[tokio::test(flavor = "multi_thread")]
7895    async fn coerce_return() {
7896        let program = r#"fn foo(): number(mm) {
7897  return 42
7898}
7899
7900a = foo()
7901"#;
7902
7903        parse_execute(program).await.unwrap();
7904
7905        let program = r#"fn foo(): mm {
7906  return 42
7907}
7908
7909a = foo()
7910"#;
7911
7912        parse_execute(program).await.unwrap();
7913
7914        let program = r#"fn foo(): number(mm) {
7915  return { bar: 42 }
7916}
7917
7918a = foo()
7919"#;
7920
7921        parse_execute(program).await.unwrap_err();
7922
7923        let program = r#"fn foo(): mm {
7924  return { bar: 42 }
7925}
7926
7927a = foo()
7928"#;
7929
7930        parse_execute(program).await.unwrap_err();
7931    }
7932
7933    #[tokio::test(flavor = "multi_thread")]
7934    async fn test_sensible_error_when_missing_equals_in_kwarg() {
7935        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)"]
7936            .into_iter()
7937            .enumerate()
7938        {
7939            let program = format!(
7940                "fn foo() {{ return 0 }}
7941z = 0
7942fn f(x, y, z) {{ return 0 }}
7943{call}"
7944            );
7945            let err = parse_execute(&program).await.unwrap_err();
7946            let msg = err.message();
7947            assert!(
7948                msg.contains("This argument needs a label, but it doesn't have one"),
7949                "failed test {i}: {msg}"
7950            );
7951            assert!(msg.contains("`y`"), "failed test {i}, missing `y`: {msg}");
7952            if i == 0 {
7953                assert!(msg.contains("`z`"), "failed test {i}, missing `z`: {msg}");
7954            }
7955        }
7956    }
7957
7958    #[tokio::test(flavor = "multi_thread")]
7959    async fn default_param_for_unlabeled() {
7960        // Tests that the input param for myExtrude is taken from the pipeline value and same-name
7961        // keyword args.
7962        let ast = r#"fn myExtrude(@sk, length) {
7963  return extrude(sk, length)
7964}
7965sketch001 = startSketchOn(XY)
7966  |> circle(center = [0, 0], radius = 93.75)
7967  |> myExtrude(length = 40)
7968"#;
7969
7970        parse_execute(ast).await.unwrap();
7971    }
7972
7973    #[tokio::test(flavor = "multi_thread")]
7974    async fn dont_use_unlabelled_as_input() {
7975        // `length` should be used as the `length` argument to extrude, not the unlabelled input
7976        let ast = r#"length = 10
7977startSketchOn(XY)
7978  |> circle(center = [0, 0], radius = 93.75)
7979  |> extrude(length)
7980"#;
7981
7982        parse_execute(ast).await.unwrap();
7983    }
7984
7985    #[tokio::test(flavor = "multi_thread")]
7986    async fn ascription_in_binop() {
7987        let ast = r#"foo = tan(0): number(rad) - 4deg"#;
7988        parse_execute(ast).await.unwrap();
7989
7990        let ast = r#"foo = tan(0): rad - 4deg"#;
7991        parse_execute(ast).await.unwrap();
7992    }
7993
7994    #[tokio::test(flavor = "multi_thread")]
7995    async fn neg_sqrt() {
7996        let ast = r#"bad = sqrt(-2)"#;
7997
7998        let e = parse_execute(ast).await.unwrap_err();
7999        // Make sure we get a useful error message and not an engine error.
8000        assert!(e.message().contains("sqrt"), "Error message: '{}'", e.message());
8001    }
8002
8003    #[tokio::test(flavor = "multi_thread")]
8004    async fn non_array_fns() {
8005        let ast = r#"push(1, item = 2)
8006pop(1)
8007map(1, f = fn(@x) { return x + 1 })
8008reduce(1, f = fn(@x, accum) { return accum + x}, initial = 0)"#;
8009
8010        parse_execute(ast).await.unwrap();
8011    }
8012
8013    #[tokio::test(flavor = "multi_thread")]
8014    async fn non_array_indexing() {
8015        let good = r#"a = 42
8016good = a[0]
8017"#;
8018        let result = parse_execute(good).await.unwrap();
8019        let mem = result.exec_state.stack();
8020        let num = mem
8021            .memory
8022            .get_from_owned("good", result.mem_env, SourceRange::default(), 0)
8023            .unwrap()
8024            .as_ty_f64()
8025            .unwrap();
8026        assert_eq!(num.n, 42.0);
8027
8028        let bad = r#"a = 42
8029bad = a[1]
8030"#;
8031
8032        parse_execute(bad).await.unwrap_err();
8033    }
8034
8035    #[tokio::test(flavor = "multi_thread")]
8036    async fn coerce_unknown_to_length() {
8037        let ast = r#"x = 2mm * 2mm
8038y = x: number(Length)"#;
8039        let e = parse_execute(ast).await.unwrap_err();
8040        assert!(
8041            e.message().contains("could not coerce"),
8042            "Error message: '{}'",
8043            e.message()
8044        );
8045
8046        let ast = r#"x = 2mm
8047y = x: number(Length)"#;
8048        let result = parse_execute(ast).await.unwrap();
8049        let mem = result.exec_state.stack();
8050        let num = mem
8051            .memory
8052            .get_from_owned("y", result.mem_env, SourceRange::default(), 0)
8053            .unwrap()
8054            .as_ty_f64()
8055            .unwrap();
8056        assert_eq!(num.n, 2.0);
8057        assert_eq!(num.ty, NumericType::mm());
8058    }
8059
8060    #[tokio::test(flavor = "multi_thread")]
8061    async fn one_warning_unknown() {
8062        let ast = r#"
8063// Should warn once
8064a = PI * 2
8065// Should warn once
8066b = (PI * 2) / 3
8067// Should not warn
8068c = ((PI * 2) / 3): number(deg)
8069"#;
8070
8071        let result = parse_execute(ast).await.unwrap();
8072        assert_eq!(result.exec_state.issues().len(), 2);
8073    }
8074
8075    #[tokio::test(flavor = "multi_thread")]
8076    async fn non_count_indexing() {
8077        let ast = r#"x = [0, 0]
8078y = x[1mm]
8079"#;
8080        parse_execute(ast).await.unwrap_err();
8081
8082        let ast = r#"x = [0, 0]
8083y = 1deg
8084z = x[y]
8085"#;
8086        parse_execute(ast).await.unwrap_err();
8087
8088        let ast = r#"x = [0, 0]
8089y = x[0mm + 1]
8090"#;
8091        parse_execute(ast).await.unwrap_err();
8092    }
8093
8094    #[tokio::test(flavor = "multi_thread")]
8095    async fn getting_property_of_plane() {
8096        let ast = std::fs::read_to_string("tests/inputs/planestuff.kcl").unwrap();
8097        parse_execute(&ast).await.unwrap();
8098    }
8099
8100    #[tokio::test(flavor = "multi_thread")]
8101    async fn no_artifacts_from_within_hole_call() {
8102        // Test that executing stdlib KCL, like the `hole` function
8103        // (which is actually implemented in KCL not Rust)
8104        // does not generate artifacts from within the stdlib code,
8105        // only from the user code.
8106        let ast = std::fs::read_to_string("tests/inputs/sample_hole.kcl").unwrap();
8107        let out = parse_execute(&ast).await.unwrap();
8108
8109        // Get all the operations that occurred.
8110        let actual_operations = out.exec_state.global.root_module_artifacts.operations;
8111
8112        // There should be 5, for sketching the cube and applying the hole.
8113        // If the stdlib internal calls are being tracked, that's a bug,
8114        // and the actual number of operations will be something like 35.
8115        let expected = 5;
8116        assert_eq!(
8117            actual_operations.len(),
8118            expected,
8119            "expected {expected} operations, received {}:\n{actual_operations:#?}",
8120            actual_operations.len(),
8121        );
8122    }
8123
8124    #[tokio::test(flavor = "multi_thread")]
8125    async fn feature_tree_annotation_on_user_defined_kcl() {
8126        // The call to foo() should not generate an operation,
8127        // because its 'feature_tree' attribute has been set to false.
8128        let ast = std::fs::read_to_string("tests/inputs/feature_tree_annotation_on_user_defined_kcl.kcl").unwrap();
8129        let out = parse_execute(&ast).await.unwrap();
8130
8131        // Get all the operations that occurred.
8132        let actual_operations = out.exec_state.global.root_module_artifacts.operations;
8133
8134        let expected = 0;
8135        assert_eq!(
8136            actual_operations.len(),
8137            expected,
8138            "expected {expected} operations, received {}:\n{actual_operations:#?}",
8139            actual_operations.len(),
8140        );
8141    }
8142
8143    #[tokio::test(flavor = "multi_thread")]
8144    async fn no_feature_tree_annotation_on_user_defined_kcl() {
8145        // The call to foo() should generate an operation,
8146        // because @(feature_tree) defaults to true.
8147        let ast = std::fs::read_to_string("tests/inputs/no_feature_tree_annotation_on_user_defined_kcl.kcl").unwrap();
8148        let out = parse_execute(&ast).await.unwrap();
8149
8150        // Get all the operations that occurred.
8151        let actual_operations = out.exec_state.global.root_module_artifacts.operations;
8152
8153        let expected = 2;
8154        assert_eq!(
8155            actual_operations.len(),
8156            expected,
8157            "expected {expected} operations, received {}:\n{actual_operations:#?}",
8158            actual_operations.len(),
8159        );
8160        assert!(matches!(actual_operations[0], Operation::GroupBegin { .. }));
8161        assert!(matches!(actual_operations[1], Operation::GroupEnd));
8162    }
8163
8164    #[tokio::test(flavor = "multi_thread")]
8165    async fn custom_warning() {
8166        let warn = r#"
8167a = PI * 2
8168"#;
8169        let result = parse_execute(warn).await.unwrap();
8170        assert_eq!(result.exec_state.issues().len(), 1);
8171        assert_eq!(result.exec_state.issues()[0].severity, Severity::Warning);
8172
8173        let allow = r#"
8174@warnings(allow = unknownUnits)
8175a = PI * 2
8176"#;
8177        let result = parse_execute(allow).await.unwrap();
8178        assert_eq!(result.exec_state.issues().len(), 0);
8179
8180        let deny = r#"
8181@warnings(deny = [unknownUnits])
8182a = PI * 2
8183"#;
8184        let result = parse_execute(deny).await.unwrap();
8185        assert_eq!(result.exec_state.issues().len(), 1);
8186        assert_eq!(result.exec_state.issues()[0].severity, Severity::Error);
8187    }
8188
8189    /// KCL 3.0 renamed `@warnings` to `@diagnostics`. Under KCL 3.0-preview
8190    /// or later, `@diagnostics` customizes diagnostics the way `@warnings`
8191    /// does in earlier versions.
8192    #[tokio::test(flavor = "multi_thread")]
8193    async fn diagnostics_attribute_in_v3() {
8194        let warn = "@settings(kclVersion = \"3.0-preview\")\na = PI * 2\n";
8195        let result = parse_execute(warn).await.unwrap();
8196        let issues = result.exec_state.issues();
8197        assert_eq!(issues.len(), 1, "{issues:#?}");
8198        assert_eq!(issues[0].severity, Severity::Warning);
8199        assert_eq!(issues[0].tag, crate::errors::Tag::UnknownNumericUnits);
8200
8201        let allow = "@settings(kclVersion = \"3.0-preview\")\n@diagnostics(allow = unknownUnits)\na = PI * 2\n";
8202        let result = parse_execute(allow).await.unwrap();
8203        assert!(
8204            result.exec_state.issues().is_empty(),
8205            "{:#?}",
8206            result.exec_state.issues()
8207        );
8208
8209        let deny = "@settings(kclVersion = \"3.0-preview\")\n@diagnostics(deny = [unknownUnits])\na = PI * 2\n";
8210        let result = parse_execute(deny).await.unwrap();
8211        let issues = result.exec_state.issues();
8212        assert_eq!(issues.len(), 1, "{issues:#?}");
8213        assert_eq!(issues[0].severity, Severity::Error);
8214        assert_eq!(issues[0].tag, crate::errors::Tag::UnknownNumericUnits);
8215    }
8216
8217    /// Before KCL 3.0, `@diagnostics` is not an attribute: it gets the usual
8218    /// unknown-annotation warning and has no effect, while `@warnings` keeps
8219    /// working.
8220    #[tokio::test(flavor = "multi_thread")]
8221    async fn diagnostics_attribute_is_unknown_before_v3() {
8222        for version in ["1.0", "2.0"] {
8223            let code = format!("@settings(kclVersion = {version})\n@diagnostics(allow = unknownUnits)\na = PI * 2\n");
8224            let result = parse_execute(&code).await.unwrap();
8225            let issues = result.exec_state.issues();
8226            assert_eq!(issues.len(), 2, "code={code}, issues={issues:#?}");
8227            assert_eq!(issues[0].severity, Severity::Warning);
8228            assert_eq!(issues[0].message, "Unknown annotation");
8229            assert_eq!(
8230                &code[issues[0].source_range.start()..issues[0].source_range.end()],
8231                "@diagnostics(allow = unknownUnits)"
8232            );
8233            assert_eq!(issues[1].severity, Severity::Warning);
8234            assert_eq!(issues[1].tag, crate::errors::Tag::UnknownNumericUnits);
8235
8236            let code = format!("@settings(kclVersion = {version})\n@warnings(allow = unknownUnits)\na = PI * 2\n");
8237            let result = parse_execute(&code).await.unwrap();
8238            assert!(
8239                result.exec_state.issues().is_empty(),
8240                "code={code}, issues={:#?}",
8241                result.exec_state.issues()
8242            );
8243        }
8244    }
8245
8246    /// Errors for a malformed attribute name the attribute that was written:
8247    /// `warnings` before KCL 3.0 and `diagnostics` in KCL 3.0-preview or
8248    /// later.
8249    #[tokio::test(flavor = "multi_thread")]
8250    async fn diagnostics_attribute_errors_use_the_attribute_name() {
8251        for (version, attr, noun) in [
8252            ("1.0", "warnings", "warning"),
8253            ("2.0", "warnings", "warning"),
8254            ("\"3.0-preview\"", "diagnostics", "diagnostic"),
8255        ] {
8256            let settings = format!("@settings(kclVersion = {version})\n");
8257
8258            let code = format!("{settings}@{attr}\n");
8259            let error = parse_execute(&code).await.unwrap_err();
8260            assert_eq!(error.message(), format!("Empty `{attr}` annotation"), "code={code}");
8261
8262            let code = format!("{settings}@{attr}(warn = unknownUnits)\n");
8263            let error = parse_execute(&code).await.unwrap_err();
8264            assert_eq!(
8265                error.message(),
8266                format!("Unexpected {attr} key: `warn`; expected one of `allow`, `deny`"),
8267                "code={code}"
8268            );
8269
8270            let code = format!("{settings}@{attr}(allow = 1)\n");
8271            let error = parse_execute(&code).await.unwrap_err();
8272            assert_eq!(
8273                error.message(),
8274                format!(
8275                    "Unexpected {attr} value, expected a name or array of names, e.g., `unknownUnits` or `[unknownUnits, deprecated]`"
8276                ),
8277                "code={code}"
8278            );
8279
8280            let code = format!("{settings}@{attr}(allow = bogus)\n");
8281            let error = parse_execute(&code).await.unwrap_err();
8282            let expected_prefix = format!("Unexpected {noun} value: `bogus`; accepted values: unknownUnits, ");
8283            assert!(
8284                error.message().starts_with(&expected_prefix),
8285                "code={code}, message={}",
8286                error.message()
8287            );
8288        }
8289    }
8290
8291    /// KCL 3.0: using the old `@warnings` name is a non-fatal error that
8292    /// explains the rename and offers the fix. The attribute is ignored, so
8293    /// the warning it tried to allow is still reported.
8294    #[tokio::test(flavor = "multi_thread")]
8295    async fn warnings_attribute_is_renamed_in_v3() {
8296        let code = "@settings(kclVersion = \"3.0-preview\")\n@warnings(allow = unknownUnits)\na = PI * 2\n";
8297        let result = parse_execute(code).await.unwrap();
8298        let issues = result.exec_state.issues();
8299        assert_eq!(issues.len(), 2, "{issues:#?}");
8300
8301        let renamed = &issues[0];
8302        assert_eq!(renamed.severity, Severity::Error);
8303        assert_eq!(
8304            renamed.message,
8305            "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."
8306        );
8307        assert_eq!(
8308            &code[renamed.source_range.start()..renamed.source_range.end()],
8309            "@warnings(allow = unknownUnits)"
8310        );
8311        let suggestion = renamed.suggestion.as_ref().unwrap();
8312        assert_eq!(suggestion.title, "Rename to `@diagnostics`");
8313        assert_eq!(
8314            suggestion.apply(code),
8315            "@settings(kclVersion = \"3.0-preview\")\n@diagnostics(allow = unknownUnits)\na = PI * 2\n"
8316        );
8317
8318        assert_eq!(issues[1].severity, Severity::Warning);
8319        assert_eq!(issues[1].tag, crate::errors::Tag::UnknownNumericUnits);
8320
8321        // Since the attribute is ignored, its properties aren't checked.
8322        let code = "@settings(kclVersion = \"3.0-preview\")\n@warnings(allow = bogus)\n";
8323        let result = parse_execute(code).await.unwrap();
8324        let issues = result.exec_state.issues();
8325        assert_eq!(issues.len(), 1, "{issues:#?}");
8326        assert_eq!(issues[0].severity, Severity::Error);
8327        assert!(issues[0].message.starts_with("The `@warnings` attribute was renamed"));
8328    }
8329
8330    #[tokio::test(flavor = "multi_thread")]
8331    async fn sketch_block_unqualified_functions_use_sketch2() {
8332        let ast = r#"
8333s = sketch(on = XY) {
8334  line1 = line(start = [var 0mm, var 0mm], end = [var 1mm, var 0mm])
8335  line2 = line(start = [var 1mm, var 0mm], end = [var 1mm, var 1mm])
8336  coincident([line1.end, line2.start])
8337}
8338"#;
8339        let result = parse_execute(ast).await.unwrap();
8340        let mem = result.exec_state.stack();
8341        let sketch_value = mem
8342            .memory
8343            .get_from_owned("s", result.mem_env, SourceRange::default(), 0)
8344            .unwrap();
8345
8346        let KclValue::Object { value, .. } = sketch_value else {
8347            panic!("Expected sketch block to return an object, got {sketch_value:?}");
8348        };
8349
8350        assert!(value.contains_key("line1"));
8351        assert!(value.contains_key("line2"));
8352        // Ensure sketch2 aliases used during execution are not returned as
8353        // sketch block fields.
8354        assert!(!value.contains_key("line"));
8355        assert!(!value.contains_key("coincident"));
8356    }
8357
8358    #[tokio::test(flavor = "multi_thread")]
8359    async fn solver_module_is_not_available_outside_sketch_blocks() {
8360        let err = parse_execute("a = solver::ORIGIN").await.unwrap_err();
8361        assert!(err.message().contains("solver"), "Error message: '{}'", err.message());
8362
8363        let err = parse_execute(
8364            r#"@settings(experimentalFeatures = allow)
8365
8366import "std::solver""#,
8367        )
8368        .await
8369        .unwrap_err();
8370        assert!(
8371            err.message().contains("only available inside sketch blocks"),
8372            "Error message: '{}'",
8373            err.message()
8374        );
8375    }
8376
8377    #[tokio::test(flavor = "multi_thread")]
8378    async fn cannot_solid_extrude_an_open_profile() {
8379        // This should fail during mock execution, because KCL should catch
8380        // that the profile is not closed.
8381        let code = std::fs::read_to_string("tests/inputs/cannot_solid_extrude_an_open_profile.kcl").unwrap();
8382        let program = crate::Program::parse_no_errs(&code).expect("should parse");
8383        let exec_ctxt = ExecutorContext::new_mock(None).await;
8384        let mut exec_state = ExecState::new(&exec_ctxt);
8385
8386        let err = exec_ctxt.run(&program, &mut exec_state).await.unwrap_err().error;
8387        assert!(matches!(err, KclError::Semantic { .. }));
8388        exec_ctxt.close().await;
8389    }
8390}