Project Status
Typhon is in active development, and has reached its first beta.
The current release is v1.0.0-beta.2 (2026-10-04), the first published beta: v1.0.0-beta.1 plus a Windows build fix (beta.1’s release build failed on Windows, so it was never published).
The v1.0.0-alpha milestone brought the proven production surface together with the type-system frontier earlier releases deferred — higher-kinded type unification, user-generic variance inference, and the general inter-procedural field-init audit. The alpha.2 → alpha.9 point releases since have been a hardening pass across five fronts:
- Soundness (alpha.2, alpha.4). Flow-narrowing invalidation across calls and alias writes, short-circuit narrowing fixes, a batch of newly-typed positions, three conservative diagnostics (
not_a_context_manager,raise_non_exception,frozen_inheritance_conflict), and the H5 scope-blind class-unification fix. - Robustness & release engineering (alpha.3). A repository-root MIT
LICENSE, the vendored Ruff notice,SECURITY.md/CONTRIBUTING.md/ Dependabot, CI-gated release tagging, linear nested-generic assignability, six VM ↔ CPython parity fixes, a 256 MiB LSP stack, atomictyc fmt, and theTYC_NO_INTROSPECTkill-switch. - Performance (alpha.5). VM performance Tier 1 (
tyc run) — an allocation-light two-representation integer, method-dispatch caching, slot-resolved locals — plus the[optimise]profile /tyc build -O, seven advice-onlyperflints, a free-threading parallelisation wave, and native PEP 810 lazy imports on 3.15 targets. - Maintenance (alpha.6, alpha.8, alpha.9). The July dependency wave carried safely across the
toml0.8 → 1.x major, six more secret-name lint keywords, release-pipeline re-pinning, and docs / repo hygiene (alpha.6); then a VM ↔ CPython parity fix — unseededrandomnow seeds from entropy undertyc run, as CPython does, instead of from a fixed constant, while explicitrandom.seed(n)still matches CPython byte-for-byte — plus a widening of the warn-level secret-name lint to digit and TitleCase boundaries, two allocation reductions on AST walks, the early-August dependency wave, and docs-site transition polish (alpha.8); then a large widening of that same warn-level secret-name lint — its keyword table grows from 16 entries to 55, consolidating seven overlapping proposals into one longest-first-ordered table — three more compiler allocation reductions, the mid-August dependency wave, and docs-site keyboard-accessibility and reduced-motion polish (alpha.9). - Codebase-review remediation (alpha.7). All ten 1.0 blockers from the 2026-07-28 full-codebase review closed, plus the Tier-0 verification gates: instance-attribute type-checking, reverse-MRO constructor field order, keyword-only model constructors, terminating recursive type aliases, parametric sealed-union exhaustiveness, a new warn-level nullable-field-dereference check,
let-immutability enforcement in loops and throughglobal/nonlocal, a shared-lexical-mask preprocessor rewrite, five emitter/preprocessor miscompilation fixes, VMExceptionGroup/except*support (PEP 654) plus five other VM ↔ CPython parity fixes, and two new CI gates (a full-corpus VM ↔ CPython differential baseline and an opt-in-knob codegen matrix).
v1.0.0-beta.1 is the first beta: four review-remediation waves on top of alpha.9 — the 2026-09-01 beta-readiness review and the backlog it deferred, the 2026-09-30 release-readiness review, and the W1–W7 remediation of the six 2026-10-03 full reviews. They close the ways a check-clean program could still crash, miscompile, or behave differently under tyc run than under CPython: field narrowings dropped wherever a call or write can reach them, loops that join their exit paths, match exhaustiveness over Result payloads, T?, bool, literal unions and nested sealed unions, an inline ? that keeps Python’s evaluation order, |> in every expression position, C3 method resolution and CPython-ordered sets on the VM, and an automatic CPython fallback for programs the VM does not model. There is no new syntax, and the new error-level diagnostics (alias_not_a_class, invalid_pattern, impl_forward_reference, and reserved_module_name when the program could not start) fire only on code that already crashed. One documented exception changes a default: [strictness] nullable-use is now "error".
What this means for you:
- The production path (
tyc build→ CPython 3.13+) is stable and carries no runtime dependency on the toolchain. - The language is additive on correct programs across the whole v0.3.0 → v1.0.0-alpha line — every program that type-checked and ran correctly continues to behave identically. (A few deliberate diagnostics reject only code that already crashed at runtime.)
- As of
v1.0.0-beta.1the surface listed in the compatibility policy is frozen for the beta line: those forms keep their syntax and meaning in every beta release, and a deprecation or breaking change follows the policy’s warn-first process. The policy also lists beta.1’s deliberate exceptions, chiefly[strictness] nullable-usenow defaulting to"error". - Still deferred: embedded in-process
ty(the Phase 1 subprocess path ships), typeshed-backed checking for pure-extension libraries, and the function-level HKT tail.
The sections below give the recent-release detail; the full release-by-release history lives in the changelog.
The canonical per-feature status lives in the Roadmap. This page is a quick summary.
Landed in v1.0.0-alpha (first feature-complete alpha)
Typhon’s first tagged alpha and first feature-complete milestone — the proven production surface plus the type-system frontier earlier releases deferred. Rolls up milestones M1 (“Tidy & deepen”) and M2 (“Type-system frontier”) of the alpha release plan, the early M3 polish (formatter idempotence, a performance-regression CI gate), and the rescue exception-boundary sugar. Additive on the accepted surface — every previously-accepted program type-checks identically.
Added — type-system frontier
- ✅ Higher-kinded type unification. A constructor variable (
Finclass Functor[F[_]]:/interface Functor[F[_]]:) now binds against a concrete head —F[A]againstlist[int]bindsF = list, A = intand substitutes in the return type, in-module and across module boundaries. Wrong arity or a conflicting constructor binding emits the newtyc::kind_mismatchdiagnostic. (Function-leveldef f[F[_]]params, non-class application, and constructor composition remain deferred.) - ✅ User-generic variance inference. Each user-declared class type-parameter is classified from its usage — output-only → covariant, input-only → contravariant, both → invariant — and consulted in assignability, so
class Producer[T](T in returns only) acceptsProducer[Dog]whereProducer[Animal]is expected. A bare@covariant/@contravariantclass decorator overrides the inference. Variance propagates across module boundaries. - ✅ Sound variance through generic interface bounds (closed a soundness hole, not just a relaxation).
- ✅ General inter-procedural field-init audit. A per-function summary tracks partial-instance escapes across helper chains, so a partially-initialised instance escaping a non-trivial chain fires
tyc::missing_field_init(no corpus false positives). - ✅ 2-member non-nullable unions (
Union[str, bytes],str | os.PathLike) are modelled at the introspection boundary with sound widening.
Added — boundaries, tooling & polish
- ✅
rescueexception-boundary sugar (postfixEXPR rescue e: ERRand a block form) — lambda-free,try/except-free bridging intoResult, checked against the function’s declared error type. - ✅
Annotated[T, …]is type-checked through third-party introspection (catches wrong-typed FastAPI / Typer / Pydantic kwargs). - ✅ Cross-file go-to-definition for import aliases and
.pysiblings;tyc::stdlib_module_shadownow fires ontyc build;tyc migrateno longer emits invalidmut else:. - ✅ Idempotent, semantics-preserving
tyc fmtand a performance-regression CI gate.
Compatibility & stability
The production path (tyc build → CPython 3.13+) is stable and carries no runtime dependency on the toolchain. As an alpha, the surface syntax is not yet frozen — it may change before 1.0.0 with a documented migration note (no semver stability guarantee on the language surface yet). Deferred to beta: embedded ty Phase 2 (the Phase 1 subprocess path — [checker] external = "ty" / --with-ty — ships), typeshed-backed checking for pure-extension libraries, tyc migrate hardening on the full PyPI set, and the function-level HKT tail.
Landed in v0.15.3 (tooling), v0.15.2, v0.15.0 / v0.15.1
v0.15.0 is a feature release sharpening Typhon at the library boundary, driven by a field report from building a real async app. v0.15.1 is a strict performance + docs-site point release on top of it — no language or API changes.
Added — v0.15.0
- ✅
as!composes in any expression position.EXPR as! TYPEnow lowers structurally (a bracket-, string-, and comment-aware fixpoint rewrite) instead of line by line, so a checked cast works nested in call arguments (save(row[0] as! int, label)), inside comprehensions / collection literals, across a multi-line value expression, and in statement conditions (if raw as! bool:). v0.14.0 only accepted the single-line value-position form. The VM intercepts__typhon_checked_cast__before argument evaluation, so a cast to a union / parametric type (x as! int | None,d as! dict[str, int]) runs undertyc run. - ✅
try_result(thunk[, on_err])exception→Result combinator. Bridges a library boundary into aResultin one expression instead of a hand-writtentry: return Ok(x) except E as e: return Err(...). It runsthunk()and returnsOk(result); on any exception it returnsErr(on_err(exc)), orErr(exc)when no mapper is given. A prelude name (no import in source, likeOk/Err/Result), typedResult[T, E](Tfrom the thunk,Efrom the mapper,Exceptionwhen omitted).tyc buildauto-injectsfrom typhon_runtime import try_result; the VM registers it as a prelude native. - ✅ Compiler-bundled
.dtystubs (httpx, requests).tycships curated, embedded stubs for popular libraries whose packaging defeats venv introspection, seeded into the project shape map before venv enrichment — so an imported bundled library is shaped out of the box, its construction is type-checked, and itsunintrospectable-dependencywarning is suppressed, with no.venvortyc syncrequired. Gap-fill, not override: an authored project.dty/.tywins, and the bundle takes precedence over venv introspection. Lives intyc-db::seed_bundled_stubs. - ✅
async_without_awaitunderstands async contracts. An awaitlessasync defis no longer warned when it is async only to honour a contract it can’t opt out of — implementing an asyncinterfacemethod or overriding an async base-class method. Gated on the interface method being async, so anasyncimpl of a sync method still warns.
Fixed — v0.15.0
- ✅ Qualified cross-module class references unify with their bare form.
import httpx; let r: httpx.Response = client.get(...)no longer mismatches the method’s bareResponsereturn. Two class types whose final.-separated segments match unify when at least one side is bare; two different qualified classes stay distinct (httpx.Responseis not assignable torequests.Response).
Changed — v0.15.1 (performance + docs-site accessibility)
- ✅ Source-map generation is now O(N log N) (was O(N²)).
- ✅ A
Result-exhaustiveness hot-path allocation removed. - ✅ Docs-site accessibility: a keyboard-focus ring and an anchor-target highlight animation, with a
prefers-reduced-motionfallback.
Landed in v0.14.0 (and point releases)
v0.14.0 added the as! checked boundary cast and opt-in traceback remapping; v0.14.1–v0.14.3 layered cross-module shape-propagation completeness, async-gather advice, and live LSP config refresh on top.
Added — v0.14.0
- ✅
as!checked boundary cast. A sound one-line replacement for theunsafe:-plus-re-assert dance. Lowers tochecked_castintyphon_runtime/cast.py, which performs a recursive structural shape check and raisesTypeErroron mismatch (int→floatwidening honoured). (In v0.14.0 the cast was value-position, single-line only; v0.15.0 generalised it to any expression position.) - ✅
[emit] traceback-remap(default off). Injectstyphon_runtime.traceback.install()into the entry__main__block so an uncaught exception’s traceback is auto-rewritten to.tyvia the.py.mapsidecars — the automatic counterpart to the manualtyc trace.
Added — v0.14.1 (cross-module shape-propagation completeness)
- ✅ Imported
newtypes widen to base, transparenttypealiases unwrap,enumexhaustiveness carries across module boundaries, andfrozen-class write soundness holds cross-module. What already worked within a module now works for imported declarations.
Added — v0.14.2 (async-gather advice)
- ✅
tyc::gather_opportunity(advice, default on). Flags runs of 2+ adjacent independent awaits and suggests an explicitgather:. - ✅ Cross-module
auto-gather. Now folds imported@gatherablecallees. - ✅ Advisory lints surface live in the editor (LSP).
Changed — v0.14.3 (live LSP config refresh)
- ✅ LSP refreshes diagnostics live on
typhon.tomledits viaworkspace/didChangeWatchedFiles;[strictness]knobs are cached per project root by mtime.
Landed in v0.13.0 (cross-module extend, dict-literal lowering, enum exhaustiveness, Result API)
A fresh adversarial sweep (~35 programs run through both tyc run and tyc build + CPython with output diffing) surfaced two silent-wrong-output defects on documented features, several type-system blind spots, and a batch of VM coverage gaps. All closed here; the workspace suite and the 254-file example corpus stay green. v0.13.1 / v0.13.2 are playground stress-round point releases on top (method-call match, multi-line ?, gather:-in-match import injection, fmt round-trips; async await-propagation, Task await-unwrap, VM project run, pub enum).
Fixed — silent wrong output
- ✅ Cross-module
extend ClassName:silently dropped its methods. The desugar merge only handled same-file targets; an imported target’s methods were discarded, so a clean build crashed withAttributeErrorat first call. Foreign-target blocks now lower in place to module-level functions plus class-attribute patches, which work onslots=True/ frozen dataclasses and third-party classes alike.check,run, and the VM all agree again. - ✅ TypedDict-style dict literals against a
class/modelannotation now lower.let u: User = {"id": 1, "name": "ada"}has type-checked since v0.3.0 but emitted the raw dict —u.namethen crashed at runtime. An early desugar pass rewrites the literal toUser(id=1, name="ada"), recursing into class-typed fields for nested initialisation.
Added — language / checker
- ✅ Enum
matchexhaustiveness. An enum’s member set is a closed set: covering every member satisfies the return-path analysis, and a missing member firestyc::non_exhaustive_matchnaming it. - ✅ The
Resultunwrap / query family.unwrap(),expect(msg),unwrap_or(default),unwrap_or_else(f),ok()(→T?),err()(→E?),is_ok(),is_err()onOk/Err/Result. TheResultmethod surface is now closed — an unknown method firesattribute_not_foundat check time (previously.unwrap()passed the checker and crashed at runtime). - ✅ Exhaustiveness for expression scrutinees.
match items[-1]:over a sealed union runs the same analysis as a plain-name subject. - ✅ Match-arm scrutinee narrowing.
case Action(_, _):narrows the subject variable toActioninside the arm. - ✅ Recursive type aliases.
type Json = None | bool | int | float | str | list[Json] | dict[str, Json]is now legal —tyc::cyclic_type_aliasonly fires for a cycle with no type constructor anywhere (no base case). Container literals resolve element expectations through aliases and unions, so nestedJsonliterals check. - ✅ Quoted annotations resolve as forward references.
next: "Node",-> "Tree[T]","list[Node]". The literal-singleton union form (type Color = "red" | "green") is unchanged. - ✅ Generic interface conformance.
MemRepo[int]structurally satisfiesinterface Repo[T]asRepo[int]. - ✅ Lambda arity checking. Lambdas infer as
Type::Functioncarrying their arity, soapply(lambda x: x)againstCallable[[int, int], int]is a check-time mismatch. - ✅ New warn lints:
tyc::mutable_default_param,tyc::is_literal_comparison,tyc::incompatible_override,tyc::loop_closure_capture.
Added — VM, CLI & emit
- ✅ VM cooperative asyncio.
tyc runexecutesasync def/await/gather:(incl.return_exceptions=True) /asyncio.run/TaskGroup.create_task/wait_for/spawn(thegolowering) natively through a cooperative-sequential scheduler, with output identical to CPython unless correctness depends on task interleaving. - ✅ VM CPython-style traceback frames pointing directly at the
.tyfile (previously the traceback carried no frames at all). - ✅ CPython-exact
random— a faithful MT19937 so seeded programs produce byte-identical sequences acrosstyc runandtyc build+ CPython. - ✅
tyc migrateenhancements — relocates class-body methods intoimplblocks, rewritesclass X(Enum):to theenumkeyword, simplifiesfield(default_factory=...)to bare-literal sugar, and prunes the imports those rewrites orphan; migrated output checks with zero errors and zero warnings. - ✅ Emitted Python imports the
collections.abcnames it uses (Iterator,Sequence,Callable, …), so runtime annotation resolution (typing.get_type_hints, FastAPI DI, pydantic) no longer raisesNameError.
Shipped (Phases 0 – 3 + Phase 5 complete)
Core compiler
- ✅ Cargo workspace with the crate-per-stage layout (
tyc-syntax,tyc-db,tyc-resolve,tyc-types,tyc-analyse,tyc-desugar,tyc-emit,tyc-format,tyc-diagnostics,tyc-lsp,tyc-vm,tyc). - ✅ Salsa-backed incremental queries (
preprocessed_text,module_decl_names,resolved_module). - ✅ Vendored Ruff fork (
ruff_text_size,ruff_source_file,ruff_python_trivia,ruff_python_ast,ruff_python_parser) withlet/mutsoft keywords and aMutabilityfield on assignment AST nodes. Migration offrustpython-parseris complete. - ✅ Diagnostics infrastructure (
miette+thiserror).
Language features
- ✅
let/mutfor binding immutability, with reassignment errors and module-level defaulting. - ✅ Nominal types: function signatures, assignment compatibility, primitives, classes, generic containers.
- ✅ Non-nullable by default with flow narrowing on
is None,is not None,isinstance,guard, early-return. - ✅
T?sugar forT | Nonein annotations. - ✅
class→@dataclass(slots=True);model→ PydanticBaseModel(extra="forbid");frozenmodifier;class!escape hatch for framework bases;plain classfor no-decoration / no-synthesised-__init__semantics; auto-skip forEnum/Flag/ABC/Protocol/NamedTuple/TypedDictsubclasses plus user-configurable[emit] skip-decoration-bases. - ✅
implblocks merged into class bodies at desugar;extendfor cross-module method addition;extend BUILTIN:extracts to free functions with static-receiver rewrites. - ✅ Sealed unions via
type X = A | B, exhaustivematchchecked at compile time. - ✅
Result[T, E]withOk/Errconstructors, the?operator, andwith-chains (with optionalelse err:block).typhon_runtimemodule generated when used. - ✅ Generics via PEP 695 brackets, bidirectional inference with recursive conflict-widening, bounded type vars.
- ✅ Interface declarations lowering to
class Name(Protocol):with structural conformance check on assignment;isinstance(x, Interface)rejected by default. - ✅
unsafe:lexical block, lowered toif True:for scope preservation, withUnsafe[T]boundary marker. - ✅
@pure/@memo/@pure(memo=True)decorators with the six-condition purity check. - ✅
gather:blocks lowering toasyncio.TaskGroup(default) orasyncio.gather(return_exceptions=True)(strategy="best-effort"). - ✅
go f(x)lowered throughtyphon_runtime.tasks.spawnwith a strong-ref registry. - ✅
lazy import np = numpywith a thread-safe proxy class (__TyphonLazy_np_);lazy from x import …rejected at parse time. Module-levellazy let→lazy_let(lambda: ...); class-bodylazy let→@cached_property. - ✅ Pipes (
a |> f() |> g(arg)→g(f(a), arg)); guards (guard x = expr else: ...). - ✅
comptime letbindings withenv(name, default?)lookup and the sandboxed evaluator (literals, arithmetic, comparisons, boolean ops, ternaries,int() / str() / float()casts,if/elif/else,return, local bindings).comptime defuser-defined functions usable from initialisers. - ✅
.dtystub files compiling to PEP 561.pyi;tyc check --stubsAST diff against runtime modules.
Tooling
- ✅
tyc init,tyc fmt,tyc check,tyc build(with--checkdry-run),tyc run(VM default +--compilefallback),tyc lsp,tyc repl,tyc debug(with--break <ty>:<line>source-mapped breakpoints),tyc trace,tyc profile,tyc migrate,tyc stubtest,tyc add/remove/sync,tyc ty,tyc explain,tyc cheatsheet,tyc install skill. - ✅ Source maps (
.py.mapv2 with per-statementout_line → ty_linetables) fortyc trace, cross-file go-to-definition, andtyc debug --break. - ✅
tyc-vm: in-process tree-walking interpreter for.tysource. Default execution mode fortyc run(no.pywritten, no CPython spawn).--compilefalls back to build-then-exec for CPython interop. - ✅
tower-lsp-server-backed LSP: diagnostics, hover, go-to-definition, completion (visible bindings + keywords + builtins + venv-driven member access + from-import members), “Remove unused import” code action. - ✅
tyc fmtwrapsruff formatafter the in-process whitespace pass. - ✅ Diagnostic deep-links: every
tyc::code carries a mietteurl(...)clause; 90 catalog pages underdocs/diagnostics/are embedded into the binary for offlinetyc explainlookup. - ✅ Reference VS Code extension under
editors/vscode/.
Project plumbing
- ✅ CI — nine jobs on every push to
main/dev/**/claude/**:test(cargo fmt --check→clippy -D warnings→cargo test --workspace),test-macos(the test suite on macOS),fmt-guard(no out-of-scopecargo fmtreformats),security(cargo-denyadvisories / licences / source bans),perf-gate(the build-pipeline performance regression gate),differential(the VM ↔ CPython differential gate over the full example + stress corpus),knob-matrix(the opt-in-knob codegen matrix),valid-corpus(tyc checkplus the VM ↔ CPython differential over thecorpus/valid/valid-programs corpus), andfmt-corpus(tyc fmtover a de-formatted copy of the corpus must leave every unit’s emitted Python AST unchanged). - ✅ Generated
typhon_runtime/ships as local source — no PyPI dependency.
Landed in v0.9.0 (stress-test cleanup release)
The stress-test cleanup release on top of v0.8.1. Closes 32 of the 36 findings from a v0.8.1 stress sweep spanning the type checker, VM, parser, lowering passes, diagnostics, and CLI. The VM is now usable as the daily-driver runner the docs always advertised; the type checker plugs silent-correctness gaps in covariance, variant flow, narrowing, and error propagation; the diagnostic surface gets a polish pass.
The release is additive on the accepted surface — every
previously-accepted program continues to type-check, and the new VM
features expand what tyc run accepts rather than narrowing it.
Added — VM coverage (closing the gap between tyc run and tyc build && python build/main.py)
- ✅
Resultcombinators (.map/.map_err/.and_then/.or_else) now work onOk/Errvalues in the VM via boundNativeFnwrappers that capture the receiver. Previously a typecheck-clean program crashed at run-time withAttributeError: Ok has no attribute 'and_then'. - ✅
open()honours write / append / binary modes.open(p, "w")/open(p, "a")/open(p, "wb")/open(p, "r+")and friends now all work.with-blocks honour__enter__/__exit__on the resulting file.json.load/json.dumpride on top. - ✅ Match against built-in class patterns.
match x: case str() as s:/case int() as n:/ etc. now matches; the exhaustiveness pass also recognisescase None:+case str() as s:as coveringstr?. - ✅
frozenset(...)is hashable as a dict key (newHashKey::FrozenSetvariant with insertion-order-independent hashing). - ✅ f-string
_thousands separator emits the same way,does. - ✅
bytesrepr matches CPython.b'hi'(single quotes by default),b"with 'embedded'"fallback,\xNNfor non-printable. - ✅ Native shims for
collections.deque,heapq,contextlib,pydantic. Graph / queue / heap algorithms,@contextmanageridentity decorators, andmodelclass declarations all run cleanly.dequerides onValue::Listvia newpopleft/appendleft/extendleft/rotatelist methods.pydantic.BaseModelis a placeholder so declaring amodeldoesn’tImportError. - ✅
@property/@classmethod/@staticmethod/super()builtins are present as identity-ish stubs so decorated methods no longer crash on import. - ✅
lazy import np = numpyuses the simplerimport M as Nrewrite in VM mode (the descriptor-based proxy class the build path emits has nothing to bind against in a tree-walking VM). - ✅ Multi-file projects run under both
tyc runmodes. The VM loads sibling.tymodules from the project source root, honours relative imports (from .repo import x), and caches each module’s bindings as aValue::Module.tyc run --compilenow spawnspython -m <pkg>.maininstead ofpython build/main.pyso relative imports in the entry point resolve correctly. - ✅
dataclasses.field(default_factory=list)actually invokes the factory per instance. The mutable-default rewrite no longer shares one list across every instance. - ✅
class!synthesised__init__runs.except HttpError as e: print(e.code)works againstclass! HttpError(Exception): code: int; message: str— the handler binds the userInstance, and exception-type matching walks the MRO. - ✅
freeze let CFG = {...}actually freezes (list → tuple, dict → mappingproxy-tagged dict, recursive). Mutators on a frozen dict raise the sameTypeErrorCPython’sMappingProxydoes. - ✅
comptime let X = ...inlines in the VM via the substitution pass shared withtyc build.comptime let PORT = int(env(...))no longer crashes withNameError: env is not defined. - ✅ Typed tuple unpack
let (a: int, b: str) = pair()parses in the VM (parity withtyc check).
Added — type checker
- ✅ Read-view covariance for built-in containers.
list[Subclass]/tuple[Subclass]/set[Subclass]/frozenset[Subclass]flow intoSequence[Super]/Iterable[Super]/Iterator[Super]/Collection[Super]/Container[Super]/Reversible[Super]whenSubclassinheritsSuper. Mapping / MutableMapping coverdict[K, V](K invariant, V covariant). - ✅ Variant → parametric sealed union assignability.
Cons[T]/Cons(wheretype LL[T] = Cons[T] | Nil) is assignable intoLL[T]. Required for recursive ADT walks likemut cur: LL[T] = self. - ✅
while True:reachability. A loop whose body always returns / raises on every branch and contains nobreakis recognised as exiting; the post-loop point is unreachable andmissing_returndoesn’t fire. - ✅ Post-while-loop narrowing. After
while y is None: y = load()(nobreak), the post-loopyis narrowed to non-None. Matches pyright / mypy / pyrefly. - ✅
assert x is not Nonenarrows. The standard Python static- checker idiom now works. - ✅
*args/**kwargsrequire annotations (Rule 1). Canonical idiom is*args: object/**kwargs: object. - ✅
extend list:dispatches onlist[T]-annotated receivers. The synthetic__typhon_builtin_ext_listclass shape is consulted beforeattribute_not_foundfires. - ✅ Exhaustive
matchonT?recognises built-in class patterns.case None: ...; case str() as s: ...againststr?no longer surfacesmissing_return. - ✅
with-chain explicitelse err: return Err(err)validates the error type against the function’s declared return. Previously the check was gated on the synthetic?-op temp shape, so awith-chain could silently return the wrong error class. - ✅
func[T](args)explicit type instantiation now fires a clear check-time error (was: runtime'function' object is not subscriptable). - ✅
comptime let T: type = intlowers to a PEP 695type T = intalias statement soTis substitutable wherever a type is expected.tyc checkalso runs the substitution before parsing the resolved module so check mode sees the same shape as build. - ✅
freeze let X = <expr>validates the freezability of the RHS at check time. Newtyc::freeze_not_freezablefires when the RHS constructs a non-frozenuser class, instead of letting the failure surface as a runtimeTypeErrorat first import. - ✅
pub *name collisions surface intyc check(not justtyc build). The detection logic fromtyc buildis exposed asdetect_pub_star_diagnosticsand called from the check command before the per-file loop so CI catches collisions before they reach build.
Added — diagnostics polish
- ✅
interface_not_conformingarity message now reads “got N non-self parameter(s), expected M” instead of the ambiguous “arity N; expected M”. - ✅
invalid_question_ophelp text mentions both theResult-return cause AND the comprehension carve-out. - ✅ Sealed-union impl distribution dedupe.
impl Alias:over a sealed union duplicates each method body across every variant; the type-checker dedupes diagnostics by(code, rendered message)so a 10-variant union no longer reports 10 identical errors. - ✅
class_attr_shadows_slotno longer false-positives on a class whose only annotated defaults are mutable literals (list[str] = []etc.). Those becomedefault_factoryper-instance fields, not shared constants. - ✅
MissingAnnotationtext drops the double-backtick wrapping (was rendered as`parameter `x“).
Added — language docs
- ✅ Cheat sheet documents
class X frozen(Base):(the modifier comes BETWEEN the class name and the base list) and the*args: object/**kwargs: objectidiom for genuinely variadic functions.
Known limitations carried forward
- ⏳ Preprocess line-number leakage (B15) — diagnostics still
report preprocessed-buffer line numbers for
impl Alias:distribution over sealed unions. The dedupe pass above cuts the count of noise diagnostics, but each surviving diagnostic still points at a synthetic line index past EOF of the original source. Tracked for the next release — needs a proper source-map rewrite through the diagnostic constructors.
Landed in v0.8.1 (bugfix point release)
A strict bugfix point release on top of v0.8.0. No language, runtime, or stdlib changes beyond the carve-out.
Fixed — type system
- ✅
tyc::attribute_not_foundno longer fires on venv-introspected third-party classes. The v0.8.0 firing-site widening incorrectly trusted shapes built by runtime introspection (inspect.signature(Cls)) to be method-complete, soobj.method(...)against any third-party Python class with a known__init__flagged the call as missing the attribute (uvicorn.Server.serve(...),httpx.AsyncClient.aclose(...),fastapi.Request.body(...), …).InterfaceShapenow carries apartialflag thatclass_shape_from_paramssets on every venv-derived shape;class_hierarchy_fully_knownreturnsfalsewhenever any class in the inheritance chain is partial, so attribute access stays permissive on third-party APIs whose method surface we can’t see. All fifteen apps underexamples/apps/build clean again.
Landed in v0.8.0 (stress-test sweep)
The stress-test sweep release on top of v0.7.1. Closes 41 findings from a multi-file v0.7.1 stress report spanning the type checker, VM, parser, lowering passes, diagnostics, and CLI.
The release is mostly additive on the accepted surface; the BigInt switch in the VM means programs that relied on silent i64 wrap-around now compute different (correct) results.
Added — type system
- ✅
tyc::attribute_not_foundnow fires on class instances and generic classes, not justTypeVar-bounded parameters. The diagnostic was already documented but had no firing site for the most common case. Foreign / venv-introspected classes are tracked with a newpartialshape marker and keep the permissive degrade-to-Unknownbehaviour so adapters around external libraries don’t get false positives. Skipped inunsafe:regions and for dunder / leading-underscore names. - ✅ Interface parameter type conformance.
interface_missing_membersnow compares parameter types position-by-position (contravariant on params) in addition to arity, so aclass BadRepoclaiming to implementinterface Repo: def save(self, item: str) -> boolwith adef save(self, item: int) -> boolimpl is rejected at conformance time. - ✅
Type::LitStr(String)— string-literal singleton types.type Color = "red" | "green" | "blue"andLiteral["a", "b"]produceLitStrslots in the resultingUnion; assignability rejectspaint("orange")againstColor. Bidirectional inference widens string literals toLitStronly when the expected type carries one, so unannotatedlet s = "hi"still infers plainstr. - ✅
?propagation insidewith-chains.result_error_mismatchfires when the implicit return form ofwith x = f()?: …routes a mismatching error type through the chain. - ✅
tyc::pattern_shadows_outerfires when amatchcapture binds a name that already exists in the outer scope. - ✅
field_default_orderingskipsClassVarfields. - ✅
newtype Foo = "literal"is rejected with the newtyc::newtype_invalid_basediagnostic. - ✅ Exhaustive-match-with-guards no longer fires
missing_returnwhen every variant has at least one (possibly-guarded) case. - ✅ Function parameter rebinding requires
mut, matching thelet/mutrule everywhere else.
Added — parser & lowering
- ✅ HKT scaffold
class Functor[F[_]]:parses. - ✅
impl[T] SealedUnionAlias[T]:distributes the methods across every variant of the sealed-union alias. - ✅
class X[T] frozen:(generic + frozen) parses cleanly. - ✅
async defininterfacebodies auto-completes the: ...body (syncdefalready did). - ✅ Outer-annotation tuple unpack
let (a, b): tuple[int, str] = …is accepted.
Added — VM (tree-walking interpreter)
- ✅ Arbitrary-precision integers.
Value::Intis now backed bynum_bigint::BigInteverywhere.2 ** 100andfib(99)no longer overflow. Behaviour change: programs that relied on the VM’s silent i64 wrap-around now produce mathematically-correct results. - ✅ Dict insertion order preserved.
RcDictis now anindexmap::IndexMap; the same.tyfile no longer prints dicts in different orders undertyc runvstyc build && python build/main.py. - ✅ f-string format flags fully wired. Zero-pad, alternate-form,
[fill]align, sign, width, comma, precision, and type all match CPython output. - ✅ Mapping match patterns (
case {"type": "circle"},case {…, **rest}) and sequence-with-star patterns (case [x, *rest, y]) implemented. - ✅ Recursion limit raised to 1000 (was 256) to match CPython.
- ✅
yieldandasync defemit a clearNotImplementedErrorpointing attyc build && pythonas the fallback instead of crashing the interpreter. - ✅ Extend-builtin rewrites apply in VM mode.
- ✅ Subclass constructors inherit fields.
class Dog(Animal): breed: stracceptsDog(name=…, age=…, breed=…)undertyc run. - ✅
freeze letandnewtypeshims are now native builtins so VM mode no longer crashes withNameErroron the lowered call. - ✅ Larger native stdlib. Adds
re,typing,collections(OrderedDict,defaultdict,Counter,namedtuple),functools(lru_cache,cache,cached_property,reduce,partial),itertools(chain,count,cycle,accumulate,combinations,permutations,product,islice,takewhile,dropwhile,groupby),dataclasses,pathlib. - ✅
from typing import Callableno longer crashes — VM-mode lowering strips pure-type imports.
Added — diagnostics & CLI
- ✅ Synthetic preprocess lines no longer leak into source listings.
SanitisedDiagnosticwraps every emitted diagnostic and hides theclass __typhon_impl_Foo(object):/from typhon_runtime import …/?-scaffolding lines, restoring the original text for the user. - ✅ Dedicated parse-error hints for multi-line
|>chains (wrap in parens) andfreeze letat non-module scope. - ✅
wrong_arg_countrephrasing for kw-only mismatches — the self-contradictory “expected 2, got 2” message is replaced with a “pass them by name” help block. - ✅ Collection variance hint suggests
Sequence[Animal]/Mapping[K, V]/frozenset[T]instead of the previously-unhelpful “widen tolist[Animal] | list[Dog]”. - ✅ Dict-to-model mismatch points users at the constructor form
(
UserCreate(name=…, age=…, email=…)). - ✅
tyc check lib.dtynow accepts a single.dtyfile directly. - ✅
tyc run --compilerejects single-file inputs up-front. - ✅
tyc migratestrips trivial__init__methods and emits the resulting class as plainclass(notclass!), preserving any leading class docstring. - ✅ New lint warnings:
tyc::empty_collection_no_annotation,tyc::typing_alias_in_annotation,tyc::contains_secret_literal.
Changed
- ✅
unused_importdefault severity is nowwarn(waserror). Set[strictness] unused-import = "error"intyphon.tomlto restore the old default.
Landed in v0.7.1 (LSP bugfix)
A strict bugfix point release on top of v0.7.0. No language, runtime, or stdlib changes.
- ✅ Semantic-token positions now line up with the original
.tysource instead of the preprocessed Python view. The LSP computed token coordinates against the post-preprocess source (withpub,comptime,freeze,lazy,newtypeline-prefix modifiers stripped) but the editor applied those coordinates to the original file. The remap pass now translates token spans into original-source coordinates and validates each one by string match, dropping synthetic identifiers the preprocessor injects.
Landed in v0.7.0 (Round-3 carry-over)
The carry-over minor release that closes the Round-3 apps-feedback
campaign. A third stress round built five additional production-shaped
apps on top of the original ten (real-time game server, static site
generator, vector DB, API gateway, stream processor — under
examples/apps/11-… through examples/apps/15-…). Every remaining
ergonomics gap from that round is turned into a compiler feature here.
The release is strictly additive on the language surface — every previously-accepted program continues to compile to identical Python.
Added — language & runtime
- ✅
pub *wildcard re-export aggregation in__init__.ty, including transitive aggregation through sub-packages. A singlepub *at the top of a package’s__init__.tyre-exports every direct-sibling module’spubnames. Direct sub-packages contribute their own effective public surface — theirpubnames plus, recursively, whatever their ownpub *aggregates one level deeper, cycle-safe via avisitedset. Two new diagnostics:tyc::pub_name_collision(sibling-export name clash) andtyc::pub_star_outside_init(advice: marker outside__init__.ty). - ✅ Declare-only
let NAME: Twith arm-assignment. Thelet loaded: Cfg; match _load(): case Ok(v): loaded = v; case Err(e): return Err(e)shape no longer firestyc::missing_initialiser. Siblingmatch/if/elif/elsearms each count as a separate first-assignment path. Companiontyc::use_of_uninitialiseddefinite-assignment analysis covers reads on paths that didn’t assign. - ✅
with cm() as r:typesrfrom__enter__/__aenter__and from@contextmanager/@asynccontextmanagerfactories. Three lookup paths in priority order: decorator-aware yield-type inference, concrete-class__enter__/__aenter__returns, fall-through toUnknown. The canonical@asynccontextmanager async def session() -> AsyncIterator[Session]: yield Session(...)factory shape now types as-targets correctly. - ✅
awaiton aCallable[..., Awaitable[T]]/Coroutine[Y, S, T]unwraps toT. The canonical async-middleware shapenext: Callable[[Req], Awaitable[Resp]]followed bylet r: Resp = await next(req)now type-checks without a spurioustyc::type_mismatch. The biggest single Round-3 finding. - ✅ Same-newtype arithmetic preserves the newtype.
newtype LogIndex = intfollowed bylast_idx + 1orLogIndex + LogIndexno longer widens toUnknownacross+ - * // % **. Two distinct newtypes with the same base (LogIndex + Term) still firetyc::operator_type_mismatch. - ✅ Cross-module generic method dispatch propagates class
TypeVars.
s: Stream[int].map(f)recordsCallable[[int], U]as the expected parameter and returnsStream[U]bound at the call site. Same fix benefits field access.
Fixed — resolver, syntax, and checker
- ✅ Nested
from X import Yinif/for/while/with/try/matcharms now binds (parser already accepted; resolver silently skipped). - ✅ Sibling
if/elifbranches no longer tripno_block_shadowfor same-namedletbindings. - ✅ Multi-line
go expr(...)calls parse. Implicit line continuation inside parens now works forgoeverywhere. - ✅ Ternary
body if test else orelsenarrows.isinstance(x, T)andx is not Nonerefinexon the truthy side (and the negated form on the falsy side) inside the expression form, matching the statement-level behaviour. - ✅
tyc::field_default_orderingcatches non-default-after-default class fields at check time — Python would otherwise reject the synthesised__init__at import time with a misleadingTypeError.
Added — examples & tooling
- ✅ Five new reference apps (11–15). Real-time game server,
static site generator, vector DB, API gateway, stream processor.
Each
tyc checks clean,tyc builds, runs through CPython, and carries a README of the friction surfaced during the build. - ✅ All fifteen apps re-organised into grouped subdirectories.
No more flat
src/— every app now has 2-5 grouped subdirectories (domain/,storage/,runtime/,transport/, …) withpub *__init__.tyfacades so import paths stay short. - ✅ VS Code extension
0.1.9 → 0.2.0. Grammar updates:pub def/pub async defhighlightpubas a storage modifier;pub *re-export in__init__.tyhighlights as a single construct; the modifier slot accepts every combination ofpub/freeze/comptime/lazyin front of bindings.
Landed in v0.6.1 (polish)
A polish release on top of v0.6.0. No previously-accepted program changes behaviour.
- ✅
let name: lowercase_type = …highlights as a type annotation. The standalonebinding-declarationrule was a one-shotmatchthat stopped at the binding name; it now owns the: Typeslot. - ✅
.py.mapsidecars now live under<out>/.sourcemaps/. Mirrors the emitted Python tree (build/foo.py→build/.sourcemaps/foo.py.map,build/pkg/bar.py→build/.sourcemaps/pkg/bar.py.map). Map resolvers fall back to the legacy adjacent layout for existing build directories.
Landed in v0.6.0 (apps-feedback minor release)
An apps-feedback minor release on top of v0.5.2. A two-round stress
campaign built ten multi-file production-shaped apps under
examples/apps/ (event-sourced banking, distributed key-value store,
mini-compiler, search engine, GraphQL server, game ECS, trading
engine, ML orchestrator, web crawler, task scheduler). Every issue
that campaign surfaced is closed, and the apps themselves ship as
canonical multi-file reference programs.
The release is strictly additive on the language surface — every previously-accepted program continues to compile to identical Python.
Added — language & runtime
- ✅
Ok/Errexpose the standard Result combinators as methods.map,map_err,and_then,or_elseare now bound on the runtime classes (not just the free functions intyphon_runtime/result.py), so heterogeneous-error pipelines can normalise per stage in chain form:let toks = tokenize(src).map_err(_lex_to_pipeline)?. The free-function versions still exist for callers that prefer qualified access. - ✅
implon a sealed-union alias distributes to every variant.impl Event:wheretype Event = A | B | …used to firetyc::impl_unknown_class; the desugar pass now replicates the impl body’s methods on every variant class and the type checker mirrors the same fold. Per-variant dispatch viamatch self:still works because the runtime class onselfonly matches its own arm. Thetyc::duplicate_methodcheck from the concrete-class branch is mirrored into the union branch. - ✅
tyc::stdlib_module_shadowwarning. A project.tyfile whose stem matches a Python 3.13 stdlib top-level module (types,ast,string,io,json,dataclasses,logging, …) emitsbuild/<name>.py, which the defaultpython build/main.pyentry point puts onsys.pathahead of the stdlib. Transitive imports then resolve to the project module instead, producing bafflingImportErrors blamed on innocent stdlib packages. The new warning fires per-file intyc check, is gated on atyphon.tomlbeing present, and points at the rename pattern (lang_types.ty,records.ty, …). Severity iswarn(non-fatal).
Fixed — compiler
- ✅ Cross-module sealed-union variant flow.
pub type Event = A | Bdeclared inlib.tyletsA(...)flow into anEvent-typed slot withinlib.ty, but consumer modules that imported both the variants and the alias used to hittyc::type_mismatch. BothModuleShapesandExternalShapesnow carrysealed_unionsandinterfacesmaps; the CLI / LSP re-key them under each local import name (including alias renames). - ✅ Cross-module function signatures preserve parameter and return types. Functions imported via
from foo import fused to be registered withType::Unknownplaceholders for every parameter and the return type —ArityInfocarried names + counts but no types. A nullable parameterdef takes(p: Price?) -> int:consumed from another module would render intyc::nullable_useas the literal placeholder?.ArityInfonow recordsparam_types,kwonly_types, andreturn_type, so an imported function looks identical to a localdefat call sites. - ✅ Exhaustive
matchon a sealed union satisfies missing-return for any subject expression.match get_state(): case A(): ...; case B(): ...against a sealed union returned by a function used to fire a false-positivetyc::missing_returnbecause the analyser only inferred subject types for bare names and attribute access. It now falls back to expression inference for any subject shape — function calls, subscripts, arbitrary expressions all flow through the exhaustiveness path. - ✅ Partial keyword pattern satisfies match exhaustiveness.
case Foo(field=x):(binds onlyfield, ignores the rest) was treated as non-exhaustive even though Python’smatchaccepts it. Keyword patterns are now folded into the per-variant coverage tally the same way positional patterns are. - ✅ Sibling
casearms can bind the sameletname.case A(): let key = ...; case B(): let key = ...firedtyc::no_block_shadoweven though at most one arm runs at runtime. The resolver now drains arm-local bindings into a side buffer between cases, mirroring the per-arm scope-stack pattern the type checker already uses. - ✅ For-target no longer rebinds outer
letbindings. Python’s for-target is an assignment, not a fresh declaration. The resolver used to triptyc::immutable_assignagainst a priorletin the enclosing scope, even when the prior let was inside an unrelated sibling for-loop body. The same silencing now applies to any prior binding when the new binding is a for / with / except / comprehension target. Manual body-level assignment (i = i + 1) is unaffected. - ✅
pub freeze let X = …parses. Thepub-prefix stripper didn’t recognisefreeze letas a multi-word keyword form.pub freeze let DEFAULT: dict[str, int] = {...}now lowers correctly and round-trips throughtyc fmt. - ✅
pub defis visible to the?operator validator. The?propagation pass walked function declarations to check the enclosing return type acceptsResult, butpub def f() -> Result[T, E]:looked like a baredefwith no return type because thepub-stripper ran after the walk. The stripper now runs upstream. - ✅
tyc::nullable_useno longer renders?as the expected type. Companion fix to the cross-module signature seed: where the bound was stillType::Unknown, the formatter used to print a bare?. It now substitutes the resolved bound or falls back to a clearer phrasing. - ✅
loop.run_until_complete(coro())no longer firestyc::missing_await. Added to the coro-acceptor whitelist alongsideasyncio.run(...). - ✅
@contextmanagerfactory bodies are exempt fromtyc::resource_not_managed. A@contextmanager-decorated function whose body opens a file or socket is the resource manager — the check now skips function bodies carrying@contextmanageror@asynccontextmanager(bare or dotted-module form).
Improved — diagnostics & docs
- ✅
tyc::type_mismatchhelp text now suggests widening, not narrowing. Was: “change the value, or update the annotation to<found>”. Now: “change the value so it produces<expected>, or widen the annotation to<expected> | <found>if both are intended”. - ✅ New
docs/diagnostics/stdlib_module_shadow.mdwith a rename table (types.ty → lang_types.ty,dataclasses.ty → records.ty, etc.) and an explanation of theImportErrorcascade it prevents. - ✅
docs/diagnostics/class_attr_shadows_slot.mdgains a “nullary sealed-union variants” section pointing at thepub class TyInt frozen: passidiom. - ✅
docs/guides/07-sealed-unions-and-match.mddocuments keyword patterns as the recommended form for variants with more than two or three fields. - ✅
docs/cli.mddocumentstyc explain --list.
Added — examples & tooling
- ✅
examples/apps/— ten production-shaped multi-file apps. Each apptyc checks clean,tyc builds, runs through CPython, and carries aFRICTION.mdor README cataloguing the gaps the build surfaced.examples/apps/TYPHON_FEEDBACK.mdaggregates the campaign findings. - ✅ VS Code extension
0.1.7 → 0.1.8. No new keywords or grammar surface in this batch — the deep audit in v0.5.1 already covers every construct landed since. Version bump only.
Landed in v0.5.2 (correctness + documentation point release)
A correctness + documentation point release on top of v0.5.1. The two compiler fixes broaden the accepted surface — every previously- accepted program continues to compile to identical Python.
Fixed — compiler
- ✅
tyc-syntax:<ident>?propagation now lowers correctly in value position.let x: T = a?(andreturn a?/yield a?/raise a?) silently rewrote tox: T = a | Nonebecause the?-pass only recognisedf()?(paren-prefixed). The resulting.pycrashed at runtime withTypeError: unsupported operand type(s) for |: 'Ok' and 'NoneType'. The pass now disambiguates by RHS position: an=on the line with the identifier-then-?on the RHS, or areturn/yield/raiseprefix, both trigger the standard__typhon_q_N__ladder. Pure annotation forms (let x: int?,let x: list[int]?) are unchanged. Unblocks the naturalgather:→?→Ok(...)shape used throughout the tour, first-program, and recipes docs. - ✅
tyc-types:set - setandfrozenset - frozensetaccepted. The operator-compatibility check for-accepted only numeric operands; the sibling&/|/^already worked because they fell through to the permissive arm. Added an explicit carve-out matching the existing+carve-out forlist/listandtuple/tuple.
Changed — docs site (~40 files)
- ✅ Side-by-side Typhon / Emitted-Python tabs across the user-facing docs. Every complete, runnable Typhon example in the tour, types, reference, recipes, getting-started, and lowering sections is now presented as a
<Tabs>pair, with the Python side produced by running the example through the actualtyc buildpipeline rather than written by hand. - ✅ Stale claims surfaced by the audit were corrected.
lazy let X: T:colon-block form (which doesn’t parse) is replaced with the workinglazy let X: T = exprshape, and the generatorlazy[T]return-type form is now documented as roadmapped.model X frozen:(also not parsed) is replaced with a.pyescape-hatch example.let-shadowing claims inreference/let-mut.mdx,diagnostics/binding-errors.mdx, andtour/five-rules.mdxare corrected (Typhon rejects alllet-shadowing because Python is function-scoped). The fall-through analysis claim intour/control-flow.mdxis replaced with the actualtyc::missing_returnbehaviour.lowering/runtime.mdxis rewritten to show the exact emitted-runtime source. Multi-line|>pipes inreference/pipes.mdxnow wrap in parens (the working form).diagnostics/compile-errors.mdx’s lazy-let example moves into animplblock where the feature belongs.
Added — tooling
- ✅
docs-site/scripts/verify_examples.py— a re-runnable harness that walks every.mdx, classifies each code block, and tries to compile complete-program blocks viatyc build.--real-onlyfilters partial-snippet noise; exits non-zero when real issues remain so the audit can wire into CI. Parallelised; the default 4-way pool audits ~135 files in under a minute.
Landed in v0.5.1 (correctness + tooling point release)
A correctness + tooling point release on top of v0.5.0. No language-semantics change.
Fixed — compiler (PR #120)
- ✅
tyc-format: triple-quote tracker desync. The whitespace pass treated"""as three independent toggles of its single-quote tracker. On a line like""", encoding="utf-8")the tracker came out of sync and rewrote"utf-8"as"utf - 8"— an encoding name Python rejects withLookupError. Added a triple-quote state machine that consumes everything up to the matchingqqqcloser verbatim. - ✅
tyc-desugar:case Ok(...)/case Err(...)patterns didn’t trigger auto-import.stmts_use_result_nameswalked match-case bodies and guards but skipped the patterns themselves, so a file that only ever pattern-matched onOk/Err(without constructing or returning aResult) didn’t getfrom typhon_runtime import Ok, Err, Resultinjected and the emitted.pyNameError’d at runtime. Addedpattern_uses_result_namesthat walks everyPatternvariant. - ✅
tyc-syntax: duplicate__typhon_Err__alias dedupe. The de-dupe check compared a trimmed line (still carrying its trailing newline) againstIMPORT_LINE.trim_end()(no newline), so equality never matched and a secondfrom typhon_runtime import Err as __typhon_Err__line slipped through whenever?propagation andwith-chain lowering both ran.
Changed — VS Code extension (PRs #119, #121)
- ✅ Deep TextMate grammar audit against ~19k lines of real Typhon code from
examples/andstress/. Splitexpressionintoexpression+expression-inner; subscripts[...], dict / set literals{...}, and lambda bodies handle their own:so it stops being eaten as a spurious type annotation. Fixesxs[1:4:2],{k: v for k, v in xs},lambda x: x + 1. Balanced#parensmatcher in expression-inner sos: T = Depends(get_store)consumes its own). - ✅ Tightened type-annotation lookahead. The colon in single-line
if isinstance(...): return ...andcase Foo(x): bar()no longer fires as a type annotation. - ✅ ~18 additional miscoloring fixes across keyword spans (
pub/freeze/extend/unsafe), f-string nesting, regex literals,matcharms, and decorator argument lists. Extension version0.1.5 → 0.1.7.
Added — examples
- ✅ 22 new stdlib-only exercises (47–68): mini-app,
newtypeIDs, Fibonacci memo, linked list, BST, stack & queue, sorting, graph traversal, word frequency, state machine, iterators / generators, context managers, matrix ops, Caesar cipher, tic-tac-toe, priority queue, event bus, URL router, INI parser, rate limiter, trie, JSON-RPC builder. Each ships.tysource + an emitted.pycompanion. - ✅ Emitted
.pycompanions for examples 01–46 so readers can see the lowering without runningtyc build. - ✅
examples/testing/gains a calculator + pytest companion exercising theResult-in-tests pattern.
Landed in v0.5.0 (post-v0.4 roadmap sweep)
The v0.5.0 release lands the seven Phase 4+ items shipped on PR #105 plus four follow-up epics (PRs #110, #111, #112, #113) that close the open frontier work flagged during that sweep.
Incremental compilation
- ✅ Salsa cache shared across
check_file_with_imports. Newpreprocessed_fulltracked query returns the fullPreprocessResultsopreprocessed_text,resolved_module,module_decl_names, and the newcheck_source_file_with_importsentry point all share one preprocess pass per revision. The LSP now calls the SourceFile-backed entry directly so a per-keystroke cross-module check hits the cached parse + resolve on every unchanged sibling. - ✅ Eliminated double-resolve in
check_source_file_with_imports.ArcResolvedModulenow carries the resolver diagnostics alongside the resolved module (both behindArcfor pointer-equalitysalsa::Update), so the secondresolve_module_withcall that previously ran just to harvest diagnostics is gone.
tyc debug and tyc ty
- ✅ Typhon-aware pdb wrapper.
tyc debugwrites a one-shot Python wrapper that subclassespdb.Pdband prints[ty] <src>:<line>after every pause; it loads every.py.mapsidecar under the build directory at startup. - ✅ Full UI translation. The pdb subclass overrides
do_list,do_where,format_stack_entry, and thepromptproperty so the entire debugger surface reads.tycoordinates. Source-snippet rendering (list) reads the.tyfile slice when a.py.mapresolves the source path. - ✅
tyc tydiagnostic remapper handles paths with spaces.parse_py_refwalks left from each.py:occurrence and yields successively longer candidate prefixes, taking the longest match that corresponds to a real.py.mapsidecar.
tyc migrate
- ✅ Three new line-level rewrites.
@dataclass(frozen=True[, ...])(and@dataclasses.dataclass) →class X frozen:;class X(Protocol):/class X(Protocol[T]):→interface X:/interface X[T]:; module-levelNAME = NewType("NAME", BASE)→newtype NAME = BASE. MatchingProtocol/NewTypefrom typing import …entries are pruned alongside.
Type checker
- ✅ Cross-function field-init audit. A pre-scan recognises the trivial factory-helper shape
def make(): return X.__new__(X)(and the two-statementobj = X.__new__(X); return objvariant). Call siteslet c = make()register the LHS as a tracked uninit instance so a downstream escape firestyc::missing_field_init. - ✅ Variance table expansion.
generic_param_variancegainsAsyncContextManager,KeysView,ValuesView,ItemsView,Type/type, andCounter. - ✅ Higher-Kinded Types foundation. New
Type::TypeConstructor { name, arity }variant represents type constructors with unbound parameters.type_from_annotationrecognisesF[_]parameter syntax inside class / function generic parameter lists;walk_typevarstraverses the new variant. The full unification surface is staged on this scaffold; the design docTYPE_SYSTEM_FRONTIER.mdrecords the deferred work.
Analyser
- ✅ Parallel comprehensions: set-comp and dict-comp support.
{f(x) for x in xs}rewrites toset(typhon_runtime.parallel.map_pure(lambda x: f(x), xs));{k_expr: f(v) for k, v in items}rewrites to a dict-literal unpack form that avoidsdictshadowing. Both opt-in via[strictness] auto-parallel. - ✅
comptimetypes-as-values. NewComptimeValue::Type(String)variant letscomptime let T: type = intround-trip through the comptime evaluator. The bare-name resolution covers the eight primitive heads (int,str,bool,float,bytes,None,type,object);Anyis intentionally rejected unless imported.
Test infrastructure
- ✅ Third-party Python corpus round-trip sweep.
stress/third-party-py-corpus/ships six representative Python fixtures and the integration testthird_party_corpus_round_trips_cleanlyexercises the fulltyc migrate→tyc checkchain on each. - ✅ PyPI sweep harness.
stress/pypi-sweep/sweep.pypip-installs typed packages into a tempdir, runstyc migrate+tyc build, and semantic-diffs smoke-script output. Opt-in nightly; not wired into per-PR CI. - ✅
python_semantic_driftaudit round 4.stress/round-2026-05-23-drift-round-4/ships 17 fresh probes covering walrus-in-comprehension, augmented-assignment narrowing,yield from, match-pattern*capture, string multiplication,raise X from Y, andf(*args, **kwargs)unpacking. All probes accept. - ✅ Inter-procedural field-init audit design.
stress/interprocedural-audit-design.mdrecords the summary-IR sketch for generalising the trivial-factory audit to multi-step factories.
Landed in v0.4.0 (type-checker correctness sweep)
- ✅
bool ⊆ intsubtype widening.let x: int = True,1 + True,-Truenow type-check the way CPython evaluates them. One-way only —let b: bool = 1still rejects. - ✅ Subclass constructors see inherited fields.
effective_class_shape()walks the inheritance chain (parent fields first, child overrides on collision, cycle guard). - ✅ Dotted-attribute annotations resolve to the foreign class shape.
let c: foo.ApiClient = ...producesClass("foo.ApiClient")matching call-site convention. - ✅
tyc::missing_field_initcatches container-literal + alias escapes. Partially-initialised instances escaping vialet configs: list[Config] = [c]orlet alias: Config = cno longer slip past. - ✅ Fixed-arity tuple covariance on every slot.
tuple[int, int]now widens slot 0 and slot 1 uniformly. Mutable-container invariance unchanged. - ✅ De Morgan narrowing on
not (A or B).if not (x is None or y is None): use(x, y)correctly refines both names in the post-ifbranch. - ✅ Triple-quoted strings round-trip as triple-quoted. Multi-line docstrings stop emitting as single-line
\n-escaped blobs. - ✅ Corpus round-trip CI sweep. Every
.tyunderexamples/musttyc checkclean on every PR. - ✅ 266 vendored Ruff insta tests re-enabled across
ruff_python_parser/ruff_python_ast/ruff_text_size.
Landed in v0.3.1 (correctness follow-up)
- ✅ Three CRITICAL silent-wrong-output fixes.
not (a or b)no longer round-trips asnot a or b(De Morgan violation);not (x if c else y)keeps its parens; the in-process VM’smatcharm writes propagate back to the enclosing scope instead of being discarded (everyResultwalker / sealed-union aggregator / state machine used to read0fromtyc runand the right value fromtyc run --compile). - ✅
tyc rungates the VM behind a real static check. Programs with unresolved names now surfacetyc::unknown_nameat check time instead of crashing with a Python-styleNameErrorat VM time. - ✅
tyc migraterewritesGeneric[T]→ PEP 695. Pre-3.12 generic idiom (T = TypeVar("T"),class Box(Generic[T]):) is rewritten toclass Box[T]:and the deadTypeVar/Genericimports are elided; multi-parameter, mixed-base, and qualifiedtyping.Genericforms covered. - ✅ Typed tuple-unpacking
let.let (a: int, b: str) = func(x, y)desugars to a hidden temp plus per-elementlets carrying user-supplied annotations. Compound annotations and mixed-capture forms covered. - ✅
tyc::duplicate_method. Twoimpl Foo:/extend Foo:blocks definingdef get(self) -> …used to merge silently with Python keeping whichever came last. Now anchored at the seconddefwith rename / delete / merge advice. - ✅
tyc::newtype_violationcovers boundary mismatches. A bareintflowing into aUserId-typed parameter now routes through the newtype diagnostic instead oftype_mismatchwith wrong-direction advice. - ✅
?inside a comprehension → targeted diagnostic. The previous behaviour silently hoisted past thefor-binding; now rejected at desugar. - ✅
match self.<field>:exhaustiveness. Subject-type resolver now delegatesExpr::Attributetoinfer_expr_readonly, so a class with a sealed-union field can match it directly without binding to a local. - ✅
from __future__ import annotationsnot duplicated when the user authored their own. - ✅ Comptime
str.join(...)added to the sandbox. - ✅ LSP polish. Bare-import attribute access (
nn.Module,pd.DataFrame) paints as a class via a(receiver, attr) → kindmap fed from the venv-introspection cache; introspection failures surface in hover with a recovery hint;import torch.nn as nnprewarmstorch.nn(not justtorch); per-module timeout 3 s → 10 s; VS Code TextMate grammar gains v0.3.0 keywords (freeze,newtype,pub,frozen,plain,class!) and stacked-modifier (pub freeze let) support. - ✅
tyc checkgroups errors by source file with a per-code summary tally (1 error(s): tyc::arg_count, …) and antyc explain <code>suggestion footer. - ✅ Batched venv signature recovery + Salsa-shared preprocess across the resolver / type-checker / analyser / desugar passes.
Landed in Phase 6 (Python-annoyances surface) — v0.3.0
- ✅
newtype Name = Base— nominal aliases over base types. Keeps same-shaped primitives (UserIdvsPostId,USDvsEUR) from being silently swapped. Asymmetric: aUserIdflows into anintslot, but a bareintrequiresUserId(x)to satisfy aUserId-typed target. Compiles to a zero-costtyping.NewTypecall. Newtyc::newtype_violationdiagnostic. - ✅
freeze let X = expr— deep-immutable bindings. Wraps the RHS throughtyphon_runtime.freeze.deep_freeze, recursively replacinglist → tuple,dict → MappingProxyType,set → frozensetso the value (not just the name) is locked. RaisesTypeErroron values without a clean immutable equivalent (file handles, sockets, generators). - ✅
pubmodule-level visibility modifier. When at least one name ispub, desugar synthesises a top-of-file__all__ = [...]sofrom foo import *, Sphinx autoapi, IDE re-export filters, and the checker’s re-export inference all see the public surface — without anyone hand-maintaining the list. - ✅
tyc::blocking_in_async— flags direct calls to known-blocking stdlib functions (time.sleep,requests.get,socket.recv,subprocess.run,input,urllib.request.urlopen, …) inside anasync defbody. Suggestsasyncio.to_thread(...). Suppressed insideunsafe:regions. - ✅
tyc::resource_not_managed— flags bare assignments of context-manager-returning calls (open,socket.socket,sqlite3.connect,tempfile.*) that aren’t wrapped in awithstatement. Severity defaults towarn; controlled by[strictness] resource-not-managed. - ✅
tyc::div_by_zero_literal— constant-fold safety lint forx / 0,x // 0,x % 0when the divisor is a literal (0,0.0,-0,-0.0, unary-negated zero). Pure constant-fold with zero false positives. - ✅ Cross-platform install. Pre-built
tycbinaries for Linux (x86_64 + aarch64) and Windows (x86_64) join the existing macOS (Apple Silicon + Intel) matrix. Newinstall.ps1PowerShell installer for Windows; the existinginstall.shnow detects Linux fromuname -sand resolves the matching tarball. Release workflow runs a five-job matrix and uploads a combinedSHA256SUMS. - ✅ Findings sweep — every open finding closed. Across nine stress campaigns (May 17–21 2026, ~600 hand-written
.typrograms, ~120 distinct findings), the Open column ondocs/findings.mdis empty for the first time since tracking began. Notable closures:tyc fmtgains five PEP 8 rules (def f( x:int,y:int)->int:→def f(x: int, y: int) -> int:); TypedDict-style dict literals type-check against class shapes; inline?works (Ok(add(parse(s)?, parse(t)?)));Sized-style Protocols match every built-in container;tyc migraterewritesUnion[T, None]→T?; the VM’sResultrepr matches CPython’s dataclass default; newtyc::unsafe_value_leak,tyc::pattern_shadows_outer, andtyc::extend_builtindiagnostics.
Landed in Phase 5.5 (constructor / method arity safety) — v0.2.0
- ✅
tyc::arg_counton constructors. The auto-generated__init__ofclassandmodeldeclarations is now arity-checked at every call site.ApiClient(base_url="…")for a class with a requiredapi_key: strfield is rejected attyc check/tyc buildtime instead of crashing withTypeError: missing 1 required positional argumentat runtime.T?without an explicit= Noneis still required. - ✅
tyc::arg_countonimplmethods. Method signatures carry fullArityInfo(param names, defaults,*args/**kwargs) rather than a single arity count.u.greet()is flagged whengreetdeclares a requiredprefix: strparameter. - ✅ Cross-module shape propagation.
from foo import ApiClient; ApiClient(…)andimport foo as f; f.ApiClient(…)both flow through the new arity checks via a project-wide shape registry built once per invocation..tysource and.dtystubs participate on equal footing — stubs win on collisions. Works intyc check,tyc build, and the LSP. - ✅ Salsa-cached LSP shape extraction. A
tyc_db::module_shapes_querysalsa-tracked query caches per-file shape extraction; a keystroke in one file only re-runs extraction for that file. - ✅
tyc::missing_field_initpost-construction audit. CatchesX.__new__(X)/object.__new__(X)bypass patterns where the instance escapes the function (return / call argument) with required fields unassigned. Dropped conservatively onsetattr, onobj.method(…)calls, and insideunsafe:regions.
Landed in Phase 5 (interop & DX) — v0.1.6
- ✅
plain class X:keyword and auto-skip forEnum/Flag/ABCfamily (configurable via[emit] skip-decoration-bases). - ✅
[emit] class-defaultrejects unknown values at config load (tyc::invalid_config_value). - ✅
or/andtyped as truthy/falsy union of operands (notbool); generator functions structurally assignable toIterable[T]/Iterator[T]. - ✅
tyc explain <code>andtyc cheatsheetfor discoverability; richertyc initscaffold; docs link footer intyc --help. - ✅
.pyfiles insrc/copy verbatim into the build output;tyc::orphan_py_importwarns on out-of-src/relative imports. - ✅ Diagnostic deep-links (
url(https://github.com/CodeHalwell/Typhon/blob/main/docs/diagnostics/<code>.md)on everytyc::diagnostic) with 50+ catalog pages. - ✅
tyc build --checkdry-run;tyc::contains_secret_literallint. - ✅
tyc fmtwrapsruff format;tyc debug --break <ty>:<line>source-mapped breakpoints.
Landed in Phase 4+
- ✅ Automatic
asyncio.gatherinference (opt-in via[strictness] auto-gather). - ✅ Loop parallelisation for pure list / set / dict comprehensions on free-threaded Python (opt-in via
[strictness] auto-parallel, thresholdparallel-min-size). Dict-comp coverage landed in v0.5.0. - ✅ PGO via
tyc profile(opt-in via[strictness] pgo-memoise). - ✅ LSP completions (including venv-driven member-access introspection and from-import members from sibling project files) and “Remove unused import” code action.
- ✅ Cross-file go-to-definition across
.ty/.pyboundaries. - ✅
tyc migrate(typed Python → Typhon). - ✅
tyc repl,tyc debug,tyc run(with thetyc-vminterpreter as default). - ✅
tyc add/remove/syncpackage-manager surface overuv. - ✅
tyc stubtestruntime probe viamypy.stubtest. - ✅
comptimetypes-as-values viaComptimeValue::Type(v0.5.0). The frontier work — types flowing into annotations through emit — is staged on this variant. - ✅ HKT foundation (
Type::TypeConstructor,F[_]parameter syntax) shipped in v0.5.0. The unification piece is the remaining work. - ✅ Cross-function field-init audit (trivial factory helpers, v0.5.0).
- ✅ Native debugger UI translation:
do_list,do_where,format_stack_entry, prompt all read.tycoordinates (v0.5.0).
Deferred
These items appear on the roadmap but are not actively being worked. They will land when the value justifies the effort.
- ✅ (landed in v1.0.0-alpha) HKT unification — a constructor variable
Finclass Functor[F[_]]:now binds against a concrete head, withtyc::kind_mismatchon wrong arity / conflicting binding. Still deferred: function-leveldef f[F[_]]params, non-class constructor application, and constructor composition. SeeTYPE_SYSTEM_FRONTIER.mdfor the design sketch. - ✅ (landed in v1.0.0-alpha) Variance inference on user-declared generics — covariant / contravariant type-params are inferred from usage, including across module boundaries, with
@covariant/@contravariantoverrides. - ✅ (landed in v1.0.0-alpha) General inter-procedural field-init audit — partial instances are tracked across helper chains, not just the trivial-factory case.
- ⏳
tyPhase 2 — embedded library sharing the Salsa db (currently a subprocess call viatyc ty). See docs/ty-integration.md. - ⏳ Cached parsed-module AST behind
salsa::Update. Todaycheck_source_file_with_importsre-parses on every call; anArcParsedModulewrapper would close that loop. - ⏳ Accumulator-loop parallelisation.
for x in xs: out.append(...)still rewrites manually; the comprehension shape is the only auto-parallel form. - ✅ (shipped)
[emit] model-extra—"forbid"(default) /"ignore"/"allow"control theConfigDict(extra=…)injected into everymodelclass. See[emit]. - ⏳ Async public-API stability rules if Phase 4+ ever introduces async inference on exported functions. (Likely answer: inference applies to file-internal functions only; exported functions stay explicit.)
What “Phase X complete” means
A phase is “complete” when every must-have feature from that phase has landed and is covered by tests. It does not mean the implementation is fully polished or that every diagnostic has the perfect wording. It means the feature is shippable and you can use it without hitting “not implemented” errors.
Versioning
Typhon is pre-1.0. The semantics described in these docs are the current semantics, not the locked-in v1 spec. Breaking changes to surface syntax are tracked in the changelog and called out in tyc migrate. Changes to emitted Python (the lowering) follow the same rules.
Once the language hits 1.0, syntax is locked under semver. Until then, expect occasional renames and tightenings — none should break programs that follow the Five Rules.
How to follow along
- GitHub — issues, PRs, milestones.
- Roadmap — canonical per-feature status.
- Risks — known hazards and how we mitigate them.
- Performance Baseline — measured numbers we will not regress.
If a feature you want is not in the shipped list, file an issue. If a feature you do not want is in the shipped list, file an issue. Active development means the surface is still moveable.