Skip to main content

runtime

runtime marks code that executes when the decorated program runs, not at build time. It is the interception phase of the decorator pipeline: runtime { ... } wraps every invocation of the decorated function/type and may inspect arguments, short-circuit, or delegate to the original via call.proceed() (TokenKind::Proceed). Outside decorators, a standalone runtime { ... } block makes explicit that its body must not be constant-folded.

Ground truth: Lexer TokenKind::Runtime (src/parsing/lexer.rs:1017, keyword "runtime"), AST DecoratorPhase::Runtime(Arc<Vec<Stmt>>) (src/parsing/ast.rs:702-704), TokenKind::Proceed (lexer.rs:1021, ast.rs:2077), StmtKind::Decorator + DecoratorDef::phases (ast.rs:711-718), pipeline DecoratorPipelineStage (ast.rs:720-728).


Syntax

runtime_block ::= "runtime" "{" stmt* "}"
decorator_body ::= "decorator" ident "(" params ")" "{" phase* "}"
phase ::= "compile" "{" stmt* "}"
| "runtime" "{" stmt* "}"
| "typecheck" "{" stmt* "}"
| "emit" "{" stmt* "}"
proceed_call ::= "call" "." "proceed" "(" args? ")"

runtime is reserved (TokenKind::Runtime). Inside a runtime phase, the implicit bindings call, target, and args are in scope; call.proceed() is only legal there.

Minimal decorator with runtime phase

decorator logged(target) {
runtime {
print("enter " + target.name);
let result = call.proceed(); // invokes the original function
print("exit");
return result;
}
}

@logged
fn add(a: i32, b: i32): i32 { return a + b; }

Standalone marker (rare)

runtime {
// Explicitly runtime-evaluated even inside a compile-heavy file.
print("evaluated when the program runs");
}

Semantics

Compilation model — where runtime sits in the pipeline

Source ──► Lex(Runtime) ──► Parse(DecoratorPhase::Runtime) ──► HIR build

┌──────────────┼──────────────┐
│ │ │
compile{} typecheck{} runtime{} (captured, not executed)
executed checked │
└──────────────┼──────────────┘

DecoratorPipeline per fn
┌─────────────────────┐
│ PipelineStage { │
│ phase: Runtime, │
│ args: Vec<Expr> │
│ } ordered by │
│ definition order │
└─────────────────────┘


Call site wrapper
call.proceed() chain
  1. Lex/Parseruntime { ... } is captured as DecoratorPhase::Runtime(Arc<Vec<Stmt>>) and stored in DecoratorDef.phases.
  2. Pipeline construction (src/parsing/decorator_pipeline.rs) — for each decorated Function, a DecoratorPipeline { fn_id, stages: Vec<DecoratorPipelineStage>, pipeline_hash } is built. Each @decorator / @decorator(args) pushes a DecoratorPipelineStage { decorator_name, phase: Runtime(...), args }.
  3. LoweringDecoratorPipeline is hashed (pipeline_hash: u64) for caching; the wrapper function is synthesized. call.proceed() lowers to a direct call to the next stage or the original body.
  4. Execution — at call time, the outermost runtime runs first; call.proceed() dispatches inward. Omitting proceed short-circuits the chain (the original body never runs).

Implicit bindings inside runtime

BindingTypeDescription
targetFunction/ClassDecl referenceThe decorated declaration (name, params, decorators)
callcall-site objectcall.args: [Value], call.proceed(*modified_args)
call.proceedfn(*args): ReturnDelegates to next pipeline stage or original (TokenKind: Proceed)

call.proceed() may be invoked 0 or 1 times per runtime invocation. Calling it 0 times suppresses the original; calling it >1 times is a runtime LangError.

runtime vs. other phases

PhaseTokenKindcall.proceed()Access to ValueCan emit?Typical use
compile (ast.rs:701)CompileNoAST onlyYesInject fields/metadata
typecheck (ast.rs:705)TypecheckNoTypes onlyNoType constraints
runtimeRuntimeYesFull runtime valuesNoLogging, caching, retry
emit (ast.rs:707)EmitNoIR handlesYes (IR)Backend directive mutation

Ordering and stacking

Decorators stack: @a @b fn f() means a's runtime wraps b's runtime wraps f. pipeline_hash distinguishes order for caching. Within a single decorator, if multiple phases are present they execute in pipeline order: typecheckcompileruntime (emit may interleave at lowering).


Examples

Example 1 — Logging and argument mutation with call.proceed()

decorator logged(target) {
compile {
print("compile: decorating " + target.name + " with " + target.params.len() + " params");
}
runtime {
print("enter " + target.name + " args=" + call.args);
let start = now_ms();
let result = call.proceed(); // invoke original (or next decorator)
let elapsed = now_ms() - start;
print("exit " + target.name + " -> " + result + " in " + elapsed + "ms");
return result;
}
}

@logged
fn multiply(a: i32, b: i32): i32 { return a * b; }

print(multiply(6, 7)); // enter multiply args=[6,7] → exit multiply -> 42 in 0ms → 42

compile printed at build time; runtime prints on every call. Removing call.proceed() would prevent multiply's body from ever executing.

Example 2 — Caching / memoization decorator (stacked runtime phases)

decorator memoize(target) {
compile {
// Per-site cache table — compile-time fixed map injected via emit.
emit(`let __cache_${target.name}: Map<string, any> = Map.new();`);
}
runtime {
let key = call.args.map(x => x.to_string()).join(",");
let cache = get_cache(target.name); // lookup emitted map via reflection
if (cache.has(key)) {
return cache.get(key); // short-circuit: don't call original
}
let value = call.proceed(); // cache miss — delegate
cache.set(key, value);
return value;
}
}

decorator retry(attempts: i32) {
runtime {
let last_err: any = null;
for i in 0..attempts {
try { return call.proceed(); }
catch e { last_err = e; print("retry " + (i+1) + "/" + attempts); }
}
throw last_err;
}
}

@memoize
@retry(3)
fn fetch(url: string): string {
// runtime wrappers nest: memoize(runtime(retry(runtime(fetch))))
return http_get(url);
}

print(fetch("https://example.com")); // memoized after first success; retried on failure

Stacking order matters: @memoize outermost means retries happen inside the cache miss path only.

Example 3 — Standalone runtime{} and interaction with compile{}

// File with mixed compile-time and runtime work.
compile {
const SEED: u64 = 0xDEADBEEFu64;
const LOOKUP: [u8; 4] = [3u8, 1u8, 4u8, 1u8];
emit(`const LOOKUP = [3u8, 1u8, 4u8, 1u8];`); // freeze for runtime
}

runtime {
// Explicit marker: even though LOOKUP was built at compile time, this block
// is guaranteed to re-evaluate at runtime (not folded away).
let sum = 0;
for v in LOOKUP { sum += v as i32; }
print("runtime sum " + sum); // 9 — uses emitted LOOKUP
}

// Contrast:
fn dynamic(add: i32): i32 {
runtime { return LOOKUP[0] as i32 + add; } // forced runtime
// vs. compile { LOOKUP[0] } would have constant-folded before lowering
}

print(dynamic(10)); // 13

Restrictions / Errors

KindTriggerDiagnosticHelp
Lexicallet runtime = 1unexpected token 'runtime', expected identifierTokenKind::Runtime is reserved (lexer.rs:1017)
Parseruntime; or runtime exprExpect '{' after 'runtime'Always runtime { ... } with braces
Semanticcall.proceed() outside runtimeproceed is only valid inside runtime phase (TokenKind::Proceed)Move call into runtime {}; other phases have no call-site
Semanticcall.proceed() called twiceLangError: proceed already called for this invocationGuard with a flag; wrapper must delegate at most once
Semanticruntime phase without call.proceed() that should return valueNo error — valid short-circuitreturn early or omit proceed to suppress original; ensure return type matches
Typecall.proceed(bad_arity) with wrong arityTypeError: proceed expects N args, found MPass call.args or matching signature
PipelineDecorator applied but HIR not rebuiltStale pipeline_hash mismatch; wrapper not updatedClean build after editing decorator phase list
Asyncawait call.proceed() where target is not asyncawait is only valid in async functionMark wrapper async or avoid await

Common pitfall — forgetting return before call.proceed():

decorator broken(target) {
runtime {
call.proceed(); // result discarded!
// implicit return null — caller gets null instead of original return
}
}
decorator fixed(target) {
runtime { return call.proceed(); } // correct — forwards return value
}

See Also

  • decoratorTokenKind::Decorator, defining compile/runtime/typecheck/emit phases (src/parsing/ast.rs:701-718)
  • compileTokenKind::Compile compile-time AST transformation
  • typecheckTokenKind::Typecheck type-constraint phase (runs before runtime)
  • emitTokenKind::Emit IR/backend hooks
  • requireTokenKind::Require contract preconditions usable in any phase
  • jumpTokenKind::Jump low-level transfer (rare; not a phase)
  • src/parsing/lexer.rs:1017,1021, src/parsing/ast.rs:699-737, src/parsing/decorator_pipeline.rs, src/parsing/decorator_registry.rs