Skip to content

Source Maps (.py.map)

Every emitted .py ships with a sidecar .py.map file in v2 format. It carries a per-statement (out_line → ty_line) table the LSP and tyc trace use.

What’s in a .py.map v2

{
"version": 2,
"source_file": "src/main.ty",
"mappings": [
{"out_line": 1, "ty_line": 1},
{"out_line": 5, "ty_line": 4},
{"out_line": 7, "ty_line": 7},
...
]
}

Each mapping records the first line of a statement in the output file and the corresponding line in the input. Statements that span multiple lines map to their start lines.

Why per-statement, not per-token

TypeScript’s .js.map is per-token, which is great for column-level navigation. Typhon’s .py.map is per-statement because:

  • Typhon’s transformations are statement-grained (a gather: block becomes a multi-line TaskGroup, but the first statement of each preserves its original line).
  • Per-statement maps are 10× smaller and 10× faster to parse.
  • Column-level mapping would be a Phase-5 enhancement; we haven’t needed it yet.

How tyc trace uses it

Terminal window
python build/main.py 2>err.log
tyc trace err.log

tyc trace reads the Python traceback, looks up each File "build/main.py", line N frame, finds the matching out_line → ty_line mapping, and rewrites the frame to File "src/main.ty", line M.

The output looks like a normal Python traceback but points at your source.

How the LSP uses it

For cross-file go-to-definition across the .ty / .py boundary. When you’re in a .ty file and you go-to-definition on an import from a .py (or vice versa), the LSP uses the source maps to land on the right line.

How [emit] traceback-remap uses it

[emit] traceback-remap = true (v0.14.0, default off) is the automatic counterpart to the manual tyc trace. With the knob on, tyc build injects typhon_runtime.traceback.install() into the entry module’s __main__ block. That install() call sets a sys.excepthook that consumes the same .py.map sidecars at runtime: when an uncaught exception unwinds, each File "build/main.py", line N frame is rewritten in place to File "src/main.ty", line M before the traceback is printed — no separate tyc trace step. It’s the same per-statement out_line → ty_line lookup, applied live instead of after the fact.

Where the maps live

Alongside the emitted .py:

build/
├── main.py
├── main.py.map
├── lib/users.py
└── lib/users.py.map

Safe to delete; they regenerate on the next tyc build. Don’t commit them.

Implementation

The hand-written printer in tyc-emit records each statement’s start line as it prints. When the printer is done, it emits the JSON sidecar.

The choice to hand-write the printer (rather than vendor ruff_python_codegen) was driven by exactly this requirement: upstream codegen doesn’t expose the per-statement line-offset hook. See Architecture for more on the printer.

v1 → v2 migration

Older .py.map files used v1 format (single offset for the whole file). v2 is per-statement. tyc trace reads both formats but emits v2 only.

Where next