Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 39 additions & 37 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,21 @@

## Values

| | |
| ---------------------------------- | -------------------------------------------- |
| `equals(a, b)` | deeply compares two Vals of the same type |
| `Val.of<V>(value)` | applies the default seal with the type named |
| `Val.of.nocopy<V>(value)` | makes a payload the value without copying |
| `Val.unwrap(value)` | returns a mutable copy of the payload |
| `Val.sealer<V>()` | creates a callable default sealer |
| `Val.sealer<V>().impl(fns)` | adds the type's members, and ends the chain |
| `Val.companion<V>()` | starts a companion without a callable sealer |
| `Val.companion<V>().impl(fns)` | the same, on a companion |
| `.implTrait(Tr, fns)` | implements a trait the type declares |
| `Val.companion<V>().implSeal(f)` | registers a custom seal |
| `Val.companion<V>().implCreate(f)` | registers a function that creates a payload |
| `Val.companion<V>().fixed<K>()` | excludes keys from `patch` |
| | |
| ---------------------------------- | -------------------------------------------------------- |
| `equals(a, b)` | deeply compares two Vals of the same type |
| `Val.of<V>(value)` | applies the default seal with the type named |
| `Val.of.nocopy<V>(value)` | makes a payload the value without copying |
| `Val.unwrap(value)` | returns a mutable copy of the payload |
| `Val.sealer<V>()` | creates a callable default sealer |
| `Val.sealer<V>().impl(fns?)` | adds the type's members, and ends the chain |
| `Val.companion<V>()` | starts a companion without a callable sealer |
| `Val.companion<V>().impl(fns?)` | the same, on a companion |
| `.implTrait(Tr, fns?)` | implements a trait the type declares |
| `.implTrait<Tr>(fns)` | the same, for a trait that implements nothing of its own |
| `Val.companion<V>().implSeal(f)` | registers a custom seal |
| `Val.companion<V>().implCreate(f)` | registers a function that creates a payload |
| `Val.companion<V>().fixed<K>()` | excludes keys from `patch` |

## Companion members

Expand Down Expand Up @@ -58,29 +59,30 @@ re-export a companion.

### Enum

| | |
| ------------------------------------ | ------------------------------------------------- |
| `Enum<K, D, X>` | a closed set of variants, as one union |
| `Enum.sealer<E>(tag?)` | starts an enum whose variants are callable |
| `Enum.companion<E>(tag?)` | the same, for an enum with a seal of its own |
| `E.match(value, handlers)` | dispatches on the tag, exhaustively |
| `E[Variant](payload)` | builds that variant, writing the tag |
| `E[Variant].create(payload)` | the same on a companion, through its seal |
| `E[Variant].patch(value, patch)` | derives a variant, never reaching the tag |
| `E(payload)` / `E.seal(payload)` | selects the variant from the tag, and seals |
| `.impl(fns?)` | adds members taking the union, and ends the chain |
| `.implVariant(N, sealer => …)` | builds one variant from its own steps |
| `.implSeal(seal)` | replaces the seal every variant passes |
| `.implTrait(Tr, fns)` | implements a trait the enum declares |
| `Tag<T>` | names the tag field, intersected into `X` |
| `VariantOf<E, N>` | the type of one variant |
| `SeedFor<E, N>` | what that variant's constructor takes |
| `SealedPayload<E>` | what `E(payload)` takes, tag included |
| `VariantsOf<E>` / `SharedOf<E>` | the declared variants, and the shared fields |
| `NameOf<E>` / `TagOf<E>` | the enum's name, and the tag field's name |
| `AnyEnum` | a constraint over any enum |
| `EnumSealer<E>` / `EnumBuilder<E>` | an enum with every step still open |
| `EnumSealed<E>` / `EnumCompanion<E>` | a finished enum companion |
| | |
| ------------------------------------ | -------------------------------------------------------- |
| `Enum<K, D, X>` | a closed set of variants, as one union |
| `Enum.sealer<E>(tag?)` | starts an enum whose variants are callable |
| `Enum.companion<E>(tag?)` | the same, for an enum with a seal of its own |
| `E.match(value, handlers)` | dispatches on the tag, exhaustively |
| `E[Variant](payload)` | builds that variant, writing the tag |
| `E[Variant].create(payload)` | the same on a companion, through its seal |
| `E[Variant].patch(value, patch)` | derives a variant, never reaching the tag |
| `E(payload)` / `E.seal(payload)` | selects the variant from the tag, and seals |
| `.impl(fns?)` | adds members taking the union, and ends the chain |
| `.implVariant(N, sealer => …)` | builds one variant from its own steps |
| `.implSeal(seal)` | replaces the seal every variant passes |
| `.implTrait(Tr, fns?)` | implements a trait the enum declares |
| `.implTrait<Tr>(fns)` | the same, for a trait that implements nothing of its own |
| `Tag<T>` | names the tag field, intersected into `X` |
| `VariantOf<E, N>` | the type of one variant |
| `SeedFor<E, N>` | what that variant's constructor takes |
| `SealedPayload<E>` | what `E(payload)` takes, tag included |
| `VariantsOf<E>` / `SharedOf<E>` | the declared variants, and the shared fields |
| `NameOf<E>` / `TagOf<E>` | the enum's name, and the tag field's name |
| `AnyEnum` | a constraint over any enum |
| `EnumSealer<E>` / `EnumBuilder<E>` | an enum with every step still open |
| `EnumSealed<E>` / `EnumCompanion<E>` | a finished enum companion |

### Trait

Expand Down
38 changes: 37 additions & 1 deletion notes/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { Val } from "valof";
- **§14 valof-lint** companion のメンバが静的解析から見えない問題。パーサ選定、同梱の判断、却下した ts-morph(§14.5)、カスタム equals を持つ子の規則(§14.7)、ルールの表現と構成(§14.8)、エディタ統合(§14.9、overlay まで実装)、テストの穴(§14.10)、テストの置き場所(§14.11)、型名と一致しないブランド(§14.12)、Val / Trait の 2 つ目の名前(§14.25)、却下した自己参照でない `Rec` の規則(§14.26)、型名と一致しない companion(§14.21)、型と別ファイルの companion(§14.22)、companion を持つ型の `Val.of`(§14.23)、型引数を書かない `Val.of`(§14.24)、`Val` の綴り(§14.13)、欠けている disable コメント(§14.14)、効いていない disable コメント(§14.15)、ファイル全体の disable(§14.16)、`--no-` を受けない規則(§14.17)、指示についての規則の見せ方(§14.18)、oxlint の版と設定の正本(§14.19)、LSP でのホスト統合テスト(§14.20)、Trait の宣言・実装・`dyn` の構文追跡(§15.1)
- **§15 v2 候補**
- **15.1 `Trait`** `Final<F>` マーカーと 1 段の `impl`、交差する trait ブランドと型引数だけで落とす `|`(却下したタプル)、`Self` マーカーと戻り値禁止、`dyn`(`Box<dyn Trait>` 相当)、却下した WeakMap ディスパッチ、需要と `dyn` を落とせる形の却下、experimental subpath(却下した機能ごとの subpath)、`impl` のコールバック形(引数は実装済みの final だけ、却下した実装側 companion)
- **15.2 `Enum`** §7.4 の見直し。Variant をレコードに宣言して union を導出、ブランドの導出、タグ名のカスタムと `tag-mismatch`、companion に置く `match`、ts-pattern との線引き、型を確かめた記録(共通フィールド、`match` の型引数、`VariantOf` の表示、却下した戻り値の型引数・自由関数の `match`・Val のレコード、Trait の実装、タグ名を `Tag<…>` で渡すこと、トップレベルの条件型が宣言出力を壊すこと)、実装して分かったこと(Fault の置き場所、Variant 1 個の禁止、`then` を 3 箇所で落とす、宣言出力の CI、variance 測定と型コスト)、Variant ごとの seal と union の `seal`(入口を 2 つに分ける、builder を callback で渡す、`impl` で鎖を閉じる、却下した値の形)、`implVariant` を Variant ごとの鎖にしたこと、steps を枠そのものにしたこと、タグ名の渡し方を変える 4 案の却下
- **15.2 `Enum`** §7.4 の見直し。Variant をレコードに宣言して union を導出、ブランドの導出、タグ名のカスタムと `tag-mismatch`、companion に置く `match`、ts-pattern との線引き、型を確かめた記録(共通フィールド、`match` の型引数、`VariantOf` の表示、却下した戻り値の型引数・自由関数の `match`・Val のレコード、Trait の実装、タグ名を `Tag<…>` で渡すこと、トップレベルの条件型が宣言出力を壊すこと)、実装して分かったこと(Fault の置き場所、Variant 1 個の禁止、`then` を 3 箇所で落とす、宣言出力の CI、variance 測定と型コスト)、Variant ごとの seal と union の `seal`(入口を 2 つに分ける、builder を callback で渡す、`impl` で鎖を閉じる、却下した値の形)、`implVariant` を Variant ごとの鎖にしたこと、steps を枠そのものにしたこと、タグ名の渡し方を変える 4 案の却下、companion を渡さない形(val.ts の門を共有する、`PayloadKeys` の分配、入口 2 つぶんのテスト)
- **15.3 `path`** seal をまたぐ patch の合成。`abort` を合成側に置く判断、ハンドラが最終段である理由(HKT)、`glue` の `open` / `close`、`each` / `where`、却下した `deepPatch`
- **15.4 `.impl` のコールバック形** 自分の companion を参照すると推論が回らない(TS7022)。contextual typing がコールバック越しでも効くことの実測、`implTrait` も callback、予算、却下したメンバのカリー化と、下流の `.impl` を型で塞ぐ案。**鎖は 2026-09-18 に閉じた**(1 回だけ、兄弟は注釈で呼ぶ、Trait の実装の受け手を `Tr` に、却下した型レベルの案内と lint 規則)
- **§16 予算の責務** バンドルと型を別のスクリプトに割る。宣言のバイト数を type-perf へ、予算を 64 kB に上げた理由
Expand Down Expand Up @@ -5218,6 +5218,42 @@ payload と trait の 2 段になる。得るものが無い。
**`implTrait` もコールバック形が要る。**enum の trait 実装は `match` で書くのが普通で、そこで自分の
companion を参照する。§15.4 の範囲に `implTrait` も入る。

#### companion を渡さない形、2026-09-20

**Val にあって Enum に無かった。**§15.1 の `implTrait<Tr>({…})` が Enum の入口 2 つに届いていない。
却下した記録は無く、この節にも §9 にも項目が無い。判断ではなく、書かれていなかっただけ。

利用者に出るのは TS2558「Expected 2 type arguments, but got 1」で、§15.1 が呼び間違いのために
用意した「pass the members this trait leaves open, or name the trait as the type argument」に
届かない。第 2 引数の contextual typing も一緒に消える。

**欠けていた門は 5 つ。**`src/enum.ts` が `Takes` と `Passes` を自前で持ち、val.ts の薄い写しに
なっていた。呼び間違いの文、他の trait が同名に答えていないかの検査、payload のフィールド名との
衝突の検査、`Complete`、`Alone`。持っていたのは「trait を宣言しているか」だけ。

**共有は宣言を減らす。**val.ts の `Takes` / `Complete` / `Passes` / `Alone` / `PayloadKeys` は、
すでに enum.ts の宣言と同じ chunk に出ていた。`export type` にして enum.ts が借りると、重複
2 本が消えるぶんがオーバーロードの倍化を上回る。宣言は 58.81 → 58.67 kB。§15.1 が Val で
+2.1 kB 払ったのと逆になる。

**`PayloadKeys` を分配させた。**`Declared<V>` は `V` に分配するので payload の union になり、
`keyof` がそれを取ると共通フィールドだけが残る。`PayloadKeys<Shape>` は `"_tag" | "id"` で、
Variant 固有の `r` や `side` が落ちていた。trait のメンバが 1 つの Variant のフィールドを隠す形が
通ってしまう。`V extends unknown` で分配させると全 Variant のキーの和になる。Val は union では
ないので変わらない(core の instantiations は 5,733 のまま)。

**実行時は `?? trait` の 1 トークン。**val.ts:1103 と同じ。Enum の gzip は残り 106 → 105 B。

**`@ts-expect-error` が別のエラーを吸う形を 2 回書いた。**`Alone` のテストのつもりで `Final` を
持たない trait を書き、実際には `Implement` が弾いていた。`Complete` のテストはディレクティブを
呼び出し行に置き、第 2 引数のエラーを吸っていた。`Complete` を外してもエラー自体は出るので、
ディレクティブは緑のまま残る。val.ts 側(`tests/val.test.ts:1939`)は**第 1 引数の行**に置いて
いる。そこが `Complete` のメッセージを持つ位置。

**入口が 2 つなら門も 2 つ。**テストを `Enum.sealer` だけで書いたので、`EnumBuilder` は
`Alone` のオーバーロードを消しても `Takes` を丸ごと外しても緑のままだった。test-audit が
見つけた。検査を足したら、両方の入口で 1 本ずつ落ちることを確かめる。

#### タグ名は `Tag<…>` を交差して渡す、2026-09-16

**`Enum<"Event", { Click: … }, "kind">` は、`"kind"` が何なのか見て分からない。**位置が意味を持つ引数は、
Expand Down
57 changes: 27 additions & 30 deletions src/enum.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import type {
AnyTrait,
Implement,
Members,
MembersOf,
NamesOf,
TraitCompanion,
TraitsOf,
Unbound,
} from "./trait.ts";
import type { AnyTrait, Members, MembersOf, Unbound } from "./trait.ts";
import {
define,
Val,
type Alone,
type AnyVal,
type CompanionMembers,
type Complete,
type DeepReadonly,
type Invalid,
type Passes,
type Patch,
type Takes,
type Wired,
} from "./val.ts";

Expand Down Expand Up @@ -430,16 +425,6 @@ type Boundary<C> = { [N in keyof C]: Yields<C[N]> }[keyof C];
type UnionMembers<E extends AnyEnum> = CompanionMembers<E> &
Partial<Record<(keyof VariantsOf<E> & string) | "match", never>>;

/** Whether the enum declares the trait at all. The check is placed on the parameter (notes §11.1). */
type Takes<E extends AnyEnum, Tr extends AnyTrait, Ok> = [NamesOf<Tr>] extends [TraitsOf<E>]
? Ok
: "the type does not declare this trait";

/** The second argument, absent where the trait answered for every member itself. */
type Passes<Tr extends AnyTrait, G, E> = [keyof Omit<MembersOf<Tr>, keyof G>] extends [never]
? [impl?: Implement<Tr, G, E>]
: [impl: Implement<Tr, G, E>];

/** Dispatches on the tag. See {@link EnumCompanion.match}. */
type Match<E extends AnyEnum> = <H extends Handlers<E, unknown>>(
value: E,
Expand Down Expand Up @@ -552,10 +537,17 @@ export type EnumBuilder<
* Implements a trait the enum declares. The implementation takes the union, so a member that
* differs per variant is a `match` inside it.
*/
implTrait: <Tr extends AnyTrait, G>(
trait: Takes<E, Tr, TraitCompanion<Tr, G>>,
...impl: Passes<Tr, G, E>
) => EnumBuilder<E, VM, M & Unbound<MembersOf<Tr>, E>, F>;
implTrait: {
// No companion to pass when the trait implements nothing of its own: the type argument is
// the whole of it, and the members arrive where the companion would have.
<Tr extends AnyTrait>(
impl: Takes<E, Tr, M, Alone<Tr, E>>,
): EnumBuilder<E, VM, M & Unbound<MembersOf<Tr>, E>, F>;
<Tr extends AnyTrait, G>(
trait: Takes<E, Tr, M, Complete<Tr, G>>,
...impl: Passes<Tr, G, E>
): EnumBuilder<E, VM, M & Unbound<MembersOf<Tr>, E>, F>;
};
};

/**
Expand Down Expand Up @@ -585,10 +577,15 @@ export type EnumSealer<
) => R,
) => EnumSealer<E, VM & { [P in N]: R }, M>;
/** See {@link EnumBuilder.implTrait}. */
implTrait: <Tr extends AnyTrait, G>(
trait: Takes<E, Tr, TraitCompanion<Tr, G>>,
...impl: Passes<Tr, G, E>
) => EnumSealer<E, VM, M & Unbound<MembersOf<Tr>, E>>;
implTrait: {
<Tr extends AnyTrait>(
impl: Takes<E, Tr, M, Alone<Tr, E>>,
): EnumSealer<E, VM, M & Unbound<MembersOf<Tr>, E>>;
<Tr extends AnyTrait, G>(
trait: Takes<E, Tr, M, Complete<Tr, G>>,
...impl: Passes<Tr, G, E>
): EnumSealer<E, VM, M & Unbound<MembersOf<Tr>, E>>;
};
};

/** Forgotten, misspelled, or passed when the default holds: the type rejects all three. */
Expand Down Expand Up @@ -724,10 +721,10 @@ const state = (
// The trait's own members and the enum's, merged the way `val.ts` merges them.
return open
? (
trait: { __valof_shared: Record<string, unknown> },
trait: { __valof_shared?: Record<string, unknown> },
fns: Record<string, unknown> = {},
) => {
const grown = { ...traits, ...trait.__valof_shared, ...fns };
const grown = { ...traits, ...(trait.__valof_shared ?? trait), ...fns };
return step(builds, { ...members, ...grown }, grown, seal, true);
}
: undefined;
Expand Down
Loading
Loading