Programming Language Foundations

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:

𝑡1𝒞︀𝑡2𝒞︀==𝑓𝑡1==𝑡2𝒞︀𝑡1𝒞︀𝑡2𝒞︀<𝑓𝑡1<𝑡2𝒞︀𝑡1𝒞︀𝑡2𝒞︀>𝑓𝑡1>𝑡2𝒞︀

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 𝑛𝑙+𝑛𝑟:

l𝑛𝑙r𝑛𝑟Add+ l r𝑛𝑙+𝑛𝑟

We define addition between different Value as follows:

Value::Int(𝑛1)+Value::Int(𝑛2)Value::Int(𝑛1+𝑛2)Value::Err(𝑒)+_Value::Err(𝑒)_+Value::Err(𝑒)Value::Err(𝑒)_+_Value::Err(Error::TypeError("..."))

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 𝑛1,𝑛2 if 𝑛1+𝑛2i32 then

Value::Int(𝑛1)+Value::Int(𝑛2)Value::Err(Error::ValueError)

How to check if an arithmetic operation is within legal range in Rust is left as an exercise. Hint: checked methods.

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:

𝑡1true𝑡2𝑏2IfTrueif𝑡1then𝑡2else𝑡3𝑏2𝑡1false𝑡3𝑏3IfFalseif𝑡1then𝑡2else𝑡3𝑏3

We changed 1 to true and 0 to false to avoid confusion between integers and booleans.

The rules currently don’t address the situation if the condition 𝑡1 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.

𝑡1Value::Err(𝑒)IfErrif𝑡1then𝑡2else𝑡3Value::Err(𝑒)

Let’s not forget to also “raise” an error if the condition is an integer.

𝑡1Value::Num(_)IfNumif𝑡1then𝑡2else𝑡3Value::Err(Error::TypeMismatch(...))

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 ValueValueValue, and is defined with the following equations:

Value::Int(𝑛1)==Value::Int(𝑛2)Value::Bool(𝑛1=𝑛2)Value::Bool(𝑏1)==Value::Bool(𝑏2)Value::Bool(𝑏1=𝑏2)Value::Err(𝑒)==_Value::Err(𝑒)_==Value::Err(𝑒)Value::Err(𝑒)_==_Value::Err(Error::TypeError)

In the definition, you should read the mathematical comparison 𝑛1=𝑛2 and 𝑏1=𝑏2 as an operation that returns a boolean.

With this operation on values, we can define the semantics of the == operator in one evaluation rule:

𝑡1𝑣1𝑡2𝑣2==𝑡1==𝑡2𝑣1==𝑣2

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 results
  • Value::Bool(bool) for boolean results
  • Value::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. Propagate Value::Err(e) from either side. Otherwise return Value::Err(Error::TypeMismatch(...)).
  • < and > are defined for (Int, Int) only. Propagate Value::Err(e) from either side. Otherwise return Value::Err(Error::TypeMismatch(...)).

Conditionals:

  • if cond then if_true else if_false evaluates cond first.
  • Propagate error from evaluating cond.
  • If it evaluates to Value::Bool(true), evaluate and return if_true.
  • If it evaluates to Value::Bool(false), evaluate and return if_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

:SugaredASTAST

The shared fragment behaves in the usual way

SugaredAST::Num(n)=AST::Num(n)+ l r=+lrtrue=trueif𝑡1then𝑡2else𝑡3=if𝑡1then𝑡2else𝑡3

Here’s an example of how to define desugaring for <=

l <= r=(l < r) or (l == r)

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).

  1. First, fill in the missing Value operations and AST::eval for the core language (the non-sugared AST) using your solution to the previous exercise.

  2. Then, implement SugaredAST::desugar(self) -> AST.

What is graded

Only evaluation after desugaring is tested:

  • SugaredAST::eval is provided and should not be modified.
  • The internal structure of your desugaring is not graded, as long as it produces an AST that 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 != r is desugared as ⟦!(l == r)⟧
    • l <= r is desugared as ⟦(l < r) or (l == r)⟧
    • l >= r is desugared as ⟦(l > r) or (l == r)⟧
    • Tip: Build the above derived constructs using SugaredAST and 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.