Appendix A. Mountain Goat Language Reference¶
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.