KCL: Comparison operators (#4025)

Add `==, !=, >, >=, <, <=` to the language parser, executor and tests.

Closes https://github.com/KittyCAD/modeling-app/issues/4020

Currently these comparison operators are associative, allowing users to chain them, e.g. (x <= y <= z). This should not be allowed, will do in a follow-up. See https://github.com/KittyCAD/modeling-app/issues/4155
This commit is contained in:
Adam Chalmers
2024-10-14 10:46:39 -07:00
committed by GitHub
parent 3382b66075
commit 1d3ade114f
10 changed files with 573 additions and 2 deletions

View File

@ -83520,6 +83520,48 @@
"enum": [
"^"
]
},
{
"description": "Are two numbers equal?",
"type": "string",
"enum": [
"=="
]
},
{
"description": "Are two numbers not equal?",
"type": "string",
"enum": [
"!="
]
},
{
"description": "Is left greater than right",
"type": "string",
"enum": [
">"
]
},
{
"description": "Is left greater than or equal to right",
"type": "string",
"enum": [
">="
]
},
{
"description": "Is left less than right",
"type": "string",
"enum": [
"<"
]
},
{
"description": "Is left less than or equal to right",
"type": "string",
"enum": [
"<="
]
}
]
},
@ -87076,6 +87118,48 @@
"enum": [
"^"
]
},
{
"description": "Are two numbers equal?",
"type": "string",
"enum": [
"=="
]
},
{
"description": "Are two numbers not equal?",
"type": "string",
"enum": [
"!="
]
},
{
"description": "Is left greater than right",
"type": "string",
"enum": [
">"
]
},
{
"description": "Is left greater than or equal to right",
"type": "string",
"enum": [
">="
]
},
{
"description": "Is left less than right",
"type": "string",
"enum": [
"<"
]
},
{
"description": "Is left less than or equal to right",
"type": "string",
"enum": [
"<="
]
}
]
},
@ -90636,6 +90720,48 @@
"enum": [
"^"
]
},
{
"description": "Are two numbers equal?",
"type": "string",
"enum": [
"=="
]
},
{
"description": "Are two numbers not equal?",
"type": "string",
"enum": [
"!="
]
},
{
"description": "Is left greater than right",
"type": "string",
"enum": [
">"
]
},
{
"description": "Is left greater than or equal to right",
"type": "string",
"enum": [
">="
]
},
{
"description": "Is left less than right",
"type": "string",
"enum": [
"<"
]
},
{
"description": "Is left less than or equal to right",
"type": "string",
"enum": [
"<="
]
}
]
},
@ -115048,6 +115174,48 @@
"enum": [
"^"
]
},
{
"description": "Are two numbers equal?",
"type": "string",
"enum": [
"=="
]
},
{
"description": "Are two numbers not equal?",
"type": "string",
"enum": [
"!="
]
},
{
"description": "Is left greater than right",
"type": "string",
"enum": [
">"
]
},
{
"description": "Is left greater than or equal to right",
"type": "string",
"enum": [
">="
]
},
{
"description": "Is left less than right",
"type": "string",
"enum": [
"<"
]
},
{
"description": "Is left less than or equal to right",
"type": "string",
"enum": [
"<="
]
}
]
},
@ -118997,6 +119165,48 @@
"enum": [
"^"
]
},
{
"description": "Are two numbers equal?",
"type": "string",
"enum": [
"=="
]
},
{
"description": "Are two numbers not equal?",
"type": "string",
"enum": [
"!="
]
},
{
"description": "Is left greater than right",
"type": "string",
"enum": [
">"
]
},
{
"description": "Is left greater than or equal to right",
"type": "string",
"enum": [
">="
]
},
{
"description": "Is left less than right",
"type": "string",
"enum": [
"<"
]
},
{
"description": "Is left less than or equal to right",
"type": "string",
"enum": [
"<="
]
}
]
},
@ -122553,6 +122763,48 @@
"enum": [
"^"
]
},
{
"description": "Are two numbers equal?",
"type": "string",
"enum": [
"=="
]
},
{
"description": "Are two numbers not equal?",
"type": "string",
"enum": [
"!="
]
},
{
"description": "Is left greater than right",
"type": "string",
"enum": [
">"
]
},
{
"description": "Is left greater than or equal to right",
"type": "string",
"enum": [
">="
]
},
{
"description": "Is left less than right",
"type": "string",
"enum": [
"<"
]
},
{
"description": "Is left less than or equal to right",
"type": "string",
"enum": [
"<="
]
}
]
},
@ -126107,6 +126359,48 @@
"enum": [
"^"
]
},
{
"description": "Are two numbers equal?",
"type": "string",
"enum": [
"=="
]
},
{
"description": "Are two numbers not equal?",
"type": "string",
"enum": [
"!="
]
},
{
"description": "Is left greater than right",
"type": "string",
"enum": [
">"
]
},
{
"description": "Is left greater than or equal to right",
"type": "string",
"enum": [
">="
]
},
{
"description": "Is left less than right",
"type": "string",
"enum": [
"<"
]
},
{
"description": "Is left less than or equal to right",
"type": "string",
"enum": [
"<="
]
}
]
},

View File

@ -82,6 +82,78 @@ Raise a number to a power.
----
Are two numbers equal?
**enum:** `==`
----
Are two numbers not equal?
**enum:** `!=`
----
Is left greater than right
**enum:** `>`
----
Is left greater than or equal to right
**enum:** `>=`
----
Is left less than right
**enum:** `<`
----
Is left less than or equal to right
**enum:** `<=`
----

View File

@ -2778,6 +2778,12 @@ impl BinaryExpression {
BinaryOperator::Div => (left / right).into(),
BinaryOperator::Mod => (left % right).into(),
BinaryOperator::Pow => (left.powf(right)).into(),
BinaryOperator::Eq => (left == right).into(),
BinaryOperator::Neq => (left != right).into(),
BinaryOperator::Gt => (left > right).into(),
BinaryOperator::Gte => (left >= right).into(),
BinaryOperator::Lt => (left < right).into(),
BinaryOperator::Lte => (left <= right).into(),
};
Ok(KclValue::UserVal(UserVal {
@ -2861,6 +2867,30 @@ pub enum BinaryOperator {
#[serde(rename = "^")]
#[display("^")]
Pow,
/// Are two numbers equal?
#[serde(rename = "==")]
#[display("==")]
Eq,
/// Are two numbers not equal?
#[serde(rename = "!=")]
#[display("!=")]
Neq,
/// Is left greater than right
#[serde(rename = ">")]
#[display(">")]
Gt,
/// Is left greater than or equal to right
#[serde(rename = ">=")]
#[display(">=")]
Gte,
/// Is left less than right
#[serde(rename = "<")]
#[display("<")]
Lt,
/// Is left less than or equal to right
#[serde(rename = "<=")]
#[display("<=")]
Lte,
}
/// Mathematical associativity.
@ -2889,6 +2919,12 @@ impl BinaryOperator {
BinaryOperator::Div => *b"div",
BinaryOperator::Mod => *b"mod",
BinaryOperator::Pow => *b"pow",
BinaryOperator::Eq => *b"eqq",
BinaryOperator::Neq => *b"neq",
BinaryOperator::Gt => *b"gtr",
BinaryOperator::Gte => *b"gte",
BinaryOperator::Lt => *b"ltr",
BinaryOperator::Lte => *b"lte",
}
}
@ -2899,6 +2935,8 @@ impl BinaryOperator {
BinaryOperator::Add | BinaryOperator::Sub => 11,
BinaryOperator::Mul | BinaryOperator::Div | BinaryOperator::Mod => 12,
BinaryOperator::Pow => 13,
Self::Gt | Self::Gte | Self::Lt | Self::Lte => 9,
Self::Eq | Self::Neq => 8,
}
}
@ -2908,6 +2946,7 @@ impl BinaryOperator {
match self {
Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Mod => Associativity::Left,
Self::Pow => Associativity::Right,
Self::Gt | Self::Gte | Self::Lt | Self::Lte | Self::Eq | Self::Neq => Associativity::Left, // I don't know if this is correct
}
}
}

View File

@ -303,6 +303,12 @@ fn binary_operator(i: TokenSlice) -> PResult<BinaryOperator> {
"*" => BinaryOperator::Mul,
"%" => BinaryOperator::Mod,
"^" => BinaryOperator::Pow,
"==" => BinaryOperator::Eq,
"!=" => BinaryOperator::Neq,
">" => BinaryOperator::Gt,
">=" => BinaryOperator::Gte,
"<" => BinaryOperator::Lt,
"<=" => BinaryOperator::Lte,
_ => {
return Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
@ -3705,6 +3711,8 @@ const my14 = 4 ^ 2 - 3 ^ 2 * 2
5
}"#
);
snapshot_test!(be, "let x = 3 == 3");
snapshot_test!(bf, "let x = 3 != 3");
snapshot_test!(bg, r#"x = 4"#);
}

View File

@ -0,0 +1,65 @@
---
source: kcl/src/parser/parser_impl.rs
expression: actual
---
{
"start": 0,
"end": 14,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 0,
"end": 14,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 14,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "x",
"digest": null
},
"init": {
"type": "BinaryExpression",
"type": "BinaryExpression",
"start": 8,
"end": 14,
"operator": "==",
"left": {
"type": "Literal",
"type": "Literal",
"start": 8,
"end": 9,
"value": 3,
"raw": "3",
"digest": null
},
"right": {
"type": "Literal",
"type": "Literal",
"start": 13,
"end": 14,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
},
"digest": null
}
],
"kind": "const",
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
}

View File

@ -0,0 +1,65 @@
---
source: kcl/src/parser/parser_impl.rs
expression: actual
---
{
"start": 0,
"end": 14,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 0,
"end": 14,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 14,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "x",
"digest": null
},
"init": {
"type": "BinaryExpression",
"type": "BinaryExpression",
"start": 8,
"end": 14,
"operator": "!=",
"left": {
"type": "Literal",
"type": "Literal",
"start": 8,
"end": 9,
"value": 3,
"raw": "3",
"digest": null
},
"right": {
"type": "Literal",
"type": "Literal",
"start": 13,
"end": 14,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
},
"digest": null
}
],
"kind": "const",
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
}

View File

@ -27,7 +27,7 @@ pub fn token(i: &mut Located<&str>) -> PResult<Token> {
'.' => alt((number, double_period, period)),
'#' => hash,
'$' => dollar,
'!' => bang,
'!' => alt((operator, bang)),
' ' | '\t' | '\n' => whitespace,
_ => alt((operator, keyword,type_, word))
}
@ -90,7 +90,7 @@ fn word(i: &mut Located<&str>) -> PResult<Token> {
fn operator(i: &mut Located<&str>) -> PResult<Token> {
let (value, range) = alt((
">=", "<=", "==", "=>", "!= ", "|>", "*", "+", "-", "/", "%", "=", "<", ">", r"\", "|", "^",
">=", "<=", "==", "=>", "!=", "|>", "*", "+", "-", "/", "%", "=", "<", ">", r"\", "|", "^",
))
.with_span()
.parse_next(i)?;
@ -1522,6 +1522,18 @@ const things = "things"
assert_tokens(expected, actual);
}
#[test]
fn not_eq() {
let actual = lexer("!=").unwrap();
let expected = vec![Token {
token_type: TokenType::Operator,
value: "!=".to_owned(),
start: 0,
end: 2,
}];
assert_eq!(actual, expected);
}
#[test]
fn test_unrecognized_token() {
let actual = lexer("12 ; 8").unwrap();

View File

@ -0,0 +1,13 @@
assert(3 == 3, "equality")
assert(3.0 == 3.0, "equality of floats")
assert(3 != 4, "non-equality")
assert(3.0 != 4.0, "non-equality of floats")
assert(3 < 4, "lt")
assert(3 <= 4, "lte but actually lt")
assert(4 <= 4, "lte but actually eq")
assert(4 > 3, "gt")
assert(4 >= 3, "gte but actually gt")
assert(3 >= 3, "gte but actually eq")
assert(0.0 == 0.0, "equality of zero")
assert(0.0 == -0.0, "equality of zero and neg zero")

View File

@ -0,0 +1 @@
assert(3 == 3 == 3, "this should not compile")

View File

@ -57,6 +57,7 @@ async fn run_fail(code: &str) -> KclError {
gen_test!(property_of_object);
gen_test!(index_of_array);
gen_test!(comparisons);
gen_test_fail!(
invalid_index_str,
"semantic: Only integers >= 0 can be used as the index of an array, but you're using a string"
@ -99,4 +100,5 @@ gen_test!(if_else);
// if_else_no_expr,
// "syntax: blocks inside an if/else expression must end in an expression"
// );
gen_test_fail!(comparisons_multiple, "syntax: Invalid number: true");
gen_test!(add_lots);