Skip to content

How Typhon Lowers

Typhon is a compile-to-Python language. Each Typhon construct lowers to an explicit, readable Python form — the same code an experienced Pythonista would have written by hand. This section maps every Typhon-specific form to its emitted Python.

The pipeline

.ty source
│
▼ tyc-syntax (lex + parse)
▼ tyc-resolve (name resolution, let/mut classification)
▼ tyc-types (type check, narrowing)
▼ tyc-analyse (purity, async, comptime, auto-gather)
▼ tyc-desugar (Typhon AST → Python AST: merge impl, expand ?, lower gather/go, ...)
▼ tyc-emit (Python source + .py.map)
▼ tyc-format (ruff format post-process)
▼
.py + .sourcemaps/*.py.map + (typhon_runtime/*.py if used)

See Architecture Overview for the compiler internals.

Quick reference

Classes → dataclass

class User: → @dataclass(slots=True). model User: → Pydantic BaseModel. class! → bare class with synthesised __init__. Details →

Result → typhon_runtime

Result[T, E], Ok, Err lower to frozen dataclasses in a generated typhon_runtime module. ? lowers to inline isinstance(_t, Err): return _t. The rescue boundary sugar lowers to try_result(...)? (postfix) or a try / except … return Err(...) block. Details →

gather: → TaskGroup

gather: lowers to asyncio.TaskGroup. gather(strategy="best-effort"): lowers to asyncio.gather(..., return_exceptions=True). Details →

go → spawn registry

go f(x) lowers to typhon_runtime.tasks.spawn(f(x)) — strong-ref registry prevents GC mid-flight. Details →

lazy → proxies

lazy import np = numpy → bespoke __TyphonLazy_np_ proxy class. lazy let X → sentinel-cached lazy_let (imported as __typhon_lazy_let) at module scope; @cached_property at class scope. The lazy[T] return-type form is roadmapped. Details →

comptime → inlined literals

comptime let PORT = int(env("PORT", "8080")) → PORT: int = 8080 after sandbox evaluation. Details →

extend → free functions

extend str: def to_slug(): ... → def __typhon_ext_str__to_slug(self): ... with call-site rewrites for statically-annotated receivers. Details →

Source maps

Each emitted .py ships with a .py.map v2 file recording per-statement (out_line → ty_line) for tyc trace and the LSP. Details →

Principles

Three rules guide every lowering decision:

  1. The emitted Python should be code you would have written. No bespoke ABI, no metaclass tricks, no runtime fingerprints. Production crashes happen in Python tracebacks; you should be able to read them without learning a Typhon-specific runtime layer.
  2. Helpers ship inline, not on PyPI. When Typhon needs runtime support (the Result ADT, the lazy-import proxy, the strong-ref task registry), the code emits as local typhon_runtime/ source. Production servers do not install Typhon-specific anything.
  3. Source maps preserve the link back to .ty. Per-statement (out_line → ty_line) tables let tyc trace remap tracebacks, and let the LSP do cross-file go-to-definition across the .ty / .py boundary.

What’s generated

When you tyc build, the emitted tree contains:

  • build/<your modules>.py — your compiled source.
  • build/.sourcemaps/<your modules>.py.map — source maps (moved under .sourcemaps/ in v0.6.1; the legacy adjacent layout is still readable).
  • build/typhon_runtime/ — only when you use features that need it:
    • result.py — Ok, Err, Result + combinators and try_result.
    • tasks.py — spawn(coro) with the strong-ref _BACKGROUND set.
    • lazy.py — lazy_let(lambda: ...) + the _LazyValue proxy.
    • freeze.py, cast.py, parallel.py, traceback.py, stdlib.py — freeze let, as!, auto-parallel, [emit] traceback-remap, and internal helpers. See The typhon_runtime module.
  • build/<your modules>.pyi — for every .dty you wrote.

Nothing else. No __pycache__/ (Python creates that at runtime), no Cargo artefacts, no Typhon-marker files.

Where next

Drill into a specific lowering: