Add logical not operator using bang ! (#3230)

* Add logical not operator using bang !

* Change to be more concise

* Add codemirror syntax highlighting for bang operator

* Add LSP semantic token type

* Change to runtime error for bang on non-bool

* Add additional assert check

* Fix tests to verify runtime values, not parsing

* Fix test failure messages to be more helpful

* Fix semantic token tests to not care about the index

---------

Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
This commit is contained in:
Jonathan Tran
2024-08-14 02:38:37 -04:00
committed by GitHub
parent 5b798c2aa3
commit b2b62ec163
7 changed files with 269 additions and 12 deletions

View File

@ -6,6 +6,7 @@ export const klcHighlight = styleTags({
'true false': t.bool,
nil: t.null,
'AddOp MultOp ExpOp': t.arithmeticOperator,
BangOp: t.logicOperator,
CompOp: t.logicOperator,
'Equals Arrow': t.definitionOperator,
PipeOperator: t.controlOperator,

View File

@ -38,7 +38,7 @@ expression[@isGroup=Expression] {
expression !exp ExpOp expression |
expression !comp CompOp expression
} |
UnaryExpression { AddOp expression } |
UnaryExpression { UnaryOp expression } |
ParenthesizedExpression { "(" expression ")" } |
CallExpression { expression !call ArgumentList } |
ArrayExpression { "[" commaSep<expression | IntegerRange { expression !range ".." expression }> "]" } |
@ -48,6 +48,8 @@ expression[@isGroup=Expression] {
PipeExpression { expression (!pipe PipeOperator expression)+ }
}
UnaryOp { AddOp | BangOp }
ObjectProperty { PropertyName ":" expression }
ArgumentList { "(" commaSep<expression> ")" }
@ -80,6 +82,7 @@ commaSep<term> { (term ("," term)*)? ","? }
AddOp { "+" | "-" }
MultOp { "/" | "*" | "\\" }
ExpOp { "^" }
BangOp { "!" }
CompOp { $[<>] "="? | "!=" | "==" }
Equals { "=" }
Arrow { "=>" }

View File

@ -3196,6 +3196,18 @@ pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
}
}
/// JSON value as bool. If it isn't a bool, returns None.
pub fn json_as_bool(j: &serde_json::Value) -> Option<bool> {
match j {
JValue::Null => None,
JValue::Bool(b) => Some(*b),
JValue::Number(_) => None,
JValue::String(_) => None,
JValue::Array(_) => None,
JValue::Object(_) => None,
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
@ -3336,6 +3348,27 @@ impl UnaryExpression {
pipe_info: &PipeInfo,
ctx: &ExecutorContext,
) -> Result<KclValue, KclError> {
if self.operator == UnaryOperator::Not {
let value = self
.argument
.get_result(memory, dynamic_state, pipe_info, ctx)
.await?
.get_json_value()?;
let Some(bool_value) = json_as_bool(&value) else {
return Err(KclError::Semantic(KclErrorDetails {
message: format!("Cannot apply unary operator ! to non-boolean value: {}", value),
source_ranges: vec![self.into()],
}));
};
let negated = !bool_value;
return Ok(KclValue::UserVal(UserVal {
value: serde_json::Value::Bool(negated),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let num = parse_json_number_as_f64(
&self
.argument

View File

@ -2056,6 +2056,15 @@ mod tests {
Ok(memory)
}
/// Convenience function to get a JSON value from memory and unwrap.
fn mem_get_json(memory: &ProgramMemory, name: &str) -> serde_json::Value {
memory
.get(name, SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
}
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_assign_two_variables() {
let ast = r#"const myVar = 5
@ -2726,6 +2735,172 @@ const bracket = startSketchOn('XY')
parse_execute(ast).await.unwrap();
}
#[tokio::test(flavor = "multi_thread")]
async fn test_unary_operator_not_succeeds() {
let ast = r#"
fn returnTrue = () => { return !false }
const t = true
const f = false
let notTrue = !t
let notFalse = !f
let c = !!true
let d = !returnTrue()
assert(!false, "expected to pass")
fn check = (x) => {
assert(!x, "expected argument to be false")
return true
}
check(false)
"#;
let mem = parse_execute(ast).await.unwrap();
assert_eq!(serde_json::json!(false), mem_get_json(&mem, "notTrue"));
assert_eq!(serde_json::json!(true), mem_get_json(&mem, "notFalse"));
assert_eq!(serde_json::json!(true), mem_get_json(&mem, "c"));
assert_eq!(serde_json::json!(false), mem_get_json(&mem, "d"));
}
#[tokio::test(flavor = "multi_thread")]
async fn test_unary_operator_not_on_non_bool_fails() {
let code1 = r#"
// Yup, this is null.
let myNull = 0 / 0
let notNull = !myNull
"#;
assert_eq!(
parse_execute(code1).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: null".to_owned(),
source_ranges: vec![SourceRange([56, 63])],
})
);
let code2 = "let notZero = !0";
assert_eq!(
parse_execute(code2).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: 0".to_owned(),
source_ranges: vec![SourceRange([14, 16])],
})
);
let code3 = r#"
let notEmptyString = !""
"#;
assert_eq!(
parse_execute(code3).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: \"\"".to_owned(),
source_ranges: vec![SourceRange([22, 25])],
})
);
let code4 = r#"
let obj = { a: 1 }
let notMember = !obj.a
"#;
assert_eq!(
parse_execute(code4).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: 1".to_owned(),
source_ranges: vec![SourceRange([36, 42])],
})
);
let code5 = "
let a = []
let notArray = !a";
assert_eq!(
parse_execute(code5).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: []".to_owned(),
source_ranges: vec![SourceRange([27, 29])],
})
);
let code6 = "
let x = {}
let notObject = !x";
assert_eq!(
parse_execute(code6).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Semantic(KclErrorDetails {
message: "Cannot apply unary operator ! to non-boolean value: {}".to_owned(),
source_ranges: vec![SourceRange([28, 30])],
})
);
let code7 = "
fn x = () => { return 1 }
let notFunction = !x";
let fn_err = parse_execute(code7).await.unwrap_err().downcast::<KclError>().unwrap();
// These are currently printed out as JSON objects, so we don't want to
// check the full error.
assert!(
fn_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: "),
"Actual error: {:?}",
fn_err
);
let code8 = "
let myTagDeclarator = $myTag
let notTagDeclarator = !myTagDeclarator";
let tag_declarator_err = parse_execute(code8).await.unwrap_err().downcast::<KclError>().unwrap();
// These are currently printed out as JSON objects, so we don't want to
// check the full error.
assert!(
tag_declarator_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: {\"type\":\"TagDeclarator\","),
"Actual error: {:?}",
tag_declarator_err
);
let code9 = "
let myTagDeclarator = $myTag
let notTagIdentifier = !myTag";
let tag_identifier_err = parse_execute(code9).await.unwrap_err().downcast::<KclError>().unwrap();
// These are currently printed out as JSON objects, so we don't want to
// check the full error.
assert!(
tag_identifier_err
.message()
.starts_with("Cannot apply unary operator ! to non-boolean value: {\"type\":\"TagIdentifier\","),
"Actual error: {:?}",
tag_identifier_err
);
let code10 = "let notPipe = !(1 |> 2)";
assert_eq!(
// TODO: We don't currently parse this, but we should. It should be
// a runtime error instead.
parse_execute(code10).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Syntax(KclErrorDetails {
message: "Unexpected token".to_owned(),
source_ranges: vec![SourceRange([14, 15])],
})
);
let code11 = "
fn identity = (x) => { return x }
let notPipeSub = 1 |> identity(!%))";
assert_eq!(
// TODO: We don't currently parse this, but we should. It should be
// a runtime error instead.
parse_execute(code11).await.unwrap_err().downcast::<KclError>().unwrap(),
KclError::Syntax(KclErrorDetails {
message: "Unexpected token".to_owned(),
source_ranges: vec![SourceRange([54, 56])],
})
);
// TODO: Add these tests when we support these types.
// let notNan = !NaN
// let notInfinity = !Infinity
}
#[tokio::test(flavor = "multi_thread")]
async fn test_math_negative_variable_in_binary_expression() {
let ast = r#"const sigmaAllow = 35000 // psi

View File

@ -972,11 +972,21 @@ async fn test_kcl_lsp_semantic_tokens() {
assert_eq!(semantic_tokens.data[0].length, 13);
assert_eq!(semantic_tokens.data[0].delta_start, 0);
assert_eq!(semantic_tokens.data[0].delta_line, 0);
assert_eq!(semantic_tokens.data[0].token_type, 8);
assert_eq!(
semantic_tokens.data[0].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::FUNCTION)
.unwrap()
);
assert_eq!(semantic_tokens.data[1].length, 4);
assert_eq!(semantic_tokens.data[1].delta_start, 14);
assert_eq!(semantic_tokens.data[1].delta_line, 0);
assert_eq!(semantic_tokens.data[1].token_type, 3);
assert_eq!(
semantic_tokens.data[1].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::STRING)
.unwrap()
);
} else {
panic!("Expected semantic tokens");
}
@ -1229,29 +1239,64 @@ const sphereDia = 0.5"#
assert_eq!(semantic_tokens.data[0].length, 15);
assert_eq!(semantic_tokens.data[0].delta_start, 0);
assert_eq!(semantic_tokens.data[0].delta_line, 0);
assert_eq!(semantic_tokens.data[0].token_type, 6);
assert_eq!(
semantic_tokens.data[0].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::COMMENT)
.unwrap()
);
assert_eq!(semantic_tokens.data[1].length, 232);
assert_eq!(semantic_tokens.data[1].delta_start, 0);
assert_eq!(semantic_tokens.data[1].delta_line, 1);
assert_eq!(semantic_tokens.data[1].token_type, 6);
assert_eq!(
semantic_tokens.data[1].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::COMMENT)
.unwrap()
);
assert_eq!(semantic_tokens.data[2].length, 88);
assert_eq!(semantic_tokens.data[2].delta_start, 0);
assert_eq!(semantic_tokens.data[2].delta_line, 2);
assert_eq!(semantic_tokens.data[2].token_type, 6);
assert_eq!(
semantic_tokens.data[2].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::COMMENT)
.unwrap()
);
assert_eq!(semantic_tokens.data[3].length, 5);
assert_eq!(semantic_tokens.data[3].delta_start, 0);
assert_eq!(semantic_tokens.data[3].delta_line, 1);
assert_eq!(semantic_tokens.data[3].token_type, 4);
assert_eq!(
semantic_tokens.data[3].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::KEYWORD)
.unwrap()
);
assert_eq!(semantic_tokens.data[4].length, 9);
assert_eq!(semantic_tokens.data[4].delta_start, 6);
assert_eq!(semantic_tokens.data[4].delta_line, 0);
assert_eq!(semantic_tokens.data[4].token_type, 1);
assert_eq!(
semantic_tokens.data[4].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::VARIABLE)
.unwrap()
);
assert_eq!(semantic_tokens.data[5].length, 1);
assert_eq!(semantic_tokens.data[5].delta_start, 10);
assert_eq!(semantic_tokens.data[5].token_type, 2);
assert_eq!(
semantic_tokens.data[5].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::OPERATOR)
.unwrap()
);
assert_eq!(semantic_tokens.data[6].length, 3);
assert_eq!(semantic_tokens.data[6].delta_start, 2);
assert_eq!(semantic_tokens.data[6].token_type, 0);
assert_eq!(
semantic_tokens.data[6].token_type,
server
.get_semantic_token_type_index(&SemanticTokenType::NUMBER)
.unwrap()
);
} else {
panic!("Expected semantic tokens");
}

View File

@ -1136,11 +1136,11 @@ fn unary_expression(i: TokenSlice) -> PResult<UnaryExpression> {
let (operator, op_token) = any
.try_map(|token: Token| match token.token_type {
TokenType::Operator if token.value == "-" => Ok((UnaryOperator::Neg, token)),
// TODO: negation. Original parser doesn't support `not` yet.
TokenType::Operator => Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{EXPECTED} but found {} which is an operator, but not a unary one (unary operators apply to just a single operand, your operator applies to two or more operands)", token.value.as_str(),),
})),
TokenType::Bang => Ok((UnaryOperator::Not, token)),
other => Err(KclError::Syntax(KclErrorDetails { source_ranges: token.as_source_ranges(), message: format!("{EXPECTED} but found {} which is {}", token.value.as_str(), other,) })),
})
.context(expected("a unary expression, e.g. -x or -3"))

View File

@ -72,6 +72,7 @@ impl TryFrom<TokenType> for SemanticTokenType {
TokenType::Operator => Self::OPERATOR,
TokenType::QuestionMark => Self::OPERATOR,
TokenType::String => Self::STRING,
TokenType::Bang => Self::OPERATOR,
TokenType::LineComment => Self::COMMENT,
TokenType::BlockComment => Self::COMMENT,
TokenType::Function => Self::FUNCTION,
@ -83,7 +84,6 @@ impl TryFrom<TokenType> for SemanticTokenType {
| TokenType::DoublePeriod
| TokenType::Hash
| TokenType::Dollar
| TokenType::Bang
| TokenType::Unknown => {
anyhow::bail!("unsupported token type: {:?}", token_type)
}