How Glyph is hardened

We don't just claim Glyph is safe. We attack it.

A language whose whole pitch is “your agents can't quietly break it” has to earn that. So we run fleets of AI agents that role-play skeptical engineers (a frustrated React developer, a backend contrarian, a safety adversary), have them build real apps and try to break the guarantees, then fix everything they surface. Here is that loop, and exactly what it found.

Five bugs that pass tsc --strict

Each is two files: a .ts that type-checks clean under tsc --strict, and the same program in Glyph that does not compile. A build gate re-runs all five and fails if either half stops behaving, so a guarantee that quietly disappears takes the build with it.

python3 scripts/check_catches.py
catches: 5/5 verified both ways (verifiability: 5, greppability: 0, abstraction: 0, diff stability: 0).
TypeScript checked with tsc --strict and @types/node 26.6.6.
Glyph saysWhat TypeScript let throughFiles
E0200A case added to a union, and one handler that forgot itcatches/non-exhaustive-match/
E0224A read from a map, typed as if the key were always therecatches/absent-map-key/
E0230A string split into two names, when it may have no separatorcatches/split-into-a-pair/
E0304A validator that reports success for a socket it never checkedcatches/unverifiable-parse/
TS2322A parsed body assigned to a typed name without being checkedcatches/unvalidated-boundary-read/

All five are verifiability. None of them is evidence for greppability, abstraction or diff stability, and the gate prints those zeros. Four earlier cases were dropped before the launch because TypeScript catches them too, or because the two halves were not the same program; catches/README.md says which and why.

The map read is refused even when the key is spelled right, because a Record<string, string> says nothing about which keys exist. TypeScript catches it with noUncheckedIndexedAccess, which --strict does not turn on. The missing union case can be caught in TypeScript too, with typescript-eslint's switch-exhaustiveness-check or an assertNever default, if the project adopts them. In Glyph neither check is optional.

The last one is refused by tsc, not by Glyph's own checker, which misses it today (G289). glyph build type-checks the TypeScript it emits, and there json.parse returns a Result, not a User. TypeScript's JSON.parse returns any, and the annotation on user launders it.

The adversarial loop

Every finding is reproduced against the compiler, fixed at the root, and independently re-verified before it counts.

1

Explore

Persona agents build real programs (forms, backends, stateful UI) and record every place the language fought them.

2

Reproduce

Each finding is minimized to the smallest failing program and re-run against the compiler. Anything that can't be reproduced is dropped.

3

Fix

Root-cause fix in the Rust compiler with a regression test that fails before and passes after. The full suite must stay green, or the change reverts.

4

Verify

An independent pass re-runs every repro against the freshly built compiler, catching “the agent thought it was fixed” gaps before anything ships.

The ones that mattered most: silent-correctness holes

Each of these compiled clean and could ship wrong code that passed the type check: exactly the class of bug Glyph exists to prevent. Every one is now caught at compile time.

Critical, fixed

Nullary error variants in a Result match

A match over Result<T, Empty | BadQty(...)> lowered to two duplicate case "Err" branches; the first swallowed every error and silently dispatched to the wrong arm.

Now: one grouped case with a correct inner switch; each variant routes right.

Critical, fixed

Misspelled constructor as a silent catch-all

A typo'd variant in a match arm (Loded for Loaded) was silently bound as an irrefutable catch-all, masking non-exhaustiveness and misrouting at runtime, with no diagnostic.

Now: an unknown constructor is rejected at the typo, like a real name.

High, fixed

Partial bool match slipped past exhaustiveness

match n > 0 { true => ... } (missing the false arm) compiled and passed tsc, then threw at runtime.

Now: a compile-time non-exhaustive-match error.

High, fixed

Multi-parameter components mis-bound props

component C(a, b) emitted positional params, but React calls components props-first, so a silently bound to the whole props object. It passed tsc.

Now: a clear compile error pointing to the single props-record form.

High, fixed

JSX <match> skipped exhaustiveness

A JSX <match> with a missing <case> compiled green and threw at runtime, unlike value-level match, which was checked.

Now: the same exhaustiveness check applies; a missing case is a compile error.

High, fixed

Record payload bound to the wrong field

Binding a record-payload variant to a name (BadQty(b)) read a nonexistent .value off the flattened payload.

Now: the payload binds correctly, so b.sku works.

Rules the checker now enforces

Soundness the compiler used to leave to tsc, now caught by Glyph itself.

Fixed

Call arity

Too few or too many arguments were accepted (and ran to NaN). Now E0213 rejects a wrong argument count.

Fixed

The ? error type, across the stdlib boundary

The exact-error-type rule was skipped when the error came from a stdlib wrapper. Now it's enforced there too.

Fixed

Aliasing an owned handle

let g = f on a resource handle created a second binding that could double-close. Now rejected (E0215).

Fixed

Unreachable arms & deeper validation

An arm after a catch-all is now flagged; json.parse<T> now structurally validates Record fields instead of only checking presence.

First-contact friction, removed

The small things that made a bad first impression, none of which served any purpose.

Fixed

Accessibility & data attributes

aria-label and data-* parsed as subtraction. Now they work, and emit as proper React props.

Fixed

npm package names

Hyphenated and scoped specifiers (react-hook-form, @hookform/resolvers/zod) were rejected. Now they import directly.

Fixed

Browser-bundle first run

The runtime bootstrap wasn't loaded in a browser build, crashing on first run. Every emitted module now pulls it in.

Fixed

Error messages that misdirect

Writing if now points at match; a range pattern says so directly; JSX whitespace and a bare $ in text are handled.

Still open: on the record

Hardening means naming what isn't done, too. These are known and tracked, not hidden.

Deliberately deferred or awaiting a design decision:

Rejecting implicit-any closures in Glyph's own checker needs contextual inference to avoid false positives on validly-typed callbacks. Ergonomic FFI, using a complex-typed TS library without a per-library adapter, is the largest interop question still open, tracked in open questions. A map that arrives from another module or the standard library is still read unchecked, so E0224 does not yet reach a sqlite.Row, and the two-binding for i, x in xs still loses its element types.

← See the engineer questions these fixes came from