Skip to content

Draw commit records as semantic diffs of the compiler's objects - #8882

Open
cristianoc wants to merge 1 commit into
masterfrom
cristianoc/record-object-kinds
Open

cristianoc wants to merge 1 commit into
masterfrom
cristianoc/record-object-kinds

Conversation

@cristianoc

Copy link
Copy Markdown
Collaborator

Commit records name the objects a compiler change touches, and the Semantic Diff page draws each of them: a parsetree or typed tree as a tree, a typing rule as an inference rule, the match compiler's clause matrix as a matrix, Lambda and JavaScript as highlighted code. A change that replaces one shape by another is one block on one example, with the parent commit's and this commit's versions side by side on aligned rows.

  • scripts/recordRender.js parses a record and draws it. It is a pure module, with no file system and no git. It also checks the record against its diff: changelog fragments, the required trailers, Tests against the test files in both directions, generated JavaScript against Output, magic numbers, the v0 PPX bridge, super_errors fixtures and ERROR_VARIANTS.md.
  • scripts/recordFacts.js extracts what a record shows from a compiler build, so Before and After come from the compiler rather than being written by hand. That covers the parser's tree, the tree after the built-in PPX, the typed tree, Lambda, the generated JS, formatter output and diagnostics.
  • scripts/commitRecords.js reads git and writes the page. Its options (--out, --pr, --title, --fragment), the Semantic Diff workflow and make semantic-diff work as before.
  • scripts/commitRecordHook.sh is an optional commit-msg hook that refuses a message without the required trailers. .claude/skills/records/SKILL.md explains how to enable it.
  • The skill defines each kind of object, how to extract it, and how to draw it. It keeps the format agreed in Semantic diff: commit records rendered as a page linked on each PR #8870 and Read the semantic diff's changelog from changes/ fragments #8881: changes/ fragments are the changelog, Tests is required (or none, because …), there are no Ran or Depends trailers, and Signed-off-by comes last.

Example. #8879's diff, re-committed on this branch by an agent following the skill, renders as one AST modified block on let f = %raw(`(a) => a`) and %%raw("var x = 1"). The left side is the parent compiler's tree (Pexp_extension "raw" … Pexp_template, Pstr_extension "raw" … PConst_raw_source), and the right side is the new one (Pexp_raw / Raw_template, Pstr_raw / Raw_string). Both come from bsc -bs-no-builtin-ppx -only-parse -dparsetree. The parsetree type excerpts sit side by side in the same way. All checks pass on that card.

Test plan

  • node --test scripts/commitRecords.test.js: 9 passing. The tests cover a full record, added and removed blocks at full width, aligned modified rows, the --pr/--title header with escaping, both directions of the Tests checks (generated snapshots are exempt), two changelog fragments in one commit, and the hook accepting and refusing messages.
  • npx biome check scripts/ and npx tsc: clean.
  • Rendering master's recent commits flags only the squash merges that carry no record.

🤖 Generated with Claude Code

Why:
    A record states its change as prose sections and a JavaScript
    Before/After pair. The objects most compiler changes touch, a typing
    rule, a parsetree node, a match compiler's clause matrix, a Lambda
    rewrite, an error message, have no form of their own in it: the reviewer
    reconstructs them from the code diff of typecore.ml or matching.ml.

Code added (scripts/recordRender.js, the drawing of a record):
    A pure module, with no file system and no git: it parses a record
    (sections, change blocks, git trailers) and draws it as HTML. Each kind of
    object has its drawing: a typing rule as an inference rule, an AST or
    typed tree fragment as a tree, a clause matrix as a matrix, Lambda and
    JavaScript as highlighted code with Before and After side by side, a
    runtime representation as `ReScript ↦ JavaScript` lines, a diagnostic as
    its text. The kinds are a typed table, so a kind without a drawing does
    not type-check. A modified block's two versions of code, a tree or text
    are drawn as aligned rows: a line both have faces itself, the changed
    lines between are padded with gaps, and on a narrow screen the rows read
    as a unified diff. An added or removed block's one version spans the
    block. A long line wraps, its continuation indented past the line's own
    indentation or tree connectors. It checks each record against its diff:
    changelog fragments, the required trailers, the Tests trailer against
    the test files both ways (every listed path is in the diff; every test
    file in the diff other than a generated snapshot is listed, the missing
    ones named), generated JavaScript against Output, magic numbers, the v0
    PPX bridge, super_errors fixtures and tests/ERROR_VARIANTS.md.

Code modified (scripts/commitRecords.js, the Semantic Diff page):
    Reads each commit's record, files, magic-number diff and changes/
    fragments from git and hands them to recordRender.js, which parses,
    checks and draws them. Its options are --out, --pr, --title and
    --fragment.

Code added (scripts/recordFacts.js, the facts a record abstracts):
    Prints, for one example and one compiler build, the parser's tree, the
    tree after the built-in PPX, the JSX transform's output, the
    formatter's output, the typed tree, the inferred interface, Lambda as
    translated and after each pass that changes it, the generated ES module
    and the diagnostic, each as the compiler prints it, under a line naming
    the compiler and the commit its checkout is at. Run with --bsc on a
    build of the parent commit, it gives a modified block's Before.

Code added (scripts/commitRecordHook.sh, a commit-msg hook, not installed):
    Refuses a message without the Area, Output, Tests and Signed-off-by
    trailers, with a trailer after Signed-off-by, or with a malformed Output
    or Tests; for a commit that changes compiler, runtime, build system or
    editor tooling sources, also one without Why: or a change block.

Docs modified (.claude/skills/records/SKILL.md, the record format):
    The skill defines the kinds of object, each with the code that owns
    it, how to extract its exact form, its drawing and what a change of it
    brings; and the trailers, each with the check the card makes. A change
    that replaces one shape by another is one modified block on one
    example, Before extracted from a build of the parent commit and After
    from this commit's, the Source line naming both builds; added and
    removed blocks are for objects with no counterpart.

Before (text):
    <subject>
    <paragraph>
    Example (rescript), Before (js), After (js), Source, Breaking, Review
    Area, Output, Formatting, Formats, PPX, Editor, Errors, Tests,
    Measured, PR, Signed-off-by

After (text):
    <subject>
    Why
    <Kind> <added|modified|removed> (<object>), with Before and After
    extracted from the parent commit's build and this commit's
    Example, Effect, Source, Breaking, Review
    Area, Output, Tests, Measured, PR, Signed-off-by

Command (sh):
    node scripts/recordFacts.js -e 'let f = (c, a, b) => c ? a : b' parse lambda js
    node scripts/commitRecords.js --out review.html --pr 1234 --title "Title" master..my-branch
    ln -sf "$PWD/scripts/commitRecordHook.sh" "$(git rev-parse --git-path hooks)/commit-msg"

Review: Do records in the earlier format still render? Yes: a top-level
    `Before (js):` and `After (js):` pair draws side by side, and trailers
    outside the format (Formats, Errors, ...) draw as facts.

Review: Where do Formatting, Formats, PPX and Editor go? A formatter or
    editor change is a Format or Editor change block; the card reads the
    magic numbers, the v0 bridge fixtures and the error catalogue from the
    diff.

Area: scripts docs tests
Output: unchanged
Tests: ~scripts/commitRecords.test.js
PR: #8882
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com>
@cristianoc
cristianoc force-pushed the cristianoc/record-object-kinds branch from 18bff7c to 6aed21f Compare October 17, 2026 00:43
@cristianoc
cristianoc marked this pull request as ready for review October 17, 2026 00:43
@github-actions

Copy link
Copy Markdown

@pkg-pr-new

pkg-pr-new Bot commented Oct 17, 2026

Copy link
Copy Markdown

Open in StackBlitz

rescript

npm i https://pkg.pr.new/rescript@8882

@rescript/belt

npm i https://pkg.pr.new/@rescript/belt@8882

@rescript/darwin-arm64

npm i https://pkg.pr.new/@rescript/darwin-arm64@8882

@rescript/darwin-x64

npm i https://pkg.pr.new/@rescript/darwin-x64@8882

@rescript/linux-arm64

npm i https://pkg.pr.new/@rescript/linux-arm64@8882

@rescript/linux-x64

npm i https://pkg.pr.new/@rescript/linux-x64@8882

@rescript/runtime

npm i https://pkg.pr.new/@rescript/runtime@8882

@rescript/win32-x64

npm i https://pkg.pr.new/@rescript/win32-x64@8882

commit: 6aed21f

@github-actions

Copy link
Copy Markdown

@cknitt

cknitt commented Oct 17, 2026

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6aed21f3a8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +55 to +56
if git diff --cached --name-only |
grep -Eq '^(compiler|rewatch/src|analysis|tools/src|packages/@rescript/(runtime|belt))/'; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce records for the OCaml build system

Include rewatch-ocaml/ in this source-path predicate. When a commit stages a file such as rewatch-ocaml/build_state.ml, the condition is false, so the new hook accepts the commit without the required Why: section or change block even though this directory contains the OCaml build-system implementation.

AGENTS.md reference: AGENTS.md:L35-L38

Useful? React with 👍 / 👎.

Comment thread scripts/recordRender.js
Comment on lines +1244 to +1246
const missing = listed.filter(
t => !r.files.some(f => f.path.startsWith(t.path)),
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Match listed tests on path boundaries

Require either an exact path match or a / boundary after a listed directory. With the current prefix check, Tests: ~tests/foo is considered present when the diff changes only tests/foobar.res; the reciprocal unlisted-file check uses the same prefix logic, so both checks report success even though the changed test was never listed.

Useful? React with 👍 / 👎.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-17T05:28:27.532135Z 6aed21f Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@cknitt

cknitt commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Proposal: records per PR

Motivation

#8882 makes every commit message a structured record, with Before/After extracted from a build of the parent commit. Trying it on the %raw stack (#8879 → #8876, 10 commits) showed several problems:

  1. Per-commit records describe history nobody keeps. This repository squash-merges, so a PR's commits never reach master. Intermediate commits are often review fixes, and their "Before" is a state that never shipped.
  2. The cost doesn't scale. Each compiler commit needs a build of its parent: 8 builds for that stack, plus a hand-written record per commit, many of them for small follow-up fixes.
  3. Authors restate what the diff already says. Tests, Output and Area are claims the checker then verifies against the diff. CI could compute them directly.
  4. Commit messages get long. That works against short, readable git log messages.
  5. The obvious alternatives don't fit either:
    • Changelog fragments are user-facing release notes, one sentence each, concatenated into CHANGELOG.md. Trees and Lambda don't belong there.
    • The PR description is free-form Markdown that isn't versioned or reviewed as part of the diff, isn't tied to a commit, isn't available locally, and can change, even after merge, without CI noticing. It reaches git history only if the repository's squash default changes from COMMIT_MESSAGES to the PR description.

Proposal

One record per PR, in a file, with Before/After generated by CI.

Per commit

A short message: subject, one to three lines of why, sign-off. No required structure or trailers.

Per PR, written by the author

  • Changelog fragments, unchanged: changes/<name>.<category>.md, one bullet per user-facing change.

  • A record file, changes/<name>.record, holding only what needs judgement:

    • Why: the problem, and the program that fails.
    • Change blocks naming the objects that change (AST, typing rule, JS output, format, diagnostic, …).
    • Examples with the views to extract, e.g. Example (parse, js): followed by the program.
    • Optionally Breaking:, Review: (question and answer) and Measured:.

    It uses the plain-text section format Draw commit records as semantic diffs of the compiler's objects #8882 already parses, but without pasted Before/After forms or computed trailers.

  • The PR description stays free-form for discussion. No checks depend on it.

Per PR, generated by CI

  • Before/After for each example and view, from two builds: the PR's base (master, or the PR below in a stack) and its head. CI already builds the head, and scripts/recordFacts.js does the extraction.
  • Facts from the PR's whole diff:
    • which tests were added, changed or removed;
    • whether committed generated JavaScript changed;
    • magic-number bumps for marshaled types;
    • v0 bridge changes with an ast-mapping fixture or bridge test;
    • fragment validity;
    • the error catalog.
  • The Semantic Diff page, rendered by scripts/recordRender.js from the record file plus the generated forms and facts, and linked on the PR as today.

Rules

  • Record file required when a PR changes compiler, runtime, build system or editor-tooling sources, whatever its changelog label.
  • Checked locally (make semantic-diff) and in CI. Since it's a file in the diff, any change to it is a commit and runs CI.
  • Lifecycle: yarn changelog:release deletes record files together with fragments. A record describes a change, not the current code, so it's transient for the same reason a fragment is. It survives in git history (part of the PR's squash commit) and on the archived page (cdn.rescript-lang.org/semantic-diff/pr-<N>/…).
  • Fragment checker: scripts/changelog.js must accept .record files in changes/, which it currently rejects.

What changes in #8882

  • The record is read from changes/*.record instead of commit messages. The rendering and the extraction are reused as they are.
  • Before/After are generated from declared examples instead of written by authors.
  • The Area, Output and Tests trailers and the commit-msg hook go away. Their checks become computed facts.

Example: #8879

No commit rewriting. One file:

changes/raw-extension-nodes.record

Why:
    %raw, %ffi and %%raw were extensions whose string payload the frontend
    had to recognise; templates lost their decoding through a PPX.

AST modified (raw extensions):
Example (parse):
    let f = %raw(`(a) => a`)
    %%raw("var x = 1")

CI would render the Pexp_extension "raw" → Pexp_raw / Raw_template and Pstr_extension → Pstr_raw trees side by side, as in the trial on #8879.

@cknitt

cknitt commented Oct 17, 2026

Copy link
Copy Markdown
Member

One more thing, separate from the proposal: the ERROR_VARIANTS.md check is too strict.

It fails any commit that adds a super_errors fixture without touching tests/ERROR_VARIANTS.md. But the catalog itself excludes plain Location.raise_errorf messages (see its Scope section), and warnings have no per-warning rows. So a commit adding a fixture for either can't pass in any meaningful way.

For example, #8876 adds fixtures for two raise_errorf errors (a non-literal %raw payload and an optional %raw argument) and for warning 101 (an unused attribute next to %raw). None of them has a row to update.

Could the check apply only to fixtures that exercise a catalogued variant? Alternatively, it could be a hint rather than a failure.

@cknitt

cknitt commented Oct 17, 2026

Copy link
Copy Markdown
Member

A follow-up on the format of the record file: it could be YAML, validated against a JSON Schema, instead of a custom plain-text format.

Why a schema

  • The structure is defined in one standard schema rather than by a hand-written parser, and validated the same way locally and in CI.
  • Editors can check a record as it's written, given a $schema hint.
  • Agents and tools can read and write records without guessing at a text format.
  • Fields become typed, for example an example's views as a list instead of syntax inside a section header.

Why YAML rather than JSON
A record is mostly prose and ReScript code. In JSON both become escaped strings: every newline is \n, and every " and \ needs escaping. ReScript examples are full of those (%raw("\"x\""), `a\`b`), so the file would be unreadable in review diffs. YAML uses the same data model and can be checked against the same JSON Schema, but block scalars (|) keep prose and code verbatim.

#8879's record as YAML

# changes/raw-extension-nodes.record.yaml
why: |
  %raw, %ffi and %%raw were extensions whose string payload the frontend had
  to recognise. Through a PPX, template payloads lost their decoding.
changes:
  - kind: AST
    change: modified
    object: raw extensions
    examples:
      - views: [parse]
        code: |
          let f = %raw(`(a) => a`)
          %%raw("var x = 1")
breaking: none
review:
  - question: Do PPXs see a different tree?
    answer: |
      No. The v0 bridge maps the nodes to the same extensions as before and
      reads them back.

CI would add the generated parts to this: Before/After for each example and view from the base and head builds, plus the facts computed from the diff. scripts/recordRender.js would take the parsed object instead of parsing text.

@cknitt

cknitt commented Oct 17, 2026

Copy link
Copy Markdown
Member

One more idea for later, once the record format has settled: the changelog entries could move into the record file, so a PR has a single changes/<name>.yaml instead of fragments plus a record.

# changes/raw-extension-nodes.yaml
changelog:
  - category: internal
    entry: Represent `%raw`, `%ffi` and `%%raw` with parsetree nodes. https://github.com/rescript-lang/rescript/pull/8879
  - category: fix
    entry: Fix template and braced payloads of `%raw` and `%ffi` compiling differently when a PPX is used. https://github.com/rescript-lang/rescript/pull/8879
why: |
  …
changes:
  - …

yarn changelog:release would read the changelog entries, write them into CHANGELOG.md and delete the files, as it does with fragments now. One file per PR, under one schema, would then hold both the release notes and the record of the change.

For the transition, changelog.js could accept both fragments and YAML files for a release or two, so open PRs don't need rewriting.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants