Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,23 @@ TinyCompiler compiles a tiny tensor language (`.tiny`) to two real chips: tiny-g
assembly on its Python simulator) and tiny-tpu (`.ttpu.json` tile schedules on its
Verilator→WASM RTL). Pipeline: `.tiny → AST → Graph IR → Tile IR | Kernel IR → .ttpu.json | .tgpu`.

## Commands (planned; fill in as Phase 0 lands)
## Commands (backends and `--run` arrive in later phases)

```bash
git submodule update --init
python3 -m venv .venv && .venv/bin/pip install -e ".[dev]" && .venv/bin/pip install -e targets/tiny-gpu --no-deps
source scripts/env.sh
tinyc examples/mm_relu.tiny --target both -O2 --run --stats
pytest -q # full suite incl. 200 fuzz examples
tinyc examples/mm_relu.tiny --locs # Graph IR (or --emit ast); later: --target both -O2 --run --stats
pytest -q # full suite
pytest --update-golden # rewrite tests/golden/* after reviewing the diff
cd web && pnpm dev | build | lint | typecheck | test
```

## Branching
`main` is protected: work on `feat/*`, `fix/*`, `docs/*` branches and open PRs.

## Commit and PR rules
- **Never add Claude (or any AI) as author or co-author** of a commit, PR or anything else: no
`Co-Authored-By: Claude ...` trailer, no "Generated with Claude Code" footer, no AI `--author`.
Commits carry only the repository owner's git identity.
- Before pushing: `git log --format=%B | grep -i co-authored` must print nothing.
4 changes: 3 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ faked; host-side work is always labelled as host work.

## Status
- Phase 0 (scaffold + drive both targets from pytest): DONE. 17 tests green (runners + target contract), ruff/black clean.
- Next: Phase 1 (frontend + Graph IR + oracle).
- Phase 1 (frontend + Graph IR + oracle): DONE. 90 tests green: lexer/parser/diagnostics (20 golden error messages), Graph IR
printer/parser round trip, verifier, numpy oracle + independent scalar interpreter, 6 examples.
- Next: Phase 2 (pass manager + graph passes).

## Fixed decisions (do not re-litigate; change only via docs/ARCHITECTURE.md)
| Decision | Choice |
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ One tiny tensor language, two real chips: `C = relu(A @ B)` compiles to
[tiny-gpu](https://github.com/deaneeth/tiny-gpu) assembly or to a
[tiny-tpu](https://github.com/deaneeth/tiny-tpu) tile schedule, and runs on the real simulator / RTL.

**Status: Phase 0.** No compiler yet. Both targets can be driven from pytest. See
[docs/PLAN.md](docs/PLAN.md) for the roadmap.
**Status: Phase 1.** The frontend and Graph IR work (`tinyc examples/mm_relu.tiny`); no backends yet.
Both targets can be driven from pytest. See [docs/PLAN.md](docs/PLAN.md) for the roadmap,
[docs/LANGUAGE.md](docs/LANGUAGE.md) and [docs/IR.md](docs/IR.md) for the language and IR.

```bash
git clone --recurse-submodules <repo> && cd tiny-compiler
Expand Down
35 changes: 35 additions & 0 deletions docs/IR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Graph IR (text format)

One function, straight-line SSA. Values are `%name`: inputs keep their source names, op results
are numbered `%0, %1, ...`. Every type is printed without spaces (`i8[4,8]`).

```
func @main(%A: i8[4,8], %B: i8[8,4]) -> (i32[4,4]) {
%0 = matmul %A, %B : i32[4,4] loc("mm_relu.tiny":4:10)
%1 = relu %0 : i32[4,4] loc("mm_relu.tiny":4:5)
return %1
}
```

| Op | Syntax |
|---|---|
| `const` | `%x = const [[1, 2], [3, 4]] : i8[2,2]` |
| `matmul` | `%x = matmul %a, %b : T` |
| `add` | `%x = add %a, %b : T` |
| `relu` | `%x = relu %a : T` |

- `loc("file":line:col)` is optional and points at the start of the source expression. It survives
every lowering so the UI can map any output line back to the source.
- `//` starts a comment. Layout is free-form when parsing; the printer is canonical.
- `ir.printer.print_func` and `ir.parser.parse_func` round-trip exactly (tested over every example).

## Code map
| Module | Role |
|---|---|
| `ir/types.py` | `TensorType` and `infer_result_type`, the one source of typing rules |
| `ir/graph.py` | `Value`, `Op`, `Func`, `FuncBuilder` |
| `ir/verifier.py` | structural + type checks; reports every problem |
| `ir/ref.py` | **the oracle**: numpy int64 evaluation, `RangeError` on i32 overflow |
| `ir/interp.py` | independent scalar interpreter (plain Python ints), cross-checked against the oracle |

Inspect a program: `tinyc examples/mm_relu.tiny --locs` (or `--emit ast`).
45 changes: 45 additions & 0 deletions docs/LANGUAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# The `.tiny` language (v1)

Integer tensors, static shapes, rank 1 or 2, no control flow. Statements end at a newline
(newlines inside `(...)` and `[...]` do not end a statement). `#` starts a comment.

```
input A : i8[4, 8] # declare an input
const W : i8[2, 2] = [[1, -2], # declare a constant (shape and range are checked)
[3, 4]]
C = relu(A @ B + b) # define a value (names are single-assignment)
output C, D # one or more outputs (at least one is required)
```

Reserved words: `input const output relu i8 i32`.

## Types
`i8` or `i32`, with 1 or 2 positive dimensions: `i8[4, 8]`, `i32[6]`.

## Expressions
`@` binds tighter than `+`; both are left-associative; parentheses group.

| Expression | Rule | Result |
|---|---|---|
| `a @ b` | both rank 2, `a` is `[M,K]` and `b` is `[K,N]`; operand types may be `i8` or `i32` | `i32[M,N]` |
| `a + b` | same shape, or `[M,N] + [N]` in either order (the row is broadcast); `i8` operands are widened | `i32`, the larger shape |
| `relu(a)` | any | same shape and type as `a` |

## Semantics
All arithmetic is exact (widened); **every op result must fit in `i32`**, otherwise evaluation
fails with `RangeError`, because tiny-gpu raises on overflow rather than wrapping. Inputs must
fit their declared type.

## Errors
The first problem stops compilation and is shown with the source line and a caret:

```
matmul_inner_mismatch.tiny:3:5: error: matmul inner dimensions differ: 8 vs 4 (i8[4,8] @ i8[4,4])
3 | C = A @ B
| ^^^^^
```
Every message is snapshot-tested (`tests/golden/errors/`).

## Target legality (checked later, Phase 2)
Whether a program can run on a chip is not a language rule: tiny-tpu needs `i8` matmul operands,
and tiny-gpu has a memory budget. See `docs/PLAN.md` §3.3.
5 changes: 5 additions & 0 deletions examples/big_mm.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# 12x12x12: 27 tiny-tpu passes once tiled.
input A : i8[12, 12]
input B : i8[12, 12]
C = relu(A @ B)
output C
6 changes: 6 additions & 0 deletions examples/const_mm.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Weights baked into the program as a constant.
input X : i8[3, 2]
const W : i8[2, 2] = [[1, -2],
[3, 4]]
Y = relu(X @ W)
output Y
8 changes: 8 additions & 0 deletions examples/mlp_gpu.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Two layers. The second matmul has an i32 operand, so only tiny-gpu can run it (tiny-tpu needs i8).
input X : i8[4, 8]
input W1 : i8[8, 6]
input b1 : i32[6]
input W2 : i8[6, 3]
H = relu(X @ W1 + b1)
Y = H @ W2
output Y
5 changes: 5 additions & 0 deletions examples/mm.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Plain 4x4 int8 matmul: one tiny-tpu pass, one thread per output element on tiny-gpu.
input A : i8[4, 4]
input B : i8[4, 4]
C = A @ B
output C
6 changes: 6 additions & 0 deletions examples/mm_bias_relu.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# A dense layer: matmul, row-broadcast bias, relu.
input X : i8[4, 8]
input W : i8[8, 4]
input b : i32[4]
Y = relu(X @ W + b)
output Y
5 changes: 5 additions & 0 deletions examples/mm_relu.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# The headline program: C = relu(A @ B).
input A : i8[4, 8]
input B : i8[8, 4]
C = relu(A @ B)
output C
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,11 @@ dependencies = ["numpy>=2.0"]
[project.optional-dependencies]
dev = ["pytest>=8", "hypothesis>=6.100", "black>=24.10", "ruff>=0.6"]

[tool.setuptools]
packages = ["tinyc", "runtime"]
[tool.setuptools.packages.find]
include = ["tinyc*", "runtime*"]

[project.scripts]
tinyc = "tinyc.cli:main"

[tool.pytest.ini_options]
testpaths = ["tests"]
Expand Down
22 changes: 22 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,25 @@
@pytest.fixture
def rng() -> np.random.Generator:
return np.random.default_rng(SEED)


def pytest_addoption(parser):
parser.addoption(
"--update-golden", action="store_true", help="rewrite tests/golden/* from current output"
)


@pytest.fixture
def golden(request):
"""`golden(path, actual)`: compare to a checked-in file; `--update-golden` rewrites it."""
update = request.config.getoption("--update-golden")

def check(path, actual: str) -> None:
if update:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(actual)
return
assert path.exists(), f"missing golden file {path}; run `pytest --update-golden`"
assert actual == path.read_text(), f"{path} differs; review, then `pytest --update-golden`"

return check
21 changes: 21 additions & 0 deletions tests/frontend/test_diagnostics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""Error messages are a feature: each broken program's rendered diagnostic is a golden file."""

from pathlib import Path

import pytest

from tinyc.frontend import CompileError, compile_to_graph

GOLDEN = Path(__file__).resolve().parent.parent / "golden" / "errors"
CASES = sorted(GOLDEN.glob("*.tiny"))


def test_cases_exist():
assert len(CASES) >= 15


@pytest.mark.parametrize("src", CASES, ids=lambda p: p.stem)
def test_diagnostic_snapshot(src, golden):
with pytest.raises(CompileError) as exc:
compile_to_graph(src.read_text(), src.name)
golden(src.with_suffix(".txt"), exc.value.render() + "\n")
72 changes: 72 additions & 0 deletions tests/frontend/test_lexer_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import pytest

from tinyc.frontend import CompileError, SourceFile, ast, parse
from tinyc.frontend.lexer import lex


def toks(text):
return [t.kind for t in lex(SourceFile("t", text))]


def prog(text):
return parse(SourceFile("t", text))


def test_newlines_collapse_and_comments_are_skipped():
assert toks("\n\n# hi\nC = A # trailing\n\n") == ["NAME", "=", "NAME", "NEWLINE", "EOF"]


def test_newlines_inside_brackets_do_not_end_a_statement():
kinds = toks("const W : i8[1, 2] = [\n 1,\n 2 ]\n")
assert kinds.count("NEWLINE") == 1


def test_keywords_are_reserved():
assert toks("input relu i8 i32 const output x") == [
"input", "relu", "i8", "i32", "const", "output", "NAME", "NEWLINE", "EOF",
] # fmt: skip


def test_matmul_binds_tighter_than_add_and_both_are_left_associative():
(stmt,) = prog("C = A + B @ D @ E + F\n").stmts
# ((A + ((B @ D) @ E)) + F)
top = stmt.expr
assert isinstance(top, ast.Add) and top.rhs == ast.Name("F", top.rhs.span)
inner = top.lhs
assert isinstance(inner, ast.Add) and isinstance(inner.rhs, ast.MatMul)
assert isinstance(inner.rhs.lhs, ast.MatMul)


def test_parentheses_widen_the_span_but_not_the_tree():
(stmt,) = prog("C = (A + B) @ D\n").stmts
assert isinstance(stmt.expr, ast.MatMul) and isinstance(stmt.expr.lhs, ast.Add)
assert stmt.expr.lhs.span.col == 5 # starts at '(' not at 'A'


def test_negative_literals_and_nested_lists():
(stmt,) = prog("const W : i8[2, 2] = [[1, -2], [-3, 4]]\n").stmts
flat = [i.value for row in stmt.value.items for i in row.items]
assert flat == [1, -2, -3, 4]


def test_statements_parse_to_the_right_nodes():
p = prog("input A : i8[2, 3]\nB = relu(A)\noutput B, A\n")
assert [type(s) for s in p.stmts] == [ast.InputDecl, ast.Assign, ast.Output]
assert p.stmts[0].type.shape == (2, 3)
assert [n.name for n in p.stmts[2].names] == ["B", "A"]


def test_empty_program_parses_to_no_statements():
assert prog("\n# nothing\n").stmts == ()


def test_error_spans_point_at_the_offending_token():
with pytest.raises(CompileError) as exc:
prog("C = A @\n")
s = exc.value.diagnostic.span
assert (s.line, s.col) == (1, 8) # the end-of-line token


def test_ast_dump_is_stable_and_omits_spans():
out = ast.dump(prog("C = relu(A)\n").stmts)
assert out == "Assign(name='C')\n Relu()\n Name(name='A')"
4 changes: 4 additions & 0 deletions tests/golden/errors/add_shape_mismatch.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
input A : i8[4, 4]
input B : i8[3, 4]
C = A + B
output C
3 changes: 3 additions & 0 deletions tests/golden/errors/add_shape_mismatch.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
add_shape_mismatch.tiny:3:5: error: add shapes are incompatible: i8[4,4] + i8[3,4] (shapes must match, or be [M,N] + [N])
3 | C = A + B
| ^^^^^
2 changes: 2 additions & 0 deletions tests/golden/errors/bad_type.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
input A : f32[2, 2]
output A
3 changes: 3 additions & 0 deletions tests/golden/errors/bad_type.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
bad_type.tiny:1:11: error: expected a type ('i8' or 'i32'), found 'f32'
1 | input A : f32[2, 2]
| ^^^
2 changes: 2 additions & 0 deletions tests/golden/errors/const_out_of_range.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const W : i8[1, 2] = [[1, 300]]
output W
4 changes: 4 additions & 0 deletions tests/golden/errors/const_out_of_range.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const_out_of_range.tiny:1:27: error: value 300 does not fit in i8
1 | const W : i8[1, 2] = [[1, 300]]
| ^^^
= note: i8 holds -128..127
2 changes: 2 additions & 0 deletions tests/golden/errors/const_too_deep.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const W : i8[2] = [[1], [2]]
output W
3 changes: 3 additions & 0 deletions tests/golden/errors/const_too_deep.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const_too_deep.tiny:1:20: error: expected a number here, found a list
1 | const W : i8[2] = [[1], [2]]
| ^^^
2 changes: 2 additions & 0 deletions tests/golden/errors/const_too_flat.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const W : i8[2, 2] = [1, 2]
output W
3 changes: 3 additions & 0 deletions tests/golden/errors/const_too_flat.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const_too_flat.tiny:1:23: error: expected a list of 2 values, found a number
1 | const W : i8[2, 2] = [1, 2]
| ^
3 changes: 3 additions & 0 deletions tests/golden/errors/const_wrong_length.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const W : i8[2, 2] = [[1, 2, 3],
[4, 5]]
output W
4 changes: 4 additions & 0 deletions tests/golden/errors/const_wrong_length.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
const_wrong_length.tiny:1:23: error: expected a list of 2 values, found 3
1 | const W : i8[2, 2] = [[1, 2, 3],
| ^^^^^^^^^
= note: declared type is i8[2,2]
4 changes: 4 additions & 0 deletions tests/golden/errors/matmul_inner_mismatch.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
input A : i8[4, 8]
input B : i8[4, 4]
C = A @ B
output C
3 changes: 3 additions & 0 deletions tests/golden/errors/matmul_inner_mismatch.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
matmul_inner_mismatch.tiny:3:5: error: matmul inner dimensions differ: 8 vs 4 (i8[4,8] @ i8[4,4])
3 | C = A @ B
| ^^^^^
4 changes: 4 additions & 0 deletions tests/golden/errors/matmul_rank1.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
input A : i8[4]
input B : i8[4, 4]
C = A @ B
output C
3 changes: 3 additions & 0 deletions tests/golden/errors/matmul_rank1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
matmul_rank1.tiny:3:5: error: matmul needs rank-2 operands (got i8[4] @ i8[4,4])
3 | C = A @ B
| ^^^^^
3 changes: 3 additions & 0 deletions tests/golden/errors/missing_equals.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
input A : i8[2, 2]
C relu(A)
output C
3 changes: 3 additions & 0 deletions tests/golden/errors/missing_equals.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
missing_equals.tiny:2:3: error: expected '=', found 'relu'
2 | C relu(A)
| ^^^^
3 changes: 3 additions & 0 deletions tests/golden/errors/missing_paren.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
input A : i8[2, 2]
C = relu(A
output C
3 changes: 3 additions & 0 deletions tests/golden/errors/missing_paren.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
missing_paren.tiny:3:1: error: expected ')', found 'output'
3 | output C
| ^^^^^^
2 changes: 2 additions & 0 deletions tests/golden/errors/no_outputs.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
input A : i8[2, 2]
C = relu(A)
4 changes: 4 additions & 0 deletions tests/golden/errors/no_outputs.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
no_outputs.tiny:2:1: error: program has no outputs
2 | C = relu(A)
| ^^^^^^^^^^^
= note: add a line like `output C`
2 changes: 2 additions & 0 deletions tests/golden/errors/output_twice.tiny
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
input A : i8[2, 2]
output A, A
Loading
Loading