Skip to content

Inline anonymous variants borrow their type from an unrelated candid type name #172

Description

@marc0olo

An inline anonymous variant takes its TypeScript type from whichever named candid type declared the same tags first. Interning follows env.to_sorted_iter(), so "first" means alphabetically first — and a method's published type therefore depends on the names of unrelated candid types.

Reproduction

type RepairStatus          = variant { delivered; inProgress; ready; received };
type SoftwareServiceStatus = variant { delivered; inProgress; ready; received };

service : () -> {
  peek : () -> (variant { delivered; inProgress; ready; received }) query;
}
peek(): Promise<RepairStatus>;

peek returns RepairStatus only because R sorts before S. Adding an unrelated type AaaStatus with the same tags silently changes peek's return type, breaking callers with no diagnostic. The .did edit that causes it is semantically irrelevant.

The decision

Should an inline anonymous variant's type be derived from its own tags, or borrowed from a named type that happens to match?

Borrowing is what creates the coupling; deriving is what makes it stable. Everything below follows from that one choice.

Options

peek returns stable interop with a same-valued named enum
A — its own Variant_<tags> enum Variant_delivered_inProgress_ready_received yes neither direction
B — a string-literal union "delivered" | "inProgress" | "ready" | "received" yes both directions
C — keep borrowing, document it RepairStatus no full

A is the obvious fix and the weakest. TypeScript enums are nominal, so it trades an unstable-but-usable type for a stable-but-isolated one — const s: RepairStatus = await actor.peek() compiles today and would not afterwards:

error TS2322: Type 'Variant_delivered_ready' is not assignable to type 'RepairStatus'.

It also adds a declaration whose members duplicate an existing enum.

B derives the type from the tags, so it is stable, and a string enum member is assignable to its literal type and back — RepairStatus.ready still flows in and out. It removes a declaration rather than adding one. The cost is that the type stops being a named thing consumers can import; they get a structural union.

C is defensible on frequency: the hazard needs an inline anonymous variant, a named type with identical tags, and someone later adding another such type.

Notes

Pre-existing — the previous HashMap-keyed interning had the same insertion-order dependence. #173 restructures this code and keeps the behaviour deliberately, so it neither fixes nor worsens it.

Whichever of A or B is chosen is a change to a published type for every anonymous variant, which is a wider blast radius than an ordinary bug fix and belongs in its own PR.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions