Skip to content

feat(ts): option fields of a generic type at a unit instance - #612

Merged
moq-bot[bot] merged 10 commits into
mainfrom
quest/m2/ts-model/unit-option-fields
Nov 7, 2026
Merged

moq-bot[bot] merged 10 commits into
mainfrom
quest/m2/ts-model/unit-option-fields

Conversation

@kixelated

@kixelated kixelated commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Finishes quest/m2/ts-model/unit-option-fields.md: a generic type's Option<T> or Poll<T> field now translates at an instance whose T is already undefined or pending in JS, such as moq-net's Idle<()> (util/idle.rs's parked) and tokio's oneshot::Inner<()> (UnsafeCell<Option<T>>).

Changes

  • In a generic type's definition, an Option<T> whose Some is a bare type parameter is typed rs.OptionOf<T>, and a Poll<T> rs.PollOf<T>. These are new runtime conditional types: T | (undefined extends T ? None : undefined) and T | (typeof Pending extends T ? OuterPending : typeof Pending). TypeScript resolves each instance to the type that instance's own Option or Poll already has, so Idle<undefined>.parked is undefined | rs.None and Idle<number>.parked stays number | undefined. Monomorphized bodies already pick rs.None or rs.OuterPending by the instance's type, so reads need no cast.
  • A value made into such a field is cast to the definition's type at the instance's arguments (field_value, through a new decided_fields map). Without the cast, TypeScript infers the class parameter from the None or Pending argument, for example Parked<symbol | u8 | undefined>.
  • generic_erasure now refuses only what this can't represent:
    • an instance where TypeScript's type of T and Rust's erasure disagree, e.g. MaybeUninit<u32>: its TS type is number, but its Option's None is rs.None;
    • a field type that can't tell its values apart at the instance, e.g. Maybe<Option<()>>'s Option<Option<()>>;
    • another form of the parameter that can be undefined or pending at the instance (the old message);
    • an Option<MaybeUninit<T>> field, typed MaybeUninit<T> | rs.None, at a byte T, whose MaybeUninit<u8> is a number, so its Option's None would be undefined. Before this PR nothing refused it.
    • Each field is checked on its own, so an instance with a parameter still open, such as a field typed ByteAnd<u8, U>, is still checked on its fields that don't depend on U.
  • Trait objects: an object's Option<Self::Item> is now rs.OptionOf<Item>, and fits in dynamic.rs accepts what that resolves to. dyn Source<Item = Option<u8>>, dyn Source<Item = ()>, and a generic trait's Option<T> at T = () or Option<u8> translate. They were refused before (reject/objects.rs's nested_item, received, passed).
  • README: the type table, the generic-type paragraph, the trait-object paragraph, and the refusal list.

Tests

  • lang/shapes gets a new generics::units case: a Parked<T> with Option<T>, Poll<T>, and UnsafeCell<Option<T>> fields, at (), Option<u8>, Poll<u8>, and u8. It parks, readies, sends, takes, and compares, checked differentially against native Rust.
  • lang/dyns gets a new dyns::options case: objects returning Option<Option<u8>> and Option<()> through an associated type and a generic trait's parameter.
  • reject: the cases that now translate are removed. Added: values::generic_uninit (MaybeUninit<u32>), values::generic_nested (Option<Option<()>> through a field), values::generic_uninit_byte and values::generic_uninit_byte_open (Option<MaybeUninit<T>> at u8, directly and through a generic field with an open parameter), and objects::uninit_item (the trait object counterpart).
  • Census at the moq pin: no would be rs.None or would be rs.OuterPending refusals are left in the model (66 refusals now) or in moq-net as a whole (385). No new Option-related refusal replaced them.
  • just ci passes locally.

Choices made without asking

  • Conditional type instead of per-field instance tracking. The quest's Plan said to decide per field from the instances the crate uses, keeping the bare form elsewhere. A conditional type does that per instance at the type level, with no tracking of reached instances and no casts on reads. The cost is that every generic Option<T> field's declaration now reads rs.OptionOf<T> instead of T | undefined, even where no instance needs rs.None; it still resolves to T | undefined at those instances.
  • Only a bare parameter gets the conditional. Option<Box<T>> and Option<&T> also translate, because their TS type is T. Forms whose TS type isn't T, such as rs.Ptr<T>, keep the old refusal.
  • Removed reject/futures.rs's generic_pending. It now translates and generics::units covers it. I found no Poll case the new check would refuse, so it has no replacement.

Public API

The translator's output: generic types' Option<T>/Poll<T> fields and trait-object method types are now rs.OptionOf<T>/rs.PollOf<T>, the same as before at every instance that translated before. Constructions of such types cast those fields' values. The runtime adds the OptionOf and PollOf types.

Follow-ups

None.

Caused by

No single PR: the Option<MaybeUninit<T>> gap at u8 is as old as the generic-field erasure check.

Models

Size: S
Implementer: Claude Code, claude-opus-5-5, high effort

Review

mechanical fix by quest land watch: merged main at 91a905c94899792e4edade5f89dafa84e16c5034 with the quest merge driver (head f952a04)

Rewritten by quest review after each round.

  • Reviewer: Codex, gpt-6.1-sol, medium effort
  • Session: 01a1b39a-f5da-7812-81eb-10d920c23e97, round 1
  • Head: 1a1c60c
  • Base: origin/main at b86a186
  • Score: 10/10, No actionable defects found in the specified diff; runtime and cached fixture TypeScript checks passed, but differential execution was blocked by the read-only environment’s inability to acquire the required test gate.
  • Findings: none

@pullfrog

pullfrog Bot commented Nov 6, 2026

Copy link
Copy Markdown

Pullfrog  | Review this ➔

kixelated and others added 8 commits November 6, 2026 14:55
A generic type's Option<T> and Poll<T> of a type parameter are rs.OptionOf<T>
and rs.PollOf<T>, conditional types that resolve at each instance to that
instance's own Option or Poll, so Idle<()> and oneshot::Inner<()> translate,
as do trait objects whose Option<Self::Item> holds an Item that can be
undefined.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t-option-fields

# Conflicts:
#	ts/src/lower/mod.rs
…onOf

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An instance with an open parameter, such as a field's ByteAnd<u8, U>, still
decides its other fields' None, so skip only the fields over that parameter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@kixelated
kixelated marked this pull request as ready for review November 7, 2026 01:27
@coderabbitai

coderabbitai Bot commented Nov 7, 2026 •

Copy link
Copy Markdown

Important

  • Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6860b9ba-58a9-4145-9af9-a7e6b0ef1234
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@kixelated kixelated left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review by review (OpenAI)

Reviewed head 1a1c60cbaa177cb18295a7db059dd87936e5e6b2 against base 777c50c1028039f8f3bab7aa6737fa4ca347edfe.

No actionable findings in the inspected changes. The conditional OptionOf/PollOf types agree with the existing sentinel selection for unit, optional, pending, and ordinary payloads. Constructor casts preserve the instance argument instead of inferring it from the sentinel. The field-level checks retain rejection of nested sentinel collisions and the MaybeUninit cases where TypeScript's type and Rust's erasure disagree, including a concrete field under a partially open instance. The trait-object compatibility check applies the same distinction.

Direction: the conditional-type approach is reasonable and avoids tracking all reached instances; no changes requested. The added shapes and trait-object fixtures exercise the newly accepted representations, and reject fixtures cover the incompatible representations.

Verification: GitHub-only static inspection of lowering/runtime changes, surrounding erasure and trait-object logic, fixture sources and generated examples. No local commands, TypeScript checks, differential tests, or census run performed. The exact-head PR workflow returned by GitHub is Queue, skipped; this is not a passing-build claim.

@kixelated
kixelated added this pull request to the merge queue Nov 7, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Nov 7, 2026
@moq-bot

moq-bot Bot commented Nov 7, 2026

Copy link
Copy Markdown

quest land watch: the merge queue removed this pull request at 1a1c60c because it conflicts with main (merge_conflict).

The land fixer works on it in the background, and the watcher re-enqueues it at the back of the queue once it is fixed or judged flaky, or hands it back here with what it needs.

(written by quest land watch)

@moq-bot
moq-bot Bot enabled auto-merge November 7, 2026 02:48
@moq-bot

moq-bot Bot commented Nov 7, 2026

Copy link
Copy Markdown

quest land watch: armed auto-merge on this pull request at f952a04, so it joins the back of the merge queue once its required checks pass, with main at 91a905c merged in by the quest merge driver.

(written by quest land watch)

@moq-bot
moq-bot Bot added this pull request to the merge queue Nov 7, 2026
Merged via the queue into main with commit 324a7b7 Nov 7, 2026
7 checks passed
@moq-bot
moq-bot Bot deleted the quest/m2/ts-model/unit-option-fields branch November 7, 2026 02:57
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