From 74c2db9e3aa9307ae2f23f091406a32e6af2bc67 Mon Sep 17 00:00:00 2001 From: Kinflou Date: Mon, 19 Oct 2026 00:03:18 +0800 Subject: [PATCH] docs: generator support, as conformance proves it A Reference page with the feature matrix, copied from generation's FEATURES.md by scripts/sync-generator-support.py, with issue references linked. A drift workflow runs the script in check mode, per PR and weekly. The CLI reference gains `targets --features [--json]`, and the validator guide links the page from its TypeScript note. --- .github/workflows/generator-support-drift.yml | 50 ++++++++++ docs/docs/guide/idl/validator.md | 4 +- docs/docs/reference/cli.md | 2 +- docs/docs/reference/generator-support.md | 75 +++++++++++++++ docs/mkdocs.yml | 1 + scripts/sync-generator-support.py | 96 +++++++++++++++++++ 6 files changed, 226 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/generator-support-drift.yml create mode 100644 docs/docs/reference/generator-support.md create mode 100644 scripts/sync-generator-support.py diff --git a/.github/workflows/generator-support-drift.yml b/.github/workflows/generator-support-drift.yml new file mode 100644 index 0000000..5f1bdf6 --- /dev/null +++ b/.github/workflows/generator-support-drift.yml @@ -0,0 +1,50 @@ +name: Generator support drift + +# `reference/generator-support.md` carries a copy of generation's +# `FEATURES.md`: the matrix its conformance suite proves. This checks the copy +# is current. Like the IDL drift check it needs no Rust build, only a fetch, +# so it runs per pull request. It's kept separate from the Docs workflow, +# because a fetch from another repo shouldn't be able to block a deploy. + +on: + schedule: + # A generator gaining a feature never touches this repo, so a + # push-triggered run would not see it. Weekly catches that side. + - cron: '45 7 * * 1' + pull_request: + paths: + - 'docs/docs/reference/generator-support.md' + - 'scripts/sync-generator-support.py' + - '.github/workflows/generator-support-drift.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: generator-support-drift-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - uses: actions/setup-python@v7 + with: + python-version: "3.14" + + - name: Check for drift + shell: bash + run: | + if ! python3 scripts/sync-generator-support.py --check | tee drift.txt; then + { + echo '### Generator support drift' + echo + echo '```' + cat drift.txt + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi diff --git a/docs/docs/guide/idl/validator.md b/docs/docs/guide/idl/validator.md index 31bfc08..c6a4cd0 100644 --- a/docs/docs/guide/idl/validator.md +++ b/docs/docs/guide/idl/validator.md @@ -5,7 +5,9 @@ put on a value of a type it doesn't apply to, a keyword argument that isn't a property or whose literal type is wrong, a missing argument for a property without a default, or an undeclared `params.*` reference all fail the build. Generated **Rust** code then - [runs them](#in-generated-code). Generated TypeScript doesn't yet. + [runs them](#in-generated-code). Generated TypeScript doesn't yet: it + generates the types without the checks, and `comline generate` warns. See + [Generator support](../../reference/generator-support.md). A **validator** is a named, parameterised check you attach to a [struct](structure.md) or [error](error.md) field, or to a diff --git a/docs/docs/reference/cli.md b/docs/docs/reference/cli.md index c191824..4eff182 100644 --- a/docs/docs/reference/cli.md +++ b/docs/docs/reference/cli.md @@ -40,7 +40,7 @@ parse → resolve imports → validate → (freeze into .comline/) → (generate | `comline clean [--dry-run]` | Remove generated code (the `generate` output root or files). Never touches `.comline/`. | | `comline reset [--force] [--dry-run]` | **Destructive.** Delete `.comline/` — the whole version history, no undo — *and* generated code. Next `build` starts over at `0.0.1`. On a terminal it shows how many versions will be lost and makes you type `reset`; without a terminal it refuses (exit `2`) unless `--force`. | | `comline completions ` | Print a completion script to stdout. `bash` `zsh` `fish` `powershell` `elvish`. | -| `comline targets` | Print one `language#version` per line to stdout — the token syntax `config.idp`'s `code_generation.languages` expects — for every generator this binary was compiled with. Ignores `--path`: it reflects the build, not a project. | +| `comline targets [--features [--json]]` | Print one `language#version` per line to stdout — the token syntax `config.idp`'s `code_generation.languages` expects — for every generator this binary was compiled with. Ignores `--path`: it reflects the build, not a project. `--features` also lists what each generator supports, feature by feature ([Generator support](generator-support.md)); `--json` prints that as JSON. | `--out` / `--layout` / `--mode` override one target and need `--target` when several are configured. `COMLINE_GENERATE_OUT` / `_LAYOUT` / `_MODE` do the same diff --git a/docs/docs/reference/generator-support.md b/docs/docs/reference/generator-support.md new file mode 100644 index 0000000..f8a8552 --- /dev/null +++ b/docs/docs/reference/generator-support.md @@ -0,0 +1,75 @@ +# Generator support + +What each code generator implements, feature by feature. The table below is +the one the generators' conformance suite produces, so every entry in it has +been checked, not just claimed. + +## What the states mean + +| State | Generating a schema that uses the feature | +|---|---| +| ✅ **supported** | Works. | +| 🟡 **partial** | Works, with the limit the note states. | +| ⚪ **ignored** | Succeeds, but the output is what it would be without the feature: the types are right, the feature has no effect. `comline generate` warns, and so does the editor. | +| ❌ **unsupported** | Is refused, with an error naming every use. The editor warns at each use. | + +## How a claim is proven + +Each generator states its support in a `SUPPORT` table in its own crate. The +conformance suite in +[`ComlineProject/generation`](https://github.com/ComlineProject/generation) +runs every generator over a corpus of schemas and checks each claim against +what the generator actually does: + +- **supported** or **partial**: a schema in the corpus uses the feature, + generates, and matches its checked-in output. Where the feature can be taken + out of the schema, the output must change without it; otherwise the feature + is really being ignored. +- **ignored**: the output is identical with the feature taken out, apart from + the IR hash, which names the schema. +- **unsupported**: every schema using the feature is refused, with an error + naming the feature. + +A claim that no schema in the corpus can back fails the suite. So a new +feature, or a generator that gains one, comes with a schema that proves it. +The proving schemas are named in each cell below. + +## The matrix + + + + +| Feature | rust 1.70.0 | typescript 5.0 | +|---|---|---| +| structs
`structs` | ✅ supported
proven by `primitives`, `optional_and_arrays`, `type_refs` +8 more | ✅ supported
proven by `primitives`, `optional_and_arrays`, `type_refs` +10 more | +| enums
`enums` | ✅ supported
proven by `enum_basic` | ✅ supported
proven by `enum_basic` | +| optional fields
`optional-fields` | ✅ supported
proven by `optional_and_arrays`, `type_refs`, `shapes` +3 more | ✅ supported
proven by `optional_and_arrays`, `type_refs`, `shapes` +4 more | +| arrays
`arrays` | ✅ supported
proven by `optional_and_arrays`, `type_refs`, `shapes` +3 more | ✅ supported
proven by `optional_and_arrays`, `type_refs`, `shapes` +4 more | +| fixed-length arrays
`fixed-arrays` | ✅ supported
proven by `shapes` | 🟡 partial: rendered as `T[]`: the length isn't part of the type
proven by `shapes`, `large_fixed_arrays_nested` | +| fixed arrays over 32 elements, nested
`large-fixed-arrays-nested` | ❌ unsupported: a fixed array over 32 elements renders only as a struct or error field's own, non-optional type (serde's derive stops at 32)
proven by `large_fixed_arrays_nested` | 🟡 partial: rendered as `T[]`: the length isn't part of the type
proven by `large_fixed_arrays_nested` | +| unions
`unions` | ❌ unsupported: Rust has no anonymous sum type to render one as
proven by `unions` | ✅ supported
proven by `unions` | +| type aliases
`type-aliases` | ✅ supported
proven by `validators`, `validators_on_decode` | ✅ supported
proven by `validators`, `validators_on_decode`, `unions` | +| constants
`constants` | ✅ supported
proven by `constant`, `shapes`, `validators` +1 more | ✅ supported
proven by `constant`, `shapes`, `validators` +1 more | +| protocols
`protocols` | ✅ supported
proven by `protocol`, `error_shapes`, `validators` +2 more | ✅ supported
proven by `protocol`, `error_shapes`, `validators` +2 more | +| errors
`errors` | ✅ supported
proven by `protocol`, `error_shapes` | ✅ supported
proven by `protocol`, `error_shapes` | +| functions that throw
`throws` | ✅ supported
proven by `protocol`, `error_shapes` | ✅ supported
proven by `protocol`, `error_shapes` | +| validators
`validators` | ✅ supported
proven by `validators`, `validators_on_decode` | ⚪ ignored: the types are generated; the checks aren't run yet ([core#96](https://github.com/ComlineProject/core/issues/96))
proven by `validators`, `validators_on_decode` | +| imports between schemas
`imports` | ✅ supported
proven by `cross_schema`, `import_alias`, `import_glob` +4 more | ✅ supported
proven by `cross_schema`, `import_alias`, `import_glob` +4 more | +| nested namespaces
`nested-namespaces` | ✅ supported
proven by `nested_namespace`, `external_std`, `validators` +1 more | ✅ supported
proven by `nested_namespace`, `external_std`, `validators` +1 more | +| `lib` mode
`lib-mode` | ✅ supported
proven by `nested_namespace`, `external_std` | ✅ supported
proven by `nested_namespace`, `external_std` | +| `external_std`
`external-std` | ✅ supported
proven by `external_std` | ✅ supported
proven by `external_std` | +| `validate_on_decode`
`validate-on-decode` | ✅ supported
proven by `validators_on_decode` | ⚪ ignored: no checks are generated to run on decode yet ([core#96](https://github.com/ComlineProject/core/issues/96))
proven by `validators_on_decode` | +| JSON-RPC framing
`framing-jsonrpc` | ✅ supported
proven by `framing_jsonrpc` | ✅ supported
proven by `framing_jsonrpc` | + + + +## Elsewhere + +- `comline targets --features` prints this for the generators your `comline` + was built with, and `--json` prints it as JSON (see the [CLI](cli.md)). +- In the editor, a schema using a feature a language declared in `config.idp` + can't generate gets a warning at the use. Hovering the language in + `config.idp` shows what its generator supports. +- Each generator's repository README carries its own column: + [comline-rust](https://github.com/ComlineProject/comline-rust#features), + [comline-typescript](https://github.com/ComlineProject/comline-typescript#features). diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 2ed9a73..7d77b2e 100755 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -64,6 +64,7 @@ nav: - Tools: - Tools: 'reference/tools/index.md' - CLI: 'reference/cli.md' + - Generator support: 'reference/generator-support.md' - Design: diff --git a/scripts/sync-generator-support.py b/scripts/sync-generator-support.py new file mode 100644 index 0000000..a2ef6eb --- /dev/null +++ b/scripts/sync-generator-support.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Keep the generator support page in step with what conformance proved. + +`ComlineProject/generation`'s conformance suite checks every generator's +feature claims against its corpus and writes the result to `FEATURES.md` at +the repository root. This copies that file's matrix into +`docs/docs/reference/generator-support.md`, between its +`` / `` markers, so the page says +exactly what was proven. The issue references in it (`core#96`) become links. + + python3 scripts/sync-generator-support.py # rewrite the page + python3 scripts/sync-generator-support.py --check # exit 1 on drift + python3 scripts/sync-generator-support.py --file ../generation/FEATURES.md + +Stdlib only: no Rust toolchain needed, so CI runs it per pull request. +""" + +from __future__ import annotations + +import argparse +import re +import sys +import urllib.error +import urllib.request +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +PAGE = ROOT / "docs/docs/reference/generator-support.md" +RAW = "https://raw.githubusercontent.com/ComlineProject/generation/{rev}/FEATURES.md" +START = "" +END = "" + +# Repositories an issue reference may name; anything else is left as text +# (`rust#1.70.0` is a target, not an issue). +REPOS = ("core", "generation", "cli", "comline-rust", "comline-typescript", "language-server", "runtime", "docs") +ISSUE_RE = re.compile(r"\b(" + "|".join(re.escape(r) for r in REPOS) + r")#(\d+)\b(?![\d.])") + + +def read_features(file: Path | None, rev: str) -> str: + if file: + return file.read_text(encoding="utf-8") + url = RAW.format(rev=rev) + try: + with urllib.request.urlopen(url, timeout=30) as response: + return response.read().decode("utf-8") + except urllib.error.HTTPError as error: + sys.exit(f"error: fetching {url} failed ({error.code} {error.reason})") + except urllib.error.URLError as error: + sys.exit(f"error: fetching {url} failed ({error.reason})") + + +def section(features: str) -> str: + """The marked section the page should hold: the matrix, issues linked.""" + table = [line for line in features.splitlines() if line.startswith("|")] + if not table: + sys.exit("error: FEATURES.md has no table") + linked = "\n".join( + ISSUE_RE.sub(lambda m: f"[{m[0]}](https://github.com/ComlineProject/{m[1]}/issues/{m[2]})", line) + for line in table + ) + return ( + f"{START}\n" + "\n\n" + f"{linked}\n\n{END}" + ) + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--check", action="store_true", help="exit 1 if the page is out of date") + parser.add_argument("--file", type=Path, help="read FEATURES.md from here instead of fetching it") + parser.add_argument("--rev", default="master", help="generation revision to fetch (default: master)") + args = parser.parse_args() + + page = PAGE.read_text(encoding="utf-8") + start, end = page.find(START), page.find(END) + if start < 0 or end < 0: + sys.exit(f"error: {PAGE.relative_to(ROOT)} has no {START} ... {END} section") + current = page[start : end + len(END)] + wanted = section(read_features(args.file, args.rev)) + + if current == wanted: + print("generator support page is up to date") + return 0 + if args.check: + print(f"{PAGE.relative_to(ROOT)} is out of date with generation's FEATURES.md.") + print("Run: python3 scripts/sync-generator-support.py") + return 1 + PAGE.write_text(page[:start] + wanted + page[end + len(END) :], encoding="utf-8") + print(f"updated {PAGE.relative_to(ROOT)}") + return 0 + + +if __name__ == "__main__": + sys.exit(main())