Semantic Diff

#9149 Keep only the written fields of a record literal in the typed tree

2 commits

b3f2c80522296c2944b16929e7ff9807c55e5e39..pr-9149

ceaaf61f6 · cknitt + Claude · Oct 31, 2026, 07:01 UTC

Keep only the written fields of a record literal in the typed tree

Why

A record literal stored one entry per field of its type: Kept for a field copied from a spread, and a synthesized None expression for every optional field it left out. JSX DOM props have 612 optional fields, so each DOM element added about 610 expressions to the typed tree, ~31 KB to the .cmt, and as many Lambda constants (#9148). With 800 elements, compiling took 0.80 s and 422 MB; now 0.08 s and 50 MB, and the .cmt shrinks from 25 MB to 0.8 MB. Small records lose a third of their .cmt size. The generated JavaScript does not change.

modifiedTypedtree record literals

type r = {a: int, b?: string}let x = {a: 1}
typed
Before
After
Texp_recordTexp_record└─ fields =└─ fields =      ├─ "a"      └─ "a"      │  └─ expression         └─ expression      │     └─ Texp_constant Const_int 1            └─ Texp_constant Const_int 1      └─ "b"         └─ expression            ├─ Texp_construct "None"            └─ []

modifiedAnalysis find references

type labelled = {name: string, label?: string}let withLabel = {name: "a", label: "b"}let withoutLabel = {name: "c"}let _ = withLabel.label
text
Before references of label
After references of label
26:31 declaration26:31 declaration27:28 withLabel's field27:28 withLabel's field28:25 inside {name: "c"}, which has no label 29:18 withLabel.label29:18 withLabel.label

Review

  • Where do the left-out fields come from now? Translcore rebuilds the positional view from lbl_all: a field missing from fields is read from the spread record when there is one, and is undefined otherwise.
  • What else went? The record_label_definition type, the unused type of Kept fields, and the tuple's optional flag, which was always false.
9f7babec6 · cknitt + Claude · Oct 31, 2026, 07:49 UTC

Treat omitted mutable fields as allocated in the value restriction

Codex review: is_nonexpansive now saw only the written fields, so with type cell<'a> = {tag: int, mutable value?: 'a} the binding let cell = {tag: 0} generalized 'a, and cell.value could be written as option<int> and read as option<string>. Without a spread, every label of the record is allocated, so all must be immutable; with a spread, the check is unchanged. Master rejects the example as well; the new super_errors fixture covers it.