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
.tysource directly using the in-processtyc-vmtree-walking interpreter. No.pyis written, nobuild/directory is created, no CPython process is spawned. --compile(alias--no-vm). Builds the project and runs the emitted Python under CPython — the same thingtyc rundoes on its own when the program imports a module the VM does not model (see Automatic fallback).
Examples
# VM mode (default)tyc run # resolve src/main.ty from typhon.tomltyc run src/cli.ty # run a specific .ty filetyc run -- --port 8080 ./input.csv # forward args to the script
# Compile-and-exec modetyc run --compile # build into the configured out dir, exec build/main.pytyc run --compile --temp # ephemeral build, deleted on exittyc run --compile --entry api.py # different entry pointtyc 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/mutbindings, control flow, functions and closures, classes (withimpl-block method merging), comprehensions,try/except,match(including class patterns on built-in types, mapping patterns, sequence-with-star patterns — all v0.8.0+),withonopen()and@contextmanager-decorated factories. Since v0.11.0 alsoenum Name:declarations (sugars overenum.Enumwithenum.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 / supportslen/ is membership-testable / is re-iterable, matching CPython.- Bare
super()rewriting (since v0.11.0).tyc-desugarrewrites baresuper()inside method bodies to the explicitsuper(EnclosingClass, self)form, so@dataclass(slots=True)(which orphans the__class__cell) no longer crashes. Explicitsuper(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 feedingin/list.index/list.sort),__str__/__repr__/__len__/__getitem__/__contains__, and@propertygetters /@classmethodcls-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 backsdefaultdict). - Finite generators (since v0.10.0).
yield/yield fromwork 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/Errare intercepted on import fromtyphon_runtime; the?operator works because preprocess already lowers it to anisinstancecheck 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 ** 100andfib(99)compute correctly. - Insertion-ordered dicts (since v0.8.0 via
indexmap::IndexMap) —tyc runandtyc build && pythonprint 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 addsdivmod,pow(2- and 3-arg modular),format,ascii,int(str, base)(incl.base=0);min/maxacceptkey=/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), plusre(used only under--no-fallback; see below),typing,collections,functools,itertools,dataclasses,pathlib(v0.8.0), pluscollections.deque,heapq,contextlib.contextmanager(v0.9.0), andpydantic.BaseModelwhosemodel_validate/model_dump/model_dump_jsonconstruct and round-trip flatmodelclasses since v0.10.0. v0.11.0 adds nativeenum(enum.Enum,enum.auto(), declaration-order iteration, CPython-matching repr) anddatetime(naïve / UTC) shims;collections.defaultdictnow actually invokes the factory on missing-key access via the subscript__missing__hook;pathlibgains__truediv__(Path("a") / "b"),.suffixes,.parts;re.Match.group(n)/.groups()/.groupdict()return the real capture groups;itertools.groupbyhonourskey=;builtins.rounduses banker’s rounding. Also resolvable:os.path,collections.abcandabc(identity natives for the abstract types and@abstractmethod; v0.15.6), andasyncio(the cooperative-sequential shim described below). Any other module takes the automatic fallback to CPython. - Multi-file projects (since v0.9.0). Sibling
.tymodules load from the project source root on demand; relative imports (from .repo import x) resolve through aValue::Modulecache.tyc run --compilespawnspython -m <pkg>.mainfor the compiled path. str/list/dict/setmethods dispatched through a Rust registry;listalso exposesdeque’spopleft/appendleft/extendleft/rotate(v0.9.0). v0.10.0 fills in the missingstrmethods (center/ljust/rjust/zfill/partition/removeprefix/expandtabs/ … andchars-honouringstrip), the namedsetalgebra (union/intersection/difference/symmetric_difference/issubset/isdisjoint/update),list.sort(key=, reverse=), anddict(other)/dict(**kwargs). v0.11.0 addsbytesmethods (decode/hex/fromhex/count/find/rfind/startswith/endswith/split/strip) and the pure-keyword formstr.split(maxsplit=…). v1.0.0-beta.1 completes thebytessurface (theis*predicates,title/capitalize/swapcase,partition/rpartition,ljust/rjust/center/zfill,removeprefix/removesuffix,expandtabs,splitlines,hex(sep, group)) and puts thestrcharacter predicates on generated Unicode tables, soisdigit/isdecimal/isnumericare the three separate questions CPython asks,isprintablealso drivesrepr()’s escaping, andtitle/capitalizeuse 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);
reprisName(field=value, ...); instances are hashable viaHashKey::Instanceso frozen-dataclass dict / set keys work. Set / frozenset equality is order-independent and repr sorts elements by canonical key. Floatreprmatches CPython’s shortest round-tripping form with scientific notation for exp < -4 or ≥ 16 (e+NN/e-NN, ≥ 2 exponent digits),-0.0preserved. - String identity follows CPython (unreleased).
isbetween 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**kwargskeys are interned, and""and one-Latin-1-character strings are singletons. Operations CPython returns unchanged (str(s),s[:],f"{s}", astripthat strips nothing, and so on) keep the same object.sys.internis available. freeze letactually freezes (since v0.9.0) — recursively wraps list → tuple, dict → mappingproxy, set → frozenset, matchingtyc buildbehaviour.comptime letsubstitution 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 likeclass! HttpError(Exception): code: intwork end-to-end includingexcept 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.tynote: `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:
tyc run -- --check; echo "exit=$?"# Whatever the script exited withParse, 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):
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.
tyc run --compile # build/ persistsls 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.
tyc run --compile --temp # nothing left behind after exitFlags
| Flag | Mode | Effect |
|---|---|---|
--compile (alias --no-vm) | switch | Build to .py and exec CPython instead of using the VM. |
--no-fallback | VM | Never fall back to CPython; an import the VM does not model fails with ModuleNotFoundError. |
--entry FILE | compile only | Entry-point file relative to the build dir. Default main.py. |
--python PATH | compiled path | Python 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 / -t | compile only | Build into a tempdir that is deleted on exit. Mutually exclusive with --no-build. |
--no-build | compile only | Skip 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; usetyc buildwhen you want emitted Python on disk.