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:
@ -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,
|
||||
|
||||
@ -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 { "=>" }
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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");
|
||||
}
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user