Skip to main content

kcl_lib/execution/
exec_ast.rs

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