Repository: tweag/nickel
Pull request #193 (2020-10-30 23:07:43)

Add arithmetic operators (sub, div, mult, modulo)
Partially address #191. Add the missing arithmetic operators `*`, `/`, `-` and `%`, following the usual precedence and associativity rules (based on the rules for [Nix](https://gist.github.com/joepie91/c3c047f3406aea9ec65eebce2ffd449d) and [this general table](https://en.wikipedia.org/wiki/Order_of_operations#Programming_languages)).

Due to the lack of [precedence annotation in LALRPOP](https://github.com/lalrpop/lalrpop/issues/67), we have to encode each level of precedence using a distinct rule.


Patch:
diff --git a/src/grammar.lalrpop b/src/grammar.lalrpop
index 299b6b8d..ee1ff38d 100644
--- a/src/grammar.lalrpop
+++ b/src/grammar.lalrpop
@@ -19,6 +19,10 @@ SpTerm<Rule>: RichTerm =
         }
     };
 
+LeftOp<Op, Current, Previous>: RichTerm =
+    <t1: Current> <op: Op> <t2: Previous> => RichTerm::new(Term::Op2(op, t1,
+    t2));
+
 RichTerm: RichTerm = {
     <l: @L> "fun" <ps:Pattern+> "=>" <t: SpTerm<Term>> <r: @R> => {
         let pos = Some(mk_span(src_id, l, r));
@@ -32,12 +36,7 @@ RichTerm: RichTerm = {
     "if" <b:SpTerm<Term>> "then" <t:SpTerm<Term>> "else" <e:SpTerm<Term>> =>
         RichTerm::app(RichTerm::app(RichTerm::new(Term::Op1(UnaryOp::Ite(), b)), t), e),
     "import" <s: Str> => RichTerm::new(Term::Import(s)),
-    SpTerm<Operation>
-};
-
-Operation: RichTerm = {
-    <t1: SpTerm< Applicative>> <op: BOpIn> <t2: SpTerm<Operation>> => RichTerm::new(Term::Op2(op, t1, t2)),
-    SpTerm< Applicative>,
+    SpTerm<InfixExpr>,
 };
 
 Applicative: RichTerm = {
@@ -203,12 +202,65 @@ switch_default: RichTerm = {
     "_" "=>" <SpTerm<Atom>> "," => <>,
 }
 
-BOpIn: BinaryOp<RichTerm> = {
-    "+" => BinaryOp::Plus(),
+
+
+// TODO: convenience for messing with precedence levels during development. Once
+// operators are fixed, we can inline `InfixExpr0` into `InfixExpr1`
+InfixExpr0: RichTerm = {
+    SpTerm<Applicative>,
+}
+
+InfixExpr1: RichTerm = {
+    InfixExpr0,
+    "-" <t: InfixExpr1> =>
+        RichTerm::new(Term::Op2(BinaryOp::Sub(), Term::Num(0.0).into(), t)),
+}
+
+BinOp2: BinaryOp<RichTerm> = {
     "++" => BinaryOp::PlusStr(),
-    "==" => BinaryOp::Eq(),
     "@" => BinaryOp::ListConcat(),
-};
+}
+
+InfixExpr2: RichTerm = {
+    SpTerm<InfixExpr1>,
+    LeftOp<BinOp2, InfixExpr2, InfixExpr1> => <>,
+}
+
+BinOp3: BinaryOp<RichTerm> = {
+    "*" => BinaryOp::Mult(),
+    "/" => BinaryOp::Div(),
+    "%" => BinaryOp::Modulo(),
+}
+
+InfixExpr3: RichTerm = {
+    InfixExpr2,
+    LeftOp<BinOp3, InfixExpr3, InfixExpr2> => <>,
+}
+
+BinOp4: BinaryOp<RichTerm> = {
+    "+" => BinaryOp::Plus(),
+    "-" => BinaryOp::Sub(),
+}
+
+InfixExpr4: RichTerm = {
+    InfixExpr3,
+    LeftOp<BinOp4, InfixExpr4, InfixExpr3> => <>,
+}
+
+BinOp5: BinaryOp<RichTerm> = {
+    "==" => BinaryOp::Eq(),
+}
+
+InfixExpr5: RichTerm = {
+    InfixExpr4,
+    LeftOp<BinOp5, InfixExpr5, InfixExpr4> => <>,
+}
+
+// TODO: convenience for adding precedence levels during development. Once
+// operators are fixed, we should turn the last level into `InfixExpr` directly
+InfixExpr: RichTerm = {
+    InfixExpr5,
+}
 
 BOpPre: BinaryOp<RichTerm> = {
     "unwrap" => BinaryOp::Unwrap(),
@@ -318,6 +370,10 @@ extern {
         "-$" => Token::Normal(NormalToken::MinusDollar),
 
         "+" => Token::Normal(NormalToken::Plus),
+        "-" => Token::Normal(NormalToken::Minus),
+        "*" => Token::Normal(NormalToken::Times),
+        "/" => Token::Normal(NormalToken::Div),
+        "%" => Token::Normal(NormalToken::Percent),
         "++" => Token::Normal(NormalToken::DoublePlus),
         "==" => Token::Normal(NormalToken::DoubleEq),
         "@" => Token::Normal(NormalToken::At),
diff --git a/src/operation.rs b/src/operation.rs
index 69c10508..5fe5b033 100644
--- a/src/operation.rs
+++ b/src/operation.rs
@@ -585,6 +585,118 @@ fn process_binary_operation(
                 ))
             }
         }
+        BinaryOp::Sub() => {
+            if let Term::Num(n1) = *t1 {
+                if let Term::Num(n2) = *t2 {
+                    Ok(Closure::atomic_closure(Term::Num(n1 - n2).into()))
+                } else {
+                    Err(EvalError::TypeError(
+                        String::from("Num"),
+                        String::from("-, 2nd argument"),
+                        snd_pos,
+                        RichTerm {
+                            term: t2,
+                            pos: pos2,
+                        },
+                    ))
+                }
+            } else {
+                Err(EvalError::TypeError(
+                    String::from("Num"),
+                    String::from("-, 1st argument"),
+                    fst_pos,
+                    RichTerm {
+                        term: t1,
+                        pos: pos1,
+                    },
+                ))
+            }
+        }
+        BinaryOp::Mult() => {
+            if let Term::Num(n1) = *t1 {
+                if let Term::Num(n2) = *t2 {
+                    Ok(Closure::atomic_closure(Term::Num(n1 * n2).into()))
+                } else {
+                    Err(EvalError::TypeError(
+                        String::from("Num"),
+                        String::from("*, 2nd argument"),
+                        snd_pos,
+                        RichTerm {
+                            term: t2,
+                            pos: pos2,
+                        },
+                    ))
+                }
+            } else {
+                Err(EvalError::TypeError(
+                    String::from("Num"),
+                    String::from("*, 1st argument"),
+                    fst_pos,
+                    RichTerm {
+                        term: t1,
+                        pos: pos1,
+                    },
+                ))
+            }
+        }
+        BinaryOp::Div() => {
+            if let Term::Num(n1) = *t1 {
+                if let Term::Num(n2) = *t2 {
+                    if n2 == 0.0 {
+                        Err(EvalError::Other(String::from("division by zero"), pos_op))
+                    } else {
+                        Ok(Closure::atomic_closure(Term::Num(n1 / n2).into()))
+                    }
+                } else {
+                    Err(EvalError::TypeError(
+                        String::from("Num"),
+                        String::from("/, 2nd argument"),
+                        snd_pos,
+                        RichTerm {
+                            term: t2,
+                            pos: pos2,
+                        },
+                    ))
+                }
+            } else {
+                Err(EvalError::TypeError(
+                    String::from("Num"),
+                    String::from("/, 1st argument"),
+                    fst_pos,
+                    RichTerm {
+                        term: t1,
+                        pos: pos1,
+                    },
+                ))
+            }
+        }
+        BinaryOp::Modulo() => {
+            if let Term::Num(n1) = *t1 {
+                if let Term::Num(n2) = *t2 {
+                    Ok(Closure::atomic_closure(Term::Num(n1 % n2).into()))
+                } else {
+                    Err(EvalError::TypeError(
+                        String::from("Num"),
+                        String::from("%, 2nd argument"),
+                        snd_pos,
+                        RichTerm {
+                            term: t2,
+                            pos: pos2,
+                        },
+                    ))
+                }
+            } else {
+                Err(EvalError::TypeError(
+                    String::from("Num"),
+                    String::from("%, 1st argument"),
+                    fst_pos,
+                    RichTerm {
+                        term: t1,
+                        pos: pos1,
+                    },
+                ))
+            }
+        }
         BinaryOp::PlusStr() => {
             if let Term::Str(s1) = *t1 {
                 if let Term::Str(s2) = *t2 {
diff --git a/src/parser/lexer.rs b/src/parser/lexer.rs
index dd178678..07f21d42 100644
--- a/src/parser/lexer.rs
+++ b/src/parser/lexer.rs
@@ -40,7 +40,7 @@ pub enum NormalToken<'input> {
 
     #[regex("_?[a-zA-Z][_a-zA-Z0-9]*")]
     Identifier(&'input str),
-    #[regex("-?[0-9]*\\.?[0-9]+", |lex| lex.slice().parse())]
+    #[regex("[0-9]*\\.?[0-9]+", |lex| lex.slice().parse())]
     NumLiteral(f64),
 
     #[token("Dyn")]
@@ -101,6 +101,14 @@ pub enum NormalToken<'input> {
 
     #[token("+")]
     Plus,
+    #[token("-")]
+    Minus,
+    #[token("*")]
+    Times,
+    #[token("/")]
+    Div,
+    #[token("%")]
+    Percent,
     #[token("++")]
     DoublePlus,
     #[token("==")]
diff --git a/src/program.rs b/src/program.rs
index d03b8713..02baa76e 100644
--- a/src/program.rs
+++ b/src/program.rs
@@ -1268,4 +1268,18 @@ Assume(#alwaysTrue -> #alwaysFalse, not ) true
             "[\"baz\", \"foo\"]"
         );
     }
+
+    #[test]
+    fn arithmetic_expr() {
+        assert_peq!("1+1", "2");
+        assert_peq!("1-2+3-4", "-2");
+        assert_peq!("2-3-4", "-5");
+        assert_peq!("-1-2", "-3");
+        assert_peq!("2*2 + 2*3 - 2*4", "2");
+        assert_peq!("1/2 + 1/4 - 1/8", "0.625");
+        assert_peq!("(10 + 1/4) % 3", "1.25");
+        assert_peq!("10 + 1/4 % 3", "10.25");
+
+        eval_string("1 + 1 / (1 - 1)").unwrap_err();
+    }
 }
diff --git a/src/term.rs b/src/term.rs
index de8a0521..929dc75e 100644
--- a/src/term.rs
+++ b/src/term.rs
@@ -576,6 +576,14 @@ impl<Ty> UnaryOp<Ty> {
 pub enum BinaryOp<CapturedTerm> {
     /// Addition of numerals.
     Plus(),
+    /// Substraction of numerals.
+    Sub(),
+    /// Multiplication of numerals.
+    Mult(),
+    /// Floating-point division of numerals.
+    Div(),
+    /// Modulo of numerals.
+    Modulo(),
     /// Concatenation of strings.
     PlusStr(),
     /// Unwrap a tagged term.
@@ -617,6 +625,10 @@ impl<Ty> BinaryOp<Ty> {
         match self {
             DynExtend(t) => DynExtend(f(t)),
             Plus() => Plus(),
+            Sub() => Sub(),
+            Div() => Div(),
+            Mult() => Mult(),
+            Modulo() => Modulo(),
             PlusStr() => PlusStr(),
             Unwrap() => Unwrap(),
             GoField() => GoField(),
diff --git a/src/typecheck.rs b/src/typecheck.rs
index b2b0507b..2e676c89 100644
--- a/src/typecheck.rs
+++ b/src/typecheck.rs
@@ -1399,7 +1399,11 @@ pub fn get_bop_type(
 ) -> Result<TypeWrapper, TypecheckError> {
     match op {
         // Num -> Num -> Num
-        BinaryOp::Plus() => Ok(TypeWrapper::Concrete(AbsType::arrow(
+        BinaryOp::Plus()
+        | BinaryOp::Sub()
+        | BinaryOp::Mult()
+        | BinaryOp::Div()
+        | BinaryOp::Modulo() => Ok(TypeWrapper::Concrete(AbsType::arrow(
             Box::new(TypeWrapper::Concrete(AbsType::Num())),
             Box::new(TypeWrapper::Concrete(AbsType::arrow(
                 Box::new(TypeWrapper::Concrete(AbsType::Num())),


Test patch:
diff --git a/src/parser/tests.rs b/src/parser/tests.rs
index 619c001a..68a9e33b 100644
--- a/src/parser/tests.rs
+++ b/src/parser/tests.rs
@@ -61,13 +61,13 @@ fn strings() {
         parse_without_pos("\"hello\" ++ \"World\" ++ \"!!\" "),
         Op2(
             BinaryOp::PlusStr(),
-            mk_single_chunk("hello"),
             Op2(
                 BinaryOp::PlusStr(),
+                mk_single_chunk("hello"),
                 mk_single_chunk("World"),
-                mk_single_chunk("!!")
             )
-            .into()
+            .into(),
+            mk_single_chunk("!!")
         )
         .into()
     )