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
38 changes: 38 additions & 0 deletions .github/workflows/web.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: web

on:
push:
paths: ["web/**", "runtime/**", "tinyc/**", "scripts/build_web.py", "targets/tiny-tpu/web/public/**", ".github/workflows/web.yml"]
pull_request:
paths: ["web/**", "runtime/**", "tinyc/**", "scripts/build_web.py", "targets/tiny-tpu/web/public/**", ".github/workflows/web.yml"]
workflow_dispatch:

jobs:
web:
runs-on: ubuntu-latest
timeout-minutes: 30
defaults:
run:
working-directory: web
steps:
- uses: actions/checkout@v4
with:
submodules: true
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: pnpm
cache-dependency-path: web/pnpm-lock.yaml
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test
- run: pnpm build
- name: Playwright smoke
run: pnpm exec playwright install --with-deps chromium && pnpm exec playwright test
5 changes: 4 additions & 1 deletion CONTEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ faked; host-side work is always labelled as host work.
Verified in a real browser: default example bit-exact on both chips; mlp_gpu at -O2 bit-exact on gpu and rejected on tpu
with the legality message; phone width (375px) has no horizontal scroll. Not done: CodeMirror editor (plain textarea),
the TPU PE-grid animation and pass-by-pass replay, Vitest/Playwright tests, the L1-L4 level tabs beyond the four views.
- Next: Phase 8 (docs pages, landing page with hero clip, README verification table, Vercel deploy).
- Phase 7 follow-up: web toolchain is pnpm (CLAUDE.md). `pnpm lint|typecheck|test|e2e` in `web/`; Playwright smoke (5 tests,
incl. warm compile < 5 s) passes; CI in `.github/workflows/web.yml`. The light/dark check only confirms the page renders in both.
Still open from PLAN §Phase 7: CodeMirror editor, PE-grid TPU view, pass-by-pass replay.
- Phase 8 (docs, landing page, README verification table, Vercel deploy): DONE, deployed. Hero clip still not made.

## Fixed decisions (do not re-litigate; change only via docs/ARCHITECTURE.md)
| Decision | Choice |
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ 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 7.** The [Compiler Explorer](web/) runs the whole compiler in the browser. The frontend, optimizing pass pipeline and **both backends** work. One program,
**Status: Phase 8 (launch).** The [Compiler Explorer](web/) runs the whole compiler in the browser. The frontend, optimizing pass pipeline and **both backends** work. One program,
two chips, bit-exact against numpy on each:

```bash
Expand Down Expand Up @@ -36,3 +36,18 @@ cd web && npm install && npm run dev # predev copies the Python bundle and

Open `/app`: edit a program, pick an example and `-O` level, and see every pass, both chips' output, and
the results of running them on the real simulator and RTL.

## Verification

Every result is checked against a numpy oracle computed from the source program, on the real chips.
Details: [docs/VERIFICATION.md](docs/VERIFICATION.md).

| What | Count / result |
|---|---|
| Tests (CPython; run on every push) | 620 |
| Random programs, differential fuzzing, two independent runs | 5,000 + 5,000, no failures |
| Corner-case regression programs, replayed on every backend | 13 |
| tiny-gpu optimization levels checked on the simulator | -O0, -O1, -O2 |
| TPU cost model vs. measured RTL cycles | exact (pinned in `tests/perf_baseline.json`) |

Documentation: [docs](https://tiny-compiler.vercel.app/docs/) (generated from `docs/`).
85 changes: 85 additions & 0 deletions scripts/build_web.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def main() -> None:
(out_py / "bundle.json").write_text(json.dumps(data))
print(f"bundle.json: {len(data['files'])} python files")

(WEB / "public" / "runtime").mkdir(parents=True, exist_ok=True)
tpu_public = ROOT / "targets" / "tiny-tpu" / "web" / "public"
for name in ("tiny_tpu.mjs", "tiny_tpu.wasm"):
shutil.copyfile(tpu_public / name, WEB / "public" / name)
Expand All @@ -56,5 +57,89 @@ def main() -> None:
print(f"examples: {', '.join(examples)}")


# --- docs pages -------------------------------------------------------------------------------

DOC_PAGES = [
# (slug, source file in docs/, title, one-line description)
(
"language",
"LANGUAGE.md",
"The language",
"Types, operators, broadcasting, semantics and errors.",
),
("ir", "IR.md", "Graph IR", "The text format of the SSA graph and its fused matmul form."),
(
"passes",
"PASSES.md",
"Passes and optimization levels",
"Every graph and backend pass, and the range analysis.",
),
(
"architecture",
"ARCHITECTURE.md",
"Architecture",
"Decisions, and the measured effect of each GPU optimization.",
),
(
"tpu-backend",
"TTPU_FORMAT.md",
"The tiny-tpu backend",
"The .ttpu.json program format and its executor.",
),
(
"gpu-backend",
"TGPU_FORMAT.md",
"The tiny-gpu backend",
"Kernels, registers, shared memory and barriers, and the launch format.",
),
(
"verification",
"VERIFICATION.md",
"Verification",
"Every layer that checks the compiler, and the bugs it found.",
),
]


def _rewrite_links(text: str) -> str:
"""Local .md links become the generated page URLs; other repo paths point at GitHub."""
import re

slugs = {src: f"/docs/{slug}" for slug, src, _, _ in DOC_PAGES}

def fix(m: re.Match) -> str:
label, target = m.group(1), m.group(2)
if target in slugs:
return f"[{label}]({slugs[target]})"
if target.endswith(".md") and target.startswith("docs/"):
return f"[{label}]({slugs.get(target[5:], target)})"
if not target.startswith(("http", "#", "/")):
return f"[{label}](https://github.com/deaneeth/tiny-compiler/blob/main/{target})"
return m.group(0)

return re.sub(r"\[([^\]]+)\]\(([^)\s]+)\)", fix, text)


def build_docs() -> None:
out = WEB / "src" / "pages" / "docs"
out.mkdir(parents=True, exist_ok=True)
for slug, src, title, desc in DOC_PAGES:
body = _rewrite_links((ROOT / "docs" / src).read_text())
# the page's own title is the frontmatter; drop a leading H1 so it is not shown twice
lines = body.splitlines()
if lines and lines[0].startswith("# "):
lines = lines[1:]
frontmatter = (
"---\n"
f"layout: ../../layouts/Doc.astro\n"
f"title: {json.dumps(title)}\n"
f"description: {json.dumps(desc)}\n"
"---\n\n"
)
(out / f"{slug}.md").write_text(frontmatter + "\n".join(lines) + "\n")
print(f"docs pages: {len(DOC_PAGES)}")


if __name__ == "__main__":
main()
build_docs()
5 changes: 5 additions & 0 deletions web/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ public/tiny_tpu.mjs
public/tiny_tpu.wasm
public/runtime/ttpu-exec.mjs
src/lib/examples.generated.ts
src/pages/docs/*.md
.vercel
.env*
test-results/
playwright-report/
54 changes: 54 additions & 0 deletions web/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { expect, test } from "@playwright/test";

// The first compile downloads Pyodide and numpy, so it is excluded from the warm-cache budget.
const WARM_BUDGET_MS = 5000;

test.describe("Compiler Explorer smoke", () => {
test("compiles and runs the default example bit-exact on both chips", async ({ page }) => {
await page.goto("/app");
const button = page.getByRole("button", { name: "Compile & run" });
await button.click();
await expect(page.getByText("bit-exact").first()).toBeVisible({ timeout: 150_000 });
const rows = page.getByRole("row").filter({ hasText: "%1" });
await expect(rows.getByText("bit-exact")).toHaveCount(2);
});

test("a warm second compile finishes within the budget", async ({ page }) => {
await page.goto("/app");
const button = page.getByRole("button", { name: /Compile & run|Working/ });
await button.click();
await expect(page.getByText("bit-exact").first()).toBeVisible({ timeout: 150_000 });
await expect(page.getByRole("button", { name: "Compile & run" })).toBeEnabled();

const start = Date.now();
await page.getByRole("button", { name: "Compile & run" }).click();
await expect(page.getByRole("button", { name: "Working…" })).toBeVisible();
await expect(page.getByRole("button", { name: "Compile & run" })).toBeEnabled({ timeout: WARM_BUDGET_MS });
expect(Date.now() - start).toBeLessThan(WARM_BUDGET_MS);
await expect(page.getByText("bit-exact").first()).toBeVisible();
});

test("a rejected program is shown as an error, not a crash", async ({ page }) => {
await page.goto("/app");
await page.getByLabel("source").fill("input A : i8[2, 2]\nC = A @ A +\noutput C\n");
await page.getByRole("button", { name: "Compile & run" }).click();
await expect(page.getByText("expected an expression")).toBeVisible({ timeout: 150_000 });
});

test("phone width has no horizontal scroll", async ({ page }) => {
await page.setViewportSize({ width: 375, height: 812 });
await page.goto("/app");
const overflow = await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth);
expect(overflow).toBeLessThanOrEqual(0);
});

test("the page renders in both light and dark color schemes", async ({ page }) => {
for (const scheme of ["light", "dark"] as const) {
await page.emulateMedia({ colorScheme: scheme });
await page.goto("/app");
const bg = await page.evaluate(() => getComputedStyle(document.body).backgroundColor);
expect(bg).not.toBe("rgba(0, 0, 0, 0)");
await expect(page.getByRole("button", { name: "Compile & run" })).toBeVisible();
}
});
});
12 changes: 12 additions & 0 deletions web/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import js from "@eslint/js";
import tseslint from "typescript-eslint";

export default tseslint.config(
{ ignores: [".vercel/", "dist/", ".astro/", "node_modules/", "src/lib/examples.generated.ts", "src/pages/docs/*.md", "public/"] },
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["**/*.ts", "**/*.tsx"],
rules: { "@typescript-eslint/no-explicit-any": "error" },
},
);
Loading
Loading