checker: the power-of-two rule for an inferred Flags #48

Closed
opened 2026-10-05 21:07:14 +00:00 by orion · 4 comments
Collaborator

Context

The rule reaches every written Flags<Case> but not one whose type is only inferred (Flags.of(Permission.All)).

Source

Acceptance criteria

  • The rule applies to inferred Flags types
  • A checker test

Dependencies

None known.

Moved from the planning lists of the docs; the design record stays the specification.

## Context The rule reaches every written `Flags<Case>` but not one whose type is only inferred (`Flags.of(Permission.All)`). ## Source - [FLAGS.md section 6](https://git.torb.dev/torbscript/language/src/commit/393ef2608e4a60b8ff76d8b6b408951b822d887d/docs/design/FLAGS.md#L107) ## Acceptance criteria - [ ] The rule applies to inferred `Flags` types - [ ] A checker test ## Dependencies None known. _Moved from the planning lists of the docs; the design record stays the specification._
Author
Collaborator

[loop:torbscript] intake: in scope (author orion, label loop). FLAGS.md section 6 names the gap; the rule itself is specified, so this is a checker change with a test and likely no owner question. Touches the checker (compiler/src), so an opus/high implementer; the seed is not expected to change. next: refine (ready buffer is 6, so not before it drops below 2).

[loop:torbscript] intake: in scope (author orion, label loop). FLAGS.md section 6 names the gap; the rule itself is specified, so this is a checker change with a test and likely no owner question. Touches the checker (compiler/src), so an opus/high implementer; the seed is not expected to change. next: refine (ready buffer is 6, so not before it drops below 2).
Author
Collaborator

[loop:torbscript] claim refine by session:loop-torbscript-20261008 until 2026-10-08T13:24:50Z

[loop:torbscript] claim refine by session:loop-torbscript-20261008 until 2026-10-08T13:24:50Z
Author
Collaborator

[loop:torbscript] refine: ready.

Where the rule lives. requireFlagBits (compiler/src/semantics/checker/case-value.trb) is called from exactly one place, typeFromReference (compiler/src/semantics/checker/lowering.trb), i.e. only for a type the source writes (Flags<Permission> in an annotation, parameter, field or type argument). A type that is only inferred, such as the result of Flags.of(Permission.All), Flags.from(list) or Flags.fromBits(...), is built in checkCall (compiler/src/semantics/checker/call.trb, solved = resolveTypes ... and the returned resolveType checker, inner.result) and never passes through typeFromReference, so Case is solved to Permission without any look at its case values.

Design record. docs/design/FLAGS.md section 2 ("Every value must be a power of two for a type used as Flags<Case>") is the rule; section 6 names this gap. The rule and its message are already specified, only its reach is missing, so there is no open language choice.

Goal. The existing diagnostic also fires when the type Flags<Case> is only inferred.

Acceptance criteria (tests in compiler/tests/case-values.test.trb, group Flags<Case>, using the existing collections stub and problemsOf):

  1. type Permission { case Read = 1 case Write = 2 case Both = 3 } and const access = Flags.of(Permission.Read) (no annotation) reports exactly one problem: "`Permission.Both` stands for 3, and `Flags<Permission>` needs a power of two for every case", at the call expression. The note is the existing one.
  2. Same type, inferred through a second route (Flags.from([Permission.Read]) or Flags.fromBits(1) bound to a const without annotation) reports the same message once.
  3. No double report: const access: Flags<Permission> = Flags.of(Permission.Read) still yields exactly one problem (the written type and the inferred call result must not both report).
  4. A type with only powers of two (Read = 1, Write = 2, Delete = 64) used through Flags.of(Permission.Read) yields no problem, and so does a generic use where Case is a type parameter.
  5. torb check . still reports "no problems" (nothing in std/ or compiler/ infers a Flags of a non-power-of-two type).

Implementation sketch. Run the same check on the resolved result type of a call in checkCall (reusing requireFlagBits, which already returns early for anything that is not a nominal Flags<T>; it needs the current module, checker.report module vs checker.reportHere), and make sure a written type does not report a second time at the call. Free to pick the cleanest hook as long as 1-5 hold.

Docs in the same PR (docs: commit or part of the same commit): docs/design/FLAGS.md (section 2 bullet "where Flags<Case> is written (requireFlagBits)" and remove the section 6 bullet; mark the gap as built with today's date), docs/language/types/case-values.md line 87, docs/standard-library/collections.md line 220 and the doc comment in std/collections/src/flags.trb ("wherever Flags<Case> is written" becomes "written or inferred"), then torb docs skill docs skills to regenerate skills/.

Size. One PR, two Conventional Commits: fix(checker): hold an inferred Flags to the power-of-two rule (compiler/src + test) and docs(flags): the rule reaches an inferred Flags (docs + std doc comment + regenerated skills).

Seed: no (no syntax change, no new native, no driver flag, no new std name looked up by string; wellKnown.flags is already in use). Not a breaking change for existing code per criterion 5.

Implementer model: opus/high (compiler/src). Gates run in forge CI only (sh tools/gates.sh a).

next: implement

[loop:torbscript] refine: ready. **Where the rule lives.** `requireFlagBits` (`compiler/src/semantics/checker/case-value.trb`) is called from exactly one place, `typeFromReference` (`compiler/src/semantics/checker/lowering.trb`), i.e. only for a type the source *writes* (`Flags<Permission>` in an annotation, parameter, field or type argument). A type that is only inferred, such as the result of `Flags.of(Permission.All)`, `Flags.from(list)` or `Flags.fromBits(...)`, is built in `checkCall` (`compiler/src/semantics/checker/call.trb`, `solved = resolveTypes ...` and the returned `resolveType checker, inner.result`) and never passes through `typeFromReference`, so `Case` is solved to `Permission` without any look at its case values. **Design record.** `docs/design/FLAGS.md` section 2 ("Every value must be a power of two for a type used as `Flags<Case>`") is the rule; section 6 names this gap. The rule and its message are already specified, only its reach is missing, so there is no open language choice. **Goal.** The existing diagnostic also fires when the type `Flags<Case>` is only inferred. **Acceptance criteria** (tests in `compiler/tests/case-values.test.trb`, group `Flags<Case>`, using the existing `collections` stub and `problemsOf`): 1. `type Permission { case Read = 1 case Write = 2 case Both = 3 }` and `const access = Flags.of(Permission.Read)` (no annotation) reports exactly one problem: ``"`Permission.Both` stands for 3, and `Flags<Permission>` needs a power of two for every case"``, at the call expression. The note is the existing one. 2. Same type, inferred through a second route (`Flags.from([Permission.Read])` or `Flags.fromBits(1)` bound to a `const` without annotation) reports the same message once. 3. No double report: `const access: Flags<Permission> = Flags.of(Permission.Read)` still yields exactly one problem (the written type and the inferred call result must not both report). 4. A type with only powers of two (`Read = 1`, `Write = 2`, `Delete = 64`) used through `Flags.of(Permission.Read)` yields no problem, and so does a generic use where `Case` is a type parameter. 5. `torb check .` still reports "no problems" (nothing in `std/` or `compiler/` infers a `Flags` of a non-power-of-two type). **Implementation sketch.** Run the same check on the resolved result type of a call in `checkCall` (reusing `requireFlagBits`, which already returns early for anything that is not a nominal `Flags<T>`; it needs the current module, `checker.report module` vs `checker.reportHere`), and make sure a written type does not report a second time at the call. Free to pick the cleanest hook as long as 1-5 hold. **Docs in the same PR** (`docs:` commit or part of the same commit): `docs/design/FLAGS.md` (section 2 bullet "where `Flags<Case>` is written (`requireFlagBits`)" and remove the section 6 bullet; mark the gap as built with today's date), `docs/language/types/case-values.md` line 87, `docs/standard-library/collections.md` line 220 and the doc comment in `std/collections/src/flags.trb` ("wherever `Flags<Case>` is written" becomes "written or inferred"), then `torb docs skill docs skills` to regenerate `skills/`. **Size.** One PR, two Conventional Commits: `fix(checker): hold an inferred Flags to the power-of-two rule` (compiler/src + test) and `docs(flags): the rule reaches an inferred Flags` (docs + std doc comment + regenerated skills). **Seed:** no (no syntax change, no new native, no driver flag, no new std name looked up by string; `wellKnown.flags` is already in use). Not a breaking change for existing code per criterion 5. Implementer model: opus/high (`compiler/src`). Gates run in forge CI only (`sh tools/gates.sh a`). next: implement
Author
Collaborator

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T17:28:22Z

[loop:torbscript] claim implement by session:loop-torbscript-20261008 until 2026-10-08T17:28:22Z
argus closed this issue 2026-10-08 12:35:05 +00:00
Sign in to join this conversation.
No labels
loop
ready
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
torbscript/language#48
No description provided.