Skip to content

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)

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) # → 8000
comptime let MY_GRADE: str = grade(82) # → "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)
...

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:

FormSupported?
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") with DATABASE_URL unset 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 def calls are recursion-checked (depth limit 64); a self-recursive call fails the build instead of hanging it.
  • Unsupported form — e.g. a for loop in a comptime def body: “for-loop is 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 sandbox

Use 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 acc

Loops, 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