Skip to content

Appendix A. Mountain Goat Language Reference

Mountain goats on the mountain drawn as a blueprint

This appendix is the whole language on one page, in the order a reference is read: what a program is made of, the grammar, the types, every operator and function with its shape rule, and the errors the front end gives. The Language Tour page teaches the same material by example; this page is for looking things up. The grammar and the lists of built-in functions come from the front end's own header (docs/part44/code/mgfront.py); every snippet in the last section was run through the real compiler, and appendixA/code/verify_reference.py fails if any result differs from what is stated here.

Compile and run

cd docs/part44/code && ./build.sh        # once: builds mg-opt (needs mlir-18-tools, libmlir-18-dev, cmake)
cd ../../appendixA/code
./verify_reference.py                    # runs all 27 snippets through the compiler and compares (about 1 minute)
../../part44/code/mgc run my_program.mg  # compile and run one of your own programs

1. What a program is

A program is a list of lines. Each line is a comment (starts with #, runs to the end of the line, and may follow a statement on the same line), blank, or one statement:

statement meaning
let NAME = expr give a value a name; a later let of the same name replaces it
print expr print a matrix (a scalar alone cannot be printed: it has no shape)
def NAME(p: tensor[RxC], ...) = expr define a function of tensors; its parameter shapes are checked at every call

There is no loop, no conditional, no indexing, no string, no assignment to part of a value. Computation is built from whole-matrix operations (that is the point of the language: the compiler can see the shapes). One statement fits on one line.

2. Grammar

program := (def | stmt)*
def     := 'def' NAME '(' [NAME ':' type (',' NAME ':' type)*] ')' '=' expr
stmt    := 'let' NAME '=' expr | 'print' expr
type    := 'tensor' '[' dim 'x' dim ']'        dim := INT | '?'
expr    := product (('+' | '-') product)*
product := unary (('*' | '/' | '@') unary)*    unary := '-' unary | term
term    := NAME | NUMBER | matrix | 'transpose' '(' expr ')' | NAME '(' expr (',' expr)* ')' | '(' expr ')'
matrix  := '[' row (',' row)* ']'              row := '[' NUM (',' NUM)* ']'

Numbers are written as digits with an optional fraction and an optional exponent (3, 0.5, 1e-05); a minus sign in front is the unary operator. Names are letters, digits and underscores, not starting with a digit.

3. Values and types

Every matrix is two-dimensional and holds 64-bit floating-point numbers (f64). A matrix literal is a list of equal-length rows: [[1, 2, 3], [4, 5, 6]] is a 2x3 matrix. A scalar is a number known when the program is compiled. It combines with a matrix elementwise and can be bound with let (let s = 2 then a * s), but it has no shape, so it cannot be printed on its own.

A function parameter's type is tensor[RxC], where each of R and C is an integer or ?. A ? is a dynamic dimension: the function is compiled once and works for any size there, (A function that averages along a dynamic axis is refused: col_mean needs a static size along the averaged axis, not '?'.)

Tensors of other ranks exist only through reshape, permute and contract (Chapter 28): a value of rank other than 2 can be reshaped, permuted, contracted, bound with let or printed, and nothing else; every other operation needs a matrix.

4. Operators

From tightest to loosest binding; operators on the same line group left to right:

operator meaning shape rule
- (unary) negate same shape
* / @ * and / elementwise (* is the Hadamard product, not the matrix product); @ matrix product * /: same shape or a size-1 stretch; @: RxK and KxC give RxC
+ - elementwise add and subtract same shape or a size-1 stretch

The stretch rule. In + - * /, a dimension of size 1 is stretched to meet the other operand's size, in each dimension separately: RxC with 1xC, Rx1 or 1x1 gives RxC. A dimension that is neither equal nor 1 is an error. @ never stretches: the inner sizes must be equal, and then they vanish. One consequence: 2 * a @ b is (2 * a) @ b, since * and @ have the same precedence and group left to right.

5. Built-in functions

function result shape
transpose(a) rows and columns swapped RxC becomes CxR
relu(a) max(0, x) for each element same
exp(a), sqrt(a), log(a) elementwise e^x, square root, natural logarithm same
ge(a, b) 1.0 where a >= b, 0.0 elsewhere (a comparison; the stretch rule applies) broadcast of the two
row_sum(a), row_max(a), row_mean(a) one value per row RxC becomes Rx1
col_sum(a), col_max(a), col_mean(a) one value per column RxC becomes 1xC
reshape(a, d0, d1, ...) the same elements, viewed in another shape with the same number of elements any rank
permute(a, p0, p1, ...) reorders the axes: result axis i is input axis pi any rank
contract(a, (i, ...), b, (k, ...)) sums products over the paired axes; the result's axes are a's free axes, then b's any rank

The mean functions need the size of the averaged axis known at compile time (a ? there is an error). The reductions "squash" one axis; to subtract a column's mean from every element of the column write data - col_mean(data), which relies on the stretch rule.

6. Running a program

mgc is the compiler driver (Chapter 20 explains each stage it runs, and Appendix E lists its options):

command does
mgc run prog.mg compile to a native executable and run it; print writes the matrices
mgc mlir prog.mg show the MLIR the front end produces (Appendix B introduces it)
mgc build prog.mg -o exe compile to an executable without running it
mgc lib prog.mg compile the defs to prog.o plus a C++ header prog.h (no main), for linking into your own program (Chapter 20 introduces it)
mgc ptx prog.mg compile to GPU code (PTX); nothing is launched

7. Every snippet, run for real

Each block below is a program, followed by what the compiler printed (a matrix, or an error message with the line number removed). Numbers print with up to six significant digits. These are the outputs of verify_reference.py; its exit status is 0 only if all 27 match the results stated in the script.

## Scalars are numbers, not values: they cannot be printed alone

print 2 + 3

=> error: print needs a matrix; a scalar has no shape to print

## A matrix and a scalar combine elementwise

print [[1, 2], [3, 4]] * 2 + 1

=> [3, 5]
[7, 9]

## Unary minus

print -[[1, -2]]

=> [-1, 2]

## Matrix product with @ (inner sizes meet)

print [[1, 2, 3], [4, 5, 6]] @ [[1], [0], [2]]

=> [7]
[16]

## Precedence: * and @ bind tighter than +, and are left to right

print 2 * [[1, 2]] @ [[3], [4]]

=> [22]

## A size-1 dimension stretches (row vector against matrix)

print [[1, 2], [3, 4]] - [[1, 1]]

=> [0, 1]
[2, 3]

## A size-1 dimension stretches (column vector against matrix)

print [[1, 2], [3, 4]] / [[2], [4]]

=> [0.5, 1]
[0.75, 1]

## A scalar can be bound with let and used, but not printed

let s = 2
print [[1, 2]] * s

=> [2, 4]

## let binds a name; a later let of the same name replaces it

let a = [[1, 2]]
let a = [[3, 4]]
print a

=> [3, 4]

## def: a function of tensors, checked at every call

def f(x: tensor[2x2]) = x * 2
print f([[1, 2], [3, 4]])

=> [2, 4]
[6, 8]

## def with a dynamic dimension, written ?

def f(x: tensor[?x2]) = x * 2
print f([[1, 2], [3, 4], [5, 6]])

=> [2, 4]
[6, 8]
[10, 12]

## Reductions: col_mean leaves one value per column (1xC)

print col_mean([[1, 2], [3, 4]])

=> [2, 3]

## Reductions: row_max leaves one value per row (Rx1)

print row_max([[1, 5, 2], [7, 0, 3]])

=> [5]
[7]

## transpose swaps the two sizes

print transpose([[1, 2, 3]])

=> [1]
[2]
[3]

## reshape: the same elements in another shape

print reshape([[1, 2, 3, 4]], 2, 2)

=> [1, 2]
[3, 4]

## ge: 1 where a >= b, else 0 (a comparison)

print ge([[1, 5]], [[3, 3]])

=> [0, 1]

## The elementwise functions

print sqrt([[4, 9]]) + log([[1]]) + exp([[0]]) + relu([[-1, 2]])

=> [3, 6]

## contract: sum of products over paired axes (here an ordinary matrix product)

print contract([[1, 2], [3, 4]], (1), [[1, 0], [0, 1]], (0))

=> [1, 2]
[3, 4]

## A comment runs to the end of the line

print [[1, 2]]   # trailing comment

=> [1, 2]

## Error: an unknown name

print b

=> error: unknown name 'b'

## Error: rows of different lengths

print [[1, 2], [3]]

=> error: matrix rows have different lengths

## Error: shapes that do not fit

print [[1, 2]] + [[1, 2, 3]]

=> error: cannot add shapes 1x2 and 1x3: dimensions 2 and 3 differ and neither is 1

## Error: inner dimensions of @ differ

print [[1, 2]] @ [[1, 2]]

=> error: cannot multiply (matmul) shapes 1x2 and 1x2: inner dimensions differ

## Error: @ with a scalar

print [[1, 2], [3, 4]] @ 2

=> error: '@' (matrix product) needs two matrices, not a scalar

## Error: an argument that does not fit its parameter

def f(x: tensor[2x2]) = x * 2
print f([[1, 2, 3]])

=> error: argument shape 1x3 does not fit parameter 2x2

## Error: there are no loops

for i in 3

=> error: a statement must start with 'let', 'print' or 'def', not 'for'

## Error: an incomplete expression

print 1 +

=> error: expected an expression, found the end of the line

8. What is deliberately not in the language

No loops, conditionals, recursion, indexing, slicing, strings, user-defined types or modules; one numeric type (f64); no way to read input at run time (data is written into the program as literals). Mountain Goat is small on purpose: the book is about the compiler, and each missing feature is something a real compiler would need a section of its own to handle. Where the book needed more (training loops, attention, gradients) it wrote them as unrolled straight-line programs or as generated programs (Chapters 33 to 36, 45), which is why some programs in this book are hundreds of lines long.