KCL: If-else expressions (#4022)

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

You can review each commit separately, they're neat commits with logical purpose in each.

Future enhancements:

- https://github.com/KittyCAD/modeling-app/issues/4015
- https://github.com/KittyCAD/modeling-app/issues/4020
- Right now the parser errors are not very good, especially if you forget to put an expression in the end of an if/else block
This commit is contained in:
Adam Chalmers
2024-09-30 15:40:50 -05:00
committed by GitHub
parent 6dfadbea18
commit 125207f60c
12 changed files with 897 additions and 22 deletions

View File

@ -18,7 +18,11 @@ use tower_lsp::lsp_types::{
CompletionItem, CompletionItemKind, DocumentSymbol, FoldingRange, FoldingRangeKind, Range as LspRange, SymbolKind,
};
pub use crate::ast::types::{literal_value::LiteralValue, none::KclNone};
pub use crate::ast::types::{
condition::{ElseIf, IfExpression},
literal_value::LiteralValue,
none::KclNone,
};
use crate::{
docs::StdLibFn,
errors::{KclError, KclErrorDetails},
@ -30,12 +34,14 @@ use crate::{
std::{kcl_stdlib::KclStdLibFn, FunctionKind},
};
mod condition;
mod literal_value;
mod none;
/// Position-independent digest of the AST node.
pub type Digest = [u8; 32];
/// A KCL program top level, or function body.
#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
@ -72,6 +78,7 @@ macro_rules! compute_digest {
}
};
}
pub(crate) use compute_digest;
impl Program {
compute_digest!(|slf, hasher| {
@ -82,6 +89,14 @@ impl Program {
hasher.update(slf.non_code_meta.compute_digest());
});
/// Is the last body item an expression?
pub fn ends_with_expr(&self) -> bool {
let Some(ref last) = self.body.last() else {
return false;
};
matches!(last, BodyItem::ExpressionStatement(_))
}
pub fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> {
// Check if we are in the non code meta.
if let Some(meta) = self.get_non_code_meta_for_position(pos) {
@ -501,6 +516,7 @@ pub enum Expr {
ObjectExpression(Box<ObjectExpression>),
MemberExpression(Box<MemberExpression>),
UnaryExpression(Box<UnaryExpression>),
IfExpression(Box<IfExpression>),
None(KclNone),
}
@ -519,6 +535,7 @@ impl Expr {
Expr::ObjectExpression(oe) => oe.compute_digest(),
Expr::MemberExpression(me) => me.compute_digest(),
Expr::UnaryExpression(ue) => ue.compute_digest(),
Expr::IfExpression(e) => e.compute_digest(),
Expr::None(_) => {
let mut hasher = Sha256::new();
hasher.update(b"Value::None");
@ -562,6 +579,7 @@ impl Expr {
Expr::PipeExpression(pipe_exp) => Some(&pipe_exp.non_code_meta),
Expr::UnaryExpression(_unary_exp) => None,
Expr::PipeSubstitution(_pipe_substitution) => None,
Expr::IfExpression(_) => None,
Expr::None(_none) => None,
}
}
@ -584,6 +602,7 @@ impl Expr {
Expr::TagDeclarator(_) => {}
Expr::PipeExpression(ref mut pipe_exp) => pipe_exp.replace_value(source_range, new_value),
Expr::UnaryExpression(ref mut unary_exp) => unary_exp.replace_value(source_range, new_value),
Expr::IfExpression(_) => {}
Expr::PipeSubstitution(_) => {}
Expr::None(_) => {}
}
@ -603,6 +622,7 @@ impl Expr {
Expr::ObjectExpression(object_expression) => object_expression.start(),
Expr::MemberExpression(member_expression) => member_expression.start(),
Expr::UnaryExpression(unary_expression) => unary_expression.start(),
Expr::IfExpression(expr) => expr.start(),
Expr::None(none) => none.start,
}
}
@ -621,6 +641,7 @@ impl Expr {
Expr::ObjectExpression(object_expression) => object_expression.end(),
Expr::MemberExpression(member_expression) => member_expression.end(),
Expr::UnaryExpression(unary_expression) => unary_expression.end(),
Expr::IfExpression(expr) => expr.end(),
Expr::None(none) => none.end,
}
}
@ -639,6 +660,7 @@ impl Expr {
Expr::ObjectExpression(object_expression) => object_expression.get_hover_value_for_position(pos, code),
Expr::MemberExpression(member_expression) => member_expression.get_hover_value_for_position(pos, code),
Expr::UnaryExpression(unary_expression) => unary_expression.get_hover_value_for_position(pos, code),
Expr::IfExpression(expr) => expr.get_hover_value_for_position(pos, code),
// TODO: LSP hover information for values/types. https://github.com/KittyCAD/modeling-app/issues/1126
Expr::None(_) => None,
Expr::Literal(_) => None,
@ -670,11 +692,12 @@ impl Expr {
member_expression.rename_identifiers(old_name, new_name)
}
Expr::UnaryExpression(ref mut unary_expression) => unary_expression.rename_identifiers(old_name, new_name),
Expr::IfExpression(ref mut expr) => expr.rename_identifiers(old_name, new_name),
Expr::None(_) => {}
}
}
/// Get the constraint level for a value type.
/// Get the constraint level for an expression.
pub fn get_constraint_level(&self) -> ConstraintLevel {
match self {
Expr::Literal(literal) => literal.get_constraint_level(),
@ -692,6 +715,7 @@ impl Expr {
Expr::ObjectExpression(object_expression) => object_expression.get_constraint_level(),
Expr::MemberExpression(member_expression) => member_expression.get_constraint_level(),
Expr::UnaryExpression(unary_expression) => unary_expression.get_constraint_level(),
Expr::IfExpression(expr) => expr.get_constraint_level(),
Expr::None(none) => none.get_constraint_level(),
}
}
@ -720,6 +744,7 @@ pub enum BinaryPart {
CallExpression(Box<CallExpression>),
UnaryExpression(Box<UnaryExpression>),
MemberExpression(Box<MemberExpression>),
IfExpression(Box<IfExpression>),
}
impl From<BinaryPart> for SourceRange {
@ -743,6 +768,7 @@ impl BinaryPart {
BinaryPart::CallExpression(ce) => ce.compute_digest(),
BinaryPart::UnaryExpression(ue) => ue.compute_digest(),
BinaryPart::MemberExpression(me) => me.compute_digest(),
BinaryPart::IfExpression(e) => e.compute_digest(),
}
}
@ -755,6 +781,7 @@ impl BinaryPart {
BinaryPart::CallExpression(call_expression) => call_expression.get_constraint_level(),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_constraint_level(),
BinaryPart::MemberExpression(member_expression) => member_expression.get_constraint_level(),
BinaryPart::IfExpression(e) => e.get_constraint_level(),
}
}
@ -772,6 +799,7 @@ impl BinaryPart {
unary_expression.replace_value(source_range, new_value)
}
BinaryPart::MemberExpression(_) => {}
BinaryPart::IfExpression(e) => e.replace_value(source_range, new_value),
}
}
@ -783,6 +811,7 @@ impl BinaryPart {
BinaryPart::CallExpression(call_expression) => call_expression.start(),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.start(),
BinaryPart::MemberExpression(member_expression) => member_expression.start(),
BinaryPart::IfExpression(e) => e.start(),
}
}
@ -794,6 +823,7 @@ impl BinaryPart {
BinaryPart::CallExpression(call_expression) => call_expression.end(),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.end(),
BinaryPart::MemberExpression(member_expression) => member_expression.end(),
BinaryPart::IfExpression(e) => e.end(),
}
}
@ -809,6 +839,7 @@ impl BinaryPart {
BinaryPart::CallExpression(call_expression) => call_expression.execute(exec_state, ctx).await,
BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, ctx).await,
BinaryPart::MemberExpression(member_expression) => member_expression.get_result(exec_state),
BinaryPart::IfExpression(e) => e.get_result(exec_state, ctx).await,
}
}
@ -822,6 +853,7 @@ impl BinaryPart {
}
BinaryPart::CallExpression(call_expression) => call_expression.get_hover_value_for_position(pos, code),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.get_hover_value_for_position(pos, code),
BinaryPart::IfExpression(e) => e.get_hover_value_for_position(pos, code),
BinaryPart::MemberExpression(member_expression) => {
member_expression.get_hover_value_for_position(pos, code)
}
@ -845,6 +877,7 @@ impl BinaryPart {
BinaryPart::MemberExpression(ref mut member_expression) => {
member_expression.rename_identifiers(old_name, new_name)
}
BinaryPart::IfExpression(ref mut if_expression) => if_expression.rename_identifiers(old_name, new_name),
}
}
}
@ -1331,7 +1364,7 @@ impl CallExpression {
};
match exec_result {
Ok(()) => {}
Ok(_) => {}
Err(err) => {
// We need to override the source ranges so we don't get the embedded kcl
// function from the stdlib.
@ -3149,6 +3182,7 @@ async fn inner_execute_pipe_body(
| Expr::ObjectExpression(_)
| Expr::MemberExpression(_)
| Expr::UnaryExpression(_)
| Expr::IfExpression(_)
| Expr::None(_) => {}
};
let metadata = Metadata {

View File

@ -0,0 +1,215 @@
use crate::errors::KclError;
use crate::executor::BodyType;
use crate::executor::ExecState;
use crate::executor::ExecutorContext;
use crate::executor::KclValue;
use crate::executor::Metadata;
use crate::executor::SourceRange;
use crate::executor::StatementKind;
use super::compute_digest;
use super::impl_value_meta;
use super::ConstraintLevel;
use super::Hover;
use super::{Digest, Expr};
use databake::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sha2::{Digest as DigestTrait, Sha256};
// TODO: This should be its own type, similar to Program,
// but guaranteed to have an Expression as its final item.
// https://github.com/KittyCAD/modeling-app/issues/4015
type IfBlock = crate::ast::types::Program;
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
#[serde(tag = "type")]
pub struct IfExpression {
pub start: usize,
pub end: usize,
pub cond: Box<Expr>,
pub then_val: Box<IfBlock>,
pub else_ifs: Vec<ElseIf>,
pub final_else: Box<IfBlock>,
pub digest: Option<Digest>,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Bake)]
#[databake(path = kcl_lib::ast::types)]
#[ts(export)]
#[serde(tag = "type")]
pub struct ElseIf {
pub start: usize,
pub end: usize,
pub cond: Expr,
pub then_val: Box<IfBlock>,
pub digest: Option<Digest>,
}
// Source code metadata
impl_value_meta!(IfExpression);
impl_value_meta!(ElseIf);
impl IfExpression {
compute_digest!(|slf, hasher| {
hasher.update(slf.cond.compute_digest());
hasher.update(slf.then_val.compute_digest());
for else_if in &mut slf.else_ifs {
hasher.update(else_if.compute_digest());
}
hasher.update(slf.final_else.compute_digest());
});
fn source_ranges(&self) -> Vec<SourceRange> {
vec![SourceRange::from(self)]
}
}
impl From<IfExpression> for Metadata {
fn from(value: IfExpression) -> Self {
Self {
source_range: value.into(),
}
}
}
impl From<ElseIf> for Metadata {
fn from(value: ElseIf) -> Self {
Self {
source_range: value.into(),
}
}
}
impl From<&IfExpression> for Metadata {
fn from(value: &IfExpression) -> Self {
Self {
source_range: value.into(),
}
}
}
impl From<&ElseIf> for Metadata {
fn from(value: &ElseIf) -> Self {
Self {
source_range: value.into(),
}
}
}
impl ElseIf {
compute_digest!(|slf, hasher| {
hasher.update(slf.cond.compute_digest());
hasher.update(slf.then_val.compute_digest());
});
#[allow(dead_code)]
fn source_ranges(&self) -> Vec<SourceRange> {
vec![SourceRange([self.start, self.end])]
}
}
// Execution
impl IfExpression {
#[async_recursion::async_recursion]
pub async fn get_result(&self, exec_state: &mut ExecState, ctx: &ExecutorContext) -> Result<KclValue, KclError> {
// Check the `if` branch.
let cond = ctx
.execute_expr(&self.cond, exec_state, &Metadata::from(self), StatementKind::Expression)
.await?
.get_bool()?;
if cond {
let block_result = ctx.inner_execute(&self.then_val, exec_state, BodyType::Block).await?;
// Block must end in an expression, so this has to be Some.
// Enforced by the parser.
// See https://github.com/KittyCAD/modeling-app/issues/4015
return Ok(block_result.unwrap());
}
// Check any `else if` branches.
for else_if in &self.else_ifs {
let cond = ctx
.execute_expr(
&else_if.cond,
exec_state,
&Metadata::from(self),
StatementKind::Expression,
)
.await?
.get_bool()?;
if cond {
let block_result = ctx
.inner_execute(&else_if.then_val, exec_state, BodyType::Block)
.await?;
// Block must end in an expression, so this has to be Some.
// Enforced by the parser.
// See https://github.com/KittyCAD/modeling-app/issues/4015
return Ok(block_result.unwrap());
}
}
// Run the final `else` branch.
ctx.inner_execute(&self.final_else, exec_state, BodyType::Block)
.await
.map(|expr| expr.unwrap())
}
}
// IDE support and refactors
impl IfExpression {
pub fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> {
self.cond
.get_hover_value_for_position(pos, code)
.or_else(|| self.then_val.get_hover_value_for_position(pos, code))
.or_else(|| {
self.else_ifs
.iter()
.find_map(|else_if| else_if.get_hover_value_for_position(pos, code))
})
.or_else(|| self.final_else.get_hover_value_for_position(pos, code))
}
/// Rename all identifiers that have the old name to the new given name.
pub fn rename_identifiers(&mut self, old_name: &str, new_name: &str) {
self.cond.rename_identifiers(old_name, new_name);
self.then_val.rename_identifiers(old_name, new_name);
for else_if in &mut self.else_ifs {
else_if.rename_identifiers(old_name, new_name);
}
self.final_else.rename_identifiers(old_name, new_name);
}
/// Get the constraint level.
pub fn get_constraint_level(&self) -> ConstraintLevel {
ConstraintLevel::Full {
source_ranges: self.source_ranges(),
}
}
pub fn replace_value(&mut self, source_range: SourceRange, new_value: Expr) {
self.cond.replace_value(source_range, new_value.clone());
for else_if in &mut self.else_ifs {
else_if.cond.replace_value(source_range, new_value.clone());
}
}
}
impl ElseIf {
fn get_hover_value_for_position(&self, pos: usize, code: &str) -> Option<Hover> {
self.cond
.get_hover_value_for_position(pos, code)
.or_else(|| self.then_val.get_hover_value_for_position(pos, code))
}
/// Rename all identifiers that have the old name to the new given name.
fn rename_identifiers(&mut self, old_name: &str, new_name: &str) {
self.cond.rename_identifiers(old_name, new_name);
self.then_val.rename_identifiers(old_name, new_name);
}
}
// Linting
impl IfExpression {}
impl ElseIf {}

View File

@ -776,6 +776,25 @@ impl From<KclValue> for Vec<SourceRange> {
}
}
impl From<&KclValue> for Vec<SourceRange> {
fn from(item: &KclValue) -> Self {
match item {
KclValue::UserVal(u) => u.meta.iter().map(|m| m.source_range).collect(),
KclValue::TagDeclarator(ref t) => vec![t.into()],
KclValue::TagIdentifier(t) => t.meta.iter().map(|m| m.source_range).collect(),
KclValue::Solid(e) => e.meta.iter().map(|m| m.source_range).collect(),
KclValue::Solids { value } => value
.iter()
.flat_map(|eg| eg.meta.iter().map(|m| m.source_range))
.collect(),
KclValue::ImportedGeometry(i) => i.meta.iter().map(|m| m.source_range).collect(),
KclValue::Function { meta, .. } => meta.iter().map(|m| m.source_range).collect(),
KclValue::Plane(p) => p.meta.iter().map(|m| m.source_range).collect(),
KclValue::Face(f) => f.meta.iter().map(|m| m.source_range).collect(),
}
}
}
impl KclValue {
pub fn get_json_value(&self) -> Result<serde_json::Value, KclError> {
if let KclValue::UserVal(user_val) = self {
@ -906,6 +925,23 @@ impl KclValue {
}
}
/// If this KCL value is a bool, retrieve it.
pub fn get_bool(&self) -> Result<bool, KclError> {
let Self::UserVal(uv) = self else {
return Err(KclError::Type(KclErrorDetails {
source_ranges: self.into(),
message: format!("Expected bool, found {}", self.human_friendly_type()),
}));
};
let JValue::Bool(b) = uv.value else {
return Err(KclError::Type(KclErrorDetails {
source_ranges: self.into(),
message: format!("Expected bool, found {}", human_friendly_type(&uv.value)),
}));
};
Ok(b)
}
/// If this memory item is a function, call it with the given arguments, return its val as Ok.
/// If it's not a function, return Err.
pub async fn call_fn(
@ -1844,20 +1880,22 @@ impl ExecutorContext {
program: &crate::ast::types::Program,
exec_state: &mut ExecState,
body_type: BodyType,
) -> Result<(), KclError> {
) -> Result<Option<KclValue>, KclError> {
let mut last_expr = None;
// Iterate over the body of the program.
for statement in &program.body {
match statement {
BodyItem::ExpressionStatement(expression_statement) => {
let metadata = Metadata::from(expression_statement);
// Discard return value.
self.execute_expr(
&expression_statement.expression,
exec_state,
&metadata,
StatementKind::Expression,
)
.await?;
last_expr = Some(
self.execute_expr(
&expression_statement.expression,
exec_state,
&metadata,
StatementKind::Expression,
)
.await?,
);
}
BodyItem::VariableDeclaration(variable_declaration) => {
for declaration in &variable_declaration.declarations {
@ -1875,6 +1913,7 @@ impl ExecutorContext {
.await?;
exec_state.memory.add(&var_name, memory_item, source_range)?;
}
last_expr = None;
}
BodyItem::ReturnStatement(return_statement) => {
let metadata = Metadata::from(return_statement);
@ -1887,6 +1926,7 @@ impl ExecutorContext {
)
.await?;
exec_state.memory.return_ = Some(value);
last_expr = None;
}
}
}
@ -1903,7 +1943,7 @@ impl ExecutorContext {
.await?;
}
Ok(())
Ok(last_expr)
}
pub async fn execute_expr<'a>(
@ -1960,6 +2000,7 @@ impl ExecutorContext {
Expr::ObjectExpression(object_expression) => object_expression.execute(exec_state, self).await?,
Expr::MemberExpression(member_expression) => member_expression.get_result(exec_state)?,
Expr::UnaryExpression(unary_expression) => unary_expression.get_result(exec_state, self).await?,
Expr::IfExpression(expr) => expr.get_result(exec_state, self).await?,
};
Ok(item)
}
@ -2088,7 +2129,7 @@ pub(crate) async fn call_user_defined_function(
(result, fn_memory)
};
result.map(|()| fn_memory.return_)
result.map(|_| fn_memory.return_)
}
pub enum StatementKind<'a> {

View File

@ -10,11 +10,11 @@ use winnow::{
use crate::{
ast::types::{
ArrayExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, CommentStyle, Expr,
ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression, Identifier, Literal, LiteralIdentifier,
LiteralValue, MemberExpression, MemberObject, NonCodeMeta, NonCodeNode, NonCodeValue, ObjectExpression,
ObjectProperty, Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement, TagDeclarator,
UnaryExpression, UnaryOperator, VariableDeclaration, VariableDeclarator, VariableKind,
ArrayExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, CommentStyle, ElseIf,
Expr, ExpressionStatement, FnArgPrimitive, FnArgType, FunctionExpression, Identifier, IfExpression, Literal,
LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, NonCodeMeta, NonCodeNode, NonCodeValue,
ObjectExpression, ObjectProperty, Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement,
TagDeclarator, UnaryExpression, UnaryOperator, VariableDeclaration, VariableDeclarator, VariableKind,
},
errors::{KclError, KclErrorDetails},
executor::SourceRange,
@ -359,6 +359,7 @@ fn operand(i: TokenSlice) -> PResult<BinaryPart> {
Expr::BinaryExpression(x) => BinaryPart::BinaryExpression(x),
Expr::CallExpression(x) => BinaryPart::CallExpression(x),
Expr::MemberExpression(x) => BinaryPart::MemberExpression(x),
Expr::IfExpression(x) => BinaryPart::IfExpression(x),
};
Ok(expr)
})
@ -670,6 +671,119 @@ fn pipe_sub(i: TokenSlice) -> PResult<PipeSubstitution> {
.parse_next(i)
}
fn else_if(i: TokenSlice) -> PResult<ElseIf> {
let start = any
.try_map(|token: Token| {
if matches!(token.token_type, TokenType::Keyword) && token.value == "else" {
Ok(token.start)
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{} is not 'else'", token.value.as_str()),
}))
}
})
.context(expected("the 'else' keyword"))
.parse_next(i)?;
ignore_whitespace(i);
let _if = any
.try_map(|token: Token| {
if matches!(token.token_type, TokenType::Keyword) && token.value == "if" {
Ok(token.start)
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{} is not 'if'", token.value.as_str()),
}))
}
})
.context(expected("the 'if' keyword"))
.parse_next(i)?;
ignore_whitespace(i);
let cond = expression(i)?;
ignore_whitespace(i);
let _ = open_brace(i)?;
let then_val = program
.verify(|block| block.ends_with_expr())
.parse_next(i)
.map(Box::new)?;
ignore_whitespace(i);
let end = close_brace(i)?.end;
ignore_whitespace(i);
Ok(ElseIf {
start,
end,
cond,
then_val,
digest: Default::default(),
})
}
fn if_expr(i: TokenSlice) -> PResult<IfExpression> {
let start = any
.try_map(|token: Token| {
if matches!(token.token_type, TokenType::Keyword) && token.value == "if" {
Ok(token.start)
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{} is not 'if'", token.value.as_str()),
}))
}
})
.context(expected("the 'if' keyword"))
.parse_next(i)?;
let _ = whitespace(i)?;
let cond = expression(i).map(Box::new)?;
let _ = whitespace(i)?;
let _ = open_brace(i)?;
ignore_whitespace(i);
let then_val = program
.verify(|block| block.ends_with_expr())
.parse_next(i)
.map_err(|e| e.cut())
.map(Box::new)?;
ignore_whitespace(i);
let _ = close_brace(i)?;
ignore_whitespace(i);
let else_ifs = repeat(0.., else_if).parse_next(i)?;
ignore_whitespace(i);
let _ = any
.try_map(|token: Token| {
if matches!(token.token_type, TokenType::Keyword) && token.value == "else" {
Ok(token.start)
} else {
Err(KclError::Syntax(KclErrorDetails {
source_ranges: token.as_source_ranges(),
message: format!("{} is not 'else'", token.value.as_str()),
}))
}
})
.context(expected("the 'else' keyword"))
.parse_next(i)?;
ignore_whitespace(i);
let _ = open_brace(i)?;
ignore_whitespace(i);
let final_else = program
.verify(|block| block.ends_with_expr())
.parse_next(i)
.map_err(|e| e.cut())
.map(Box::new)?;
ignore_whitespace(i);
let end = close_brace(i)?.end;
Ok(IfExpression {
start,
end,
cond,
then_val,
else_ifs,
final_else,
digest: Default::default(),
})
}
// Looks like
// (arg0, arg1) => {
// const x = arg0 + arg1;
@ -1069,6 +1183,7 @@ fn expr_allowed_in_pipe_expr(i: TokenSlice) -> PResult<Expr> {
object.map(Box::new).map(Expr::ObjectExpression),
pipe_sub.map(Box::new).map(Expr::PipeSubstitution),
function_expression.map(Box::new).map(Expr::FunctionExpression),
if_expr.map(Box::new).map(Expr::IfExpression),
unnecessarily_bracketed,
))
.context(expected("a KCL expression (but not a pipe expression)"))
@ -3147,6 +3262,42 @@ e
let _arr = array_elem_by_elem(&mut sl).unwrap();
}
#[test]
fn basic_if_else() {
let some_program_string = "if true {
3
} else {
4
}";
let tokens = crate::token::lexer(some_program_string).unwrap();
let mut sl: &[Token] = &tokens;
let _res = if_expr(&mut sl).unwrap();
}
#[test]
fn basic_else_if() {
let some_program_string = "else if true {
4
}";
let tokens = crate::token::lexer(some_program_string).unwrap();
let mut sl: &[Token] = &tokens;
let _res = else_if(&mut sl).unwrap();
}
#[test]
fn basic_if_else_if() {
let some_program_string = "if true {
3
} else if true {
4
} else {
5
}";
let tokens = crate::token::lexer(some_program_string).unwrap();
let mut sl: &[Token] = &tokens;
let _res = if_expr(&mut sl).unwrap();
}
#[test]
fn test_keyword_ok_in_fn_args_return() {
let some_program_string = r#"fn thing = (param) => {
@ -3511,6 +3662,24 @@ const sketch001 = startSketchOn('XY')
const my14 = 4 ^ 2 - 3 ^ 2 * 2
"#
);
snapshot_test!(
bc,
r#"const x = if true {
3
} else {
4
}"#
);
snapshot_test!(
bd,
r#"const x = if true {
3
} else if func(radius) {
4
} else {
5
}"#
);
}
#[allow(unused)]

View File

@ -0,0 +1,112 @@
---
source: kcl/src/parser/parser_impl.rs
expression: actual
---
{
"start": 0,
"end": 74,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 0,
"end": 74,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 74,
"id": {
"type": "Identifier",
"start": 6,
"end": 7,
"name": "x",
"digest": null
},
"init": {
"type": "IfExpression",
"type": "IfExpression",
"start": 10,
"end": 74,
"cond": {
"type": "Literal",
"type": "Literal",
"start": 13,
"end": 17,
"value": true,
"raw": "true",
"digest": null
},
"then_val": {
"start": 32,
"end": 42,
"body": [
{
"type": "ExpressionStatement",
"type": "ExpressionStatement",
"start": 32,
"end": 33,
"expression": {
"type": "Literal",
"type": "Literal",
"start": 32,
"end": 33,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"else_ifs": [],
"final_else": {
"start": 63,
"end": 73,
"body": [
{
"type": "ExpressionStatement",
"type": "ExpressionStatement",
"start": 63,
"end": 64,
"expression": {
"type": "Literal",
"type": "Literal",
"start": 63,
"end": 64,
"value": 4,
"raw": "4",
"digest": null
},
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
},
"digest": null
}
],
"kind": "const",
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
}

View File

@ -0,0 +1,172 @@
---
source: kcl/src/parser/parser_impl.rs
expression: actual
---
{
"start": 0,
"end": 121,
"body": [
{
"type": "VariableDeclaration",
"type": "VariableDeclaration",
"start": 0,
"end": 121,
"declarations": [
{
"type": "VariableDeclarator",
"start": 6,
"end": 121,
"id": {
"type": "Identifier",
"start": 6,
"end": 7,
"name": "x",
"digest": null
},
"init": {
"type": "IfExpression",
"type": "IfExpression",
"start": 10,
"end": 121,
"cond": {
"type": "Literal",
"type": "Literal",
"start": 13,
"end": 17,
"value": true,
"raw": "true",
"digest": null
},
"then_val": {
"start": 32,
"end": 42,
"body": [
{
"type": "ExpressionStatement",
"type": "ExpressionStatement",
"start": 32,
"end": 33,
"expression": {
"type": "Literal",
"type": "Literal",
"start": 32,
"end": 33,
"value": 3,
"raw": "3",
"digest": null
},
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"else_ifs": [
{
"type": "ElseIf",
"start": 44,
"end": 90,
"cond": {
"type": "CallExpression",
"type": "CallExpression",
"start": 52,
"end": 64,
"callee": {
"type": "Identifier",
"start": 52,
"end": 56,
"name": "func",
"digest": null
},
"arguments": [
{
"type": "Identifier",
"type": "Identifier",
"start": 57,
"end": 63,
"name": "radius",
"digest": null
}
],
"optional": false,
"digest": null
},
"then_val": {
"start": 65,
"end": 89,
"body": [
{
"type": "ExpressionStatement",
"type": "ExpressionStatement",
"start": 79,
"end": 80,
"expression": {
"type": "Literal",
"type": "Literal",
"start": 79,
"end": 80,
"value": 4,
"raw": "4",
"digest": null
},
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
}
],
"final_else": {
"start": 110,
"end": 120,
"body": [
{
"type": "ExpressionStatement",
"type": "ExpressionStatement",
"start": 110,
"end": 111,
"expression": {
"type": "Literal",
"type": "Literal",
"start": 110,
"end": 111,
"value": 5,
"raw": "5",
"digest": null
},
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
},
"digest": null
},
"digest": null
}
],
"kind": "const",
"digest": null
}
],
"nonCodeMeta": {
"nonCodeNodes": {},
"start": [],
"digest": null
},
"digest": null
}

View File

@ -3,8 +3,8 @@ use std::fmt::Write;
use crate::{
ast::types::{
ArrayExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression, Expr, FormatOptions,
FunctionExpression, Literal, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject, NonCodeValue,
ObjectExpression, PipeExpression, Program, TagDeclarator, UnaryExpression, VariableDeclaration,
FunctionExpression, IfExpression, Literal, LiteralIdentifier, LiteralValue, MemberExpression, MemberObject,
NonCodeValue, ObjectExpression, PipeExpression, Program, TagDeclarator, UnaryExpression, VariableDeclaration,
},
parser::PIPE_OPERATOR,
};
@ -121,6 +121,7 @@ impl Expr {
Expr::TagDeclarator(tag) => tag.recast(),
Expr::PipeExpression(pipe_exp) => pipe_exp.recast(options, indentation_level),
Expr::UnaryExpression(unary_exp) => unary_exp.recast(options),
Expr::IfExpression(e) => e.recast(options, indentation_level, is_in_pipe),
Expr::PipeSubstitution(_) => crate::parser::PIPE_SUBSTITUTION_OPERATOR.to_string(),
Expr::None(_) => {
unimplemented!("there is no literal None, see https://github.com/KittyCAD/modeling-app/issues/1115")
@ -138,6 +139,7 @@ impl BinaryPart {
BinaryPart::CallExpression(call_expression) => call_expression.recast(options, indentation_level, false),
BinaryPart::UnaryExpression(unary_expression) => unary_expression.recast(options),
BinaryPart::MemberExpression(member_expression) => member_expression.recast(),
BinaryPart::IfExpression(e) => e.recast(options, indentation_level, false),
}
}
}
@ -403,6 +405,7 @@ impl UnaryExpression {
BinaryPart::Literal(_)
| BinaryPart::Identifier(_)
| BinaryPart::MemberExpression(_)
| BinaryPart::IfExpression(_)
| BinaryPart::CallExpression(_) => {
format!("{}{}", &self.operator, self.argument.recast(options, 0))
}
@ -413,6 +416,32 @@ impl UnaryExpression {
}
}
impl IfExpression {
fn recast(&self, options: &FormatOptions, indentation_level: usize, is_in_pipe: bool) -> String {
// We can calculate how many lines this will take, so let's do it and avoid growing the vec.
// Total lines = starting lines, else-if lines, ending lines.
let n = 2 + (self.else_ifs.len() * 2) + 3;
let mut lines = Vec::with_capacity(n);
let cond = self.cond.recast(options, indentation_level, is_in_pipe);
lines.push((0, format!("if {cond} {{")));
lines.push((1, self.then_val.recast(options, indentation_level + 1)));
for else_if in &self.else_ifs {
let cond = else_if.cond.recast(options, indentation_level, is_in_pipe);
lines.push((0, format!("}} else if {cond} {{")));
lines.push((1, else_if.then_val.recast(options, indentation_level + 1)));
}
lines.push((0, "} else {".to_owned()));
lines.push((1, self.final_else.recast(options, indentation_level + 1)));
lines.push((0, "}".to_owned()));
lines
.into_iter()
.map(|(ind, line)| format!("{}{}", options.get_indentation(indentation_level + ind), line.trim()))
.collect::<Vec<_>>()
.join("\n")
}
}
impl PipeExpression {
fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
let pipe = self
@ -477,6 +506,38 @@ mod tests {
use crate::ast::types::FormatOptions;
#[test]
fn test_recast_if_else_if_same() {
let input = r#"let b = if false {
3
} else if true {
4
} else {
5
}
"#;
let tokens = crate::token::lexer(input).unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let output = program.recast(&Default::default(), 0);
assert_eq!(output, input);
}
#[test]
fn test_recast_if_same() {
let input = r#"let b = if false {
3
} else {
5
}
"#;
let tokens = crate::token::lexer(input).unwrap();
let parser = crate::parser::Parser::new(tokens);
let program = parser.ast().unwrap();
let output = program.recast(&Default::default(), 0);
assert_eq!(output, input);
}
#[test]
fn test_recast_bug_fn_in_fn() {
let some_program_string = r#"// Start point (top left)

View File

@ -27,6 +27,7 @@ pub enum Node<'a> {
ObjectExpression(&'a types::ObjectExpression),
MemberExpression(&'a types::MemberExpression),
UnaryExpression(&'a types::UnaryExpression),
IfExpression(&'a types::IfExpression),
Parameter(&'a types::Parameter),
@ -59,6 +60,7 @@ impl From<&Node<'_>> for SourceRange {
Node::Parameter(p) => SourceRange([p.identifier.start(), p.identifier.end()]),
Node::ObjectProperty(o) => SourceRange([o.start(), o.end()]),
Node::MemberObject(m) => SourceRange([m.start(), m.end()]),
Node::IfExpression(m) => SourceRange([m.start(), m.end()]),
Node::LiteralIdentifier(l) => SourceRange([l.start(), l.end()]),
}
}
@ -94,4 +96,5 @@ impl_from!(Node, UnaryExpression);
impl_from!(Node, Parameter);
impl_from!(Node, ObjectProperty);
impl_from!(Node, MemberObject);
impl_from!(Node, IfExpression);
impl_from!(Node, LiteralIdentifier);

View File

@ -2,7 +2,7 @@ use anyhow::Result;
use crate::{
ast::types::{
BinaryPart, BodyItem, Expr, LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression,
BinaryPart, BodyItem, Expr, IfExpression, LiteralIdentifier, MemberExpression, MemberObject, ObjectExpression,
ObjectProperty, Parameter, Program, UnaryExpression, VariableDeclarator,
},
walk::Node,
@ -105,9 +105,11 @@ where
BinaryPart::CallExpression(ce) => f.walk(ce.as_ref().into()),
BinaryPart::UnaryExpression(ue) => walk_unary_expression(ue, f),
BinaryPart::MemberExpression(me) => walk_member_expression(me, f),
BinaryPart::IfExpression(e) => walk_if_expression(e, f),
}
}
// TODO: Rename this to walk_expr
fn walk_value<'a, WalkT>(node: &'a Expr, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
@ -184,6 +186,7 @@ where
Expr::ObjectExpression(oe) => walk_object_expression(oe, f),
Expr::MemberExpression(me) => walk_member_expression(me, f),
Expr::UnaryExpression(ue) => walk_unary_expression(ue, f),
Expr::IfExpression(e) => walk_if_expression(e, f),
Expr::None(_) => Ok(true),
}
}
@ -216,6 +219,33 @@ where
Ok(true)
}
/// Walk through an [IfExpression].
fn walk_if_expression<'a, WalkT>(node: &'a IfExpression, f: &WalkT) -> Result<bool>
where
WalkT: Walker<'a>,
{
if !f.walk(node.into())? {
return Ok(false);
}
if !walk_value(&node.cond, f)? {
return Ok(false);
}
for else_if in &node.else_ifs {
if !walk_value(&else_if.cond, f)? {
return Ok(false);
}
if !walk(&else_if.then_val, f)? {
return Ok(false);
}
}
let final_else = &(*node.final_else);
if !f.walk(final_else.into())? {
return Ok(false);
}
Ok(true)
}
/// walk through an [UnaryExpression].
fn walk_unary_expression<'a, WalkT>(node: &'a UnaryExpression, f: &WalkT) -> Result<bool>
where

View File

@ -0,0 +1,28 @@
// This tests evaluating if-else expressions.
let a = if true {
3
} else if true {
4
} else {
5
}
assertEqual(a, 3, 0.001, "the 'if' branch gets returned")
let b = if false {
3
} else if true {
4
} else {
5
}
assertEqual(b, 4, 0.001, "the 'else if' branch gets returned")
let c = if false {
3
} else if false {
4
} else {
5
}
assertEqual(c, 5, 0.001, "the 'else' branch gets returned")

View File

@ -0,0 +1,5 @@
let x = if true {
let y = 1
} else {
let z = 1
}

View File

@ -90,4 +90,9 @@ gen_test_fail!(
"semantic: cannot use % outside a pipe expression"
);
gen_test!(sketch_in_object);
gen_test!(if_else);
// gen_test_fail!(
// if_else_no_expr,
// "syntax: blocks inside an if/else expression must end in an expression"
// );
gen_test!(add_lots);