Skip to content

Read typescript-go's unreadable match-or-empty unions through their widened members; compile tuple spreads, Stats arrays, primitive then handlers and fielded error inspection - #1516

Merged
ctate merged 2 commits into
opt/error-subclass-headerfrom
opt/bc-r3-misc
Oct 20, 2026
Merged

ctate merged 2 commits into
opt/error-subclass-headerfrom
opt/bc-r3-misc

Conversation

@cramforce

@cramforce cramforce commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

The typescript-go panic behind s.match(re) ?? []

In s.match(re) ?? [] (and || []), the empty literal is contextually typed by the tuple-like match result. typescript-go clones an empty tuple as an array literal, and the clone keeps the tuple flag without tuple data. The API then panics whenever it serializes the union's members (interface conversion: checker.TypeData is *checker.TypeReference, not *checker.TupleType, in internal/api/proto.go). An earlier fix kept the panic from failing the whole program, but every statement that needed the members still reported SC0004.

When getTypesOfType panics, the constituent read now asks for the union's widened type instead. Its members are readable and describe the same values, so they become the union's members. If widening does not help, the existing behavior stays. The empty tuple beside a RegExpMatchArray/RegExpExecArray is one of the match array's own values, as it already was beside a plain array. With both changes, const matches = text.match(re) ?? [] is a plain match array, and checker-panic-tuple.ts now compiles and moves to the corpus.

Smaller gaps

  • Tuple spreads: [...TUPLE, ...rest], where TUPLE: [string, string, string, string, string], copies the tuple's positions into the literal's element type.
  • fs.Stats arrays: fs.Stats is now an array element kind. Stats are refcounted handles, so arrays use their retain/release adapters and need no tracing. await Promise.all(files.map((f) => stat(f))) and stats.reduce(...) compile.
  • Primitive constructors as then handlers: .then(String), .then(Number) and .then(Boolean) over a number or boolean settled value convert it through an interned closure. A string settled value already fit the constructors' value form and keeps its old path.
  • Error subclasses with fields: util.format(err), %s and util.inspect of an Error-typed value were refused when any error subclass in the program declares fields. Now, for each fielded subclass (most-derived first), an instanceof arm prints the stackless error base, then the own properties, using the same single-line/multi-line rule as any object, counting the base: [Bailable [Error]: m] { bail: true }.
    • The header applies Node's constructor-name rule (Bailable [Error] vs FetchFailedError).
    • Beyond the depth budget, an instance prints [Name].
    • A subclass whose own fields cannot render (functions, or a field that Node's error inspector treats specially) throws a named refusal on its own instances only.
    • This is stacked on Inspect program-declared Error subclasses with Node's constructor-name header #1520, which renders field-less subclasses with the same header. Field-less classes in a hierarchy that also has fielded subclasses get header-only arms here, so class Plain extends Error {} prints [Plain [Error]: m] and not [Error: m]. 6525 covers this.

Evidence

  • New corpus programs, each refused on main and matching Node byte for byte in the plain and sanitized lanes:
    • 6520 match-or-empty-widened
    • 6521 tuple-spread-into-array
    • 6522 stats-arrays
    • 6523 then-primitive-constructors
    • 6525 error-subclass-fields
    • 6524 match-or-empty-tuple (the former checker-panic-tuple.ts)
  • Zero-cost gate against origin/main e1c2717: PASS. 2550 programs are identical. 4642-acyclic-contract.ts fails on both sides, as it already does on main. Self-hosted compile time -0.01%. The bench timing interval came out inconclusive, but every program is byte-identical, so the difference is noise.
  • After the rebase onto Inspect program-declared Error subclasses with Node's constructor-name header #1520 (256b7cd), the zero-cost gate against that base also passes: 2550 programs are identical, plus the same both-fail. 6520 to 6525 and 6530 pass in both lanes.
  • pnpm lint and pnpm format:check are clean. pnpm test:ts7 --baselines-only passes, with the new baselines recorded and the moved fixture's baseline moved. diagnostics.test.ts: 203/203.
  • The self-hosted compiler's IR build reports no diagnostics, and tests/harness/self-hosting-native-driver.test.ts passes.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
scriptc Ready Ready Preview, v0 Oct 20, 2026 4:37pm UTC

Comment thread packages/compiler/src/frontend/lowering/lower-inspect.ts Outdated
…idened members; compile tuple spreads, Stats arrays, primitive then handlers and fielded error inspection

- `s.match(re) ?? []` / `|| []` made typescript-go's API panic whenever the
  union's members were serialized (the empty literal is a tuple clone
  without tuple data), poisoning the statement with SC0004. The members
  of the union's widened type are readable and describe the same values,
  so constituent reads answer with them; the empty tuple beside a match
  array is one of its values. `checker-panic-tuple.ts` compiles and moves
  to the corpus.
- Spreading a fixed tuple into an array literal copies its positions
  into the literal's element type.
- fs.Stats values are an array element kind, so `Promise.all` over
  `stat()` promises and `Stats[].reduce` compile.
- `.then(String)`, `.then(Number)` and `.then(Boolean)` over a number or
  boolean settled value convert it through an interned closure.
- Inspecting (util.format, %s, util.inspect) an Error-typed value whose
  runtime class declares fields prints the stackless base with Node's
  constructor-name header (`[Bailable [Error]: m]`) and the own
  properties, `[Bailable [Error]: m] { bail: true }`; it was refused. A
  subclass whose own fields cannot render (functions, or a field Node's
  error inspector treats specially) throws a named refusal on its own
  instances only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@cramforce
cramforce changed the base branch from main to opt/error-subclass-header October 20, 2026 12:41
…heir cycles as Node does

fieldedErrorReason checked each field of a fielded error class with a
fresh inspectSupport walk, so a field typed as the class itself
(`next: Chain`) recursed until the compiler's stack overflowed. It now
continues the caller's walk, where the class is a recursive shape.

The fielded error helper then rendered a cycle by expanding it to the
depth budget, where Node prints `<ref *1>` and `[Circular *1]`. Classes
whose fields reach the class or one of its bases now run the circular
machinery other cycle-capable objects use (circCheck before the depth
answer, seenPush, refWrap), which also covers a cycle through a
base-typed field of a subclass. New corpus program
6526-error-subclass-cycles.ts.

6187-js-match-or-empty-array.js compiles without its deferred fence now
that the match-or-empty union reads through its widened members: the
`@deferred-fences: 1` header goes, and the program calls the function
the fence used to cover.
@ctate
ctate merged commit 2924a61 into opt/error-subclass-header Oct 20, 2026
61 of 64 checks passed
@ctate
ctate deleted the opt/bc-r3-misc branch October 20, 2026 16:41

This branch was successfully deployed

1 active deployment
Preview — a48464a1 Deployed Oct 20, 2026 by vercel[bot]
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.

2 participants