Compare commits

...

1 Commits

Author SHA1 Message Date
b4fb903bd0 Add recursive isEven() test 2024-07-22 20:03:49 -04:00
5 changed files with 125 additions and 4 deletions

View File

@ -2880,6 +2880,30 @@ impl BinaryExpression {
pipe_info: &PipeInfo, pipe_info: &PipeInfo,
ctx: &ExecutorContext, ctx: &ExecutorContext,
) -> Result<MemoryItem, KclError> { ) -> Result<MemoryItem, KclError> {
// First check if we are doing short-circuiting logical operator.
if self.operator == BinaryOperator::LogicalOr {
let left_json_value = self.left.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
let left = json_to_bool(&left_json_value);
if left {
// Short-circuit.
return Ok(MemoryItem::UserVal(UserVal {
value: serde_json::Value::Bool(left),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let right_json_value = self.right.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
let right = json_to_bool(&right_json_value);
return Ok(MemoryItem::UserVal(UserVal {
value: serde_json::Value::Bool(right),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let left_json_value = self.left.get_result(memory, pipe_info, ctx).await?.get_json_value()?; let left_json_value = self.left.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
let right_json_value = self.right.get_result(memory, pipe_info, ctx).await?.get_json_value()?; let right_json_value = self.right.get_result(memory, pipe_info, ctx).await?.get_json_value()?;
@ -2909,6 +2933,9 @@ impl BinaryExpression {
BinaryOperator::Div => (left / right).into(), BinaryOperator::Div => (left / right).into(),
BinaryOperator::Mod => (left % right).into(), BinaryOperator::Mod => (left % right).into(),
BinaryOperator::Pow => (left.powf(right)).into(), BinaryOperator::Pow => (left.powf(right)).into(),
BinaryOperator::LogicalOr => {
unreachable!("LogicalOr should have been handled above")
}
}; };
Ok(MemoryItem::UserVal(UserVal { Ok(MemoryItem::UserVal(UserVal {
@ -2950,6 +2977,27 @@ pub fn parse_json_value_as_string(j: &serde_json::Value) -> Option<String> {
} }
} }
pub fn json_to_bool(j: &serde_json::Value) -> bool {
match j {
JValue::Null => false,
JValue::Bool(b) => *b,
JValue::Number(n) => {
if let Some(n) = n.as_u64() {
n != 0
} else if let Some(n) = n.as_i64() {
n != 0
} else if let Some(x) = n.as_f64() {
x != 0.0 && !x.is_nan()
} else {
false
}
}
JValue::String(s) => !s.is_empty(),
JValue::Array(a) => !a.is_empty(),
JValue::Object(_) => false,
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display, Bake)] #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, FromStr, Display, Bake)]
#[databake(path = kcl_lib::ast::types)] #[databake(path = kcl_lib::ast::types)]
#[ts(export)] #[ts(export)]
@ -2980,6 +3028,10 @@ pub enum BinaryOperator {
#[serde(rename = "^")] #[serde(rename = "^")]
#[display("^")] #[display("^")]
Pow, Pow,
/// Logical OR.
#[serde(rename = "||")]
#[display("||")]
LogicalOr,
} }
/// Mathematical associativity. /// Mathematical associativity.
@ -3008,6 +3060,7 @@ impl BinaryOperator {
BinaryOperator::Div => *b"div", BinaryOperator::Div => *b"div",
BinaryOperator::Mod => *b"mod", BinaryOperator::Mod => *b"mod",
BinaryOperator::Pow => *b"pow", BinaryOperator::Pow => *b"pow",
BinaryOperator::LogicalOr => *b"lor",
} }
} }
@ -3018,6 +3071,7 @@ impl BinaryOperator {
BinaryOperator::Add | BinaryOperator::Sub => 11, BinaryOperator::Add | BinaryOperator::Sub => 11,
BinaryOperator::Mul | BinaryOperator::Div | BinaryOperator::Mod => 12, BinaryOperator::Mul | BinaryOperator::Div | BinaryOperator::Mod => 12,
BinaryOperator::Pow => 6, BinaryOperator::Pow => 6,
BinaryOperator::LogicalOr => 3,
} }
} }
@ -3025,7 +3079,7 @@ impl BinaryOperator {
/// Taken from <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table> /// Taken from <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table>
pub fn associativity(&self) -> Associativity { pub fn associativity(&self) -> Associativity {
match self { match self {
Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Mod => Associativity::Left, Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Mod | Self::LogicalOr => Associativity::Left,
Self::Pow => Associativity::Right, Self::Pow => Associativity::Right,
} }
} }
@ -3089,6 +3143,21 @@ impl UnaryExpression {
pipe_info: &PipeInfo, pipe_info: &PipeInfo,
ctx: &ExecutorContext, ctx: &ExecutorContext,
) -> Result<MemoryItem, KclError> { ) -> Result<MemoryItem, KclError> {
if self.operator == UnaryOperator::Not {
let value = self
.argument
.get_result(memory, pipe_info, ctx)
.await?
.get_json_value()?;
let negated = !json_to_bool(&value);
return Ok(MemoryItem::UserVal(UserVal {
value: serde_json::Value::Bool(negated),
meta: vec![Metadata {
source_range: self.into(),
}],
}));
}
let num = parse_json_number_as_f64( let num = parse_json_number_as_f64(
&self &self
.argument .argument

View File

@ -2513,6 +2513,57 @@ let shape = layer() |> patternTransform(10, transform, %)"#;
); );
} }
#[tokio::test(flavor = "multi_thread")]
async fn test_execute_ycombinator_is_even() {
let ast = r#"
// Heavily inspired by: https://raganwald.com/2018/09/10/why-y.html
fn why = (f) => {
fn inner = (maker) => {
fn inner2 = (x) => {
return f(maker(maker), x)
}
return inner2
}
return inner(
(maker) => {
fn inner2 = (x) => {
return f(maker(maker), x)
}
return inner2
}
)
}
fn innerIsEven = (self, n) => {
return !n || !self(n - 1)
}
const isEven = why(innerIsEven)
const two = isEven(2)
const three = isEven(3)
"#;
let memory = parse_execute(ast).await.unwrap();
assert_eq!(
serde_json::json!(true),
memory
.get("two", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
assert_eq!(
serde_json::json!(false),
memory
.get("three", SourceRange::default())
.unwrap()
.get_json_value()
.unwrap()
);
}
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_math_execute_with_functions() { async fn test_math_execute_with_functions() {
let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#; let ast = r#"const myVar = 2 + min(100, -1 + legLen(5, 3))"#;

View File

@ -299,6 +299,7 @@ fn binary_operator(i: TokenSlice) -> PResult<BinaryOperator> {
"*" => BinaryOperator::Mul, "*" => BinaryOperator::Mul,
"%" => BinaryOperator::Mod, "%" => BinaryOperator::Mod,
"^" => BinaryOperator::Pow, "^" => BinaryOperator::Pow,
"||" => BinaryOperator::LogicalOr,
_ => { _ => {
return Err(KclError::Syntax(KclErrorDetails { return Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(), source_ranges: token.as_source_ranges(),
@ -1136,11 +1137,11 @@ fn unary_expression(i: TokenSlice) -> PResult<UnaryExpression> {
let (operator, op_token) = any let (operator, op_token) = any
.try_map(|token: Token| match token.token_type { .try_map(|token: Token| match token.token_type {
TokenType::Operator if token.value == "-" => Ok((UnaryOperator::Neg, token)), TokenType::Operator if token.value == "-" => Ok((UnaryOperator::Neg, token)),
// TODO: negation. Original parser doesn't support `not` yet.
TokenType::Operator => Err(KclError::Syntax(KclErrorDetails { TokenType::Operator => Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(), 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(),), 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,) })), 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")) .context(expected("a unary expression, e.g. -x or -3"))

View File

@ -79,7 +79,7 @@ impl From<ParseError<&[Token], ContextError>> for KclError {
// See https://github.com/KittyCAD/modeling-app/issues/784 // See https://github.com/KittyCAD/modeling-app/issues/784
KclError::Syntax(KclErrorDetails { KclError::Syntax(KclErrorDetails {
source_ranges: bad_token.as_source_ranges(), source_ranges: bad_token.as_source_ranges(),
message: "Unexpected token".to_string(), message: format!("Unexpected token: {}", bad_token.value),
}) })
} }
} }

View File

@ -90,7 +90,7 @@ fn word(i: &mut Located<&str>) -> PResult<Token> {
fn operator(i: &mut Located<&str>) -> PResult<Token> { fn operator(i: &mut Located<&str>) -> PResult<Token> {
let (value, range) = alt(( let (value, range) = alt((
">=", "<=", "==", "=>", "!= ", "|>", "*", "+", "-", "/", "%", "=", "<", ">", r"\", "|", "^", ">=", "<=", "==", "=>", "!= ", "|>", "*", "+", "-", "/", "%", "=", "<", ">", r"\", "||", "|", "^",
)) ))
.with_span() .with_span()
.parse_next(i)?; .parse_next(i)?;