Skip to main content

kcl_lib/execution/
exec_ast.rs

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