|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
| 4 | + |
| 5 | +## Project Overview |
| 6 | + |
| 7 | +Hyper is a Python template framework. Write templates in `.hyper` syntax, compile to type-safe Python code. |
| 8 | + |
| 9 | +Monorepo with 3 components: |
| 10 | +- **Rust transpiler** (`rust/transpiler/`) — Compiles `.hyper` → `.py` |
| 11 | +- **Python runtime** (`python/`) — Runtime helpers, CLI, optional content collections |
| 12 | +- **JetBrains plugin** (`editors/jetbrains-plugin/`) — IDE support via language injection |
| 13 | + |
| 14 | +## Build and Test Commands |
| 15 | + |
| 16 | +```bash |
| 17 | +# Transpiler |
| 18 | +just build transpiler # Release build |
| 19 | +just test transpiler # Run all tests (or: cd rust && cargo test) |
| 20 | +just compile <files-or-dirs> # Build + run in one step (debug, suppresses warnings) |
| 21 | +just test-accept # Regenerate all .expected.* files from current output |
| 22 | +just test-accept basic # Regenerate only files matching "basic" |
| 23 | + |
| 24 | +# Plugin |
| 25 | +just build plugin # Build transpiler + bundle binary + build plugin |
| 26 | +just run plugin # Launch sandbox IDE |
| 27 | +just test plugin # Run plugin tests |
| 28 | + |
| 29 | +# Python |
| 30 | +uv sync # Setup workspace |
| 31 | +pytest # Run Python tests |
| 32 | + |
| 33 | +# Generate .py from .hyper (uses release binary) |
| 34 | +just generate <files> |
| 35 | +``` |
| 36 | + |
| 37 | +The `compile` recipe accepts directories — it walks them for `.hyper` files. |
| 38 | + |
| 39 | +## Transpiler Architecture |
| 40 | + |
| 41 | +Three-stage pipeline in `rust/transpiler/src/lib.rs`: |
| 42 | + |
| 43 | +1. **Parser** (`parser/`) — `tokenizer.rs` lexes into tokens, `tree_builder.rs` builds AST. Line-based tokenizer with `after_structural` flag for comment detection, `is_control_flow()` requiring trailing `:` to distinguish Python keywords from content text. |
| 44 | + |
| 45 | +2. **Transformer** (`transform/`) — Visitor-pattern plugins (`HelperDetectionPlugin`, `AsyncDetectionPlugin`, `SlotDetectionPlugin`) analyze the AST and produce `TransformMetadata`. |
| 46 | + |
| 47 | +3. **Generator** (`generate/`) — `python.rs` combines consecutive text/expression/element nodes into single f-strings, emits control flow as separate statements. `injection_analyzer.rs` produces IDE language injection ranges. `output.rs` tracks source positions with UTF-16 mapping. |
| 48 | + |
| 49 | +### Compile-time HTML validation (`html.rs` + `tree_builder.rs`) |
| 50 | + |
| 51 | +The parser validates HTML at parse time: |
| 52 | +- **Void elements** — `<br>`, `<img>`, etc. cannot have children or closing tags |
| 53 | +- **Duplicate attributes** — Same attribute name twice on one element |
| 54 | +- **Invalid nesting** — Block elements inside `<p>`, nested interactive elements (`<a>` inside `<button>`) |
| 55 | + |
| 56 | +Validation uses an `element_stack` in `TreeBuilder` for parent context. |
| 57 | + |
| 58 | +### Error system (`error.rs`) |
| 59 | + |
| 60 | +`ParseError` renders with ANSI colors when stderr is a TTY: |
| 61 | +- `render()` — plain text (for piped output, tests) |
| 62 | +- `render_color()` — colored (red errors, blue line numbers, cyan related spans, yellow help) |
| 63 | +- `Display` impl outputs just the message (no redundant kind prefix) |
| 64 | +- Errors support `related_span` with custom `related_label` and multiline `help` text |
| 65 | + |
| 66 | +## Testing |
| 67 | + |
| 68 | +**Expected output tests** (`rust/transpiler/tests/expected_tests.rs`): |
| 69 | +- Test files: `rust/transpiler/tests/<category>/<name>.hyper` |
| 70 | +- Expected output: `<name>.expected.py` (compiled Python), `<name>.expected.json` (injection ranges/injections), `<name>.expected.err` (error messages) |
| 71 | +- Error tests live in `tests/errors/` and are auto-detected |
| 72 | + |
| 73 | +**Expected output workflow:** |
| 74 | +```bash |
| 75 | +cargo test # Run tests, see failures |
| 76 | +cargo run --bin accept_expected # Regenerate all .expected.* files from current compiler output |
| 77 | +cargo run --bin accept_expected basic # Regenerate only files matching "basic" |
| 78 | +``` |
| 79 | + |
| 80 | +**IMPORTANT — expected output review:** `cargo run --bin accept_expected` blindly stamps the compiler's current output as correct. Never run it after a change without manually reviewing the diffs (`git diff`) to confirm the new output is actually what you expect. A bug in the compiler will silently become the blessed expected output otherwise. When changing parser or codegen logic, always spot-check at least the directly affected `.expected.py` and `.expected.json` files before considering the change done. |
| 81 | + |
| 82 | +**CRITICAL — injection range validation:** Every Python injection range `source[start:end]` must extract to meaningful text from the source file (not mid-word garbage). After accepting expected output, verify that source positions in `.expected.json` files map to the correct source text. The test suite includes semantic validation (range text extraction checks) — if these fail, the ranges are wrong, do NOT blindly accept. Common mistakes: off-by-one in span calculations, stale expected files accepted without review, substring-matching tests that pass accidentally. |
| 83 | + |
| 84 | +**Invariant tests** (`rust/transpiler/tests/invariants/`): |
| 85 | +- Property-based checks that run across ALL `.hyper` test files automatically |
| 86 | +- Each module validates one structural invariant (roundtrip, monotonicity, bounds, html_completeness, etc.) |
| 87 | +- New invariants go in their own module file under `invariants/` |
| 88 | +- Adding a new `.hyper` test file automatically gets invariant coverage with zero extra work |
| 89 | + |
| 90 | +**Kitchen sink smoke test** (`tests/basic/kitchen_sink.hyper`): |
| 91 | +- Exercises every syntax construct in one file (elements, components, slots, control flow, decorators, attributes, expressions, comments) |
| 92 | +- After any injection change, open this file in JetBrains and visually verify highlighting |
| 93 | +- All 8 invariants run against it automatically |
| 94 | + |
| 95 | +## CLI Modes (`main.rs`) |
| 96 | + |
| 97 | +- `hyper generate <files|dirs>` — Compile to `.py` files, walks directories |
| 98 | +- `hyper generate --stdin` — Read from stdin, write to stdout |
| 99 | +- `hyper generate --json` — JSON output with source mappings |
| 100 | +- `hyper generate --daemon` — Length-prefixed JSON protocol for IDE integration |
| 101 | + |
| 102 | +## Gotchas |
| 103 | + |
| 104 | +- Transpiler binary must be rebuilt and re-bundled for plugin changes: `just build` |
| 105 | +- Plugin requires JDK 17+ |
| 106 | +- Expected output files (`.expected.py`, `.expected.json`, `.expected.err`) are managed by `cargo run --bin accept_expected` — never edit them by hand |
| 107 | +- The tokenizer is line-based; multiline Python expressions (paren/bracket spanning lines) are a known limitation |
| 108 | +- `is_control_flow()` uses trailing `:` heuristic — content text that starts with a Python keyword and ends with `:` inside an element is an edge case |
| 109 | + |
| 110 | +## Bug Fix Workflow |
| 111 | + |
| 112 | +When the user reports a bug, visual issue, or incorrect behavior — especially phrases like "not highlighted", "looks wrong", "should be X but is Y", "missing", "broken" — invoke the `/red-green-fix` skill before making any code changes. Never fix a bug without first writing a failing test that captures the expected behavior. |
| 113 | + |
0 commit comments