Conditional Arithmetic
Learning Objectives
- You can work with a language which has multiple types of values
- You know how to implement big-step evaluation for a language with multiple types of values
- You know the challenges that one needs to face when combining two languages together
- You can extend the language of conditional arithmetic with syntax sugar
Introduction
This chapter merges the prefix arithmetic language and the boolean language into one. Merging them is not entirely trivial, we need to work out for example what parts can we keep the same, what do we need to change, and what do we need to add.
Let’s start with what we know.
The AST of the arithmetic language is encoded as the following enum in Rust
pub enum AST { Num(i32), Add(Box<AST>, Box<AST>), Sub(Box<AST>, Box<AST>), Mul(Box<AST>, Box<AST>), Neg(Box<AST>), Div(Box<AST>, Box<AST>),}Likewise, the AST of booleans is as follows
enum AST { True, False, Ite { cond: Box<AST>, if_true: Box<AST>, if_false: Box<AST>, }, Not(Box<AST>), And(Box<AST>, Box<AST>), Or(Box<AST>, Box<AST>), Xor(Box<AST>, Box<AST>),}Our goal is to combine these and create a language which has both arithmetic and boolean operations.
At first, it may seem like a good approch to give distinct names for these types (like ArithAST and BoolAST) and create a sum type ArithAST + BoolAST, however the resulting language is extremely limited.
For example the term if true then 1 else 2 is clearly not in the language.
Another approach could be to make the types generic, and this is sometimes done to make the language constructs more extensible. This approach is perfectly valid approach to the problem.
Instead of going the generic route, we will write another AST which combines all of the features under one enum.
pub enum AST { Num(i32), Add(Box<AST>, Box<AST>), Sub(Box<AST>, Box<AST>), Mul(Box<AST>, Box<AST>), Neg(Box<AST>), Div(Box<AST>, Box<AST>), True, False, Ite { cond: Box<AST>, if_true: Box<AST>, if_false: Box<AST>, }, Not(Box<AST>), And(Box<AST>, Box<AST>), Or(Box<AST>, Box<AST>), Xor(Box<AST>, Box<AST>),}Next, the type of values contains both integers and booleans as well as an error which can be either a “value error” or a type mismatch.
pub enum Error { /// A runtime error caused by values that were outside their legal range for the operator ValueError(String), /// A type mismatch between operands TypeMismatch(String),}
pub enum Value { Int(i32), Bool(bool), Err(Error),}A type mismatch occurs when we try to operate on incompatible values, e.g. when trying to compare an integer with a boolean.
Runtime Errors
0 / 4 points
Consider the conditional arithmetic language 𝒞 where values can be Int(i32), Bool(bool), or Error.
Which of the following statements about errors are correct?
Grammar of onditional Arithmetic
We denote the language of conditional arithmetic with the letter and include all the language from prefix arithmetic and the boolean language . To make the language more interesting, we add the usual integer comparison operators. Their grammar is as follows:
The rest of the operators, like
!=,<=and>=will be added a bit later.
Evaluation Rules
We inherit the evaluation rules from the prefix arithmetic and the boolean language to .
As the Value enum has become more complex, we need to take this into account in multiple places.
First, in the Add rule, which adds the values :
We define addition between different Value as follows:
In the definition, if either of the summands is an error value, the result is an error.
Also, in the mathematical definition we are hiding the implementation detail for producing a ValueError when adding the i32 values would overflow.
We could express it as follows
For all if then
How to check if an arithmetic operation is within legal range in Rust is left as an exercise. Hint:
checkedmethods.
Unlike the Add rule which didn’t need to be changed directly, the if-then-else rules need to be fixed to avoid evaluation from getting stuck.
As a reminder, here are the old IfTrue and IfFalse rules:
We changed to
trueand tofalseto avoid confusion between integers and booleans.
The rules currently don’t address the situation if the condition evaluates to an error.
In that situation, we want to propagate the error, just like how is done in addition between values.
Let’s add an IfErr rule to the mix.
Let’s not forget to also “raise” an error if the condition is an integer.
Comparison Operator Evaluation Rules
Next, let’s define the semantics of the new == operator.
As our values are split across integers, booleans and errors, we need to define comparison functions between values first.
The equality comparison has the type , and is defined with the following equations:
In the definition, you should read the mathematical comparison and as an operation that returns a boolean.
With this operation on values, we can define the semantics of the == operator in one evaluation rule:
Evaluation in Conditional Arithmetic
0 / 100 points
Implement AST::eval for conditional arithmetic as described by the evaluation rules.
As the evaluation rules were not comprehensive, you need to fill in some of the details yourself.
Values and errors
Evaluation results in a Value:
Value::Int(i32)for integer resultsValue::Bool(bool)for boolean resultsValue::Err(Error)for runtime errors
The variants of the Error enum have the following meaning:
Error::ValueError(String)for value/range errors (e.g. overflow, division failure)Error::TypeMismatch(String)for operand type errors (e.g. adding a boolean)
The exact error strings are not graded, but the error variant is.
Semantics of Operators
Arithmetic operators:
- Use checked arithmetic (
checked_add,checked_sub,checked_mul,checked_div). - If the operation overflows, return
Value::Err(Error::ValueError(...)). - If either operand evaluates to
Value::Err(e), propagate the error unchanged. - If operand types are wrong, return
Value::Err(Error::TypeMismatch(...)).
Comparisons:
==is defined for(Int, Int)and(Bool, Bool)only. PropagateValue::Err(e)from either side. Otherwise returnValue::Err(Error::TypeMismatch(...)).<and>are defined for(Int, Int)only. PropagateValue::Err(e)from either side. Otherwise returnValue::Err(Error::TypeMismatch(...)).
Conditionals:
if cond then if_true else if_falseevaluatescondfirst.- Propagate error from evaluating
cond. - If it evaluates to
Value::Bool(true), evaluate and returnif_true. - If it evaluates to
Value::Bool(false), evaluate and returnif_false. - Any other value is a type mismatch: return
Value::Err(Error::TypeMismatch(...)).
Note: evaluating a conditional should only evaluate one of the two branch.
Syntax Sugar for Conditional Arithmetic
As in the previous chapter, we would like to extend the language without having to extend its semantics.
The core AST is all of the arithmetic and core boolean language, but only has the comparison operators ==, < and >.
The sugared AST on the other hand extends the core AST with the !, and, or, xor, !=, <=, >= operators.
The desugaring function has the signature
The shared fragment behaves in the usual way
Here’s an example of how to define desugaring for <=
Rather than figuring out a new way to represent <= in the core AST, we take good use of the sugared language and construct a new sugared term which has the desired behavior.
Of course one has to be careful not to make a cyclic definition, as that would lead to infinite recursion when desugaring.
Sugared Conditional Arithmetic
0 / 80 points
This exercise builds on the previous one (Evaluation in Conditional Arithmetic).
-
First, fill in the missing
Valueoperations andAST::evalfor the core language (the non-sugaredAST) using your solution to the previous exercise. -
Then, implement
SugaredAST::desugar(self) -> AST.
What is graded
Only evaluation after desugaring is tested:
SugaredAST::evalis provided and should not be modified.- The internal structure of your desugaring is not graded, as long as it produces an
ASTthat evaluates to the correct value.
Errors
Your evaluator returns Value::Err(Error) for runtime errors.
The exact error strings are not graded, but the error variant is:
Error::ValueError(...)for value/range failures (overflow, division failure)Error::TypeMismatch(...)for using operators on the wrong types
Note: Desugaring should not hide errors: if the corresponding core AST evaluation would error, the sugared evaluation should error as well.
Desugaring Tips
You are free to implement these in any equivalent way, but the following translations are recommended:
- Literals and core operators are mapped one-to-one: they “remain unchanged”.
- Derived comparisons could be implemented as follows
l != ris desugared as⟦!(l == r)⟧l <= ris desugared as⟦(l < r) or (l == r)⟧l >= ris desugared as⟦(l > r) or (l == r)⟧- Tip: Build the above derived constructs using
SugaredASTand call.desugar()on them.
Desugaring to a Smaller Core AST
0 / 8 points
Which of the following are correct about desugaring SugaredAST to AST?
Notice that even though negation is not available in the language, one can make Num(n) where n is negative directly in the AST.