📝 Document the finite-only universe decision
Record why open universes and value/stringification collisions are rejected, the `Member<T, K>` inversion, and the findings from the rejected open-universe attempt so they are not re-run.
This commit is contained in:
1 parent
3b5269f9e1
commit
aaeeef1e11
2 files changed
+88
-21
No files matched your search
@@ -25,10 +25,15 @@ Yet to be implemented
|
|||||||
|
|
||||||
## Caveats
|
## Caveats
|
||||||
|
|
||||||
- **A value and its stringification collide.** Object keys stringify, so a
|
- **Only finite universes are supported.** The factory must be given a finite
|
||||||
universe that mixes a member with the string it stringifies to — `1 | "1"`,
|
union of literals; `string`, `number` and template literals are rejected. This
|
||||||
`true | "true"`, `null | "null"` — collapses to a single handler key and both
|
is what lets the exhaustive overload be proven, so the runtime `dispatch`
|
||||||
members are routed to it. Use one form or the other.
|
throw stays unreachable through the typed API.
|
||||||
|
- **A value and its stringification must not both be present.** Object keys
|
||||||
|
stringify, so a universe containing both a member and the string it
|
||||||
|
stringifies to — `1 | "1"`, `true | "true"`, `null | "null"` — is rejected at
|
||||||
|
the factory. Either form alone is fine, and one value's string form may
|
||||||
|
coexist with a _different_ value's bare form (`"true" | false`).
|
||||||
- **`symbol` and `bigint` are not supported.** A `symbol` brand is a
|
- **`symbol` and `bigint` are not supported.** A `symbol` brand is a
|
||||||
compile-time phantom with nothing to match at runtime, and a `bigint` is not a
|
compile-time phantom with nothing to match at runtime, and a `bigint` is not a
|
||||||
valid property key; neither satisfies the matcher's universe constraint.
|
valid property key; neither satisfies the matcher's universe constraint.
|
||||||
|
|||||||
+79
-17
@@ -97,9 +97,11 @@ Each factory is two overloads whose order is load-bearing:
|
|||||||
|
|
||||||
#### Decision (2026-09)
|
#### Decision (2026-09)
|
||||||
|
|
||||||
`src/matcher-shared.ts` holds the seven universe-agnostic pieces both matchers
|
`src/matcher-shared.ts` holds the universe-agnostic pieces both matchers use:
|
||||||
use: `UnaryFn`, `PatternReturns`, `RedundantFallback`, `HandlerMap`, the shared
|
`UnaryFn`, `PatternReturns`, `RedundantFallback`, `HandlerMap`, the shared
|
||||||
`Matchable` universe, and the `PatternKey` / `PatternParam` key projection.
|
`Matchable` universe, the `PatternKey` key projection and its `Member` inverse,
|
||||||
|
and the `Stringified` / `Collisions` / `UnsupportedReason` / `UnsupportedUniverse`
|
||||||
|
/ `UniverseGate` universe gate.
|
||||||
|
|
||||||
#### Why
|
#### Why
|
||||||
|
|
||||||
@@ -118,8 +120,65 @@ use: `UnaryFn`, `PatternReturns`, `RedundantFallback`, `HandlerMap`, the shared
|
|||||||
`Fallback` and `MustBePartial`.** Each is built from its own universe
|
`Fallback` and `MustBePartial`.** Each is built from its own universe
|
||||||
(`Tags`/`MapTaggedUnion` vs the primitive values); abstracting over the
|
(`Tags`/`MapTaggedUnion` vs the primitive values); abstracting over the
|
||||||
F-bounded `Handled` constraint that makes the remainder work risks the
|
F-bounded `Handled` constraint that makes the remainder work risks the
|
||||||
contextual typing it exists to preserve. `Matchable` and the `PatternKey` /
|
contextual typing it exists to preserve. `Matchable`, the `PatternKey` /
|
||||||
`PatternParam` projection are the pieces both universes genuinely share.
|
`Member` projection and the `UniverseGate` are the pieces both universes
|
||||||
|
genuinely share.
|
||||||
|
|
||||||
|
## Supported universes
|
||||||
|
|
||||||
|
#### Decision (2026-09)
|
||||||
|
|
||||||
|
A universe must be a **finite union of literals** with **no
|
||||||
|
value/stringification collision**. Broad types (`string`, `number`, a template
|
||||||
|
literal) and `"true" | true` / `1 | "1"` are rejected; the factory intersects
|
||||||
|
`UniverseGate<T>` (the `UnsupportedUniverse<Reason>` diagnostic) into the
|
||||||
|
handler and fallback parameters. `Member<T, K>` replaces the `PatternParam<K>`
|
||||||
|
inversion: the handler parameter is the member(s) of `T` whose `PatternKey` is
|
||||||
|
`K`, so a standalone `"true"` is `"true"`, not `true`.
|
||||||
|
|
||||||
|
#### Why
|
||||||
|
|
||||||
|
- **`PatternKey` is not injective.** `"true"` and `true` (and `1` / `"1"`)
|
||||||
|
share a runtime key, so `PatternParam<K>` cannot recover the member.
|
||||||
|
`Member<T, K>` inverts against `T`, which is exact.
|
||||||
|
- **Broad types cannot be proven exhaustive.** An index-like map lets a partial
|
||||||
|
object satisfy the exhaustive overload and reaches the `dispatch` throw.
|
||||||
|
Rejecting at the boundary avoids threading an open/closed branch through
|
||||||
|
`Handlers`, `Fallback` and `MustBePartial`.
|
||||||
|
- **Collisions are rejected, not merged.** `Member<T, K>` would be sound (the
|
||||||
|
handler gets the union), but the API is one handler per member; rejecting
|
||||||
|
keeps `Member` a singleton and the remainder exact.
|
||||||
|
- **The collision predicate is type-checkable.** `Collisions<T> =
|
||||||
|
Extract<T, Stringified<T>>` catches numeric collisions too.
|
||||||
|
- **The gate is an intersection, not a branch,** so `R` inference and the popup
|
||||||
|
survive; a conditional parameter type would not.
|
||||||
|
|
||||||
|
#### Rejected
|
||||||
|
|
||||||
|
- **Open universes with a required fallback** (`fix/open-universe-*`): sound,
|
||||||
|
but left the collision hole and added an `IsLiteral` /
|
||||||
|
`OpenUniverseNeedsFallback` branch through every handler type. Findings, kept
|
||||||
|
so they are not re-run: `{}` satisfies an index signature (and `Exact` misses
|
||||||
|
it); an index signature dominates contextual typing; `R` infers only from a
|
||||||
|
non-self-referential parameter type (`{ [K in keyof H]: … R … }` gives
|
||||||
|
`unknown`); an F-bounded guard referencing `keyof Handled` in `Handled`'s own
|
||||||
|
constraint sees the constraint, not the map; all handlers share one `R` (only
|
||||||
|
the fallback widens it); `IsLiteral` is the finite-literal predicate.
|
||||||
|
- **`Member<T, K>` without the gate:** sound, but a colliding handler gets a
|
||||||
|
union and `1 | "1"` stays one runtime key.
|
||||||
|
- **A round-trip injectivity gate** (`IsEqual<T, PatternParam<PatternKey<T>>>`):
|
||||||
|
over-rejects standalone `"true"` / `"false"` / `"null"` / `"undefined"`.
|
||||||
|
- **A case-list / ts-pattern builder:** removes the collision class but drops
|
||||||
|
the object map (footprint, popup) and reimplements an existing library.
|
||||||
|
- **Normalize numeric keys to strings:** makes `PatternKey` injective but
|
||||||
|
changes "numeric keys stay numbers" and defeats the numeric dispatch fast
|
||||||
|
path.
|
||||||
|
|
||||||
|
#### Known issue
|
||||||
|
|
||||||
|
- A multi-collision universe lists every collision in the diagnostic.
|
||||||
|
- The `dispatch` throw is unreachable through the typed API; the throw tests
|
||||||
|
widen the factory to `Function` to reach it.
|
||||||
|
|
||||||
## Tagged-union matcher
|
## Tagged-union matcher
|
||||||
|
|
||||||
@@ -160,17 +219,19 @@ The key is a separate call because `K` is inferred from its literal argument and
|
|||||||
- **`MapTaggedUnion` distributes with `Extract`.** A duplicated tag yields a
|
- **`MapTaggedUnion` distributes with `Extract`.** A duplicated tag yields a
|
||||||
union of members instead of dropping one.
|
union of members instead of dropping one.
|
||||||
- **A `boolean` / `null` / `undefined` tag goes through the shared
|
- **A `boolean` / `null` / `undefined` tag goes through the shared
|
||||||
`PatternKey` / `PatternParam` projection.** `Discriminated` admits those tags
|
`PatternKey` / `Member` projection.** `Discriminated` admits those tags
|
||||||
(they are in `Tag`), but they cannot key a mapped type, so the handler map is
|
(they are in `Tag`), but they cannot key a mapped type, so the handler map is
|
||||||
keyed by the stringified form (`true` → `"true"`) and `PatternParam` inverts
|
keyed by the stringified form (`true` → `"true"`) and `Member` inverts it
|
||||||
it to recover the member. This is the same projection the primitive-union
|
against the tag set to recover the member. This is the same projection the
|
||||||
matcher uses over its universe, which is why it lives in `matcher-shared.ts`.
|
primitive-union matcher uses over its universe, which is why it lives in
|
||||||
|
`matcher-shared.ts`.
|
||||||
|
|
||||||
#### Known issue
|
#### Known issue
|
||||||
|
|
||||||
- A member's tag must be unique across the union; two members with the same tag
|
- A member's tag must be unique across the union; two members with the same tag
|
||||||
collapse to a union under one handler. The same holds for a tag colliding with
|
collapse to a union under one handler. A tag colliding with its
|
||||||
its stringification (`true | "true"`) — see [README § Caveats](../README.md#caveats).
|
stringification (`true | "true"`) is rejected by the universe gate — see
|
||||||
|
§ Supported universes.
|
||||||
|
|
||||||
## Primitive universe
|
## Primitive universe
|
||||||
|
|
||||||
@@ -180,11 +241,12 @@ The universe (`Matchable`) is `string | number | boolean | null | undefined`,
|
|||||||
with `boolean` admitted as `true | false`.
|
with `boolean` admitted as `true | false`.
|
||||||
|
|
||||||
`boolean`/`null`/`undefined` are not property keys, so handler-map keys are a
|
`boolean`/`null`/`undefined` are not property keys, so handler-map keys are a
|
||||||
projection (`PatternKey`: each member stringified) and `PatternParam` inverts
|
projection (`PatternKey`: each member stringified) and `Member` inverts it
|
||||||
it, so callbacks receive the real member (`true`, not `"true"`). The popup
|
against the universe, so callbacks receive the real member (`true`, not
|
||||||
offers `true`, `false`, `null`, `undefined` by name (verified over LSP). The
|
`"true"`; the standalone string `"true"` stays `"true"`). The popup offers
|
||||||
same projection is shared with the tagged-union matcher; see
|
`true`, `false`, `null`, `undefined` by name (verified over LSP). The same
|
||||||
§ Tagged-union matcher.
|
projection is shared with the tagged-union matcher; see § Tagged-union matcher.
|
||||||
|
The supported universes are constrained as described in § Supported universes.
|
||||||
|
|
||||||
#### Why
|
#### Why
|
||||||
|
|
||||||
@@ -194,5 +256,5 @@ same projection is shared with the tagged-union matcher; see
|
|||||||
`String()` defeats V8's numeric-key path: measured ~2× on number-keyed
|
`String()` defeats V8's numeric-key path: measured ~2× on number-keyed
|
||||||
dispatch).
|
dispatch).
|
||||||
- `symbol`/`bigint`/`NaN`/`-0` are rejected, and a member colliding with its
|
- `symbol`/`bigint`/`NaN`/`-0` are rejected, and a member colliding with its
|
||||||
stringification is unguarded: user-facing, stated once in
|
stringification is rejected by the gate: user-facing, stated once in
|
||||||
[README § Caveats](../README.md#caveats).
|
[README § Caveats](../README.md#caveats).
|
||||||
Reference in new issue
Block a user