Skip to content

Commit

Permalink
Add factorial operator (#122)
Browse files Browse the repository at this point in the history
  • Loading branch information
triallax authored Jul 25, 2023
1 parent eb47136 commit 0329fc9
Show file tree
Hide file tree
Showing 14 changed files with 233 additions and 43 deletions.
1 change: 1 addition & 0 deletions book/src/operators.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
| Operator | Syntax |
| ------------------------- | -------------------- |
| square, cube, ... | `²`, `³`, `⁻¹`, ... |
| factorial | `!` |
| exponentiation | `^`, `**` |
| multiplication (implicit) | *whitespace* |
| modulo | `%` |
Expand Down
62 changes: 54 additions & 8 deletions numbat/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ use crate::{
pretty_print::PrettyPrint, resolver::ModulePath,
};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnaryOperator {
Factorial,
Negate,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinaryOperator {
Add,
Expand Down Expand Up @@ -36,7 +42,11 @@ pub enum Expression {
Scalar(Span, Number),
Identifier(Span, String),
UnitIdentifier(Span, Prefix, String, String),
Negate(Span, Box<Expression>),
UnaryOperator {
op: UnaryOperator,
expr: Box<Expression>,
span_op: Span,
},
BinaryOperator {
op: BinaryOperator,
lhs: Box<Expression>,
Expand All @@ -52,7 +62,11 @@ impl Expression {
Expression::Scalar(span, _) => *span,
Expression::Identifier(span, _) => *span,
Expression::UnitIdentifier(span, _, _, _) => *span,
Expression::Negate(span, expr) => span.extend(&expr.full_span()),
Expression::UnaryOperator {
op: _,
expr,
span_op,
} => span_op.extend(&expr.full_span()),
Expression::BinaryOperator {
op: _,
lhs,
Expand Down Expand Up @@ -87,7 +101,22 @@ macro_rules! identifier {
#[cfg(test)]
macro_rules! negate {
( $rhs:expr ) => {{
Expression::Negate(Span::dummy(), Box::new($rhs))
Expression::UnaryOperator {
op: UnaryOperator::Negate,
expr: Box::new($rhs),
span_op: Span::dummy(),
}
}};
}

#[cfg(test)]
macro_rules! factorial {
( $lhs:expr ) => {{
Expression::UnaryOperator {
op: UnaryOperator::Factorial,
expr: Box::new($lhs),
span_op: Span::dummy(),
}
}};
}

Expand All @@ -105,6 +134,8 @@ macro_rules! binop {
#[cfg(test)]
pub(crate) use binop;
#[cfg(test)]
pub(crate) use factorial;
#[cfg(test)]
pub(crate) use identifier;
use itertools::Itertools;
#[cfg(test)]
Expand All @@ -123,7 +154,7 @@ fn with_parens(expr: &Expression) -> Markup {
| Expression::Identifier(..)
| Expression::UnitIdentifier(..)
| Expression::FunctionCall(..) => expr.pretty_print(),
Expression::Negate(..) | Expression::BinaryOperator { .. } => {
Expression::UnaryOperator { .. } | Expression::BinaryOperator { .. } => {
m::operator("(") + expr.pretty_print() + m::operator(")")
}
}
Expand Down Expand Up @@ -280,7 +311,16 @@ impl PrettyPrint for Expression {
UnitIdentifier(_, prefix, _name, full_name) => {
m::unit(format!("{}{}", prefix.as_string_long(), full_name))
}
Negate(_, rhs) => m::operator("-") + with_parens(rhs),
UnaryOperator {
op: self::UnaryOperator::Negate,
expr,
span_op: _,
} => m::operator("-") + with_parens(expr),
UnaryOperator {
op: self::UnaryOperator::Factorial,
expr,
span_op: _,
} => with_parens(expr) + m::operator("!"),
BinaryOperator {
op,
lhs,
Expand Down Expand Up @@ -652,9 +692,15 @@ impl ReplaceSpans for Expression {
name.clone(),
full_name.clone(),
),
Expression::Negate(_, expr) => {
Expression::Negate(Span::dummy(), Box::new(expr.replace_spans()))
}
Expression::UnaryOperator {
op,
expr,
span_op: _,
} => Expression::UnaryOperator {
op: *op,
expr: Box::new(expr.replace_spans()),
span_op: Span::dummy(),
},
Expression::BinaryOperator {
op,
lhs,
Expand Down
10 changes: 7 additions & 3 deletions numbat/src/bytecode_interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::collections::HashMap;

use crate::interpreter::{Interpreter, InterpreterResult, Result, RuntimeError};
use crate::prefix::Prefix;
use crate::typed_ast::{BinaryOperator, Expression, Statement};
use crate::typed_ast::{BinaryOperator, Expression, Statement, UnaryOperator};
use crate::unit::Unit;
use crate::unit_registry::UnitRegistry;
use crate::vm::{Constant, Op, Vm};
Expand Down Expand Up @@ -55,10 +55,14 @@ impl BytecodeInterpreter {
}
}
}
Expression::Negate(_span, rhs, _type) => {
Expression::UnaryOperator(_span, UnaryOperator::Negate, rhs, _type) => {
self.compile_expression(rhs)?;
self.vm.add_op(Op::Negate);
}
Expression::UnaryOperator(_span, UnaryOperator::Factorial, lhs, _type) => {
self.compile_expression(lhs)?;
self.vm.add_op(Op::Factorial);
}
Expression::BinaryOperator(_span, operator, lhs, rhs, _type) => {
self.compile_expression(lhs)?;
self.compile_expression(rhs)?;
Expand Down Expand Up @@ -101,7 +105,7 @@ impl BytecodeInterpreter {
| Expression::Identifier(..)
| Expression::UnitIdentifier(..)
| Expression::FunctionCall(..)
| Expression::Negate(..)
| Expression::UnaryOperator(..)
| Expression::BinaryOperator(_, BinaryOperator::ConvertTo, _, _, _) => {}
Expression::BinaryOperator(..) => {
self.vm.add_op(Op::FullSimplify);
Expand Down
3 changes: 2 additions & 1 deletion numbat/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ impl ErrorDiagnostic for TypeCheckError {
];
d.with_labels(labels).with_notes(vec![inner_error])
}
TypeCheckError::NonScalarExponent(span, type_) => d
TypeCheckError::NonScalarExponent(span, type_)
| TypeCheckError::NonScalarFactorialArgument(span, type_) => d
.with_labels(vec![span
.diagnostic_label(LabelStyle::Primary)
.with_message(format!("{type_}"))])
Expand Down
4 changes: 4 additions & 0 deletions numbat/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ use thiserror::Error;
pub enum RuntimeError {
#[error("Division by zero")]
DivisionByZero,
#[error("Expected factorial argument to be a non-negative integer")]
FactorialOfNegativeNumber,
#[error("Expected factorial argument to be a finite integer number")]
FactorialOfNonInteger,
#[error("No statements in program")]
NoStatements,
#[error("{0}")]
Expand Down
1 change: 1 addition & 0 deletions numbat/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod dimension;
mod ffi;
mod interpreter;
pub mod markup;
mod math;
mod name_resolution;
mod number;
mod parser;
Expand Down
14 changes: 14 additions & 0 deletions numbat/src/math.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// Calculates the factorial of (the floor of) the given `f64`.
///
/// It is the caller's responsibility to ensure that the given `f64` is a
/// non-negative integer.
pub fn factorial(mut x: f64) -> f64 {
debug_assert!(x >= 0.0);
x = x.floor();
let mut result = 1f64;
while x >= 1. && result != f64::INFINITY {
result *= x;
x -= 1.;
}
result
}
80 changes: 59 additions & 21 deletions numbat/src/parser.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
//! Numbat Parser
//!
//! Operator precedence, low to high
//! * postfix apply ("//")
//! * conversion ("->")
//! * addition ("+")
//! * subtraction ("-")
//! * multiplication ("*")
//! * division ("/")
//! * exponentiation ("^")
//! * unary minus ("-")
//!
//! Grammar:
//! ```txt
//! statement → expression | variable_decl | function_decl | dimension_decl | unit_decl | procedure_call | module_import
Expand All @@ -23,20 +13,23 @@
//! postfix_apply → conversion ( "//" identifier ) *
//! conversion → term ( "→" term ) *
//! term → factor ( ( "+" | "-") factor ) *
//! factor → unary ( ( "*" | "/") per_factor ) *
//! factor → negate ( ( "*" | "/") per_factor ) *
//! per_factor → modulo ( "per" modulo ) *
//! modulo → unary ( "%" unary ) *
//! unary → "-" unary | ifactor
//! modulo → negate ( "%" negate ) *
//! negate → "-" negate | ifactor
//! ifactor → power ( " " power ) *
//! power → unicode_power ( "^" power )
//! power → factorial ( "^" power )
//! factorial → unicode_power "!" *
//! unicode_power → call ( "⁻" ? ("¹" | "²" | "³" | "⁴" | "⁵" ) ) ?
//! call → primary ( "(" arguments? ")" ) ?
//! arguments → expression ( "," expression ) *
//! primary → number | hex-number | oct-number | bin-number | identifier | "(" expression ")"
//! ```

use crate::arithmetic::{Exponent, Rational};
use crate::ast::{BinaryOperator, DimensionExpression, Expression, ProcedureKind, Statement};
use crate::ast::{
BinaryOperator, DimensionExpression, Expression, ProcedureKind, Statement, UnaryOperator,
};
use crate::decorator::Decorator;
use crate::number::Number;
use crate::prefix_parser::AcceptsPrefix;
Expand Down Expand Up @@ -721,7 +714,7 @@ impl<'a> Parser<'a> {
}

fn modulo(&mut self) -> Result<Expression> {
let mut expr = self.unary()?;
let mut expr = self.negate()?;
let mut full_span = expr.full_span();

while self.match_exact(TokenKind::Modulo).is_some() {
Expand All @@ -736,12 +729,16 @@ impl<'a> Parser<'a> {
Ok(expr)
}

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

Ok(Expression::Negate(span, Box::new(rhs)))
Ok(Expression::UnaryOperator {
op: UnaryOperator::Negate,
expr: Box::new(rhs),
span_op: span,
})
} else {
self.ifactor()
}
Expand All @@ -764,7 +761,7 @@ impl<'a> Parser<'a> {
}

fn power(&mut self) -> Result<Expression> {
let mut expr = self.unicode_power()?;
let mut expr = self.factorial()?;
if self.match_exact(TokenKind::Power).is_some() {
let span_op = Some(self.last().unwrap().span);
let rhs = self.power()?;
Expand All @@ -779,6 +776,22 @@ impl<'a> Parser<'a> {
Ok(expr)
}

fn factorial(&mut self) -> Result<Expression> {
let mut expr = self.unicode_power()?;

while self.match_exact(TokenKind::ExclamationMark).is_some() {
let span = self.last().unwrap().span;

expr = Expression::UnaryOperator {
op: UnaryOperator::Factorial,
expr: Box::new(expr),
span_op: span,
};
}

Ok(expr)
}

fn unicode_power(&mut self) -> Result<Expression> {
let mut expr = self.call()?;

Expand Down Expand Up @@ -1144,7 +1157,7 @@ pub fn parse_dexpr(input: &str) -> DimensionExpression {
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{binop, identifier, negate, scalar, ReplaceSpans};
use crate::ast::{binop, factorial, identifier, negate, scalar, ReplaceSpans};

fn parse_as(inputs: &[&str], statement_expected: Statement) {
for input in inputs {
Expand Down Expand Up @@ -1233,6 +1246,31 @@ mod tests {
should_fail(&["100_", "1.00_", "1e2_"]);
}

#[test]
fn factorials() {
parse_as_expression(
&["4!", "4.0!", "4 !", " 4 !", "(4)!"],
factorial!(scalar!(4.0)),
);
parse_as_expression(
&["3!^3", "(3!)^3"],
binop!(factorial!(scalar!(3.0)), Power, scalar!(3.0)),
);
parse_as_expression(
&["3²!"],
factorial!(binop!(scalar!(3.0), Power, scalar!(2.0))),
);
parse_as_expression(
&["3^3!"],
binop!(scalar!(3.0), Power, factorial!(scalar!(3.0))),
);
parse_as_expression(
&["-5!", "-(5!)", "-(5)!"],
negate!(factorial!(scalar!(5.0))),
);
parse_as_expression(&["5!!", "(5!)!"], factorial!(factorial!(scalar!(5.0))));
}

#[test]
fn negation() {
parse_as_expression(&["-1", " - 1 "], negate!(scalar!(1.0)));
Expand Down
8 changes: 5 additions & 3 deletions numbat/src/prefix_transformer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,11 @@ impl Transformer {
Expression::UnitIdentifier(_, _, _, _) => {
unreachable!("Prefixed identifiers should not exist prior to this stage")
}
Expression::Negate(span, expr) => {
Expression::Negate(span, Box::new(self.transform_expression(*expr)))
}
Expression::UnaryOperator { op, expr, span_op } => Expression::UnaryOperator {
op,
expr: Box::new(self.transform_expression(*expr)),
span_op,
},
Expression::BinaryOperator {
op,
lhs,
Expand Down
2 changes: 2 additions & 0 deletions numbat/src/tokenizer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ pub enum TokenKind {
UnicodeExponent,
At,
Ellipsis,
ExclamationMark,

// Keywords
Let,
Expand Down Expand Up @@ -376,6 +377,7 @@ impl Tokenizer {
'→' | '➞' => TokenKind::Arrow,
'-' if self.match_char('>') => TokenKind::Arrow,
'-' => TokenKind::Minus,
'!' => TokenKind::ExclamationMark,
'⁻' => {
let c = self.peek();
if c.map(is_exponent_char).unwrap_or(false) {
Expand Down
Loading

0 comments on commit 0329fc9

Please sign in to comment.