The Language Bella

Bella is the second of five languages designed for a compiler course.

1 Introduction

Bella is a simple programming language with interesting features that make it a great fit for learning (1) compiler and interpreter writing, and (2) formal language semantics.

The language features numeric literals, assignments, basic arithmetic, conditionals, loops, and simple functions. Function bodies are limited to simple expressions and may be recursive, but not mutually recursive. The typing system is static (types are determined at compile time), strong (type mismatches are errors), and inferential (type annotations are ever explicitly given). Bella is a friendly, simple language that is relatively easy to implement.

This document defines the language Bella.

Bella

2 Language Description

2.1 Programs

A program is a sequence of one or more statements, of which there are five kinds. Comments begin with // and extend to the end of the line.

let dozen = 12;                                   // let declaration
print dozen % 3 ** 1;                             // print statement
function gcd(x, y) = y == 0 ? x : gcd(y, x % y);  // function declaration
while dozen >= 3 || (gcd(1, 10) != 5) {           // while statement
  dozen = dozen - 2.75E+19 ** 1 ** 3;             // assignment statement
}

Apologies for the old-fashioned semicolons, but they do make the language somewhat easier to parse.

2.2 Values

All values in Bella are either:

2.3 Types

All values in Bella have a type. Numeric values belong to the type $\textsf{Num}$. Functions of $n$ parameters belong to the type $\textsf{Fun}\,n$.

2.4 Literals

Numeric values in Bella are denoted with literals as in JavaScript:

2
2.0
55.9
819.999e-15
2E+10
5.89999e2

There are no function literals.

2.5 Variables

A variable is a named container for a value. Variables get their name via a declaration, as described in the next section. It is possible for multiple variables to have the same name at the same time.

Variables can be mutable or immutable. All variables bound in the standard library are immutable, as are all variables bound to function values in a function declaration. All other variables are mutable.

let x = 3;
print x;
x = 2;                      // OK, mutable
print π;                    // π is from the standard library
// π = 3;                   // ERROR: immutable

A variable may only contain values of a single type throughout its lifetime.

2.6 Declarations

A declaration binds an identifier to a variable. There are three kinds of declarations:

Here is a short example showing all three kinds:

let sister = 5 + 1;          // let declaration of sister (Ⅴ + Ⅰ = Ⅵ)
function triple(x) = x * 3;  // function declaration of triple
                             // ...and parameter declaration of x

Each occurrence of an identifier is either a defining occurrence or a using occurrence. Using occurrences are legal only in the visible region of the declaration that binds the identifier. In Bella, shadowing of identifiers is permitted, so the visible region of a declaration is the scope minus any “inner” scopes of declarations with the same name; therefore the visible region may be discontinuous.

Here a block is code enclosed within a matching pair of curly braces { ... }, or the entire program itself.

Shadowing can occur from declarations that (1) appear later in a block or (2) appear in an inner scope:

let x = 3;
function f() = x;           // Body always refers to the x above
let x = 5;                  // A new, distinct variable (shadows x above)
print f();                  // prints 3
function x() = 0;           // Also shadows the x above
let x = 8;                  // Yet another shadowing of x
function h(x) = 0;          // OK! parameter x shadows global x
while x != 0 {
  print h(1);               // refers to outer h
  let h = 2;                // shadows outer h
  print h;                  // refers to the new h
  let y = 1;                // new variable y in inner scope
  x = 0;                    // Can read and write outer x
}                           // inner h and y go out of scope
// print y;                 // ERROR: y not visible here
print h(3);                 // refers to outer h (inner one is gone now)
function j(k) = π - x;      // OK
// print k;                 // ERROR: k not in scope
let k = 2;                  // OK, brand new k
x = π;
print j(100);               // prints 0
function f(f) = f * k;      // parameter f shadows function f
let k = k + 1;              // creates new k initialized using previous k

Parameter declarations may not be repeated within the same function.

// function g(a, a) = 0;    // ERROR: a already declared

An identifier bound to a variable containing a function value can only be used in a call position. It can never stand alone:

function successor(n) = n + 1;
print successor(2);              // Function call of successor, fine
// let s = successor;            // ERROR: successor cannot stand alone
// print successor;              // ERROR: successor cannot stand alone
// let plusTwo = successor + 1;  // ERROR: successor cannot stand alone
let successor = 5;               // OK, this is a shadowing of the function successor

2.7 Statements

A statement is code that is executed solely for its side effect; it produces no value. The kinds of statements are:

2.8 Expressions

An expression produces a numeric value. For numeric literals $n$, identifiers $i$ and $f$, and expressions $e$, $e_1$, and $e_2$, the Bella expressions are:

3 Standard Library

The following identifiers are pre-defined in a scope that surrounds the program. All are bound to immutable variables, though declarations within a program may shadow them.

4 Formal Syntax

The source of a Bella program is a Unicode string. Here is the syntax given as an Ohm grammar:

bella.ohm
Bella {
  Program   = Statement+
  Statement = let id "=" Exp ";"                        -- vardec
            | function id Params "=" Exp ";"            -- fundec
            | Exp7_id "=" Exp ";"                       -- assign
            | print Exp ";"                             -- print
            | while Exp Block                           -- while
  Params    = "(" ListOf<id, ","> ")"
  Block     = "{" Statement* "}"

  Exp       = ("-" | "!") Exp7                          -- unary
            | Exp1 "?" Exp1 ":" Exp                     -- ternary
            | Exp1
  Exp1      = Exp1 "||" Exp2                            -- binary
            | Exp2
  Exp2      = Exp2 "&&" Exp3                            -- binary
            | Exp3
  Exp3      = Exp4 ("<="|"<"|"=="|"!="|">="|">") Exp4   -- binary
            | Exp4
  Exp4      = Exp4 ("+" | "-") Exp5                     -- binary
            | Exp5
  Exp5      = Exp5 ("*" | "/" | "%") Exp6               -- binary
            | Exp6
  Exp6      = Exp7 "**" Exp6                            -- binary
            | Exp7
  Exp7      = num
            | true
            | false
            | id "(" ListOf<Exp, ","> ")"               -- call
            | id                                        -- id
            | "(" Exp ")"                               -- parens

  let       = "let" ~idchar
  function  = "function" ~idchar
  while     = "while" ~idchar
  true      = "true" ~idchar
  false     = "false" ~idchar
  print     = "print" ~idchar
  keyword   = let | function | while | true | false
  num       = digit+ ("." digit+)? (("E" | "e") ("+" | "-")? digit+)?
  id        = ~keyword letter idchar*
  idchar    = letter | digit | "_"
  space    += "//" (~"\n" any)*                         -- comment
}

5 Formal Semantics

The meaning of a Bella program is defined in this section via transition rules in the style of Natural Semantics. It is defined from the following abstract syntax:

Abstract Syntax of Bella
$ \begin{array}{l} n\!: \mathsf{Numeral} \\ i\!: \mathsf{Identifier} \\ e\!: \mathsf{Expression} = n \mid i \mid \mathtt{true} \mid \mathtt{false} \mid \mathit{uop} \; e \mid e_1 \; \mathit{bop} \; e_2 \mid \mathtt{call} \; i \; e^* \mid e \; \mathtt{?} \; e_1 \; \mathtt{:} \; e_2 \\ s\!: \mathsf{Statement} = \mathtt{let}\;i = e \;|\; \mathtt{func}\;i\;i^*=e \;|\; i = e \;|\; \mathtt{print}\;e \;|\; \mathtt{while}\;e\;b \\ b\!: \mathsf{Block} = \mathtt{block}\; s^* \\ p\!: \mathsf{Program} = \mathtt{program}\; b \\ \\ \mathit{uop}\!: \mathsf{UnaryOp} = \mathtt{-} \mid \mathtt{!} \\ \mathit{bop}\!: \mathsf{BinaryOp} = \mathit{aop} \mid \mathit{rop} \mid \mathit{lop} \\ \mathit{aop}\!: \mathsf{ArithmeticOp} = \mathtt{+} \mid \mathtt{-} \mid \mathtt{*} \mid \mathtt{/} \mid \mathtt{\%} \mid \mathtt{**} \\ \mathit{rop}\!: \mathsf{RelationalOp} = \mathtt{==} \mid \mathtt{!\!=} \mid \mathtt{<} \mid \mathtt{<=} \mid \mathtt{>} \mid \mathtt{>=} \\ \mathit{lop}\!: \mathsf{LogicalOp} = \mathtt{\&\&} \mid \mathtt{||} \end{array} $

5.1 Static Semantics

Constructs are statically analyzed relative to a context that keeps track of the type and mutability status of each identifier.

Statically analyzing an expression computes its type; statically analyzing a statement or block computes the resulting context; Statically analyzing a program simply determines whether it satisfies all contextual rules.

Static Semantics of Bella
$$ \frac{}{\mathsf{Num}\!:\mathsf{Type}}$$
$$ \frac{n\!:\mathsf{Nat}}{\mathsf{Fun}\;n\!:\mathsf{Type}}$$
$$ \frac{}{\textsf{ro}\!:\textsf{Access}}$$
$$ \frac{}{\textsf{rw}\!:\textsf{Access}}$$
$\Gamma\!: \mathsf{Context} =_{\textrm{def}}\; \mathsf{Map}\;\mathsf{Identifier}\;(\mathsf{Type} \times \mathsf{Access})$
$$\frac{}{ \Gamma \vdash [\![n]\!]\!: \textsf{Num}}$$
$$\frac{}{ \Gamma \vdash [\![\mathtt{true}]\!]\!: \textsf{Num}}$$
$$\frac{}{ \Gamma \vdash [\![\mathtt{false}]\!]\!: \textsf{Num}}$$
$$\frac{\Gamma(i) = (\textsf{Num}, \_)}{ \Gamma \vdash [\![i]\!]\!: \textsf{Num}}$$
$$\frac{\Gamma \vdash e\!: \textsf{Num}}{ \Gamma \vdash [\![uop\;e]\!]\!: \textsf{Num}}$$
$$\frac{\Gamma \vdash e_1\!: \textsf{Num} \quad \Gamma \vdash e_2\!: \textsf{Num}} { \Gamma \vdash [\![e_1\;bop\;e_2]\!]\!: \textsf{Num}}$$
$$\frac{\Gamma \vdash e\!: \textsf{Num} \quad \Gamma \vdash e_1\!: \textsf{Num} \quad \Gamma \vdash e_2\!: \textsf{Num}} { \Gamma \vdash [\![e \; \mathtt{?} \; e_1 \; \mathtt{:} \; e_2]\!]\!: \textsf{Num}}$$
$$\frac{\Gamma(f) = (\textsf{Fun}\;n,\,\_) \quad (\Gamma \vdash e_j\!: \textsf{Num})_{j=1}^n} { \Gamma \vdash [\![\texttt{call}\;f\;e_1,\ldots,e_n]\!]\!: \textsf{Num}}$$
$$\frac{\Gamma \vdash e\!: \textsf{Num}} {\Gamma \vdash [\![\mathtt{let}\;i=e]\!] \Longrightarrow \Gamma[i \mapsto (\textsf{Num}, \textsf{rw})]}$$
$$\frac{ p_1,\ldots,p_n \;\textrm{distinct} \quad \Gamma[f \mapsto (\textsf{Fun}\;n, \textsf{ro})][p_j \mapsto (\textsf{Num}, \textsf{ro})]_{j=1}^{n} \vdash e\!: \textsf{Num}} {\Gamma \vdash [\![\mathtt{func}\;f\;(p_1,\ldots,p_n)=e]\!] \Longrightarrow \Gamma[f \mapsto (\textsf{Fun}\;n, \textsf{ro})]}$$
$$\frac{ \Gamma(i) = (\textsf{Num}, \textsf{rw}) \quad \Gamma \vdash e\!: \textsf{Num}} {\Gamma \vdash [\![i=e]\!] \Longrightarrow \Gamma}$$
$$\frac{ \Gamma \vdash e\!: \textsf{Num}} {\Gamma \vdash [\![\texttt{print}\;e]\!] \Longrightarrow \Gamma}$$
$$\frac{ \Gamma \vdash e\!: \textsf{Num} \quad \Gamma \vdash b \Longrightarrow \Gamma'} {\Gamma \vdash [\![\texttt{while}\;e\;b]\!] \Longrightarrow \Gamma}$$
$$\frac{(\Gamma_{j-1} \vdash s_j \Longrightarrow \Gamma_{j})_{j=1}^n} { \Gamma_{0} \vdash [\![\mathtt{block}\;s_1,\ldots,s_n]\!]\Longrightarrow \Gamma_{n}}$$
$$\frac{ \Gamma_{init} \vdash b \Longrightarrow \Gamma} { \vdash [\![\mathtt{program}\;b]\!]\;\textsf{ok}}$$
$\begin{array}{l} \Gamma_{init} = \{ \\ \quad\texttt{π}\!: (\textsf{Num}, \textsf{ro}), \\ \quad\texttt{sqrt}\!: (\textsf{Fun}\,1, \textsf{ro}), \\ \quad\texttt{sin}\!: (\textsf{Fun}\,1, \textsf{ro}), \\ \quad\texttt{cos}\!: (\textsf{Fun}\,1, \textsf{ro}), \\ \quad\texttt{exp}\!: (\textsf{Fun}\,1, \textsf{ro}), \\ \quad\texttt{ln}\!: (\textsf{Fun}\,1, \textsf{ro}), \\ \quad\texttt{hypot}\!: (\textsf{Fun}\,2, \textsf{ro}) \\ \} \end{array}$
Notes

Shadowing rules lead to a simpler semantics than in languages that forbid it, since no checks are needed to prohibit redeclaration within a scope.

Since the visible region of a declaration begins after the declaration is complete, the statement let x = x + 1; in a while-body reads the enclosing x and declares a fresh local one (similar to Lua but different from JavaScript, where the corresponding region is a temporal dead zone and the reference is an error).

The while statement rule specifically produces its input context. Local declarations in the loop body therefore stay in the loop body.

The function rule places $f$ into the context before checking the body, so a function may call itself recursively. But because the parameters are added after $f$, a parameter named $f$ shadows the function itself, making recursion unavailable within such a body.

5.2 Dynamic Semantics

Dynamic meanings are computed relative to both an environment, mapping identifiers to variables, and a store, mapping variables to values. Both are required since multiple variables with the same name can live simultaneously, and captured variables in a closure must not be lost by a subsequent shadowing.

Dynamic Semantics of Bella
$\begin{array}{l} x,y\!: \textsf{Num} \\ v\!: \textsf{Var} \\ \rho\!: \textsf{Env} = \textsf{Map}\;\textsf{Identifier}\;\textsf{Var} \\ \sigma\!: \textsf{Store} = \textsf{Map}\;\textsf{Var}\;\textsf{Value} \\ c\!: \textsf{Closure} = \textsf{Identifier}^* \times \textsf{Expression} \times \textsf{Env} \\ g\!: \textsf{StdFun} = \textsf{Num}^*\to \textsf{Num} \\ z\!: \textsf{Value} = \textsf{Num} \;\mid\; \textsf{Closure} \;\mid\; \textsf{StdFun} \\ o\!: \textsf{Output} = \textsf{Num}^* \\ \\ \textsf{b2n} =_{\small\textrm{def}} \; \lambda b_{\small{\textsf{Bool}}}. \textsf{if}\;b\;\textsf{then}\;1\;\textsf{else}\;0 \\ \\ \Downarrow_{\small P} \;\subseteq \mathsf{Program} \times \mathsf{Output} \\ \Downarrow_{\small S} \;\subseteq (\textsf{Statement} \times \mathsf{Env} \times \textsf{Store} \times \mathsf{Output}) \times (\textsf{Env} \times \textsf{Store} \times \mathsf{Output}) \\ \Downarrow_{\small E} \;\subseteq (\textsf{Expression} \times \mathsf{Env} \times \textsf{Store}) \times \mathsf{Value} \end{array}$
$$\frac{}{ \rho \vdash [\![n]\!],\sigma \Downarrow n}$$
$$\frac{}{ \rho \vdash [\![\mathtt{true}]\!],\sigma \Downarrow 1}$$
$$\frac{}{ \rho \vdash [\![\mathtt{false}]\!],\sigma \Downarrow 0}$$
$$\frac{\sigma(\rho(i)) = x}{\rho \vdash [\![i]\!],\sigma \Downarrow x}$$
$$\frac{ \rho \vdash e,\sigma \Downarrow x} {\rho \vdash [\![\mathsf{-}\;e]\!],\sigma \Downarrow -x}$$
$$\frac{ \rho \vdash e,\sigma \Downarrow 0} {\rho \vdash [\![\mathtt{!}\;e]\!],\sigma \Downarrow 1}$$
$$\frac{ \rho \vdash e,\sigma \Downarrow x \quad x\neq 0} {\rho \vdash [\![\mathtt{!}\;e]\!],\sigma \Downarrow 0}$$
$$\frac{\rho \vdash e_1,\sigma \Downarrow x \quad \rho \vdash e_2,\sigma \Downarrow y} {\rho \vdash [\![e_1\;aop\;e_2]\!],\sigma \Downarrow aop(x,y)}$$
$$\frac{\rho \vdash e_1,\sigma \Downarrow x\quad \rho \vdash e_2,\sigma \Downarrow y} {\rho \vdash [\![e_1\;rop\;e_2]\!],\sigma \Downarrow \textsf{b2n}(x \;rop\; y)}$$
$$\frac{ \rho \vdash e_1,\sigma \Downarrow 0} {\rho \vdash [\![e_1\;\mathtt{\&\&}\;e_2]\!],\sigma \Downarrow 0}$$
$$\frac{ \rho \vdash e_1,\sigma \Downarrow x \quad x\neq 0 \quad \rho \vdash e_2,\sigma \Downarrow y} {\rho \vdash [\![e_1\;\mathtt{\&\&}\;e_2]\!],\sigma \Downarrow y}$$
$$\frac{ \rho \vdash e_1,\sigma \Downarrow x\quad x\neq 0} {\rho \vdash [\![e_1\;\mathtt{||}\;e_2]\!],\sigma \Downarrow x}$$
$$\frac{ \rho \vdash e_1,\sigma \Downarrow 0 \quad \rho \vdash e_2,\sigma \Downarrow y} {\rho \vdash [\![e_1\;\mathtt{||}\;e_2]\!],\sigma \Downarrow y}$$
$$\frac{ \rho \vdash e,\sigma \Downarrow x \quad x \neq 0 \quad \rho \vdash e_1,\sigma \Downarrow y} {\rho \vdash [\![e \; \mathtt{?} \; e_1 \; \mathtt{:} \; e_2]\!],\sigma \Downarrow y}$$
$$\frac{ \rho \vdash e,\sigma \Downarrow 0 \quad \rho \vdash e_2,\sigma \Downarrow y} {\rho \vdash [\![e \; \mathtt{?} \; e_1 \; \mathtt{:} \; e_2]\!],\sigma \Downarrow y}$$
$$\frac{\begin{gathered} (\rho \vdash e_j,\sigma \Downarrow x_j)_{j=1}^n \qquad \sigma(\rho(f)) = ([p_1,\ldots,p_n],\, e,\, \rho_f) \\ v_1,\ldots,v_n \notin\sigma \;\textrm{distinct} \qquad \rho_f[p_j \mapsto v_j]_{j=1}^n \vdash e,\; \sigma[v_j \mapsto x_j]_{j=1}^n \Downarrow y \end{gathered}} { \rho \vdash [\![\texttt{call}\;f\;e_1,\ldots,e_n]\!],\sigma \Downarrow y}$$
$$\frac{(\rho \vdash e_j,\sigma \Downarrow x_j)_{j=1}^n \quad \sigma(\rho(f)) = g} { \rho \vdash [\![\texttt{call}\;f\;e_1,\ldots,e_n]\!],\sigma \Downarrow g(x_1,\ldots,x_n)}$$
$$\frac{ \rho \vdash e,\sigma \Downarrow x \quad v \notin\sigma} { \rho \vdash [\![\mathtt{let}\;i=e]\!],\sigma,o \Downarrow (\rho[i \mapsto v],\; \sigma[v \mapsto x],\; o)}$$
$$\frac{ v \notin\sigma \quad \rho' = \rho[f \mapsto v]} { \rho \vdash [\![\mathtt{func}\;f\;(p_1,\ldots,p_n)=e]\!],\sigma,o \Downarrow (\rho',\; \sigma[v \mapsto ([p_1,\ldots,p_n],\,e,\,\rho')],\; o)}$$
$$\frac{ \rho \vdash e,\sigma \Downarrow x} { \rho \vdash [\![i=e]\!],\sigma,o \Downarrow (\rho,\; \sigma[\rho(i) \mapsto x],\; o)}$$
$$\frac{ \rho \vdash e,\sigma \Downarrow x} { \rho \vdash [\![\mathtt{print}\;e]\!],\sigma,o \Downarrow (\rho,\; \sigma,\; o\,\mathtt{+\!+}[x])}$$
$$\frac{ \rho \vdash e,\sigma \Downarrow 0} { \rho \vdash [\![\mathtt{while}\;e\;b]\!],\sigma,o \Downarrow (\rho,\; \sigma,\; o)}$$
$$\frac{\begin{gathered} \rho \vdash e,\sigma \Downarrow x \quad x\neq 0 \\ \rho \vdash b,\sigma,o \Downarrow (\_,\;\sigma',\;o') \\ \rho \vdash [\![\mathtt{while}\;e\;b]\!],\sigma',o' \Downarrow (\_,\;\sigma'',\;o'')\end{gathered}} { \rho \vdash [\![\mathtt{while}\;e\;b]\!],\sigma,o \Downarrow (\rho,\;\sigma'',\;o'')}$$
$$\frac{( \rho_{j-1} \vdash s_j, \sigma_{j-1}, o_{j-1} \Downarrow (\rho_j,\;\sigma_{j},\;o_{j}))_{j=1}^n} { \rho_0 \vdash [\![\mathtt{block}\;s_1,\ldots,s_n]\!],\sigma_0,o_0 \Downarrow (\rho_n,\;\sigma_n,\;o_n)}$$
$$\frac{ \rho_{init} \vdash b, \sigma_{init}, [\,] \Downarrow (\_,\;\_,\;o)} {[\![\mathtt{program}\;b]\!] \Downarrow o}$$
$\begin{array}{l} \rho_{init} = \{ \texttt{π}\mapsto v_0, \texttt{sqrt}\mapsto v_1, \texttt{sin}\mapsto v_2, \texttt{cos}\mapsto v_3, \texttt{exp}\mapsto v_4, \texttt{ln}\mapsto v_5, \texttt{hypot}\mapsto v_6 \} \\ \sigma_{init} = \{ v_0 \mapsto \pi, v_1 \mapsto \textrm{sqrt}, v_2 \mapsto \textrm{sin}, v_3 \mapsto \textrm{cos}, v_4 \mapsto \textrm{exp}, v_5 \mapsto \textrm{ln}, v_6 \mapsto \textrm{hypot} \} \end{array}$
Notes

Environments are required because a single map from identifiers to values cannot properly handle the scenario of simultaneous active bindings of one name: let x = 1; function f() = x; while c { let x = 2; print f(); }, must print $1$. Similarly, let x = 1; function f() = x; x = 2; print f(); must print $2$ .

Declarations always allocate fresh variables (allowing shadowing to work), while assignments update values of variables (allowing mutations to become visible to a closure that captured an environment mapping an identifier to that same variable).

Since function bodies are expressions, they can neither assign nor print, so there is no need to model recursion with an explicit stack of environments.