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-lineTaskGroup, 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
python build/main.py 2>err.logtyc trace err.logtyc 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.mapSafe 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
tyc trace— the consumer of source maps.- Architecture — why we hand-wrote the printer.