Skip to content

Reject a product with no fields instead of answering Unit - #118

Open
dpmills wants to merge 1 commit into
dmills/domain-setfrom
dmills/empty-product
Open

Reject a product with no fields instead of answering Unit#118
dpmills wants to merge 1 commit into
dmills/domain-setfrom
dmills/empty-product

Conversation

@dpmills

@dpmills dpmills commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

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
dpmills force-pushed the dmills/empty-product branch from 4066331 to 6763585 Compare August 20, 2026 20:51
@dpmills
dpmills force-pushed the dmills/empty-product branch from 6763585 to b3faa41 Compare August 20, 2026 23:24
@dpmills
dpmills force-pushed the dmills/empty-product branch from b3faa41 to 5ec1b3c Compare August 21, 2026 04:29
@dpmills
dpmills force-pushed the dmills/empty-product branch from 5ec1b3c to c18566f Compare August 21, 2026 04:39
@dpmills
dpmills force-pushed the dmills/empty-product branch from c18566f to a98df57 Compare August 21, 2026 04:55
@dpmills
dpmills force-pushed the dmills/empty-product branch from a98df57 to 776dd2b Compare August 21, 2026 05:47
@dpmills
dpmills force-pushed the dmills/empty-product branch from 776dd2b to 6ba5a31 Compare August 21, 2026 18:41
@dpmills
dpmills force-pushed the dmills/empty-product branch from 6ba5a31 to ce09a11 Compare August 21, 2026 19:42
@dpmills
dpmills force-pushed the dmills/empty-product branch 2 times, most recently from 89b22b5 to 9321478 Compare August 21, 2026 22:23
@dpmills
dpmills force-pushed the dmills/empty-product branch from 9321478 to f4d7b30 Compare August 22, 2026 00:08
@dpmills
dpmills force-pushed the dmills/empty-product branch from f4d7b30 to f4ad716 Compare August 22, 2026 00:22
@dpmills
dpmills force-pushed the dmills/empty-product branch from f4ad716 to 7fb1f3a Compare August 22, 2026 00:35
@dpmills
dpmills force-pushed the dmills/empty-product branch from 7fb1f3a to 9aa1871 Compare August 22, 2026 00:46
@dpmills
dpmills force-pushed the dmills/empty-product branch from 9aa1871 to 9cf6c99 Compare August 22, 2026 01:06
`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
dpmills force-pushed the dmills/empty-product branch from 9cf6c99 to fb5df09 Compare August 22, 2026 03:43
@dpmills
dpmills marked this pull request as ready for review August 22, 2026 03:50
@dpmills
dpmills requested a review from a team as a code owner August 22, 2026 03:50
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.

1 participant