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
34 changes: 32 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,37 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [0.5.0] - 2026-08-06

### Added

- Generated AST layer. A grammar can now be given a `.fltkast` sidecar and generate typed AST
node classes beside its CST: plain owned data (dataclasses on Python, structs and enums on
Rust), one type per rule, with converters in both directions (`from_cst` / `to_cst`) and
one-call entry points (`parse`/`unparse` on Python, `parse_str`/`unparse_str` on Rust) that
carry text in and out. Spans never take part in equality, so values converted from identical
text at different offsets compare equal. The sidecar shapes the result: `transparent;` erases a
rule to its payload, `flatten;` splices a wrapper's fields into its parents, `type:` coerces a
terminal to a scalar (integers, floats, `uuid`, `decimal`, or a `custom(...)` type of your own),
`key:` turns a collection into an insertion-ordered map, `fold_left:`/`fold_right:` folds a
repetition into a binary chain, and `name:`/`field`/`variant` rename anything generated.
Details will land with the AST documentation.

### Changed

- The formatter's labeled-literal trial matching is now text-aware. A CST child whose text one
spelling of a label cannot produce is declined by that spelling instead of being rendered
through it, so trees that previously rendered *wrongly* now render the branch that matches, or
fail loudly.
- Unparser generation now rejects an always-present labeled literal with more than one spelling
under one label: the unparser cannot know which spelling a value came from. The error message
names the rule, the label and the spellings, and is the migration path.
- AST generation rejects two indistinguishable branches of one alternation (values of the two
cannot be told apart at runtime, so one branch would render every one of them) and a capture
group named more than once in the pattern that rebuilds a terminal-only rule's text. Both were
previously silent corruption or a panic at the first serialize.
- Python generated converters raise `AstError` on a child of the wrong kind, where they
previously failed incidentally further down.

## [0.4.0] - 2026-08-03

Expand Down Expand Up @@ -263,4 +293,4 @@ cleanups and modernization.
[0.2.0]: https://github.com/rnortman/fltk/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/rnortman/fltk/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/rnortman/fltk/releases/tag/v0.1.0
[0.0.1]: https://github.com/rnortman/fltk/releases/tag/v0.0.1
[0.0.1]: https://github.com/rnortman/fltk/releases/tag/v0.0.1
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ The exact toolchain version is pinned in `rust-toolchain.toml` (rustup installs

**Lockfile workflow (single writer per file).** `uv.lock` and `requirements_lock.txt` (the Bazel pip lockfile) both have exactly one writer: `make regen-locks` (`uv lock` + the pinned `uv export`). `make check-locks` regenerates that pair and then diffs it together with every tracked `Cargo.lock`, failing on any difference. It does not regenerate a `Cargo.lock` itself: in full `make check` order the maturin builds under `test` rewrite a stale one in place and the `check-locks` diff catches that rewrite, while a cargo step run on its own fails immediately on `--locked` — the only gate for `crates/fltkfmt/Cargo.lock`, which no maturin target builds. So `make check-locks` alone is not sufficient to clear Cargo.lock drift after editing a `Cargo.toml`; run `make check`. A Dependabot `uv` PR bumps `pyproject.toml`/`uv.lock` only and fails `check-locks` by construction; complete it by running `make regen-locks` on the branch and pushing the regenerated `requirements_lock.txt`. Never hand-edit these files or add a second generator (a pip updater, a Bazel `lock` rule) — its edits would be silently clobbered on the next regeneration.

The two `MODULE.bazel.lock` files (repo root and `tests/bazel_consumer/`) are tracked, Bazel-written locks, and bzlmod's default `lockfile_mode` is `update`: a Bazel run repairs a stale one in place and still reports green, so a manifest edit can leave the committed lock wrong indefinitely. `make check-bazel-locks` is the diff that forces the repair to be committed, and it runs last in `CHECK_STEPS` — after `bazel-check` and `bazel-consumer-check`, which are the lanes that do the repairing. Same caveat as above: the diff step alone passes vacuously with no prior Bazel run, so `make check` is the gate that clears them.

**`CARGO_BAZEL_REPIN=1` after editing a `Cargo.toml`.** With an already-populated Bazel output base, a Bazel lane can fail with `no such target '@fltk_crates//:<crate>'` — the crate_universe repos were resolved from the previous manifests. One run with `CARGO_BAZEL_REPIN=1` (e.g. `CARGO_BAZEL_REPIN=1 bazel test //...`) re-resolves them and clears it. A fresh cache, including CI, never sees this.

**Version pins for uv and Bazel.** uv is pinned exactly via `[tool.uv] required-version` in `pyproject.toml` (CI installs the same version through setup-uv's `version-file`); every `uv` invocation self-enforces it, so a mismatch is a loud error, not silent drift. Bazel is pinned via `.bazelversion` (bazelisk honors it in CI). Neither is Dependabot-managed; bump each manually — edit the pin, run `make check`, commit.

**Build and test workflow**:
Expand Down
Loading