New math parser (#956)
* New math parser * Remove old parser * Comments * Move tests into parser_impl, remove dead code * Backport some math tests
This commit is contained in:
@ -1443,7 +1443,7 @@ describe('nests binary expressions correctly', () => {
|
||||
type: 'BinaryExpression',
|
||||
operator: '*',
|
||||
start: 15,
|
||||
end: 26,
|
||||
end: 25,
|
||||
left: { type: 'Literal', value: 2, raw: '2', start: 15, end: 16 },
|
||||
right: {
|
||||
type: 'BinaryExpression',
|
||||
|
@ -50,6 +50,9 @@ engine = []
|
||||
panic = "abort"
|
||||
debug = true
|
||||
|
||||
[profile.bench]
|
||||
debug = true # Flamegraphs of benchmarks require accurate debug symbols
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = "0.5.1"
|
||||
expectorate = "1.1.0"
|
||||
|
@ -2175,6 +2175,7 @@ impl BinaryExpression {
|
||||
BinaryOperator::Mul => (left * right).into(),
|
||||
BinaryOperator::Div => (left / right).into(),
|
||||
BinaryOperator::Mod => (left % right).into(),
|
||||
BinaryOperator::Pow => (left.powf(right)).into(),
|
||||
};
|
||||
|
||||
Ok(MemoryItem::UserVal(UserVal {
|
||||
@ -2257,13 +2258,46 @@ pub enum BinaryOperator {
|
||||
#[serde(rename = "%")]
|
||||
#[display("%")]
|
||||
Mod,
|
||||
/// Raise a number to a power.
|
||||
#[serde(rename = "^")]
|
||||
#[display("^")]
|
||||
Pow,
|
||||
}
|
||||
|
||||
/// Mathematical associativity.
|
||||
/// Should a . b . c be read as (a . b) . c, or a . (b . c)
|
||||
/// See <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#precedence_and_associativity> for more.
|
||||
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
|
||||
pub enum Associativity {
|
||||
/// Read a . b . c as (a . b) . c
|
||||
Left,
|
||||
/// Read a . b . c as a . (b . c)
|
||||
Right,
|
||||
}
|
||||
|
||||
impl Associativity {
|
||||
pub fn is_left(&self) -> bool {
|
||||
matches!(self, Self::Left)
|
||||
}
|
||||
}
|
||||
|
||||
impl BinaryOperator {
|
||||
/// Follow JS definitions of each operator.
|
||||
/// Taken from <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table>
|
||||
pub fn precedence(&self) -> u8 {
|
||||
match &self {
|
||||
BinaryOperator::Add | BinaryOperator::Sub => 11,
|
||||
BinaryOperator::Mul | BinaryOperator::Div | BinaryOperator::Mod => 12,
|
||||
BinaryOperator::Pow => 6,
|
||||
}
|
||||
}
|
||||
|
||||
/// Follow JS definitions of each operator.
|
||||
/// Taken from <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence#table>
|
||||
pub fn associativity(&self) -> Associativity {
|
||||
match self {
|
||||
Self::Add | Self::Sub | Self::Mul | Self::Div | Self::Mod => Associativity::Left,
|
||||
Self::Pow => Associativity::Right,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ pub mod docs;
|
||||
pub mod engine;
|
||||
pub mod errors;
|
||||
pub mod executor;
|
||||
pub mod math_parser;
|
||||
pub mod parser;
|
||||
pub mod server;
|
||||
pub mod std;
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
134
src/wasm-lib/kcl/src/parser/math.rs
Normal file
134
src/wasm-lib/kcl/src/parser/math.rs
Normal file
@ -0,0 +1,134 @@
|
||||
use crate::ast::types::{BinaryExpression, BinaryOperator, BinaryPart};
|
||||
|
||||
/// Parses a list of tokens (in infix order, i.e. as the user typed them)
|
||||
/// into a binary expression tree.
|
||||
pub fn parse(infix_tokens: Vec<BinaryExpressionToken>) -> BinaryExpression {
|
||||
let rpn = postfix(infix_tokens);
|
||||
evaluate(rpn)
|
||||
}
|
||||
|
||||
/// Parses a list of tokens (in postfix order) into a binary expression tree.
|
||||
fn evaluate(rpn: Vec<BinaryExpressionToken>) -> BinaryExpression {
|
||||
let mut operand_stack = Vec::new();
|
||||
for item in rpn {
|
||||
let expr = match item {
|
||||
BinaryExpressionToken::Operator(operator) => {
|
||||
let right: BinaryPart = operand_stack.pop().unwrap();
|
||||
let left = operand_stack.pop().unwrap();
|
||||
BinaryPart::BinaryExpression(Box::new(BinaryExpression {
|
||||
start: left.start(),
|
||||
end: right.end(),
|
||||
operator,
|
||||
left,
|
||||
right,
|
||||
}))
|
||||
}
|
||||
BinaryExpressionToken::Operand(o) => o,
|
||||
};
|
||||
operand_stack.push(expr)
|
||||
}
|
||||
if let BinaryPart::BinaryExpression(expr) = operand_stack.pop().unwrap() {
|
||||
*expr
|
||||
} else {
|
||||
panic!("Last expression was not a binary expression")
|
||||
}
|
||||
}
|
||||
|
||||
/// Reorders tokens from infix order to postfix order.
|
||||
fn postfix(infix: Vec<BinaryExpressionToken>) -> Vec<BinaryExpressionToken> {
|
||||
let mut operator_stack: Vec<BinaryOperator> = Vec::with_capacity(infix.len());
|
||||
let mut output = Vec::with_capacity(infix.len());
|
||||
for token in infix {
|
||||
match token {
|
||||
BinaryExpressionToken::Operator(o1) => {
|
||||
// From https://en.wikipedia.org/wiki/Shunting_yard_algorithm:
|
||||
// while (
|
||||
// there is an operator o2 at the top of the operator stack which is not a left parenthesis,
|
||||
// and (o2 has greater precedence than o1 or (o1 and o2 have the same precedence and o1 is left-associative))
|
||||
// )
|
||||
while operator_stack
|
||||
.last()
|
||||
.map(|o2| {
|
||||
(o2.precedence() > o1.precedence())
|
||||
|| o1.precedence() == o2.precedence() && o1.associativity().is_left()
|
||||
})
|
||||
.unwrap_or(false)
|
||||
{
|
||||
output.push(BinaryExpressionToken::Operator(operator_stack.pop().unwrap()));
|
||||
}
|
||||
operator_stack.push(o1);
|
||||
}
|
||||
o @ BinaryExpressionToken::Operand(_) => output.push(o),
|
||||
}
|
||||
}
|
||||
// After the while loop, pop the remaining items from the operator stack into the output queue.
|
||||
output.extend(operator_stack.into_iter().rev().map(BinaryExpressionToken::Operator));
|
||||
output
|
||||
}
|
||||
|
||||
/// Expressions are made up of operators and operands.
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub enum BinaryExpressionToken {
|
||||
Operator(BinaryOperator),
|
||||
Operand(BinaryPart),
|
||||
}
|
||||
|
||||
impl From<BinaryPart> for BinaryExpressionToken {
|
||||
fn from(value: BinaryPart) -> Self {
|
||||
Self::Operand(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BinaryOperator> for BinaryExpressionToken {
|
||||
fn from(value: BinaryOperator) -> Self {
|
||||
Self::Operator(value)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ast::types::Literal;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parse_and_evaluate() {
|
||||
/// Make a literal
|
||||
fn lit(n: u8) -> BinaryPart {
|
||||
BinaryPart::Literal(Box::new(Literal {
|
||||
start: 0,
|
||||
end: 0,
|
||||
value: n.into(),
|
||||
raw: n.to_string(),
|
||||
}))
|
||||
}
|
||||
let tests: Vec<Vec<BinaryExpressionToken>> = vec![
|
||||
// 3 + 4 × 2 ÷ ( 1 − 5 ) ^ 2 ^ 3
|
||||
vec![
|
||||
lit(3).into(),
|
||||
BinaryOperator::Add.into(),
|
||||
lit(4).into(),
|
||||
BinaryOperator::Mul.into(),
|
||||
lit(2).into(),
|
||||
BinaryOperator::Div.into(),
|
||||
BinaryPart::BinaryExpression(Box::new(BinaryExpression {
|
||||
start: 0,
|
||||
end: 0,
|
||||
operator: BinaryOperator::Sub,
|
||||
left: lit(1),
|
||||
right: lit(5),
|
||||
}))
|
||||
.into(),
|
||||
BinaryOperator::Pow.into(),
|
||||
lit(2).into(),
|
||||
BinaryOperator::Pow.into(),
|
||||
lit(3).into(),
|
||||
],
|
||||
];
|
||||
for infix_input in tests {
|
||||
let rpn = postfix(infix_input);
|
||||
let tree = evaluate(rpn);
|
||||
dbg!(tree);
|
||||
}
|
||||
}
|
||||
}
|
@ -17,12 +17,13 @@ use crate::{
|
||||
},
|
||||
errors::{KclError, KclErrorDetails},
|
||||
executor::SourceRange,
|
||||
math_parser::MathParser,
|
||||
parser::parser_impl::error::ContextError,
|
||||
std::StdLib,
|
||||
token::{Token, TokenType},
|
||||
};
|
||||
|
||||
use super::{math::BinaryExpressionToken, PIPE_OPERATOR, PIPE_SUBSTITUTION_OPERATOR};
|
||||
|
||||
mod error;
|
||||
|
||||
type PResult<O, E = error::ContextError> = winnow::prelude::PResult<O, E>;
|
||||
@ -455,7 +456,7 @@ fn object(i: TokenSlice) -> PResult<ObjectExpression> {
|
||||
/// Parse the % symbol, used to substitute a curried argument from a |> (pipe).
|
||||
fn pipe_sub(i: TokenSlice) -> PResult<PipeSubstitution> {
|
||||
any.try_map(|token: Token| {
|
||||
if matches!(token.token_type, TokenType::Operator) && token.value == "%" {
|
||||
if matches!(token.token_type, TokenType::Operator) && token.value == PIPE_SUBSTITUTION_OPERATOR {
|
||||
Ok(PipeSubstitution {
|
||||
start: token.start,
|
||||
end: token.end,
|
||||
@ -1025,35 +1026,33 @@ fn unary_expression(i: TokenSlice) -> PResult<UnaryExpression> {
|
||||
/// Consume tokens that make up a binary expression, but don't actually return them.
|
||||
/// Why not?
|
||||
/// Because this is designed to be used with .recognize() within the `binary_expression` parser.
|
||||
fn binary_expression_tokens(i: TokenSlice) -> PResult<()> {
|
||||
let _first = operand.parse_next(i)?;
|
||||
let _remaining: Vec<_> = repeat(
|
||||
fn binary_expression_tokens(i: TokenSlice) -> PResult<Vec<BinaryExpressionToken>> {
|
||||
let first = operand.parse_next(i).map(BinaryExpressionToken::from)?;
|
||||
let remaining: Vec<_> = repeat(
|
||||
1..,
|
||||
(
|
||||
preceded(opt(whitespace), binary_operator),
|
||||
preceded(opt(whitespace), operand),
|
||||
preceded(opt(whitespace), binary_operator).map(BinaryExpressionToken::from),
|
||||
preceded(opt(whitespace), operand).map(BinaryExpressionToken::from),
|
||||
),
|
||||
)
|
||||
.context(expected(
|
||||
"one or more binary operators (like + or -) and operands for them, e.g. 1 + 2 - 3",
|
||||
))
|
||||
.parse_next(i)?;
|
||||
Ok(())
|
||||
let mut out = Vec::with_capacity(1 + 2 * remaining.len());
|
||||
out.push(first);
|
||||
out.extend(remaining.into_iter().flat_map(|(a, b)| [a, b]));
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Parse an infix binary expression.
|
||||
fn binary_expression(i: TokenSlice) -> PResult<BinaryExpression> {
|
||||
// Find the slice of tokens which makes up the binary expression
|
||||
let tokens = binary_expression_tokens.recognize().parse_next(i)?;
|
||||
let tokens = binary_expression_tokens.parse_next(i)?;
|
||||
|
||||
// Pass the token slice into the specialized math parser, for things like
|
||||
// precedence and converting infix operations to an AST.
|
||||
let mut math_parser = MathParser::new(tokens);
|
||||
let expr = math_parser
|
||||
.parse()
|
||||
.map_err(error::ContextError::from)
|
||||
.map_err(ErrMode::Backtrack)?;
|
||||
Ok(expr)
|
||||
Ok(super::math::parse(tokens))
|
||||
}
|
||||
|
||||
fn binary_expr_in_parens(i: TokenSlice) -> PResult<BinaryExpression> {
|
||||
@ -1134,7 +1133,7 @@ fn big_arrow(i: TokenSlice) -> PResult<Token> {
|
||||
}
|
||||
/// Parse a |> operator.
|
||||
fn pipe_operator(i: TokenSlice) -> PResult<Token> {
|
||||
one_of((TokenType::Operator, "|>"))
|
||||
one_of((TokenType::Operator, PIPE_OPERATOR))
|
||||
.context(expected(
|
||||
"the |> operator, used for 'piping' one function's output into another function's input",
|
||||
))
|
||||
@ -1757,19 +1756,11 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
{
|
||||
// Run the original parser
|
||||
let tokens = crate::token::lexer(test_program);
|
||||
let expected = crate::parser::Parser::new(tokens.clone())
|
||||
.ast_old()
|
||||
.expect("Old parser failed");
|
||||
|
||||
// Run the second parser, check it matches the first parser.
|
||||
let actual = match program.parse(&tokens) {
|
||||
// TODO: get snapshots of what this outputs.
|
||||
let _actual = match program.parse(&tokens) {
|
||||
Ok(x) => x,
|
||||
Err(_e) => panic!("could not parse test {i}"),
|
||||
};
|
||||
assert_eq!(
|
||||
expected, actual,
|
||||
"old parser (left) and new parser (right) disagree on test {i}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2064,4 +2055,702 @@ const mySk1 = startSketchAt([0, 0])"#;
|
||||
assert_eq!(value.raw, "5");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math_parse() {
|
||||
let tokens = crate::token::lexer(r#"5 + "a""#);
|
||||
let actual = crate::parser::Parser::new(tokens).ast().unwrap().body;
|
||||
let expr = BinaryExpression {
|
||||
start: 0,
|
||||
end: 7,
|
||||
operator: BinaryOperator::Add,
|
||||
left: BinaryPart::Literal(Box::new(Literal {
|
||||
start: 0,
|
||||
end: 1,
|
||||
value: serde_json::Value::Number(serde_json::Number::from(5)),
|
||||
raw: "5".to_owned(),
|
||||
})),
|
||||
right: BinaryPart::Literal(Box::new(Literal {
|
||||
start: 4,
|
||||
end: 7,
|
||||
value: serde_json::Value::String("a".to_owned()),
|
||||
raw: r#""a""#.to_owned(),
|
||||
})),
|
||||
};
|
||||
let expected = vec![BodyItem::ExpressionStatement(ExpressionStatement {
|
||||
start: 0,
|
||||
end: 7,
|
||||
expression: Value::BinaryExpression(Box::new(expr)),
|
||||
})];
|
||||
assert_eq!(expected, actual);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_code_token() {
|
||||
let tokens = [
|
||||
Token {
|
||||
token_type: TokenType::Word,
|
||||
start: 0,
|
||||
end: 3,
|
||||
value: "log".to_string(),
|
||||
},
|
||||
Token {
|
||||
token_type: TokenType::Brace,
|
||||
start: 3,
|
||||
end: 4,
|
||||
value: "(".to_string(),
|
||||
},
|
||||
Token {
|
||||
token_type: TokenType::Number,
|
||||
start: 4,
|
||||
end: 5,
|
||||
value: "5".to_string(),
|
||||
},
|
||||
Token {
|
||||
token_type: TokenType::Comma,
|
||||
start: 5,
|
||||
end: 6,
|
||||
value: ",".to_string(),
|
||||
},
|
||||
Token {
|
||||
token_type: TokenType::String,
|
||||
start: 7,
|
||||
end: 14,
|
||||
value: "\"hello\"".to_string(),
|
||||
},
|
||||
Token {
|
||||
token_type: TokenType::Word,
|
||||
start: 16,
|
||||
end: 27,
|
||||
value: "aIdentifier".to_string(),
|
||||
},
|
||||
Token {
|
||||
token_type: TokenType::Brace,
|
||||
start: 27,
|
||||
end: 28,
|
||||
value: ")".to_string(),
|
||||
},
|
||||
];
|
||||
for (i, token) in tokens.iter().enumerate() {
|
||||
assert!(token.is_code_token(), "failed test {i}: {token:?}")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_not_code_token() {
|
||||
let tokens = [
|
||||
Token {
|
||||
token_type: TokenType::Whitespace,
|
||||
start: 6,
|
||||
end: 7,
|
||||
value: " ".to_string(),
|
||||
},
|
||||
Token {
|
||||
token_type: TokenType::BlockComment,
|
||||
start: 28,
|
||||
end: 30,
|
||||
value: "/* abte */".to_string(),
|
||||
},
|
||||
Token {
|
||||
token_type: TokenType::LineComment,
|
||||
start: 30,
|
||||
end: 33,
|
||||
value: "// yoyo a line".to_string(),
|
||||
},
|
||||
];
|
||||
for (i, token) in tokens.iter().enumerate() {
|
||||
assert!(!token.is_code_token(), "failed test {i}: {token:?}")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_abstract_syntax_tree() {
|
||||
let code = "5 +6";
|
||||
let parser = crate::parser::Parser::new(crate::token::lexer(code));
|
||||
let result = parser.ast().unwrap();
|
||||
let expected_result = Program {
|
||||
start: 0,
|
||||
end: 4,
|
||||
body: vec![BodyItem::ExpressionStatement(ExpressionStatement {
|
||||
start: 0,
|
||||
end: 4,
|
||||
expression: Value::BinaryExpression(Box::new(BinaryExpression {
|
||||
start: 0,
|
||||
end: 4,
|
||||
left: BinaryPart::Literal(Box::new(Literal {
|
||||
start: 0,
|
||||
end: 1,
|
||||
value: serde_json::Value::Number(serde_json::Number::from(5)),
|
||||
raw: "5".to_string(),
|
||||
})),
|
||||
operator: BinaryOperator::Add,
|
||||
right: BinaryPart::Literal(Box::new(Literal {
|
||||
start: 3,
|
||||
end: 4,
|
||||
value: serde_json::Value::Number(serde_json::Number::from(6)),
|
||||
raw: "6".to_string(),
|
||||
})),
|
||||
})),
|
||||
})],
|
||||
non_code_meta: NonCodeMeta::default(),
|
||||
};
|
||||
|
||||
assert_eq!(result, expected_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_file() {
|
||||
let some_program_string = r#""#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("file is empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_half_pipe_small() {
|
||||
let tokens = crate::token::lexer(
|
||||
"const secondExtrude = startSketchOn('XY')
|
||||
|> startProfileAt([0,0], %)
|
||||
|",
|
||||
);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("Unexpected token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_member_expression_double_nested_braces() {
|
||||
let tokens = crate::token::lexer(r#"const prop = yo["one"][two]"#);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_member_expression_binary_expression_period_number_first() {
|
||||
let tokens = crate::token::lexer(
|
||||
r#"const obj = { a: 1, b: 2 }
|
||||
const height = 1 - obj.a"#,
|
||||
);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_member_expression_binary_expression_brace_number_first() {
|
||||
let tokens = crate::token::lexer(
|
||||
r#"const obj = { a: 1, b: 2 }
|
||||
const height = 1 - obj["a"]"#,
|
||||
);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_member_expression_binary_expression_brace_number_second() {
|
||||
let tokens = crate::token::lexer(
|
||||
r#"const obj = { a: 1, b: 2 }
|
||||
const height = obj["a"] - 1"#,
|
||||
);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_member_expression_binary_expression_in_array_number_first() {
|
||||
let tokens = crate::token::lexer(
|
||||
r#"const obj = { a: 1, b: 2 }
|
||||
const height = [1 - obj["a"], 0]"#,
|
||||
);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_member_expression_binary_expression_in_array_number_second() {
|
||||
let tokens = crate::token::lexer(
|
||||
r#"const obj = { a: 1, b: 2 }
|
||||
const height = [obj["a"] - 1, 0]"#,
|
||||
);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_member_expression_binary_expression_in_array_number_second_missing_space() {
|
||||
let tokens = crate::token::lexer(
|
||||
r#"const obj = { a: 1, b: 2 }
|
||||
const height = [obj["a"] -1, 0]"#,
|
||||
);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_half_pipe() {
|
||||
let tokens = crate::token::lexer(
|
||||
"const height = 10
|
||||
|
||||
const firstExtrude = startSketchOn('XY')
|
||||
|> startProfileAt([0,0], %)
|
||||
|> line([0, 8], %)
|
||||
|> line([20, 0], %)
|
||||
|> line([0, -8], %)
|
||||
|> close(%)
|
||||
|> extrude(2, %)
|
||||
|
||||
show(firstExtrude)
|
||||
|
||||
const secondExtrude = startSketchOn('XY')
|
||||
|> startProfileAt([0,0], %)
|
||||
|",
|
||||
);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("Unexpected token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_greater_bang() {
|
||||
let tokens = crate::token::lexer(">!");
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let err = parser.ast().unwrap_err();
|
||||
// TODO: Better errors when program cannot tokenize.
|
||||
// https://github.com/KittyCAD/modeling-app/issues/696
|
||||
assert!(err.to_string().contains("file is empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_z_percent_parens() {
|
||||
let tokens = crate::token::lexer("z%)");
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("Unexpected token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_parens_unicode() {
|
||||
let tokens = crate::token::lexer("(ޜ");
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
// TODO: Better errors when program cannot tokenize.
|
||||
// https://github.com/KittyCAD/modeling-app/issues/696
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_negative_in_array_binary_expression() {
|
||||
let tokens = crate::token::lexer(
|
||||
r#"const leg1 = 5
|
||||
const thickness = 0.56
|
||||
|
||||
const bracket = [-leg2 + thickness, 0]
|
||||
"#,
|
||||
);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_nested_open_brackets() {
|
||||
let tokens = crate::token::lexer(
|
||||
r#"
|
||||
z(-[["#,
|
||||
);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_weird_new_line_function() {
|
||||
let tokens = crate::token::lexer(
|
||||
r#"z
|
||||
(--#"#,
|
||||
);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
// TODO: Better errors when program cannot tokenize.
|
||||
// https://github.com/KittyCAD/modeling-app/issues/696
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_weird_lots_of_fancy_brackets() {
|
||||
let tokens = crate::token::lexer(r#"zz({{{{{{{{)iegAng{{{{{{{##"#);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
// TODO: Better errors when program cannot tokenize.
|
||||
// https://github.com/KittyCAD/modeling-app/issues/696
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [], message: "file is empty" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_weird_close_before_open() {
|
||||
let tokens = crate::token::lexer(
|
||||
r#"fn)n
|
||||
e
|
||||
["#,
|
||||
);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("expected whitespace, found ')' which is brace"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_weird_close_before_nada() {
|
||||
let tokens = crate::token::lexer(r#"fn)n-"#);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.err()
|
||||
.unwrap()
|
||||
.to_string()
|
||||
.contains("expected whitespace, found ')' which is brace"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_weird_lots_of_slashes() {
|
||||
let tokens = crate::token::lexer(
|
||||
r#"J///////////o//+///////////P++++*++++++P///////˟
|
||||
++4"#,
|
||||
);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert!(result.err().unwrap().to_string().contains("Unexpected token"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_expand_array() {
|
||||
let code = "const myArray = [0..10]";
|
||||
let parser = crate::parser::Parser::new(crate::token::lexer(code));
|
||||
let result = parser.ast().unwrap();
|
||||
let expected_result = Program {
|
||||
start: 0,
|
||||
end: 23,
|
||||
body: vec![BodyItem::VariableDeclaration(VariableDeclaration {
|
||||
start: 0,
|
||||
end: 23,
|
||||
declarations: vec![VariableDeclarator {
|
||||
start: 6,
|
||||
end: 23,
|
||||
id: Identifier {
|
||||
start: 6,
|
||||
end: 13,
|
||||
name: "myArray".to_string(),
|
||||
},
|
||||
init: Value::ArrayExpression(Box::new(ArrayExpression {
|
||||
start: 16,
|
||||
end: 23,
|
||||
elements: vec![
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 0.into(),
|
||||
raw: "0".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 1.into(),
|
||||
raw: "1".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 2.into(),
|
||||
raw: "2".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 3.into(),
|
||||
raw: "3".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 4.into(),
|
||||
raw: "4".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 5.into(),
|
||||
raw: "5".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 6.into(),
|
||||
raw: "6".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 7.into(),
|
||||
raw: "7".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 8.into(),
|
||||
raw: "8".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 9.into(),
|
||||
raw: "9".to_string(),
|
||||
})),
|
||||
Value::Literal(Box::new(Literal {
|
||||
start: 17,
|
||||
end: 18,
|
||||
value: 10.into(),
|
||||
raw: "10".to_string(),
|
||||
})),
|
||||
],
|
||||
})),
|
||||
}],
|
||||
kind: VariableKind::Const,
|
||||
})],
|
||||
non_code_meta: NonCodeMeta::default(),
|
||||
};
|
||||
|
||||
assert_eq!(result, expected_result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_keyword_in_variable() {
|
||||
let some_program_string = r#"const let = "thing""#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([6, 9])], message: "Cannot assign a variable to a reserved keyword: let" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_keyword_in_fn_name() {
|
||||
let some_program_string = r#"fn let = () {}"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([3, 6])], message: "Cannot assign a variable to a reserved keyword: let" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_stdlib_in_fn_name() {
|
||||
let some_program_string = r#"fn cos = () => {
|
||||
return 1
|
||||
}"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([3, 6])], message: "Cannot assign a variable to a reserved keyword: cos" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_keyword_in_fn_args() {
|
||||
let some_program_string = r#"fn thing = (let) => {
|
||||
return 1
|
||||
}"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([12, 15])], message: "Cannot assign a variable to a reserved keyword: let" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_stdlib_in_fn_args() {
|
||||
let some_program_string = r#"fn thing = (cos) => {
|
||||
return 1
|
||||
}"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([12, 15])], message: "Cannot assign a variable to a reserved keyword: cos" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn zero_param_function() {
|
||||
let program = r#"
|
||||
fn firstPrimeNumber = () => {
|
||||
return 2
|
||||
}
|
||||
firstPrimeNumber()
|
||||
"#;
|
||||
let tokens = crate::token::lexer(program);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let _ast = parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keyword_ok_in_fn_args_return() {
|
||||
let some_program_string = r#"fn thing = (param) => {
|
||||
return true
|
||||
}
|
||||
|
||||
thing(false)
|
||||
"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_define_function_as_var() {
|
||||
for name in ["var", "let", "const"] {
|
||||
let some_program_string = format!(
|
||||
r#"{} thing = (param) => {{
|
||||
return true
|
||||
}}
|
||||
|
||||
thing(false)
|
||||
"#,
|
||||
name
|
||||
);
|
||||
let tokens = crate::token::lexer(&some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
format!(
|
||||
r#"syntax: KclErrorDetails {{ source_ranges: [SourceRange([0, {}])], message: "Expected a `fn` variable kind, found: `{}`" }}"#,
|
||||
name.len(),
|
||||
name
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_define_var_as_function() {
|
||||
let some_program_string = r#"fn thing = "thing""#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
let result = parser.ast();
|
||||
assert!(result.is_err());
|
||||
// TODO: https://github.com/KittyCAD/modeling-app/issues/784
|
||||
// Improve this error message.
|
||||
// It should say that the compiler is expecting a function expression on the RHS.
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"syntax: KclErrorDetails { source_ranges: [SourceRange([11, 18])], message: "Unexpected token" }"#
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_member_expression_sketch_group() {
|
||||
let some_program_string = r#"fn cube = (pos, scale) => {
|
||||
const sg = startSketchOn('XY')
|
||||
|> startProfileAt(pos, %)
|
||||
|> line([0, scale], %)
|
||||
|> line([scale, 0], %)
|
||||
|> line([0, -scale], %)
|
||||
|
||||
return sg
|
||||
}
|
||||
|
||||
const b1 = cube([0,0], 10)
|
||||
const b2 = cube([3,3], 4)
|
||||
|
||||
const pt1 = b1[0]
|
||||
const pt2 = b2[0]
|
||||
|
||||
show(b1)
|
||||
show(b2)"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math_with_stdlib() {
|
||||
let some_program_string = r#"const d2r = pi() / 2
|
||||
let other_thing = 2 * cos(3)"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_negative_arguments() {
|
||||
let some_program_string = r#"fn box = (p, h, l, w) => {
|
||||
const myBox = startSketchOn('XY')
|
||||
|> startProfileAt(p, %)
|
||||
|> line([0, l], %)
|
||||
|> line([w, 0], %)
|
||||
|> line([0, -l], %)
|
||||
|> close(%)
|
||||
|> extrude(h, %)
|
||||
|
||||
return myBox
|
||||
}
|
||||
let myBox = box([0,0], -3, -16, -10)
|
||||
show(myBox)"#;
|
||||
let tokens = crate::token::lexer(some_program_string);
|
||||
let parser = crate::parser::Parser::new(tokens);
|
||||
parser.ast().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_math() {
|
||||
for math_expression in [
|
||||
"1 + 2",
|
||||
"1+2",
|
||||
"1 -2",
|
||||
"1 + 2 * 3",
|
||||
"1 * ( 2 + 3 )",
|
||||
"1 * ( 2 + 3 ) / 4",
|
||||
"1 + ( 2 + 3 ) / 4",
|
||||
"1 * (( 2 + 3 ) / 4 + 5 )",
|
||||
"1 * ((( 2 + 3 )))",
|
||||
"distance * p * FOS * 6 / (sigmaAllow * width)",
|
||||
"2 + (((3)))",
|
||||
] {
|
||||
let tokens = crate::token::lexer(math_expression);
|
||||
let _expr = binary_expression.parse(&tokens).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user