Reject a product with no fields instead of answering Unit - #118
Open
dpmills wants to merge 1 commit into
Open
Conversation
Contributor
Author
|
This change is part of the following stack:
Change managed by git-spice. |
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 20, 2026 20:51
4066331 to
6763585
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 20, 2026 23:24
6763585 to
b3faa41
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 21, 2026 04:29
b3faa41 to
5ec1b3c
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 21, 2026 04:39
5ec1b3c to
c18566f
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 21, 2026 04:55
c18566f to
a98df57
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 21, 2026 05:47
a98df57 to
776dd2b
Compare
This was referenced Aug 21, 2026
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 21, 2026 18:41
776dd2b to
6ba5a31
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 21, 2026 19:42
6ba5a31 to
ce09a11
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
2 times, most recently
from
August 21, 2026 22:23
89b22b5 to
9321478
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 22, 2026 00:08
9321478 to
f4d7b30
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 22, 2026 00:22
f4d7b30 to
f4ad716
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 22, 2026 00:35
f4ad716 to
7fb1f3a
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 22, 2026 00:46
7fb1f3a to
9aa1871
Compare
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 22, 2026 01:06
9aa1871 to
9cf6c99
Compare
`materialize_record` answered `Unit` for an empty field map, which is the silent arrival `docs/chl-spec.md`, "6.6 The empty product is unit" exists to rule out. Unit is a *base* type precisely so that a product reaches it only through an operation that says so: an empty field set is a subset of every field set, so a zero-field product would be a type every product flows into with every field dropped and nothing marking the loss.
A positive merge intersects field sets, so two products sharing no field merge to the empty map — and the answer was `Unit`, a type neither operand is a subtype of (`Record([("a", Int)]) <: Unit` is rejected, as it should be). Reachable from source and silent: `(a=1) if c else (b=2)` inferred as `Unit`. It now has no type, which is the honest answer — the only type above two such records would be the product that does not exist.
That rejection is `CoalesceError::IncompatibleBounds`, the error the same conflict one level up already raises: bounds with no common shape. The empty product is that read one level down, so it needs no error of its own. `details` names the empty field set rather than the operands, which the merge has already consumed by the time the position materializes.
Both polarities reject. One site serves both, and the invariant is not polarity-dependent — there is no zero-field product to *require* either.
Nothing in the suite depended on the old behaviour: instrumenting the branch showed no compiled program reaching it, so `materialize_record`'s claim that it was reached by "a record variable that accumulated no field demands" described a case no test exercises.
`type-inference.md`'s coalesce section said "no keys at all become `Unit`" and now says what the position is instead; the `product` constructor's empty-case collapse is unchanged, because a *constructed* empty product is an operation that says so.
`products_sharing_no_field_are_incompatible_bounds` pins the rejection and `products_sharing_a_field_join_on_it` pins its companion — a non-empty intersection still joins fieldwise, so this is about the empty case and not about products at a join.
dpmills
force-pushed
the
dmills/empty-product
branch
from
August 22, 2026 03:43
9cf6c99 to
fb5df09
Compare
dpmills
marked this pull request as ready for review
August 22, 2026 03:50
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.
materialize_recordansweredUnitfor an empty field map, which is the silent arrivaldocs/chl-spec.md, "6.6 The empty product is unit" exists to rule out. Unit is a base type precisely so that a product reaches it only through an operation that says so: an empty field set is a subset of every field set, so a zero-field product would be a type every product flows into with every field dropped and nothing marking the loss.A positive merge intersects field sets, so two products sharing no field merge to the empty map — and the answer was
Unit, a type neither operand is a subtype of (Record([("a", Int)]) <: Unitis rejected, as it should be). Reachable from source and silent:(a=1) if c else (b=2)inferred asUnit. It now has no type, which is the honest answer — the only type above two such records would be the product that does not exist.That rejection is
CoalesceError::IncompatibleBounds, the error the same conflict one level up already raises: bounds with no common shape. The empty product is that read one level down, so it needs no error of its own.detailsnames the empty field set rather than the operands, which the merge has already consumed by the time the position materializes.Both polarities reject. One site serves both, and the invariant is not polarity-dependent — there is no zero-field product to require either.
Nothing in the suite depended on the old behaviour: instrumenting the branch showed no compiled program reaching it, so
materialize_record's claim that it was reached by "a record variable that accumulated no field demands" described a case no test exercises.type-inference.md's coalesce section said "no keys at all becomeUnit" and now says what the position is instead; theproductconstructor's empty-case collapse is unchanged, because a constructed empty product is an operation that says so.products_sharing_no_field_are_incompatible_boundspins the rejection andproducts_sharing_a_field_join_on_itpins its companion — a non-empty intersection still joins fieldwise, so this is about the empty case and not about products at a join.