Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logical operators #272

Merged
merged 10 commits into from
Dec 4, 2023
27 changes: 27 additions & 0 deletions numbat/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use num_traits::Signed;
pub enum UnaryOperator {
Factorial,
Negate,
LogicalNeg,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand All @@ -26,6 +27,8 @@ pub enum BinaryOperator {
GreaterOrEqual,
Equal,
NotEqual,
LogicalAnd,
LogicalOr,
}

impl PrettyPrint for BinaryOperator {
Expand All @@ -45,6 +48,8 @@ impl PrettyPrint for BinaryOperator {
GreaterOrEqual => m::space() + m::operator("≥") + m::space(),
Equal => m::space() + m::operator("==") + m::space(),
NotEqual => m::space() + m::operator("≠") + m::space(),
LogicalAnd => m::space() + m::operator("&&") + m::space(),
LogicalOr => m::space() + m::operator("||") + m::space(),
}
}
}
Expand Down Expand Up @@ -124,6 +129,24 @@ macro_rules! identifier {
}};
}

#[cfg(test)]
macro_rules! boolean {
( $name:expr ) => {{
crate::ast::Expression::Boolean(Span::dummy(), $name.into())
}};
}

#[cfg(test)]
macro_rules! logical_neg {
( $rhs:expr ) => {{
crate::ast::Expression::UnaryOperator {
op: UnaryOperator::LogicalNeg,
expr: Box::new($rhs),
span_op: Span::dummy(),
}
}};
}

#[cfg(test)]
macro_rules! negate {
( $rhs:expr ) => {{
Expand Down Expand Up @@ -173,12 +196,16 @@ macro_rules! conditional {
#[cfg(test)]
pub(crate) use binop;
#[cfg(test)]
pub(crate) use boolean;
#[cfg(test)]
pub(crate) use conditional;
#[cfg(test)]
pub(crate) use factorial;
#[cfg(test)]
pub(crate) use identifier;
#[cfg(test)]
pub(crate) use logical_neg;
#[cfg(test)]
pub(crate) use negate;
#[cfg(test)]
pub(crate) use scalar;
Expand Down
6 changes: 6 additions & 0 deletions numbat/src/bytecode_interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@ impl BytecodeInterpreter {
self.compile_expression(lhs)?;
self.vm.add_op(Op::Factorial);
}
Expression::UnaryOperator(_span, UnaryOperator::LogicalNeg, lhs, _type) => {
self.compile_expression(lhs)?;
self.vm.add_op(Op::LogicalNeg);
}
Expression::BinaryOperator(_span, operator, lhs, rhs, _type) => {
self.compile_expression(lhs)?;
self.compile_expression(rhs)?;
Expand All @@ -103,6 +107,8 @@ impl BytecodeInterpreter {
BinaryOperator::GreaterOrEqual => Op::GreatorOrEqual,
BinaryOperator::Equal => Op::Equal,
BinaryOperator::NotEqual => Op::NotEqual,
BinaryOperator::LogicalAnd => Op::LogicalAnd,
BinaryOperator::LogicalOr => Op::LogicalOr,
};
self.vm.add_op(op);
}
Expand Down
99 changes: 95 additions & 4 deletions numbat/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
//! expression ::= postfix_apply
//! postfix_apply ::= condition ( "//" identifier ) *
//! condition ::= ( "if" conversion "then" condition "else" condition ) | conversion
//! conversion ::= comparison ( ( "→" | "->" | "to" ) comparison ) *
//! conversion ::= logical_or ( ( "→" | "->" | "to" ) logical_or ) *
//! logical_or ::= logical_and ( "||" logical_and ) *
//! logical_and ::= logical_neg ( "&&" logical_neg ) *
//! logical_neg ::= ( "!" logical_neg) | comparison
//! comparison ::= term ( (">" | ">="| "≥" | "<" | "<=" | "≤" | "==" | "!=" | "≠" ) term ) *
//! term ::= factor ( ( "+" | "-") factor ) *
//! factor ::= unary ( ( "*" | "/") per_factor ) *
Expand Down Expand Up @@ -802,10 +805,10 @@ impl<'a> Parser<'a> {
}

fn conversion(&mut self) -> Result<Expression> {
let mut expr = self.comparison()?;
let mut expr = self.logical_or()?;
while self.match_any(&[TokenKind::Arrow, TokenKind::To]).is_some() {
let span_op = Some(self.last().unwrap().span);
let rhs = self.comparison()?;
let rhs = self.logical_or()?;

expr = Expression::BinaryOperator {
op: BinaryOperator::ConvertTo,
Expand All @@ -817,6 +820,53 @@ impl<'a> Parser<'a> {
Ok(expr)
}

fn logical_or(&mut self) -> Result<Expression> {
let mut expr = self.logical_and()?;
while self.match_exact(TokenKind::LogicalOr).is_some() {
let span_op = Some(self.last().unwrap().span);
let rhs = self.logical_and()?;

expr = Expression::BinaryOperator {
op: BinaryOperator::LogicalOr,
lhs: Box::new(expr),
rhs: Box::new(rhs),
span_op,
};
}
Ok(expr)
}

fn logical_and(&mut self) -> Result<Expression> {
let mut expr = self.logical_neg()?;
while self.match_exact(TokenKind::LogicalAnd).is_some() {
let span_op = Some(self.last().unwrap().span);
let rhs = self.logical_neg()?;

expr = Expression::BinaryOperator {
op: BinaryOperator::LogicalAnd,
lhs: Box::new(expr),
rhs: Box::new(rhs),
span_op,
};
}
Ok(expr)
}

fn logical_neg(&mut self) -> Result<Expression> {
if self.match_exact(TokenKind::ExclamationMark).is_some() {
let span = self.last().unwrap().span;
let rhs = self.logical_neg()?;

Ok(Expression::UnaryOperator {
op: UnaryOperator::LogicalNeg,
expr: Box::new(rhs),
span_op: span,
})
} else {
self.comparison()
}
}

fn comparison(&mut self) -> Result<Expression> {
let mut expr = self.term()?;
while let Some(token) = self.match_any(&[
Expand Down Expand Up @@ -1454,7 +1504,10 @@ mod tests {
use std::fmt::Write;

use super::*;
use crate::ast::{binop, conditional, factorial, identifier, negate, scalar, ReplaceSpans};
use crate::ast::{
binop, boolean, conditional, factorial, identifier, logical_neg, negate, scalar,
ReplaceSpans,
};

#[track_caller]
fn parse_as(inputs: &[&str], statement_expected: Statement) {
Expand Down Expand Up @@ -2283,6 +2336,44 @@ mod tests {
);
}

#[test]
fn logical_operation() {
// basic
assert_snapshot!(snap_parse(
"true || false"), @r###"
Expression(BinaryOperator { op: LogicalOr, lhs: Boolean(Span { start: SourceCodePositition { byte: 0, line: 1, position: 1 }, end: SourceCodePositition { byte: 4, line: 1, position: 5 }, code_source_id: 0 }, true), rhs: Boolean(Span { start: SourceCodePositition { byte: 8, line: 1, position: 9 }, end: SourceCodePositition { byte: 13, line: 1, position: 14 }, code_source_id: 0 }, false), span_op: Some(Span { start: SourceCodePositition { byte: 5, line: 1, position: 6 }, end: SourceCodePositition { byte: 7, line: 1, position: 8 }, code_source_id: 0 }) })
"###);
assert_snapshot!(snap_parse(
"true && false"), @r###"
Expression(BinaryOperator { op: LogicalAnd, lhs: Boolean(Span { start: SourceCodePositition { byte: 0, line: 1, position: 1 }, end: SourceCodePositition { byte: 4, line: 1, position: 5 }, code_source_id: 0 }, true), rhs: Boolean(Span { start: SourceCodePositition { byte: 8, line: 1, position: 9 }, end: SourceCodePositition { byte: 13, line: 1, position: 14 }, code_source_id: 0 }, false), span_op: Some(Span { start: SourceCodePositition { byte: 5, line: 1, position: 6 }, end: SourceCodePositition { byte: 7, line: 1, position: 8 }, code_source_id: 0 }) })
"###);
assert_snapshot!(snap_parse(
"!true"), @r###"
Expression(UnaryOperator { op: LogicalNeg, expr: Boolean(Span { start: SourceCodePositition { byte: 1, line: 1, position: 2 }, end: SourceCodePositition { byte: 5, line: 1, position: 6 }, code_source_id: 0 }, true), span_op: Span { start: SourceCodePositition { byte: 0, line: 1, position: 1 }, end: SourceCodePositition { byte: 1, line: 1, position: 2 }, code_source_id: 0 } })
"###);

// priority
#[rustfmt::skip]
parse_as_expression(
&["true || false && true || false"],
binop!( // the and binop has the maximum priority
sharkdp marked this conversation as resolved.
Show resolved Hide resolved
binop!(boolean!(true), LogicalOr, binop!(boolean!(false), LogicalAnd, boolean!(true))),
LogicalOr,
boolean!(false)
)
);

#[rustfmt::skip]
parse_as_expression(
&["!true && false"],
binop!( // The negation has precedence over the and
sharkdp marked this conversation as resolved.
Show resolved Hide resolved
logical_neg!(boolean!(true)),
LogicalAnd,
boolean!(false)
)
);
}

#[test]
fn comparisons() {
parse_as_expression(&["1 < 2"], binop!(scalar!(1.0), LessThan, scalar!(2.0)));
Expand Down
37 changes: 37 additions & 0 deletions numbat/src/tokenizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ pub enum TokenKind {
GreaterThan,
LessOrEqual,
GreaterOrEqual,
LogicalAnd,
LogicalOr,

// Keywords
Let,
Expand Down Expand Up @@ -456,6 +458,8 @@ impl Tokenizer {
return Ok(None);
}
'\n' => TokenKind::Newline,
'&' if self.match_char('&') => TokenKind::LogicalAnd,
'|' if self.match_char('|') => TokenKind::LogicalOr,
'*' if self.match_char('*') => TokenKind::Power,
'+' => TokenKind::Plus,
'*' | '·' | '⋅' | '×' => TokenKind::Multiply,
Expand Down Expand Up @@ -1001,6 +1005,39 @@ fn test_tokenize_string() {
);
}

#[test]
fn test_logical_operators() {
insta::assert_snapshot!(
tokenize_reduced_pretty("true || false").unwrap(),
@r###"
"true", True, (1, 1)
"||", LogicalOr, (1, 6)
"false", False, (1, 9)
"", Eof, (1, 14)
"###
);

insta::assert_snapshot!(
tokenize_reduced_pretty("true && false").unwrap(),
@r###"
"true", True, (1, 1)
"&&", LogicalAnd, (1, 6)
"false", False, (1, 9)
"", Eof, (1, 14)
"###
);

insta::assert_snapshot!(
tokenize_reduced_pretty("true | false").unwrap_err(),
@"Error at (1, 6): `Unexpected character: '|'`"
);

insta::assert_snapshot!(
tokenize_reduced_pretty("true & false").unwrap_err(),
@"Error at (1, 6): `Unexpected character: '&'`"
);
}

#[test]
fn test_is_currency_char() {
assert!(is_currency_char('€'));
Expand Down
54 changes: 39 additions & 15 deletions numbat/src/typechecker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ fn evaluate_const_expr(expr: &typed_ast::Expression) -> Result<Exponent> {
e @ typed_ast::Expression::UnaryOperator(_, ast::UnaryOperator::Factorial, _, _) => Err(
TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "factorial"),
),
e @ typed_ast::Expression::UnaryOperator(_, ast::UnaryOperator::LogicalNeg, _, _) => Err(
TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "logical"),
),
e @ typed_ast::Expression::BinaryOperator(_span_op, op, lhs_expr, rhs_expr, _) => {
let lhs = evaluate_const_expr(lhs_expr)?;
let rhs = evaluate_const_expr(rhs_expr)?;
Expand Down Expand Up @@ -366,6 +369,12 @@ fn evaluate_const_expr(expr: &typed_ast::Expression) -> Result<Exponent> {
| typed_ast::BinaryOperator::NotEqual => Err(
TypeCheckError::UnsupportedConstEvalExpression(e.full_span(), "comparison"),
),
typed_ast::BinaryOperator::LogicalAnd | typed_ast::BinaryOperator::LogicalOr => {
Err(TypeCheckError::UnsupportedConstEvalExpression(
e.full_span(),
"logical",
))
}
}
}
e @ typed_ast::Expression::Identifier(..) => Err(
Expand Down Expand Up @@ -445,27 +454,27 @@ impl TypeChecker {
ast::Expression::UnaryOperator { op, expr, span_op } => {
let checked_expr = self.check_expression(expr)?;
let type_ = checked_expr.get_type();
let dtype = match &type_ {
Type::Dimension(d) => d.clone(),
_ => {
return Err(TypeCheckError::ExpectedDimensionType(
checked_expr.full_span(),
type_.clone(),
))
}
};

match *op {
ast::UnaryOperator::Factorial => {
match (&type_, op) {
(Type::Dimension(dtype), ast::UnaryOperator::Factorial) => {
if !dtype.is_scalar() {
return Err(TypeCheckError::NonScalarFactorialArgument(
expr.full_span(),
dtype,
dtype.clone(),
));
}
}
ast::UnaryOperator::Negate => {}
}
(Type::Dimension(_), ast::UnaryOperator::Negate) => (),
(Type::Boolean, ast::UnaryOperator::LogicalNeg) => (),
(_, ast::UnaryOperator::LogicalNeg) => {
return Err(TypeCheckError::ExpectedBool(expr.full_span()))
}
_ => {
return Err(TypeCheckError::ExpectedDimensionType(
checked_expr.full_span(),
type_.clone(),
));
}
};

typed_ast::Expression::UnaryOperator(*span_op, *op, Box::new(checked_expr), type_)
}
Expand Down Expand Up @@ -507,6 +516,8 @@ impl TypeChecker {
| typed_ast::BinaryOperator::GreaterOrEqual
| typed_ast::BinaryOperator::Equal
| typed_ast::BinaryOperator::NotEqual => "comparison".into(),
typed_ast::BinaryOperator::LogicalAnd => "and".into(),
typed_ast::BinaryOperator::LogicalOr => "or".into(),
},
span_expected: lhs.full_span(),
expected_name: " left hand side",
Expand Down Expand Up @@ -580,6 +591,19 @@ impl TypeChecker {
));
}

Type::Boolean
}
typed_ast::BinaryOperator::LogicalAnd
| typed_ast::BinaryOperator::LogicalOr => {
let lhs_type = lhs_checked.get_type();
let rhs_type = rhs_checked.get_type();

if lhs_type != Type::Boolean {
return Err(TypeCheckError::ExpectedBool(lhs.full_span()));
} else if rhs_type != Type::Boolean {
return Err(TypeCheckError::ExpectedBool(rhs.full_span()));
}

Type::Boolean
}
};
Expand Down
3 changes: 3 additions & 0 deletions numbat/src/typed_ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,9 @@ impl PrettyPrint for Expression {
UnaryOperator(_, self::UnaryOperator::Factorial, expr, _type) => {
with_parens(expr) + m::operator("!")
}
UnaryOperator(_, self::UnaryOperator::LogicalNeg, expr, _type) => {
m::operator("!") + with_parens(expr)
}
BinaryOperator(_, op, lhs, rhs, _type) => pretty_print_binop(op, lhs, rhs),
FunctionCall(_, _, name, args, _type) => {
m::identifier(name)
Expand Down
Loading