Repository navigation
Semantic diff examples (illustration, not for merge) - #8909
Draft
cristianoc wants to merge 7 commits into
Draft
cristianoc wants to merge 7 commits into
cristianoc wants to merge 7 commits into
Conversation
This was referenced Oct 19, 2026
cristianoc
added this pull request to stack #8911
October 19, 2026 05:03
rescript
@rescript/belt
@rescript/darwin-arm64
@rescript/linux-arm64
@rescript/linux-x64
@rescript/runtime
@rescript/win32-x64
commit: |
Scaffolding for the examples, each later commit re-applying one change. Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com>
Why:
The signature arity mismatch error explained in a second sentence what
its first sentence already states.
Diagnostic modified (signature arity mismatch):
Example:
module M: {
let f: (int, int) => int
} = {
let f = (x: int) => (y: int) => x + y
}
Before (diagnostic):
We've found a bug for you!
Arity.res:3:5-5:1
1 │ module M: {
2 │ let f: (int, int) => int
3 │ } = {
4 │ let f = (x: int) => (y: int) => x + y
5 │ }
6 │
Signature mismatch:
Modules do not match:
{
let f: int => int => int
}
is not included in
{
let f: (int, int) => int
}
Values do not match:
let f: int => int => int
is not included in
let f: (int, int) => int
The implementation contains a function taking 1 argument where the interface expects one taking 2 arguments.
A function's arity is part of its type: calls are compiled to plain JavaScript calls with exactly that many arguments.
Arity.res:2:3-26: Expected declaration
Arity.res:4:7: Actual declaration
After (diagnostic):
We've found a bug for you!
Arity.res:3:5-5:1
1 │ module M: {
2 │ let f: (int, int) => int
3 │ } = {
4 │ let f = (x: int) => (y: int) => x + y
5 │ }
6 │
Signature mismatch:
Modules do not match:
{
let f: int => int => int
}
is not included in
{
let f: (int, int) => int
}
Values do not match:
let f: int => int => int
is not included in
let f: (int, int) => int
The implementation contains a function taking 1 argument where the interface expects one taking 2 arguments.
Arity.res:2:3-26: Expected declaration
Arity.res:4:7: Actual declaration
Review: Which tests expect the error text? The four
`module_sig_value_arity_mismatch` fixtures in `super_errors` and
`Iface_value_arity_mismatch` in `super_errors_multi`; this commit
updates all five.
Re-applies #8559 by @cristianoc for illustration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com>
Why:
The formatter deleted comments on dict pattern keys, inside empty dict
patterns, in `...t` and `#...t` patterns, and before the first or after
the last row of a run of rows after a spread in a dict literal (#8820).
Format modified (comments in dict and variant spread patterns):
Example:
let f = d =>
switch d {
| dict{"a" /* key */: b} => b
| dict{/* empty */} => 0
| _ => 1
}
let g = x =>
switch x {
| .../* spread */ t => 0
| _ => 1
}
Before (rescript):
let f = d =>
switch d {
| dict{"a": b} => b
| dict{} => 0
| _ => 1
}
let g = x =>
switch x {
| ...t => 0
| _ => 1
}
After (rescript):
let f = d =>
switch d {
| dict{"a" /* key */: b} => b
| dict{/* empty */} => 0
| _ => 1
}
let g = x =>
switch x {
| .../* spread */ t => 0
| _ => 1
}
Review: Does any comment position in these shapes still lose its comment?
#8850 reports a check that places a block and a line comment at every
token boundary of 15 dict pattern, variant spread pattern and dict
literal shapes: 53 cases lost the comment before and none after, and
12 of the 100 cases unstable before (output reparsing to a different
tree or changing when formatted again) are stable after.
Re-applies #8850 by @cknitt for illustration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com>
Why:
A match on consecutive integers ending at `2147483647` compiled its
upper bound as an exclusive comparison against `2147483648`, which
wraps to `-2147483648` as a Lambda constant, so matching values fell
through to the next case (#8716).
JS modified (integer patterns ending at the int32 maximum):
Example:
let top = x =>
switch x {
| 2147483645 | 2147483646 | 2147483647 => true
| _ => false
}
Before (lambda):
(let
(top/1001 =
(function x/1002
(catch
(catch
(if (>=[int] x/1002 2147483645)
(if (>=[int] x/1002 -2147483648) (exit 2) (exit 1)) (exit 2))
with (2) false)
with (1) true)))
(makeblock module/exports top/1001))
After (lambda):
(let
(top/1001 =
(function x/1002
(catch
(catch (if (>=[int] x/1002 2147483645) (exit 1) (exit 2)) with (2)
false)
with (1) true)))
(makeblock module/exports top/1001))
Before (js):
// Generated by ReScript, PLEASE EDIT WITH CARE
'use strict';
function top(x) {
if (x >= 2147483645) {
return x < -2147483648;
} else {
return false;
}
}
exports.top = top;
/* No side effect */
After (js):
// Generated by ReScript, PLEASE EDIT WITH CARE
'use strict';
function top(x) {
return x >= 2147483645;
}
exports.top = top;
/* No side effect */
Review: Which other patterns does the int32 normalization affect? Typed
integer constants are reduced to their int32 value before grouping, so
wrapped aliases such as `4294967295` and `-1` are one value: a
redundant one is reported as unused, and exhaustiveness diagnostics
can change, as the updated `intoverflow` fixture shows.
Re-applies #8717 by @fhammerschmidt for illustration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com>
Why:
A match testing an optional field both for a value and for presence
crashed the compiler or dropped a case (#8810), because the match
compiler rewrote the field pattern `Some(p)` to `p` row by row, so one
column mixed constants with `None`.
Matching modified (optional field column):
Example:
type r = {x?: int, y?: int}
let f = (d: r) =>
switch d {
| {x: 1} => 0
| {y: 2} => 0
| {x: ?None} => 1
| _ => 2
}
Before (matrix):
1 _
_ 2
None _
_ _
columns: the fields x and y
split: the constant 1 heads the group, so the None row joins the variable rows below it and reaches an assertion
After (matrix):
Some(1) _
_ Some(2)
None _
_ _
columns: the fields x and y
split: every head of the x column is a constructor of option
Diagnostic removed (assertion failure in the match compiler):
Before (diagnostic): compiling the program above
Fatal error: exception File "compiler/ml/matching.ml", line 1236, characters 9-15: Assertion failed
JS modified (value and presence tests on one optional field):
Example:
type r = {x?: int}
let f = (d: r) =>
switch d {
| {x: ?None} => 0
| {x: 1} => 1
| {x: ?Some(_)} => 2
}
Before (js): `f({x: 1})` returns 2
// Generated by ReScript, PLEASE EDIT WITH CARE
'use strict';
function f(d) {
if (d.x !== undefined) {
return 2;
} else {
return 0;
}
}
exports.f = f;
/* No side effect */
After (js): `f({x: 1})` returns 1
// Generated by ReScript, PLEASE EDIT WITH CARE
'use strict';
function f(d) {
let match = d.x;
if (match !== undefined) {
if (match !== 1) {
return 2;
} else {
return 1;
}
} else {
return 0;
}
}
exports.f = f;
/* No side effect */
Review: Does matching through `Some` cost a nested-option check now?
Only where a payload pattern needs it: a `Some` payload is read as is
when every payload pattern of the division rejects the object that
represents `Some(None)`, and a presence test is dropped when the value
test already sends `undefined` to the absent case.
Re-applies #8842 by @cknitt for illustration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com>
Why:
A character range pattern is expanded into one or-pattern alternative
per character, and typing, the exhaustiveness and unused-case checks,
default matrices and switch compilation each took time quadratic in
the number of alternatives: a range over the CJK block ran out of
memory (#8866).
Matching modified (character range patterns):
Example:
let f = c =>
switch c {
| '\u{100}' .. '\u{1100}' => 1
| _ => 0
}
Before (lambda):
(let
(f/1001 =
(function c/1002
(catch
(catch
(if (|| (>[int] c/1002 4352) (<[int] c/1002 256)) (exit 2)
(exit 1))
with (2) 0)
with (1) 1)))
(makeblock module/exports f/1001))
After (lambda):
(let
(f/1001 =
(function c/1002
(catch
(catch
(if (|| (>[int] c/1002 4352) (<[int] c/1002 256)) (exit 2)
(exit 1))
with (2) 0)
with (1) 1)))
(makeblock module/exports f/1001))
Before (text): `/usr/bin/time -l bsc` on the example, three runs
2.22 real 2.03 user 0.18 sys
1273511936 maximum resident set size
2.20 real 2.02 user 0.16 sys
1271463936 maximum resident set size
2.21 real 2.03 user 0.17 sys
1273659392 maximum resident set size
After (text): `/usr/bin/time -l bsc` on the example, three runs
0.02 real 0.02 user 0.00 sys
25395200 maximum resident set size
0.02 real 0.02 user 0.00 sys
25624576 maximum resident set size
0.03 real 0.02 user 0.00 sys
25591808 maximum resident set size
Review: Where did the time go? Typing copied the right subtree of each
nested or-pattern to rename variables even when there are none; the
checks searched the groups linearly for each row and compared all
pairs of rows for the default matrix minima; switch compilation found
each key's cell with `List.find`. Groups, minima and int, char and
string cells are now found by hashing, and the copy is skipped when
nothing is renamed.
Review: Does the generated code change? No: #8868 reports byte-identical
JavaScript and warnings across `tests/tests/src`, the `super_errors`
fixtures and the runtime.
Re-applies #8868 by @cknitt for illustration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com>
Why:
`%raw`, `%ffi` and `%%raw` were extensions whose string payload the
compiler recognized by name, with a constant `Pconst_raw_source` used
only by them; #8624 moves extension-keyed language features to
parsetree nodes. Through a PPX, a template payload lost its template
marker, so its escapes were not decoded.
AST modified (raw extensions):
Example:
let f = %raw(`(a) => a`)
%%raw("var x = 1")
Before (parse):
[
structure_item
Pstr_value Nonrec
[
<def>
pattern
Ppat_var "f"
expression
Pexp_extension "raw"
[
structure_item
Pstr_eval
expression
Pexp_template
source_segment "(a) => a"
]
]
structure_item
Pstr_extension "raw"
[
structure_item
Pstr_eval
expression
Pexp_constant PConst_raw_source "var x = 1"
]
]
After (parse):
[
structure_item
Pstr_value Nonrec
[
<def>
pattern
Ppat_var "f"
expression
Pexp_raw "raw"
Raw_template "(a) => a"
]
structure_item
Pstr_raw
Raw_string "var x = 1"
]
Before (expanded):
[
structure_item
Pstr_value Nonrec
[
<def>
pattern
Ppat_var "f"
expression
attribute "internal.expansive"
[]
Pexp_letmodule "J"
module_expr
Pmod_structure
[
structure_item
Pstr_primitive
value_description "unsafe_expr"
core_type
Ptyp_arrow
arity = 1
Nolabel
core_type
Ptyp_any
core_type
Ptyp_any
"#raw_expr"
]
expression
Pexp_apply
expression
Pexp_ident "J.unsafe_expr"
[
<arg>
Nolabel
expression
Pexp_constant PConst_raw_source "(a) => a"
]
transformed_jsx: false
]
structure_item
Pstr_eval
expression
Pexp_letmodule "J"
module_expr
Pmod_structure
[
structure_item
Pstr_primitive
value_description "unsafe_expr"
core_type
Ptyp_arrow
arity = 1
Nolabel
core_type
Ptyp_any
core_type
Ptyp_any
"#raw_stmt"
]
expression
Pexp_apply
expression
Pexp_ident "J.unsafe_expr"
[
<arg>
Nolabel
expression
Pexp_constant PConst_raw_source "var x = 1"
]
transformed_jsx: false
]
After (expanded):
[
structure_item
Pstr_value Nonrec
[
<def>
pattern
Ppat_var "f"
expression
attribute "internal.expansive"
[]
Pexp_letmodule "J"
module_expr
Pmod_structure
[
structure_item
Pstr_primitive
value_description "unsafe_expr"
core_type
Ptyp_arrow
arity = 1
Nolabel
core_type
Ptyp_any
core_type
Ptyp_any
"#raw_expr"
]
expression
Pexp_apply
expression
Pexp_ident "J.unsafe_expr"
[
<arg>
Nolabel
expression
Pexp_constant PConst_string (source="(a) => a", semantic="(a) => a")
]
transformed_jsx: false
]
structure_item
Pstr_eval
expression
Pexp_letmodule "J"
module_expr
Pmod_structure
[
structure_item
Pstr_primitive
value_description "unsafe_expr"
core_type
Ptyp_arrow
arity = 1
Nolabel
core_type
Ptyp_any
core_type
Ptyp_any
"#raw_stmt"
]
expression
Pexp_apply
expression
Pexp_ident "J.unsafe_expr"
[
<arg>
Nolabel
expression
Pexp_constant PConst_string (source="var x = 1", semantic="var x = 1")
]
transformed_jsx: false
]
Review: What does a PPX see? The `%raw`, `%ffi` and `%%raw` extensions
with the same v0 encoding as before; `Ast_mapper_from0` reads them back
into the nodes. The `.cmi`, `.cmt` and AST magic numbers are bumped.
Review: Which payloads stay extensions? Braced or attributed payloads and
non-strings, which the nodes cannot represent; they are processed as
before.
Re-applies #8879 by @cknitt for illustration.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Cristiano Calcagno <cristianoc@users.noreply.github.com>
cristianoc
force-pushed
the
cristianoc/semantic-diff-examples
branch
from
October 19, 2026 14:40
322b3b6 to
a86f7a3
Compare
cristianoc
force-pushed
the
cristianoc/commit-records-v2
branch
from
October 19, 2026 14:40
56c69ba to
0308018
Compare
cristianoc
removed this pull request from stack #8911
October 20, 2026 04:44
cristianoc
changed the base branch from
cristianoc/commit-records-v2
to
cristianoc/semantic-diff-examples-base
October 20, 2026 04:44
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A standing illustration of semantic diffs. It stays open as a reference: please don't merge or close it.
Semantic diff (#8908) is a generic mechanism with three parameters: the notion of semantic object, the format each object is written in, and the way each is rendered. This PR shows the ReScript compiler's instance on six real, already-merged changes. Each commit message states its change as the semantic objects it changes, and the Semantic Diff link on this PR draws them.
Use it in your project
The mechanism to adapt is #8908; this PR is an example of it on real changes. #8908 provides:
.claude/skills/records/SKILL.md: the skill that tells an agent how to write a commit's change as semantic objects, Before and After;scripts/recordRender.js: parsing a commit message and drawing each object;scripts/commitRecords.js: the page for a range of commits, run bymake semantic-diffand by.github/workflows/semantic_diff.yml, which links the page on every pull request.To adapt it, choose your project's semantic objects, how each is written in a commit message, and how each is drawn. Point your coding agent at #8908 as the implementation and at this PR as the example. For instance:
The six examples
This PR re-applies six merged changes. Each commit message is written in the commit-record format from #8908, so the Semantic Diff link on this PR shows what the format looks like for each kind of change. The first commit reverts the six changes, and each following commit re-applies one of them unchanged, so the final tree equals the base's. The base,
cristianoc/semantic-diff-examples-base, holds #8908's commit as it was when the examples were built, so this PR's diff is exactly the seven example commits.%raw,%ffiand%%rawwith parsetree nodesEach Before and After is the compiler's output at the commit's parent and at the commit, copied into the message. Each message credits the original PR and author.
Screenshots
Each card of this PR's Semantic Diff page, drawn from the commit's message.
Diagnostic: shorten the arity mismatch error message (#8559)
Format: keep comments in dict and variant spread patterns (#8850)
JS: integer range patterns at int32 boundaries (#8717)
Matching: optional fields as options (#8842)
Matching: character range patterns in linear time (#8868)
AST:
%raw,%ffiand%%rawas parsetree nodes (#8879)To reproduce the page locally:
🤖 Generated with Claude Code