comptime
comptime bindings and functions are evaluated by tyc at build time inside a sandboxed interpreter. Results are inlined as literals into the emitted Python.
comptime let
comptime let PORT: int = int(env("PORT", "8080"))comptime let IS_PROD: bool = env("BUILD_TAG", "dev") == "prod"
def main() -> None: print(PORT) print(IS_PROD)PORT: int = 8080IS_PROD: bool = False
def main() -> None: print(PORT) print(IS_PROD)The runtime sees only the literal — there is no runtime call to env().
comptime def
User-defined functions usable from comptime let initialisers:
comptime def double(n: int) -> int: return n * 2
comptime def grade(score: int) -> str: if score >= 90: return "A" elif score >= 80: return "B" elif score >= 70: return "C" else: return "F"
comptime let PORT: int = double(4000) # → 8000comptime let MY_GRADE: str = grade(82) # → "B"
def main() -> None: print(PORT) print(MY_GRADE)PORT: int = 8000MY_GRADE: str = "B"
def main() -> None: print(PORT) print(MY_GRADE)Both the comptime def definitions and their call sites are erased; only the resulting literal values reach the emitted output.
comptime let T: type = … — types as values (v0.5.0, lowered properly in v0.9.0)
A comptime let binding can also hold a type, in which case the binding name is substituted wherever the alias is used in annotations:
comptime let IdKind: type = int
def lookup(id: IdKind) -> User?: return USERS.get(id)
def main() -> None: let u: User? = lookup(7) ...type IdKind = int
def lookup(id: IdKind) -> User | None: return USERS.get(id)
def main() -> None: u: User | None = lookup(7) ...v0.9.0 lowers comptime let T: type = <Type> to a PEP 695 type T = <Type> alias statement. The substitution pass also runs before tyc check parses the resolved module, so check, build, and tyc run all see the same shape — no more “alias resolves at build but not at check” drift.
Use this when you want a build-switched type without writing two function signatures:
comptime let HashAlgo: type = blake3.Blake3 if FAST_HASH else hashlib.sha256
def fingerprint(data: bytes) -> HashAlgo: return HashAlgo(data).digest()VM behaviour
Since v0.9.0 comptime let X = ... value bindings inline under tyc run too, via the substitution pass shared with tyc build. So comptime let PORT = int(env(...)) no longer crashes with NameError: env is not defined under the in-process VM — the literal is substituted in before interpretation, exactly like the compiled path.
What the sandbox supports
The v1 contract:
| Form | Supported? |
|---|---|
| Integer / float / string / boolean literals | ✅ |
Arithmetic (+ - * / // % **) | ✅ |
Comparisons (== != < <= > >=) | ✅ |
Boolean ops (and, or, not) | ✅ |
Ternary (a if c else b) | ✅ |
env(name), env(name, default) | ✅ |
int(), str(), float(), bool() casts | ✅ |
if / elif / else | ✅ (inside comptime def) |
return | ✅ (inside comptime def) |
Local bindings (x = EXPR, let x: T = EXPR, mut x: T = EXPR) | ✅ |
Calls to other comptime def functions | ✅ |
Container literals ([...], {...}, (...), including empty containers and the trailing-comma single-element tuple) | ✅ (v0.3.1) |
Pure string methods (upper, lower, strip / lstrip / rstrip, replace, startswith, endswith, split, join) | ✅ (v0.3.1) |
len(), subscripts with Python negative indexing | ✅ |
Loops (for, while) | ❌ |
Exception handling (try, raise) | ❌ |
with statements | ❌ |
class / def declarations inside a comptime function | ❌ |
| I/O, subprocess, network | ❌ |
random.*, time.*, os.urandom | ❌ |
| Imports | ❌ |
The contract is intentionally small. If your comptime def couldn’t be a small pure helper over arithmetic, strings, and booleans, it probably belongs at runtime.
Recursion
comptime def calls are recursion-checked. The depth limit is currently 64 — buggy definitions fail the build rather than hanging it.
Required env vars
Declare in typhon.toml:
[env]required = ["DATABASE_URL", "API_KEY"]Missing required vars fail the build with tyc::comptime.
Failure modes
Every comptime failure reports under the single tyc::comptime code, with the
trigger spelled out in the message:
- Missing required env var —
env("DATABASE_URL")withDATABASE_URLunset and listed in[env] required: “required environment variable ‘DATABASE_URL’ is not set”. - Cast failure —
int("abc"): “cannot parse ‘abc’ as int”. - Recursion limit hit —
comptime defcalls are recursion-checked (depth limit 64); a self-recursive call fails the build instead of hanging it. - Unsupported form — e.g. a
forloop in acomptime defbody: “for-loopis not supported inside a comptime function body”.
The diagnostics are formatted to point at the source span that triggered the failure, with the underlying reason included.
Where the sandbox lives
Implementation in tyc-analyse. The interpreter walks the AST and produces a ComptimeValue ADT (Int(i64), Float(f64), Str(String), Bool(bool)). The value is then re-rendered as a Python literal by tyc-desugar.
Common patterns
Required env validation
comptime let DB_URL: str = env("DATABASE_URL")comptime let API_KEY: str = env("API_KEY")[env]required = ["DATABASE_URL", "API_KEY"]Feature flags
comptime def feature(name: str) -> bool: return env(f"FEATURE_{name.upper()}", "0") == "1"
comptime let DARK_MODE: bool = feature("dark_mode")comptime let EXPERIMENTAL: bool = feature("experimental")Derived constants
comptime let BASE_URL: str = env("BASE_URL", "https://api.example.com")comptime let USERS_URL: str = BASE_URL + "/users"Build-tag branching
comptime let BUILD: str = env("BUILD_TAG", "dev")comptime let LOG_LEVEL: str = "DEBUG" if BUILD == "dev" else "INFO"Common mistakes
comptime reading runtime values
comptime let NOW: float = time.time() # ❌ time.* forbidden in sandboxUse lazy let NOW: float = time.time() for one-shot runtime caching.
Unsupported form
comptime def total(n: int) -> int: mut acc: int = 0 for i in [1, 2, 3]: # ❌ loops are not part of the sandbox acc = acc + i return accLoops, exceptions, with, nested declarations, and imports are rejected by the sandbox; express the computation with arithmetic / ternaries and calls to other comptime def functions (recursion depth is capped at 64, and each binding has a budget of 10 million evaluation steps, so exponential recursion fails the build instead of hanging it), or compute it at runtime.
Where CPython would raise, the evaluator fails the build instead of inlining a value the program never computes: 10.0 ** 400 (CPython: OverflowError), 0.0 ** -1 and 0 ** -1 (ZeroDivisionError), a negative base to a fractional power such as (-8.0) ** 0.5 (a complex in CPython), and a string literal spelling a lone surrogate ("\ud800"). strip() removes exactly what str.isspace() matches, U+001C–U+001F included.
Where next
- Build-Time Env Validation (recipe) — worked patterns.
comptime→ inlined literals (lowering) — implementation details.[env]config section — declaring required vars.