BAGL

language reference · every example below is editable and runs in your browser

Overview

Bagl is a small statically-typed functional language with three unusual properties for its size: tensor shapes are part of the type system, so a matrix-dimension mismatch is a compile error; the compiler performs automatic differentiation, so grad turns functions into their derivatives before type inference ever runs; and the whole compiler runs in your browser, which is what powers this page.

The pipeline is a real compiler, not an interpreter:

source → lexer → parser → autodiff expansion → Hindley-Milner inference (+ shapes)
       → control-flow-graph IR → optimizer → stack bytecode → VM

The design bias throughout: an error beats a silently wrong answer. Anything the compiler cannot handle correctly is a diagnostic, never a guess.

Basics

Literals and comments

Four scalar types: int, float, bool, string. Integers and floats are distinct types with no implicit conversion. Line comments start with //, block comments are /* ... */.

Bindings

let name = value in body binds a name for the body. Bindings nest and shadow. A program evaluates to the value of its final expression.

Functions

Functions take exactly one parameter; multiple arguments are curried with nested fn. Application is juxtaposition and left-associative. Functions are first-class values and close over their environment.

Recursion

letrec makes the name visible inside its own definition. At runtime this is a self-capturing closure the VM back-patches after allocation.

Conditionals

if is an expression; both branches must have the same type and the condition must be bool. Comparison operators are == != < > <= >=; logic is && || !. Equality is defined for scalars only (see Limits).

The type system

Inference

Bagl uses Hindley-Milner type inference: you rarely write a type, and the checker finds the most general one. let-bound values generalize, so one definition serves many types. Inference is implemented with union-find unification and level-based generalization, the same technique OCaml's own checker uses.

The occurs check rejects infinite types: fn x -> x x is a compile error, not a hang.

Numeric operators

Bagl has no type classes, so + - * / and the comparisons resolve by inspecting their operands: if either side is a float, the operation is float. When both sides are still unresolved, the choice is deferred and only defaults to int when the enclosing binding is generalized, so operand order never matters:

With no constraint at all, numeric parameters default to int: fn x -> x + x is int -> int. This trades principal types for a simple single-pass checker; annotate the parameter to get the float version. Division by zero is a runtime error, integer or float, never inf.

Annotations

Optional annotations appear after a binding name or parameter. A parameter annotation is a non-arrow type; wrap arrows in parentheses. Named type variables ('a) and dimension variables ('n) are shared within one annotation, so tensor<float>['n,'n] really means square:

Tensors

Literals and rank

Tensors are float-backed, rank 1 (vectors) or rank 2 (matrices). The bracket syntax decides the rank, and single-row matrices are distinct from vectors: [1.0, 2.0] is a vector with shape [2], while [[1.0, 2.0]] is a 1×2 matrix. Integer tensors are rejected at compile time rather than silently coerced.

dot, transpose, reshape

dot covers four rank combinations, unifying the shared dimension in each:

operandsresultmeaning
[m,k] · [k,n][m,n]matrix product
[m,k] · [k][m]matrix-vector
[k] · [k,n][n]vector-matrix
[k] · [k]floatinner product

transpose works on matrices; reshape takes a literal shape and checks the element count when both sizes are known.

Element-wise arithmetic

+ - * / on two same-shape tensors apply element-wise (* is the Hadamard product, not matrix multiplication; that is dot). A float on either side broadcasts. Shapes are checked at compile time and re-validated by the VM.

Shape checking

Shapes live in the type system as a second unification domain: dimensions are union-find variables with their own occurs check, unified in parallel with ordinary types. A mismatch is a compile-time diagnostic pointing at the offending expression, and it composes: passing a bad shape through a function still reports the mismatch.

Math builtins

exp, log, sqrt, relu, and step apply to a float or element-wise to a tensor. step is the Heaviside function (1.0 for x > 0, else 0.0) and is exactly relu's derivative. Domain errors raise at runtime instead of producing nan: log of a non-positive number and sqrt of a negative number are errors.

Differentiation

grad is a source-to-source transform that runs before type inference: grad (fn x -> body) is rewritten into an ordinary Bagl function computing the derivative, which then flows through inference, the optimizer, and the VM like any other code. The derivative is type- and shape-checked. There are two modes, selected by the parameter annotation.

Scalar mode (forward)

For an unannotated or float parameter over scalar code, grad applies the sum, product, quotient, and chain rules symbolically. if differentiates each branch (the condition is data); let chains through; the builtins carry their calculus-textbook rules.

Derivatives are ordinary functions, so they compose with everything else. Newton's method where the compiler supplies f′:

Tensor mode (reverse)

With a tensor parameter annotation, grad switches to reverse-mode differentiation and returns the gradient of a scalar loss with the parameter's shape. The pullback rules cover dot (all four rank cases), transpose, element-wise arithmetic, the math builtins, scalar broadcast, and let. The annotation is required because the transform runs before inference and the dot pullbacks depend on operand ranks.

This is enough to train a model entirely in the language. Feature-mapped XOR by gradient descent, converging to the exact solution [1, 1, -2]:

Pathwise derivatives

A float-annotated parameter can be differentiated through tensor code. The rank-1 sum-reductions this needs are expressed internally as dot products. This is the pathwise derivative estimator from computational finance: the sensitivity of a Monte Carlo average, computed by differentiating through the simulation itself.

0.65 is exactly the estimator E[g · 1ITM]: the two in-the-money paths contribute (1.2 + 1.4)/4.

What is rejected, and why

Every gradient Bagl returns is exact for the program you wrote. Anything it cannot differentiate correctly is a compile error:

Runtime and tooling

Bagl compiles to a dense stack bytecode executed by a virtual machine with dynamically-sized frames. Programs serialize to .baglc binaries (baglc -c file.bagl) and reload byte-identically, negative numbers included. The optimizer runs constant folding, dead-code elimination, copy propagation, and block-local common-subexpression elimination over a control-flow-graph IR; -O0 disables it and --dump-ast/-types/-ir/-bytecode show every intermediate stage.

Limits

Stated plainly, because knowing the edges is part of knowing the language:

Grammar

program   ::= expr
expr      ::= "let" NAME [":" type] "=" expr "in" expr
            | "letrec" NAME [":" type] "=" expr "in" expr
            | "fn" NAME [":" simple-type] "->" expr
            | "if" expr "then" expr "else" expr
            | binop-expr
binop-expr::= unary { binop unary }        // precedence: || < && < ==,!= < <,>,<=,>= < +,- < *,/
unary     ::= ["-" | "!"] postfix
postfix   ::= primary { primary }          // application, left-associative
primary   ::= INT | FLOAT | STRING | "true" | "false" | NAME
            | "(" expr ")" | tensor
            | "dot" "(" expr "," expr ")" | "transpose" "(" expr ")"
            | "reshape" "(" expr "," shape ")"
            | ("exp"|"log"|"sqrt"|"relu"|"step") "(" expr ")"
            | "grad" ...                    // ordinary application; special-cased on fn literals
tensor    ::= "[" expr {"," expr} "]"       // vector
            | "[" row {"," row} "]"         // matrix, even with one row
type      ::= simple-type ["->" type]
simple-type ::= "int" | "float" | "bool" | "string" | "'" NAME
            | "tensor" "<" type ">" shape | "(" type ")"
shape     ::= "[" dim {"," dim} "]"         // dim ::= INT | "'" NAME

Every example on this page is compiled and executed by the Bagl compiler running in this tab, and each one's expected output is verified in CI.