Skip to content

Commit 1a8de2b

Browse files
authored
Add coercing default units to known units in KCL array range (#9219)
* Add failing test * Change array range to be more forgiving in terms of units * Update test output to success * Update existing test output
1 parent 119edb6 commit 1a8de2b

File tree

13 files changed

+448
-26
lines changed

13 files changed

+448
-26
lines changed

rust/kcl-lib/src/execution/exec_ast.rs

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ use crate::{
1919
state::{ModuleState, SketchBlockState},
2020
types::{NumericType, PrimitiveType, RuntimeType},
2121
},
22-
fmt,
2322
modules::{ModuleExecutionOutcome, ModuleId, ModulePath, ModuleRepr},
2423
parsing::ast::types::{
2524
Annotation, ArrayExpression, ArrayRangeExpression, AscribedExpression, BinaryExpression, BinaryOperator,
@@ -2072,33 +2071,40 @@ impl Node<ArrayRangeExpression> {
20722071
StatementKind::Expression,
20732072
)
20742073
.await?;
2075-
let (start, start_ty) = start_val
2076-
.as_int_with_ty()
2074+
let start = start_val
2075+
.as_ty_f64()
20772076
.ok_or(KclError::new_semantic(KclErrorDetails::new(
2078-
format!("Expected int but found {}", start_val.human_friendly_type()),
2077+
format!(
2078+
"Expected number for range start but found {}",
2079+
start_val.human_friendly_type()
2080+
),
20792081
vec![self.into()],
20802082
)))?;
20812083
let metadata = Metadata::from(&self.end_element);
20822084
let end_val = ctx
20832085
.execute_expr(&self.end_element, exec_state, &metadata, &[], StatementKind::Expression)
20842086
.await?;
2085-
let (end, end_ty) = end_val
2086-
.as_int_with_ty()
2087-
.ok_or(KclError::new_semantic(KclErrorDetails::new(
2088-
format!("Expected int but found {}", end_val.human_friendly_type()),
2089-
vec![self.into()],
2090-
)))?;
2087+
let end = end_val.as_ty_f64().ok_or(KclError::new_semantic(KclErrorDetails::new(
2088+
format!(
2089+
"Expected number for range end but found {}",
2090+
end_val.human_friendly_type()
2091+
),
2092+
vec![self.into()],
2093+
)))?;
20912094

2092-
if start_ty != end_ty {
2093-
let start = start_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: start_ty });
2094-
let start = fmt::human_display_number(start.n, start.ty);
2095-
let end = end_val.as_ty_f64().unwrap_or(TyF64 { n: 0.0, ty: end_ty });
2096-
let end = fmt::human_display_number(end.n, end.ty);
2095+
let (start, end, ty) = NumericType::combine_range(start, end, exec_state, self.as_source_range())?;
2096+
let Some(start) = crate::try_f64_to_i64(start) else {
20972097
return Err(KclError::new_semantic(KclErrorDetails::new(
2098-
format!("Range start and end must be of the same type, but found {start} and {end}"),
2098+
format!("Range start must be an integer, but found {start}"),
20992099
vec![self.into()],
21002100
)));
2101-
}
2101+
};
2102+
let Some(end) = crate::try_f64_to_i64(end) else {
2103+
return Err(KclError::new_semantic(KclErrorDetails::new(
2104+
format!("Range end must be an integer, but found {end}"),
2105+
vec![self.into()],
2106+
)));
2107+
};
21022108

21032109
if end < start {
21042110
return Err(KclError::new_semantic(KclErrorDetails::new(
@@ -2122,11 +2128,11 @@ impl Node<ArrayRangeExpression> {
21222128
.into_iter()
21232129
.map(|num| KclValue::Number {
21242130
value: num as f64,
2125-
ty: start_ty,
2131+
ty,
21262132
meta: meta.clone(),
21272133
})
21282134
.collect(),
2129-
ty: RuntimeType::Primitive(PrimitiveType::Number(start_ty)),
2135+
ty: RuntimeType::Primitive(PrimitiveType::Number(ty)),
21302136
})
21312137
}
21322138
}

rust/kcl-lib/src/execution/types.rs

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{collections::HashMap, fmt, str::FromStr};
1+
use std::{collections::HashMap, str::FromStr};
22

33
use anyhow::Result;
44
use kittycad_modeling_cmds::units::{UnitAngle, UnitLength};
@@ -12,6 +12,7 @@ use crate::{
1212
kcl_value::{KclValue, TypeDef},
1313
memory::{self},
1414
},
15+
fmt,
1516
parsing::{
1617
ast::types::{PrimitiveType as AstPrimitiveType, Type},
1718
token::NumericSuffix,
@@ -323,8 +324,8 @@ impl RuntimeType {
323324
}
324325
}
325326

326-
impl fmt::Display for RuntimeType {
327-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327+
impl std::fmt::Display for RuntimeType {
328+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328329
match self {
329330
RuntimeType::Primitive(t) => t.fmt(f),
330331
RuntimeType::Array(t, l) => match l {
@@ -455,8 +456,8 @@ impl PrimitiveType {
455456
}
456457
}
457458

458-
impl fmt::Display for PrimitiveType {
459-
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459+
impl std::fmt::Display for PrimitiveType {
460+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
460461
match self {
461462
PrimitiveType::Any => write!(f, "any"),
462463
PrimitiveType::None => write!(f, "none"),
@@ -750,6 +751,78 @@ impl NumericType {
750751
}
751752
}
752753

754+
/// Combine two types for range operations.
755+
///
756+
/// This combinator function is suitable for ranges where uncertainty should
757+
/// be handled by the user, and it doesn't make sense to convert units. So
758+
/// this is one of th most conservative ways to combine types.
759+
pub fn combine_range(
760+
a: TyF64,
761+
b: TyF64,
762+
exec_state: &mut ExecState,
763+
source_range: SourceRange,
764+
) -> Result<(f64, f64, NumericType), KclError> {
765+
use NumericType::*;
766+
match (a.ty, b.ty) {
767+
(at, bt) if at == bt => Ok((a.n, b.n, at)),
768+
(at, Any) => Ok((a.n, b.n, at)),
769+
(Any, bt) => Ok((a.n, b.n, bt)),
770+
771+
(Known(UnitType::Length(l1)), Known(UnitType::Length(l2))) => {
772+
Err(KclError::new_semantic(KclErrorDetails::new(
773+
format!("Range start and range end have incompatible units: {l1} and {l2}"),
774+
vec![source_range],
775+
)))
776+
}
777+
(Known(UnitType::Angle(a1)), Known(UnitType::Angle(a2))) => {
778+
Err(KclError::new_semantic(KclErrorDetails::new(
779+
format!("Range start and range end have incompatible units: {a1} and {a2}"),
780+
vec![source_range],
781+
)))
782+
}
783+
784+
(t @ Known(UnitType::Length(_)), Known(UnitType::GenericLength)) => Ok((a.n, b.n, t)),
785+
(Known(UnitType::GenericLength), t @ Known(UnitType::Length(_))) => Ok((a.n, b.n, t)),
786+
(t @ Known(UnitType::Angle(_)), Known(UnitType::GenericAngle)) => Ok((a.n, b.n, t)),
787+
(Known(UnitType::GenericAngle), t @ Known(UnitType::Angle(_))) => Ok((a.n, b.n, t)),
788+
789+
(Known(UnitType::Count), Default { .. }) | (Default { .. }, Known(UnitType::Count)) => {
790+
Ok((a.n, b.n, Known(UnitType::Count)))
791+
}
792+
(t @ Known(UnitType::Length(l1)), Default { len: l2, .. }) if l1 == l2 => Ok((a.n, b.n, t)),
793+
(Default { len: l1, .. }, t @ Known(UnitType::Length(l2))) if l1 == l2 => Ok((a.n, b.n, t)),
794+
(t @ Known(UnitType::Angle(a1)), Default { angle: a2, .. }) if a1 == a2 => {
795+
if b.n != 0.0 {
796+
exec_state.warn(
797+
CompilationError::err(source_range, "Prefer to use explicit units for angles"),
798+
annotations::WARN_ANGLE_UNITS,
799+
);
800+
}
801+
Ok((a.n, b.n, t))
802+
}
803+
(Default { angle: a1, .. }, t @ Known(UnitType::Angle(a2))) if a1 == a2 => {
804+
if a.n != 0.0 {
805+
exec_state.warn(
806+
CompilationError::err(source_range, "Prefer to use explicit units for angles"),
807+
annotations::WARN_ANGLE_UNITS,
808+
);
809+
}
810+
Ok((a.n, b.n, t))
811+
}
812+
813+
_ => {
814+
let a = fmt::human_display_number(a.n, a.ty);
815+
let b = fmt::human_display_number(b.n, b.ty);
816+
Err(KclError::new_semantic(KclErrorDetails::new(
817+
format!(
818+
"Range start and range end must be of the same type and have compatible units, but found {a} and {b}",
819+
),
820+
vec![source_range],
821+
)))
822+
}
823+
}
824+
}
825+
753826
pub fn from_parsed(suffix: NumericSuffix, settings: &super::MetaSettings) -> Self {
754827
match suffix {
755828
NumericSuffix::None => NumericType::Default {

rust/kcl-lib/src/simulation_tests.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -782,6 +782,28 @@ mod array_range_mismatch_units {
782782
super::execute(TEST_NAME, false).await
783783
}
784784
}
785+
mod array_range_units_default_count {
786+
const TEST_NAME: &str = "array_range_units_default_count";
787+
788+
/// Test parsing KCL.
789+
#[test]
790+
fn parse() {
791+
super::parse(TEST_NAME)
792+
}
793+
794+
/// Test that parsing and unparsing KCL produces the original KCL input.
795+
#[tokio::test(flavor = "multi_thread")]
796+
async fn unparse() {
797+
super::unparse(TEST_NAME).await
798+
}
799+
800+
/// Test that KCL is executed correctly.
801+
#[tokio::test(flavor = "multi_thread")]
802+
async fn kcl_test_execute() {
803+
super::execute(TEST_NAME, false).await
804+
}
805+
}
806+
785807
mod sketch_in_object {
786808
const TEST_NAME: &str = "sketch_in_object";
787809

rust/kcl-lib/tests/array_range_mismatch_units/execution_error.snap

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ description: Error from executing array_range_mismatch_units.kcl
44
---
55
KCL Semantic error
66

7-
× semantic: Range start and end must be of the same type, but found 1:
8-
number(mm) and 3: number(cm)
7+
× semantic: Range start and range end have incompatible units: mm and cm
98
╭─[1:5]
109
1a = [1mm..3cm]
1110
· ─────┬────
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
source: kcl-lib/src/simulation_tests.rs
3+
description: Artifact commands array_range_units_default_count.kcl
4+
---
5+
{}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
source: kcl-lib/src/simulation_tests.rs
3+
description: Artifact graph flowchart array_range_units_default_count.kcl
4+
extension: md
5+
snapshot_kind: binary
6+
---
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```mermaid
2+
flowchart LR
3+
```
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
---
2+
source: kcl-lib/src/simulation_tests.rs
3+
description: Result of parsing array_range_units_default_count.kcl
4+
---
5+
{
6+
"Ok": {
7+
"body": [
8+
{
9+
"commentStart": 0,
10+
"declaration": {
11+
"commentStart": 0,
12+
"end": 0,
13+
"id": {
14+
"commentStart": 0,
15+
"end": 0,
16+
"moduleId": 0,
17+
"name": "indices",
18+
"start": 0,
19+
"type": "Identifier"
20+
},
21+
"init": {
22+
"commentStart": 0,
23+
"end": 0,
24+
"endElement": {
25+
"commentStart": 0,
26+
"end": 0,
27+
"moduleId": 0,
28+
"raw": "3_",
29+
"start": 0,
30+
"type": "Literal",
31+
"type": "Literal",
32+
"value": {
33+
"value": 3.0,
34+
"suffix": "Count"
35+
}
36+
},
37+
"endInclusive": false,
38+
"moduleId": 0,
39+
"start": 0,
40+
"startElement": {
41+
"commentStart": 0,
42+
"end": 0,
43+
"moduleId": 0,
44+
"raw": "0",
45+
"start": 0,
46+
"type": "Literal",
47+
"type": "Literal",
48+
"value": {
49+
"value": 0.0,
50+
"suffix": "None"
51+
}
52+
},
53+
"type": "ArrayRangeExpression",
54+
"type": "ArrayRangeExpression"
55+
},
56+
"moduleId": 0,
57+
"start": 0,
58+
"type": "VariableDeclarator"
59+
},
60+
"end": 0,
61+
"kind": "const",
62+
"moduleId": 0,
63+
"start": 0,
64+
"type": "VariableDeclaration",
65+
"type": "VariableDeclaration"
66+
}
67+
],
68+
"commentStart": 0,
69+
"end": 0,
70+
"moduleId": 0,
71+
"start": 0
72+
}
73+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
source: kcl-lib/src/simulation_tests.rs
3+
description: Execution success array_range_units_default_count.kcl
4+
---
5+
null
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
indices = [0 ..< 3_]

0 commit comments

Comments
 (0)