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
14 changes: 14 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Check tutorial
on: [push, pull_request, workflow_dispatch]
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: node scripts/check.mjs
- run: node --test tests/*.test.mjs
28 changes: 28 additions & 0 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
name: Publish tutorial to Pages
on: workflow_dispatch
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false
jobs:
deploy:
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: node scripts/check.mjs
- run: node --test tests/*.test.mjs
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: site
- uses: actions/deploy-pages@v4
id: deployment
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
.DS_Store
__pycache__/
*.pyc
.venv/
node_modules/
test-results/
playwright-report/

# Typst/Tola local tools and generated exports
.tools/
tmp/
/draft_2.pdf

# Local historical copies; never part of the current build
/archive/
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
site/assets/paper-*.svg
5 changes: 5 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"printWidth": 100,
"tabWidth": 2,
"htmlWhitespaceSensitivity": "css"
}
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Working on this tutorial

- Work in this repository root, not the former `.tools/repository-export/checkout/` location.
- Edit article prose only in `tutorial_dual/manuscript.typ`. Both PDF and HTML include this file.
- `draft_2.typ` controls PDF layout; `tutorial_dual/components.typ` contains shared helpers.
- Rebuild using `./build_tutorial.sh both`; validate using `./build_tutorial.sh check`.
- `site/` is the standalone interactive companion, not the current article prose.
- `archive/` contains historical copies. Do not edit or build them unless explicitly requested.
- Preserve existing uncommitted work.
40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,39 @@
# Length_Generalization_Tutorial
# Length Generalization in Transformers

This directory is the current Git working copy. Edit **`tutorial_dual/manuscript.typ`** for article content; it supplies both the PDF and HTML editions.

## Current files

| Path | Purpose |
| --- | --- |
| `tutorial_dual/manuscript.typ` | Single source for the article text |
| `tutorial_dual/components.typ` | Shared layout and interactive helpers |
| `draft_2.typ` | PDF layout; includes the manuscript |
| `draft_2.pdf` | Current generated PDF |
| `tutorial_dual/public/` | Current generated HTML site |
| `site/` | Standalone interactive companion and drag-and-drop editor |
| `.tools/` | Local Typst, Tola and Node binaries and downloads |
| `archive/` | Older drafts, snapshots and previous preview artifacts; not used by builds |

## Build and preview

Run from this directory:

```sh
./build_tutorial.sh both # rebuild PDF and HTML
./build_tutorial.sh check # validate the HTML build
./build_tutorial.sh preview # serve the current article with live rebuilding
```

See [the article build guide](tutorial_dual/README.md) for details. The standalone editor has its own [companion documentation](docs/interactive-companion.md); its HTML is not the article source. `npm run preview` serves that companion, while `./build_tutorial.sh preview` serves the current article.

## Older material

The consolidation snapshot is in `archive/2026-11-04-before-consolidation/`:

- `previous-top-level/`: older top-level drafts, manuscript, exports, figures and temporary PDF previews.
- `original-interactive-snapshot/`: original imported interactive tutorial.
- `repository-export-metadata/`: previous export metadata.
- `migration-record.json`: original checkout location, Git HEAD, change list and file hashes.

The former `.tools/repository-export/checkout/` was moved here with its Git history and uncommitted changes. It is no longer a working location. Archives are kept locally and excluded from Git by default.
53 changes: 53 additions & 0 deletions build_tutorial.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/bin/sh
set -eu
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
cd "$ROOT"

# Prefer explicit overrides, then tools on PATH, then workspace-local binaries.
# Searching parents also supports a checkout nested below the workspace's .tools/.
resolve_tool() {
tool_name=$1
tool_override=$2
if [ -n "$tool_override" ]; then
if command -v "$tool_override" >/dev/null 2>&1; then
command -v "$tool_override"
return
fi
echo "Cannot execute $tool_name override: $tool_override" >&2
return 1
fi
if command -v "$tool_name" >/dev/null 2>&1; then
command -v "$tool_name"
return
fi
tool_dir=$ROOT
while :; do
for tool_candidate in "$tool_dir/.tools/$tool_name" "$tool_dir"/.tools/"$tool_name"-*/"$tool_name"; do
if [ -x "$tool_candidate" ] && [ ! -d "$tool_candidate" ]; then
echo "$tool_candidate"
return
fi
done
[ "$tool_dir" != / ] || break
tool_dir=$(dirname -- "$tool_dir")
done
echo "Cannot find $tool_name. Install it on PATH or set its binary path with TYPST_BIN / TOLA_BIN." >&2
return 1
}

build_pdf() {
TUTORIAL_TYPST=$(resolve_tool typst "${TYPST_BIN:-}")
"$TUTORIAL_TYPST" compile draft_2.typ draft_2.pdf
}
run_tola() {
TUTORIAL_TOLA=$(resolve_tool tola "${TOLA_BIN:-}")
"$TUTORIAL_TOLA" "$@" -C tutorial_dual/tola.toml --offline
}
case "${1:-both}" in
pdf) build_pdf ;;
html) run_tola build ;;
both) build_pdf; run_tola build ;;
preview) run_tola dev ;;
check) run_tola check ;;
*) echo 'Usage: ./build_tutorial.sh [pdf|html|both|preview|check]' >&2; exit 2 ;;
esac
120 changes: 120 additions & 0 deletions docs/interactive-companion.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# C-RASP and transformer length generalization

An editable, interactive companion to the C-RASP tutorial. Open `site/index.html` through a local web server; all deployable files are in `site/`.

## Preview and edit

```sh
python3 -m http.server 8000 --directory site
```

Visit http://localhost:8000 for the main tutorial, or http://localhost:8000/version2/ for the separately developed experimental version. Run the server from the repository root. `npm run preview` starts the same server. Edit article prose in `site/index.html`, styling in `site/assets/style.css`, and article interactions in `site/assets/article.js`. This first edition uses semantic HTML, CSS and JavaScript with no build step or Node dependencies. It can live in a GitHub Pages project subdirectory or on the chair's web server. The paper's Typst source remains separate; this is an editorial adaptation, not an automatically synchronized export.

The page has visible formal explanations, anchored sections, keyboard-accessible trace inspection, editable examples, program-sharing URLs, trace downloads, attention and positional-relation illustrations, published figures, and a saved-evaluation gallery. It is responsive and has a print stylesheet.

## Integrated article

The main `index.html` draft embeds two independent activities in the reading sequence: a Dyck-1 walkthrough and a general block playground. The latter starts with “contains a” and offers programs for both-symbol presence and a-before-b order. It also supports starting empty. Both activities run locally without a Python download. The article proceeds from those examples to attention constructions, positional information, idealized length generalization, empirical evidence, and limitations.

The embedded views reuse `notebook.html?embed=walk` and `notebook.html?embed=build`. Their same-origin resize messages let the article display each activity at its full height, including after edits. `article.js` handles the article widgets; `editor.js` handles the separate optional text editor. Older shared-program links to `index.html#program=…` redirect to that editor.

## The optional text editor and Python implementation

The separate `text-editor.html` page runs the original **interpreter** from [coli-saar/c-rasp, c-star-rasp](https://github.com/coli-saar/c-rasp/tree/c-star-rasp), commit `1e821d7279015ce0e77830faa7932575447c5164`. It does not compile transformer weights or train a model. It uses ordinary C-RASP execution, not the optional C*-RASP mode. Upstream source is fetched by immutable commit at runtime rather than copied into this repository. The manifest is `site/data/interpreter.json`.

Python runs in a disposable Web Worker with Pyodide 0.27.7 and antlr4-python3-runtime 4.13.1. The first Run needs internet access to jsDelivr, PyPI and raw.githubusercontent.com and can take several seconds. Program/input contents stay in the browser; only dependency downloads are sent to those hosts. Sharing a link deliberately embeds the program and input in its URL fragment. No program auto-runs from a shared link. The initial example trace was generated by the pinned interpreter and works without loading Python.

The UI limits execution to 128 tokens and 64 assignments and stops the worker after 90 seconds. Positions are zero-based, prefix counts inclusive, and a recognition program must end in a Boolean vector. The last position determines the interpreter result. This is an implementation convention, not a claimed treatment of the empty word in the papers.

## Saved experiments

Published figures 1 and 2 from Huang et al. are included unchanged, with URLs, license attribution, retrieval dates and SHA-256 checksums in `site/data/figure-provenance.json`.

`site/data/evaluations.json` intentionally starts empty. Add real saved evaluations there, or use the browser's local JSON importer. The schema is `site/data/evaluation-schema.json`. Each run records its task, model, positional encoding, seed, checkpoint, evaluation timestamp, source, training-length maximum, and optional maximum length used for model selection. Each point stores `length`, `correct`, and `total`; accuracy is derived from those counts. The only supported metric is whole-sequence accuracy. Missing evaluations must be omitted, never filled with zero. Curves are displayed individually without aggregating seeds or mixing checkpoints. Lines between measured points are visual guides, not additional measurements.

Importing does not upload files or start experiments. Do not include unpublished or private data in the tracked JSON unless intended for the site's audience.

## Checks

Node 20 or newer, no npm install:

```sh
node scripts/check.mjs
node --test tests/*.test.mjs
```

The checks validate local assets and anchors, figure checksums, data integrity, and rejection of missing/inconsistent evaluation counts. To validate the actual interpreter adapter against all 254 nonempty parenthesis strings through length 7:

```sh
python3 -m venv .venv
.venv/bin/pip install antlr4-python3-runtime==4.13.1
# Supply a local checkout of coli-saar/c-rasp at the pinned commit:
.venv/bin/python scripts/check_interpreter.py /path/to/c-rasp
```

## Hosting

For GitHub Pages, push this repository to the chosen destination, choose **Settings → Pages → Source → GitHub Actions**, then manually run **Publish tutorial to Pages**. Deployment is deliberately manual while the article is a working draft. The separate check workflow runs on pushes and pull requests. No hosting has been activated by preparing these files.

For the chair's website, copy the contents of `site/` into the desired directory. Relative asset paths support subdirectory hosting. Serve `.js` with a JavaScript MIME type and `.svg` as `image/svg+xml`. The host's content-security policy must permit the worker and Pyodide's WebAssembly/runtime dependencies; test the playground after deployment. The article and saved figures do not need a backend.

## Attribution and editorial status

This is a working adaptation of the author's tutorial, not a verbatim paper reproduction. Statements about constructions, idealized inference, and empirical accuracy are distinguished. The full paper remains the authority on formal assumptions.

The original paper figures are CC BY 4.0, credited in the page and provenance file. Upstream interpreter rights remain with its authors; the checkout inspected for this edition did not include an explicit license file. No license for the tutorial's original prose or code has been selected here; choose one before wider reuse. No blanket license is asserted over third-party material.

## Five notebook experiments

Visit `notebook.html` to compare five alternative learning interfaces:

1. **Walkthrough:** reveals one assignment at a time and highlights contributing prefix tokens.
2. **Block builder:** drag or click typed operations, select compatible earlier variables, and read vectors directly on each block. Invalid reorderings and removal of dependencies are rejected. Arrow buttons provide a keyboard/touch alternative to dragging.
3. **Repair a rule:** change a comparison and a Boolean connective, using counterexamples to understand the two conditions for balanced parentheses.
4. **Token sandbox:** flip, add, or remove symbols and inspect the changes in counts, the first invalid prefix, and the final decision.
5. **Predict a value:** predict a selected cell, then reveal it with an explanation.

These are comparative prototypes, not a required five-step course. The original text editor remains available. For a novice-facing version, a promising sequence is the walkthrough followed by the block builder; that is a design hypothesis, not a tested learning outcome.

The new page uses a synchronous JavaScript evaluator over a typed block representation. It needs no Python download or network access after the static assets load. Source generation is separate from evaluation, and the page can transfer a program into the original Python-backed text editor. The builder is limited to 24 blocks and 16 nonempty input positions; it is deliberately a subset of C-RASP. Unselected output is an execution sentinel and is never shown as an acceptance decision.

Source layout:

- `site/assets/notebook-model.js`: types, templates, validation, and code generation.
- `site/assets/notebook-engine.js`: pure block evaluation, with checked integer arithmetic.
- `site/assets/notebook.js`: the five interfaces and explanatory feedback.
- `site/assets/notebook.css`: layout for the notebook experiments.

`node --test tests/*.test.mjs` checks types, dependency ordering, and causal counting. For a differential check of complete JavaScript and Python traces, use an environment with `antlr4-python3-runtime==4.13.1` and the pinned interpreter checkout:

```sh
.venv/bin/python scripts/check_notebook_interpreter.py /path/to/c-rasp /path/to/node
```

This checks 443 traces, including deterministic random programs and every nonempty parenthesis input through length 7. It does not prove correctness of arbitrary programs.

## Version 2 (separate remote contribution)

The GitHub update adds `site/version2/`, with a chapter-based tutorial and drag-and-drop program editor. It remains self-contained and is linked from the main page header and footer. It does not replace the integrated main draft. See `site/version2/README.md` for its instructions.


## Typst/Tola manuscript (PDF and HTML)

The alternative authoring setup is in [`tutorial_dual/`](tutorial_dual/README.md).
Edit [`tutorial_dual/manuscript.typ`](tutorial_dual/manuscript.typ) for the shared
article text. Both exports include this single source; [`draft_2.typ`](draft_2.typ)
contains only the PDF layout settings.

With Typst 0.15.1 and Tola 0.8.1 on PATH:

```sh
./build_tutorial.sh both
./build_tutorial.sh check
./build_tutorial.sh preview
node --test tutorial_dual/tests/*.test.mjs
```

The build writes `draft_2.pdf` and `tutorial_dual/public/`; these generated outputs
are not tracked. The existing `site/` tutorial and its Pages deployment remain
unchanged. The Tola version embeds a local copy of the existing notebook after the
Dyck-1 example and uses expandable explanations in HTML and fully visible text in PDF.
17 changes: 17 additions & 0 deletions draft_2.typ
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// PDF entry point. The HTML entry point includes the same manuscript.
#set document(title: "Length Generalization in Transformers")
#set page(paper: "a4", margin: (x: 23mm, y: 22mm), numbering: "1")
#set text(font: "Libertinus Serif", size: 11pt, lang: "en")
#set par(justify: true, leading: 0.65em)
#set heading(numbering: "1.1")
#show heading.where(level: 4): set heading(numbering: none)
#set table(inset: 5pt, stroke: 0.4pt + luma(200))
#show table: it => block(breakable: false, it)
#align(center)[#text(size: 23pt, weight: "bold")[Length Generalization in Transformers]]
#v(1em)
#text(size: 9pt, fill: luma(100))[
Green-tinted boxes contain the explanations that can be expanded in the HTML edition.
]
#outline(title: [Contents], depth: 2)
#pagebreak()
#include "tutorial_dual/manuscript.typ"
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"name": "crasp-explorable-tutorial",
"private": true,
"type": "module",
"scripts": {
"test": "node --test tests/*.test.mjs",
"check": "node scripts/check.mjs",
"preview": "python3 -m http.server 8000 --directory site"
},
"engines": {
"node": ">=20"
}
}
31 changes: 31 additions & 0 deletions scripts/check.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import fs from "node:fs";
import path from "node:path";
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { validateEvaluations } from "../site/assets/lib.js";
const root = new URL("../site/", import.meta.url);
for (const page of ["index.html", "notebook.html", "text-editor.html"]) {
const html = fs.readFileSync(new URL(page, root), "utf8");
const ids = [...html.matchAll(/\bid="([^"]+)"/g)].map((m) => m[1]);
assert.equal(new Set(ids).size, ids.length, "Duplicate HTML IDs");
for (const [, ref] of html.matchAll(/(?:src|href)="([^"]+)"/g)) {
if (ref.startsWith("#")) {
if (ref.length > 1) assert(ids.includes(ref.slice(1)), `Missing anchor ${ref}`);
} else if (!/^[a-z]+:/i.test(ref))
assert(fs.existsSync(new URL(ref, root)), `Missing local asset ${ref}`);
}
}
validateEvaluations(JSON.parse(fs.readFileSync(new URL("data/evaluations.json", root))));
for (const f of JSON.parse(fs.readFileSync(new URL("data/figure-provenance.json", root))))
assert.equal(
createHash("sha256")
.update(fs.readFileSync(new URL(f.file, root)))
.digest("hex"),
f.sha256,
);
const trace = JSON.parse(fs.readFileSync(new URL("data/default-trace.json", root)));
assert.equal(trace.accepted, true);
assert(trace.rows.every((r) => r.values.length === trace.tokens.length));
console.log(
"Local links, anchors, evaluation data, trace shape and original figure checksums pass.",
);
22 changes: 22 additions & 0 deletions scripts/check_interpreter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Run against an explicitly supplied checkout; never downloads or trains anything."""
import itertools, json, pathlib, sys
root = pathlib.Path(__file__).resolve().parents[1]
sys.path[:0] = [str(root / 'site/runtime'), str(pathlib.Path(sys.argv[1]).resolve())]
from bridge import run_trace
source = 'C_open = # "("\nC_close = # ")"\nV = C_open < C_close\nCV = # V\nM = (CV == 0)\nB = (C_open == C_close)\nD = M && B'
def dyck(tokens):
balance = 0
for t in tokens:
balance += 1 if t == '(' else -1
if balance < 0: return False
return balance == 0
for n in range(1, 8):
for tokens in itertools.product('()', repeat=n):
assert run_trace(source, list(tokens))['accepted'] == dyck(tokens)
expected = json.loads((root / 'site/data/default-trace.json').read_text())
assert run_trace(source, list('()(())')) == expected
for bad in ['', 'A = #', 'A = ???']:
try: run_trace(bad, ['a'])
except (ValueError, TypeError): pass
else: raise AssertionError('Invalid program accepted')
print('254 parenthesis inputs match an independent Dyck recognizer; saved trace and malformed-input checks pass.')
Loading
Loading