Skip to content

tyc run

tyc run [PATH] [--compile] [--no-fallback] [--entry FILE] [--temp] [--no-build] [--python PATH] [-- ARGS...]

Runs a Typhon program. Two execution modes:

  • VM (default). Walks the parsed .ty source directly using the in-process tyc-vm tree-walking interpreter. No .py is written, no build/ directory is created, no CPython process is spawned.
  • --compile (alias --no-vm). Builds the project and runs the emitted Python under CPython — the same thing tyc run does on its own when the program imports a module the VM does not model (see Automatic fallback).

Examples

Terminal window
# VM mode (default)
tyc run # resolve src/main.ty from typhon.toml
tyc run src/cli.ty # run a specific .ty file
tyc run -- --port 8080 ./input.csv # forward args to the script
# Compile-and-exec mode
tyc run --compile # build into the configured out dir, exec build/main.py
tyc run --compile --temp # ephemeral build, deleted on exit
tyc run --compile --entry api.py # different entry point
tyc run --compile --no-build # reuse existing build/

The VM mode

The VM is in-process and tree-walking — every iteration of the inner loop touches Typhon AST nodes the parser already produced, no codegen or subprocess overhead in between. Native support covers the bulk of Typhon’s surface:

  • Core language. let / mut bindings, control flow, functions and closures, classes (with impl-block method merging), comprehensions, try / except, match (including class patterns on built-in types, mapping patterns, sequence-with-star patterns — all v0.8.0+), with on open() and @contextmanager-decorated factories. Since v0.11.0 also enum Name: declarations (sugars over enum.Enum with enum.auto() for bare members).
  • Value::Complex + dict-view (since v0.11.0). complex(re, im) is a real VM value with arithmetic promotion across int / float, reflected dunders, and hash support. dict.keys() / .values() / .items() return a dict-view that repr / iterates / supports len / is membership-testable / is re-iterable, matching CPython.
  • Bare super() rewriting (since v0.11.0). tyc-desugar rewrites bare super() inside method bodies to the explicit super(EnclosingClass, self) form, so @dataclass(slots=True) (which orphans the __class__ cell) no longer crashes. Explicit super(X, y) calls are left untouched.
  • Dunder dispatch on user instances (since v0.10.0, broadened in v0.11.0). Operator overloading (__add__ + reflected forms), rich comparisons (__eq__ / __lt__ / …, also feeding in / list.index / list.sort), __str__ / __repr__ / __len__ / __getitem__ / __contains__, and @property getters / @classmethod cls-binding (inherited through bases). v0.11.0 adds __call__ dispatch on callable instances, __post_init__ after auto-generated construction, multi-level inheritance field accumulation across the full MRO, and a subscript __missing__ hook (which backs defaultdict).
  • Finite generators (since v0.10.0). yield / yield from work via eager materialisation — the function runs to completion with each yielded value buffered (capped at 1M items), returning an iterator. Lazy / unbounded generators still need --compile.
  • type(x) as a real type object (since v0.10.0) — type(x).__name__, type(x) == int, str(type(x)) → <class 'int'> all work.
  • Result[T, E] as a native ADT: Ok / Err are intercepted on import from typhon_runtime; the ? operator works because preprocess already lowers it to an isinstance check against __typhon_Err__. Since v0.9.0 the combinator methods (.map / .map_err / .and_then / .or_else) also bind on the VM via native wrappers.
  • Arbitrary-precision integers (since v0.8.0 via num_bigint::BigInt) — 2 ** 100 and fib(99) compute correctly.
  • Insertion-ordered dicts (since v0.8.0 via indexmap::IndexMap) — tyc run and tyc build && python print dicts in the same order.
  • Full f-string format flags (since v0.8.0) — zero-pad, alternate-form, fill-align, sign, width, comma / _ thousands separator (v0.9.0), precision, type.
  • ~50 builtins including print, range, sorted, zip, enumerate, frozenset (hashable as dict keys since v0.9.0), open (read/write/append/binary modes since v0.9.0), @property / @classmethod / @staticmethod / super() stubs (v0.9.0). v0.10.0 adds divmod, pow (2- and 3-arg modular), format, ascii, int(str, base) (incl. base=0); min / max accept key= / default=.
  • Native stdlib. math (v0.10.0: gcd / lcm / factorial / isqrt / comb / perm), os, sys, json (v0.10.0: dumps(indent=…)), time (v0.10.0: perf_counter / process_time), random (v0.1.x), plus re (used only under --no-fallback; see below), typing, collections, functools, itertools, dataclasses, pathlib (v0.8.0), plus collections.deque, heapq, contextlib.contextmanager (v0.9.0), and pydantic.BaseModel whose model_validate / model_dump / model_dump_json construct and round-trip flat model classes since v0.10.0. v0.11.0 adds native enum (enum.Enum, enum.auto(), declaration-order iteration, CPython-matching repr) and datetime (naïve / UTC) shims; collections.defaultdict now actually invokes the factory on missing-key access via the subscript __missing__ hook; pathlib gains __truediv__ (Path("a") / "b"), .suffixes, .parts; re.Match.group(n) / .groups() / .groupdict() return the real capture groups; itertools.groupby honours key=; builtins.round uses banker’s rounding. Also resolvable: os.path, collections.abc and abc (identity natives for the abstract types and @abstractmethod; v0.15.6), and asyncio (the cooperative-sequential shim described below). Any other module takes the automatic fallback to CPython.
  • Multi-file projects (since v0.9.0). Sibling .ty modules load from the project source root on demand; relative imports (from .repo import x) resolve through a Value::Module cache. tyc run --compile spawns python -m <pkg>.main for the compiled path.
  • str / list / dict / set methods dispatched through a Rust registry; list also exposes deque’s popleft / appendleft / extendleft / rotate (v0.9.0). v0.10.0 fills in the missing str methods (center / ljust / rjust / zfill / partition / removeprefix / expandtabs / … and chars-honouring strip), the named set algebra (union / intersection / difference / symmetric_difference / issubset / isdisjoint / update), list.sort(key=, reverse=), and dict(other) / dict(**kwargs). v0.11.0 adds bytes methods (decode / hex / fromhex / count / find / rfind / startswith / endswith / split / strip) and the pure-keyword form str.split(maxsplit=…). v1.0.0-beta.1 completes the bytes surface (the is* predicates, title / capitalize / swapcase, partition / rpartition, ljust / rjust / center / zfill, removeprefix / removesuffix, expandtabs, splitlines, hex(sep, group)) and puts the str character predicates on generated Unicode tables, so isdigit / isdecimal / isnumeric are the three separate questions CPython asks, isprintable also drives repr()’s escaping, and title / capitalize use the titlecase mapping (dž → Dž, ß → Ss).
  • VM value semantics align with CPython (v0.11.0). Dataclass instance equality is value-based (class-identity keyed so cross-module same-name classes no longer collide); repr is Name(field=value, ...); instances are hashable via HashKey::Instance so frozen-dataclass dict / set keys work. Set / frozenset equality is order-independent and repr sorts elements by canonical key. Float repr matches CPython’s shortest round-tripping form with scientific notation for exp < -4 or ≥ 16 (e+NN / e-NN, ≥ 2 exponent digits), -0.0 preserved.
  • String identity follows CPython (unreleased). is between strings compares objects the way CPython does: a freshly built string is a new object, equal literals in one module share one, name-like literals, class and function __name__, enum member names and **kwargs keys are interned, and "" and one-Latin-1-character strings are singletons. Operations CPython returns unchanged (str(s), s[:], f"{s}", a strip that strips nothing, and so on) keep the same object. sys.intern is available.
  • freeze let actually freezes (since v0.9.0) — recursively wraps list → tuple, dict → mappingproxy, set → frozenset, matching tyc build behaviour.
  • comptime let substitution runs before VM interpretation (since v0.9.0) — comptime let PORT = int(env("PORT", "8080")) produces the same inlined literal under both modes.
  • class! synthesised __init__ runs (since v0.9.0) — typed exceptions like class! HttpError(Exception): code: int work end-to-end including except HttpError as e: print(e.code).

async / await / gather: / go run under a cooperative sequential scheduler: a coroutine is forced to completion at its await / asyncio.run / TaskGroup / spawn point, so results match CPython whenever correctness doesn’t depend on task interleaving (a producer/consumer hand-off through an asyncio.Queue fails loudly with a pointer to --compile rather than deadlocking). Features the VM still doesn’t support — a yield the tree-walk cannot suspend (in a loop test, a with item, or two in one expression, which falls back to eager collection; ordinary generators are lazy and support send()), pydantic validation (model_validate builds the model, nested sub-models included since v1.0.0-beta.1, but never raises ValidationError), template strings (t"…"), and the long tail of the CPython stdlib — raise NotImplementedError at runtime, naming the responsible feature and telling you to re-run with --compile. (A module the VM doesn’t ship never gets that far: the import scan sends the program to CPython before it starts.) @contextmanager and @asynccontextmanager generator bodies do run between setup and teardown since v1.0.0-beta.1.

See The Typhon VM for the full feature table.

Why two modes?

The VM is the fast path for iterating on pure-Typhon code: stack traces land on .ty lines directly, no tyc trace remapping needed, and you skip the build + uv sync round-trip every time you tweak a line of code. --compile is the wide-compatibility path: any third-party Python library, any free-threaded extension, the full Python ecosystem.

For pure-Typhon work the VM is the default. A program that imports numpy (or anything else the VM does not model) does not need --compile: tyc run notices the import and runs it under CPython by itself.

Automatic fallback to CPython

Before executing anything, tyc run scans the program’s imports. If one names a module the VM does not model — numpy, sqlite3, subprocess, any third-party package — or reads a module.attr the VM’s model of a module does not export, it prints a note: naming it, builds the project and runs it under CPython, exactly as tyc run --compile does. Nothing half-executes on the VM first, so output and exit code are the compiled path’s.

$ tyc run main.ty
note: `sqlite3` is not modelled by the in-process VM — running via `--compile` (build + CPython) so the program behaves as it does after `tyc build`. Pass `--no-fallback` to require the VM.

Programs that schedule tasks take this path too. The VM’s scheduler is sequential: it runs a coroutine to completion as soon as it is created. A program that uses go, gather:, or an asyncio member whose effect depends on the event loop (create_task, gather, wait, wait_for, timeout, as_completed, TaskGroup, shield, to_thread, the queues and locks, the event-loop accessors) would print in a different order or ignore a timeout, so it runs on CPython. A program that only awaits coroutines one after another stays on the VM.

re always takes this path. A program that imports re runs on compiled CPython under a normal tyc run, so dynamic patterns, lookaround, backreferences and every Python regex flag keep CPython’s semantics. The VM has its own re subset (backed by a Rust regex engine, honouring re.ASCII for the patterns it supports); it is used only with --no-fallback, where unsupported syntax or flags raise an error instead of matching differently:

import re
def main() -> None:
let m = re.search(r"(?<=a)b", "ab")
if m is not None:
print(m.group(0))
main()

tyc run prints b (via CPython). tyc run --no-fallback raises PatternError: … look-around, including look-ahead and look-behind, is not supported.

--no-fallback turns the scan into a hard failure: an unmodelled import raises the VM’s own ModuleNotFoundError instead of switching to CPython. Use it to keep a run hermetic, or to find out whether the VM covers a program.

Exit code propagation

The script’s exit code propagates verbatim to tyc run’s exit code, so shell pipelines see the child’s status unchanged:

Terminal window
tyc run -- --check; echo "exit=$?"
# Whatever the script exited with

Parse, check, build, and spawn failures surface as the usual miette errors with exit code 1.

Forwarding args

Pass -- to mark the start of args for the program (works in both modes):

Terminal window
tyc run -- --port 8080 ./input.csv
# sys.argv inside the program: ['<entry>', '--port', '8080', './input.csv']

Output modes for --compile

Persistent (default)

--compile builds into the configured out dir (default build/). Subsequent runs reuse the incremental Salsa cache, and .py.map sidecars remain on disk so a post-crash tyc trace can map frames back to .ty.

Terminal window
tyc run --compile # build/ persists
ls build/ # main.py, main.py.map, ...

--temp (-t)

Builds into a fresh tempfile::tempdir() that is deleted when the process exits. No project artefacts persist on disk. Trades the incremental cache and on-disk source map for a clean tree — ideal for one-shot iteration.

Terminal window
tyc run --compile --temp # nothing left behind after exit

Flags

FlagModeEffect
--compile (alias --no-vm)switchBuild to .py and exec CPython instead of using the VM.
--no-fallbackVMNever fall back to CPython; an import the VM does not model fails with ModuleNotFoundError.
--entry FILEcompile onlyEntry-point file relative to the build dir. Default main.py.
--python PATHcompiled pathPython interpreter for --compile and for the automatic fallback. Given explicitly it always wins; left out, the project’s .venv is preferred, then python3.<minor> for [python] target, then python3.
--temp / -tcompile onlyBuild into a tempdir that is deleted on exit. Mutually exclusive with --no-build.
--no-buildcompile onlySkip rebuilding; assume the persistent build/ is current.

The compile-only flags (--entry, --temp, --no-build) are rejected by clap unless --compile is also given, so a mistaken combination fails fast at the CLI rather than being silently ignored. --python is accepted without --compile, because the automatic fallback uses it too.

What tyc run is not

  • Not an interactive shell. For interactive evaluation use tyc repl.
  • Not a debugger. For pdb-style stepping use tyc debug.
  • Not a build artifact pipeline. The VM path doesn’t write .py; use tyc build when you want emitted Python on disk.

Where next

  • tyc build — the build step --compile mode wraps.
  • tyc repl — interactive evaluation.
  • tyc debug — launch under pdb with --break for .ty-line breakpoints.