Semantic Diff

#9213 TScert: print nested options by depth and polymorphic option code as overloads under -bs-emit-ts

3 commits

8a1bb7e7fe6db45ea59d3408535c2cc85a58c089..pr-9213

7c88be881 · cristianoc + Claude · Nov 04, 2026, 06:01 UTC

Print nested options by depth and polymorphic option code as overloads

Why

A nested match and a polymorphic function at a nested option failed tsc under -bs-emit-ts. The marker arm printed every depth as number, { readonly BS_PRIVATE_NESTED_SOME_NONE: number }, so no true declaration of valFromOption gave the payload back: at depth 0 it returns undefined, at depth d > 0 the marker of depth d - 1. And a function whose type parameter is an option's payload was bounded to the primitives, so getOr at 'a = option<int> (NestMatch.useNested) and Stdlib_Option.getOr at that instance (Memo.cached) had no signature.

modifiedRepresentation option under -bs-emit-ts

representation
Before
After
option<option<int>> ↦ undefined | number | { readonly BS_PRIVATE_NESTED_SOME_NONE: number }option<unit> ↦ undefined | void | { readonly BS_PRIVATE_NESTED_SOME_NONE: number } option<option<int>> ↦ undefined | number | { readonly BS_PRIVATE_NESTED_SOME_NONE: 0 }option<option<option<int>>> ↦ undefined | number | { …: 0 } | { …: 1 }option<unit> ↦ undefined | void | { readonly BS_PRIVATE_NESTED_SOME_NONE: 0 }option<'a> in a type declaration, or at an abstract payload ↦ undefined | a | { …: number }
ruleSome(v) is the marker of depth 0 when v is undefined, of depth d + 1 when v is the marker of depth d, and v otherwise (Primitiveoption.some); the depths are read off the payload's encoding, an option's, unit's, or a type that cannot hold undefined (no marker)

modifiedJS -bs-emit-ts polymorphic option code

let getOr = (o: option<'a>, d: 'a) =>  switch o {  | Some(v) => v  | None => d  }
js
Before
After
export function getOr<a extends string | number | boolean | bigint>(o: (undefined | a), d: a): a { export function getOr<a extends string | number | boolean | bigint>(o: (undefined | a), d: a): a;export function getOr<a extends string | number | boolean | bigint>(o: (undefined | ( (undefined | a) | { readonly BS_PRIVATE_NESTED_SOME_NONE: 0 })), d: (undefined | a)): (undefined | a);export function getOr<a extends string | number | boolean | bigint>(o: (undefined | ( (undefined | a) | { readonly BS_PRIVATE_NESTED_SOME_NONE: 0 })), d: (undefined | a)): (undefined | a) {

modifiedJS -bs-emit-ts a function returning undefined alone

let useNestedFolded = () => getOr(Some(None), Some(3))
js
Before
After
export function useNestedFolded(): (undefined | number) {export function useNestedFolded(): (undefined | number) { return;}}

The examples NestMatch (nested matches, getOr at a nested instance) and Memo (Stdlib_Option.getOr at 'a = option<int>) join the build test.

Review

  • Why is each overload true? It is the function's signature at the instance 'a := option^k<a>, k = 0 or 1, with a bounded to the primitives as before, printed by the same rule: the function's code is the code of every instance. The signature the body carries is the deepest instance's, which no caller sees; tsc checks the body there.
  • Why no conditional type for Some<A>? Some<A> at an unbounded A is true only if no value but the runtime's markers owns the key BS_PRIVATE_NESTED_SOME_NONE; a record type admits values owning further keys. The overloads hold at primitive payloads with no assumption.
  • What do the runtime declarations state? Primitive_option.some and valFromOption, and Stdlib_Option.getOr, one signature per nesting of a primitive payload, up to an option of an option; each line audited against Primitive_option.res and Stdlib_Option.res. Stdlib_Option.map keeps its primitive signature.
  • Does this change genType's output? No: the marker hook answers "no marker" outside -bs-emit-ts, and the gentype tests' outputs are unchanged.
bd978bda4 · cristianoc + Claude · Nov 04, 2026, 06:11 UTC

Print a marker of any depth at a payload that may own the marker key

Why

The depth rule read "cannot hold undefined" as "is no marker". A record naming a field BS_PRIVATE_NESTED_SOME_NONE at run time satisfies the first and owns the key Primitive_option.some reads: with x = {BS_PRIVATE_NESTED_SOME_NONE: 0, tag: "payload"}, Some(x) at option<option<keyed>> is the marker of depth 1, which the printed undefined | keyed | { …: 0 } excludes (a review found it and ran it).

modifiedRepresentation option under -bs-emit-ts, payloads that may own the key

representation
Before
After
option<option<keyed>> ↦ undefined | keyed | { readonly BS_PRIVATE_NESTED_SOME_NONE: 0 } option<option<keyed>> ↦ undefined | keyed | { readonly BS_PRIVATE_NESTED_SOME_NONE: number }
rulea payload is no marker only where it cannot hold undefined and may own no key BSPRIVATENESTEDSOMENONE: a record or an inline record naming that field at run time, an object type, and a variant case whose payload may, may own it

Review

  • Does the example fail tsc? keepKeyed passes the value through and type-checks; wrapping such a payload calls Primitive_option.some, which no runtime declaration states at a non-primitive payload, and fails tsc.
dc30f4e3b · cristianoc + Claude · Nov 04, 2026, 08:16 UTC

Record tsc's diagnostics for the emit_ts examples: none with overloads

Why

Printing nested options by depth and polymorphic option code as overloads gives NestMatch's nested match and its getOr at option<int> a signature tsc accepts, so the four errors at the primitive bound are gone.

modifiedTests tests/build_tests/emit_ts/tsc.txt

empty