Boom, A Little Language for Arithmetic

Motivation

This document defines an extended version of the Boom language we created for Homework 9, which was motivated by a common programming task. Then, motivated by the ability to write more complex programs, we extended Boom for Homework 10 to include local variables. When you know how to write language interpreters, you can make a language that meets your own needs.

To write more complex programs in Boom, it will be convenient if we are able to "evolve" numbers through a sequence of expressions. This requires the ability to change the state of a local variable and to have the new value reflected in subsequent expressions.

This version of Boom includes sequences of assignments as number expressions. They will be useful for writing programs that compute numbers that require more complex uses of Boom's operators.

Extensions to the language definition from Homework 10 are marked in this document as new. This includes additions to the BNF description and new sections in the description of semantics.

Language Grammar

BNF Description

This version of the language consists of exact numbers, both literal and named, simple arithmetic expressions, and sequences of assignment statements. Here is the BNF description of the language:

       <exp> ::= <number>
               | <varref>
               | ( <unary-op> <exp> )
               | ( <exp> <binary-op> <exp> )
               | ( let <var> = <exp> in <exp> )
               | ( do <assignment>* <exp> )      ; new

<assignment> ::= ( <varref> := <exp> )           ; new

  <unary-op> ::= - | sq
 <binary-op> ::= + | - | * | / | %
               | @ | ^ | <<

<number> can be any exact number, positive or negative, including integers and rationals. The semantics of the values and operators are defined below.

The symbols let, =, in, do, and := are keywords in the language.    — change

Example Expressions

These are all still legal Boom expressions:

2                 (2 + 4)
20                (4 * (10 ^ 3))
(sq 16)           (2 * (4 / (sq 6)))
(- (2 + 4))       ((2 * 14) + (4 << 6))
(8 << 3)          ((8 % 3) + ((- 4) * (6 @ 10)))

z                 (let z = (x * y) in (- z))
square            (let square = (x * x) in
                    (let half = (square / 2) in
                      (half + (4 * x))))

These are now also legal Boom expressions: — new

(do 0)                              ; no vars or assignments

(let x = 26 in                      ; one variable
  (do (x := (x * 2))                ; one assignment
      (x @ 30)))

(let c = 10 in                      ; one variable
  (do (c := ((sq c) + c))           ; two assignments
      (c := (c / 10))
      (c - 1)))

(let left = 0 in                    ; two variables
  (let right = 12 in
    (do (left  := (left @ right))   ; two assignments
        (right := (left @ right))
        (left * right))))

As the grammar indicates, expressions nest arbitrarily deeply.

Semantics

Unary Expressions

A unary operator takes an expression that evaluates to a number as its operand and returns a new number.
  • (- operand) returns the negation of its operand.
    (- (1 + 2)) equals -3.
  • (sq operand) returns the square of its operand.
    (sq (1 + 2)) equals 9.

Binary Expressions

The binary operators take two numbers as their operands.
  • The first three — +, -, and * — mean what you expect them to mean from past experience: addition, subtraction, and multiplication.
  • (exp1 / exp2) computes integer division.
    (17 / 4) equals 4.
  • (exp1 % exp2) computes the remainder from integer division.
    (17 % 4) equals 1.
  • (exp1 @ exp2) computes the average of its operands.
    (8 % 4) equals 6, and (17 % 4) equals 10.
  • (m ^ n) raises m to the power of n.
    (2 ^ 4) equals 16, and (10 ^ 3) equals 1000.
  • (exp << n) shifts exp to the left by n places, padding with 0s.
    (8 << 3) equals 8000.

Primitive Values

The initial environment of a Boom program contains three primitives:
  • zero, bound to the value 0
  • two, bound to the value 2
  • ten, bound to the value 10

Blocks with Local Variables

A variable reference is meaningful only within the body of the let/in expression that declares the variable. It is not meaningful in the value being assigned to the variable.

A reference to a variable that has not been defined either in the initial environment or in a containing let/in expression is an error.

A let/in expression behaves as follows:

  • First, the value of the variable is evaluated in the current environment.
  • Then, a new environment is created by extending the current environment with the new variable/value pair.
  • Finally, the body of the expression is evaluated in the new environment.

... just as local variables work in Racket.

Sequences with State — new

A do expression consists of 0 or more assignment statements followed by a number expression. The value of a do expression is the value of that final number.

An assignment statement changes the value of an existing variable. An attempt to assign a value to a variable that has not been defined by a containing let/in expression is an error.

Note that the := operator does not behave like other operators in the language, because the variable on the left hand side of the expression is not evaluated. Instead, it is the target of the assignment. The number expression on the right hand side of the statement is evaluated, including any variables it contains.

A do expression behaves as follows:

  • Evaluate the assignment statements in order. For each:
    • evaluate the number expression on the right hand side of the statement, and
    • update the value of the variable on the left hand side in the current environment.
  • Evaluate the final number expression in the current environment and return that value as the value of the do expression.

Syntactic Sugar

All of the features defined above are part of the core of the Boom language except:

In Boom, syntactic abstractions are preprocessed away before expressions are evaluated.

The remaining operators, both unary and boolean, are part of the core of the language. The behavior of the core operators is implemented directly in the Boom interpreter.