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
8 changes: 4 additions & 4 deletions book/src/example-numbat_syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,10 @@ unit thing # New base unit with automatically generated

# 8. Conditionals

fn step(x: Scalar) -> Scalar = # The construct 'if <cond> then <expr> else <expr>'
if x < 0 # is an expression, not a statement. It can span
then 0 # multiple lines.
else 1
fn bump(x: Scalar) -> Scalar = # The construct 'if <cond> then <expr> else <expr>'
if x >= 0 && x <= 1 # is an expression, not a statement. It can span
then 1 # multiple lines.
else 0

# 9. Procedures

Expand Down
3 changes: 3 additions & 0 deletions book/src/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ Numbat operators and other language constructs, ordered by precedence form *high
| subtraction | `x - y` |
| addition | `x + y` |
| comparisons | `x < y`, `x <= y`, `x ≤ y`, … `x == y`, `x != y` |
| logical negation | `!x` |
| logical 'and' | `x && y` |
| logical 'or' | `x || y` |
| unit conversion | `x -> y`, `x → y`, `x ➞ y`, `x to y` |
| conditionals | `if x then y else z` |
| reverse function call | `x // f` |
Expand Down
18 changes: 6 additions & 12 deletions examples/binomial_coefficient.nbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,15 @@
#
# Adapted from the Python version here:
# https://en.wikipedia.org/wiki/Binomial_coefficient
#
# TODO: This could really benefit from logical and/or operators

fn binomial_coefficient(n: Scalar, k: Scalar) -> Scalar =
if k < 0
if k < 0 || k > n
then 0
else if k > n
then 0
else if k > n - k # Take advantage of symmetry
then binomial_coefficient(n, n - k)
else if k == 0
then 1
else if n <= 1
then 1
else binomial_coefficient(n - 1, k) + binomial_coefficient(n - 1, k - 1)
else if k > n - k # Take advantage of symmetry
then binomial_coefficient(n, n - k)
else if k == 0 || n <= 1
then 1
else binomial_coefficient(n - 1, k) + binomial_coefficient(n - 1, k - 1)

assert_eq(binomial_coefficient(10, 0), 1)
assert_eq(binomial_coefficient(10, 1), 10)
Expand Down
24 changes: 10 additions & 14 deletions examples/booleans.nbt
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
fn not(a: Bool) = if a then false else true
fn and(a: Bool, b: Bool) = if a then b else false
fn or(a: Bool, b: Bool) = if a then true else b
assert(!false)
assert(!!true)

assert(not(false))
assert(not(not(true)))
assert(true && true)
assert(!(true && false))
assert(!(false && true))
assert(!(false && false))

assert(and(true, true))
assert(not(and(true, false)))
assert(not(and(false, true)))
assert(not(and(false, false)))

assert(or(true, true))
assert(or(true, false))
assert(or(false, true))
assert(not(or(false, false)))
assert(true || true)
assert(true || false)
assert(false || true)
assert(!(false || false))
8 changes: 4 additions & 4 deletions examples/numbat_syntax.nbt
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ unit thing # New base unit with automatically generated

# 8. Conditionals

fn step(x: Scalar) -> Scalar = # The construct 'if <cond> then <expr> else <expr>'
if x < 0 # is an expression, not a statement. It can span
then 0 # multiple lines.
else 1
fn bump(x: Scalar) -> Scalar = # The construct 'if <cond> then <expr> else <expr>'
if x >= 0 && x <= 1 # is an expression, not a statement. It can span
then 1 # multiple lines.
else 0

# 9. Procedures

Expand Down
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' operator has the highest priority
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'
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
Loading