Phase 2, Section 2: Effect Handler Infrastructure
Phase 2: AD as an Effect — algebraic effect and handler infrastructure. Section 2 of 4: Effect Handler Infrastructure.
1. What this section commits to
This section specifies the syntax, typing rules, and runtime execution model for algebraic effects and handlers in kina-ai. Specifically, it establishes:
- The grammar of effect declarations, effect rows, function types with effects, and performing/handling operations.
- The typing rules for effect rows, row polymorphism, subtyping, and handler blocks.
- The runtime fiber suspension, continuation capture, resumption, and stack/unwind execution semantics.
2. Grammar Extensions
We extend the Kina surface syntax (.kina) to support first-class algebraic effects and handlers.
2.1 Lexical & Syntactic Grammar
Type ::= Tensor[Dtype, Shape, Refinements]
| fn(Type, ...) !EffectRow -> Type
| Scalar[Dtype]
| Unit
EffectRow ::= { EffectName, ... } -- closed row
| { EffectName, ... | ρ } -- open row (row variable ρ)
Decl ::= ...
| effect EffectName { EffectOp ... }
| handler HandlerName(Param, ...) { HandlerBranch, ... }
EffectOp ::= fn OpName(Param, ...) -> Type
HandlerBranch ::= return(Ident) => Expr
| EffectName.OpName(Ident, ...), Ident => Expr
Expr ::= ...
| perform EffectName.OpName(Expr, ...)
| handle Expr with HandlerName(Expr, ...)
| handle Expr with { HandlerBranch, ... }
2.2 Key Commitments
- Effects are explicitly declared at top level. Anonymous or inline operations are rejected to ensure clear compilation paths and SMT-provable effect safety.
- Row variables ($\rho$) enable row polymorphism. Functions that call functions with arbitrary effects preserve polymorphism: e.g.,
fn map(f: fn(A) !ρ -> B, xs: List[A]) !ρ -> List[B]. - Explicit
performconstruct. Performing an effect is visually demarcated in code, preventing unexpected control-flow jumps.
3. Type System for Effects
The type checker tracks the set of active effects alongside the value context. The typing judgment is extended to: $$\Gamma; \Sigma \vdash e : T ! E$$ Which reads: "in value context $\Gamma$ and shape/refinement context $\Sigma$, expression $e$ has type $T$ under active effect row $E$."
3.1 Well-Formedness of Effect Rows
An effect row $E$ is well-formed ($\Gamma \vdash E \text{ row}$) if all named effects are declared in the global signature, and any row variable $\rho$ is bound in $\Gamma$.
3.2 Typing Rules
(OpName : fn(T₁, ..., Tₙ) -> T_ret) ∈ EffectName
Γ; Σ ⊢ e₁ : T₁ ! E ... Γ; Σ ⊢ eₙ : Tₙ ! E
EffectName ∈ E
──────────────────────────────────────────────────────── (perform)
Γ; Σ ⊢ perform EffectName.OpName(e₁, ..., eₙ) : T_ret ! E
Γ; Σ ⊢ e_body : A ! ({EffectName} ∪ E)
Γ; Σ ⊢ return(x) => e_pure : A -> B ! E
∀ Op ∈ EffectName.
Γ; Σ ⊢ Op(args), k => e_op : (Args_Op × (Res_Op -> B ! E)) -> B ! E
─────────────────────────────────────────────────────────────────────── (handle)
Γ; Σ ⊢ handle e_body with { return(x) => e_pure, ... } : B ! E
3.3 Row Polymorphism & Subtyping
Effect rows support subtyping via row extension. An effect row $E_1$ is a subtype of $E_2$ ($E_1 \sqsubseteq E_2$) if the set of effects in $E_1$ is a subset of the effects in $E_2$, and their row variables (if present) match.
Coercion from $A ! E_1$ to $A ! E_2$ is implicit when $E_1 \sqsubseteq E_2$.
4. Detailed Syntax Examples
4.1 State Effect Declaration and Usage
Defining a simple State effect that carries a 0D tensor:
effect State {
fn get() -> tensor<F32, []>
fn set(val: tensor<F32, []>) -> Unit
}
// A function performing state operations
fn increment() !{State} -> Unit {
let current = perform State.get();
perform State.set(current + 1.0)
}
4.2 Pure State Handler
The functional state-passing handler runs a computation f under State, producing a state-transforming function tensor<F32, []> -> A:
fn run_state(init: tensor<F32, []>, f: fn() !{State} -> A) -> A {
let state_runner = handle f() with {
return(x) => fn(s: tensor<F32, []>) -> A {
x
},
State.get(), k => fn(s: tensor<F32, []>) -> A {
k(s)(s)
},
State.set(v), k => fn(s: tensor<F32, []>) -> A {
k(())(v)
}
};
state_runner(init)
}
5. Runtime Execution Model: Fibers and Continuations
The Rust runtime implementing algebraic effects avoids copying heavy system threads. Instead, it utilizes Monomorphized Fibers on the stack.
+─────────────────────────────────────────────────────────────+
| Fiber Stack Frame (Rust Allocation) |
| |
| +───────────────────+ |
| | Caller context | |
| +───────────────────+ |
| | Saved registers | |
| +───────────────────+ |
| | Captured yield pt | (Suspension state target) |
| +───────────────────+ |
| |
+─────────────────────────────────────────────────────────────+
5.1 Continuation Capture (resume)
- Suspension: When
perform Effect.Op(...)is called, the current fiber registers the operation code and arguments on the active handler's boundary, saves local register states, and suspends execution. - Continuation Binding: The compiler matches the operation against the surrounding handler block. The handler branch is invoked, receiving the operation arguments and a stack-allocated continuation object
k. - Resumption: The continuation
kis a callable entity. Executingk(val)restores the saved register context, putsvalinto the resumption register, and jumps back to the instruction immediately following theperformboundary. - Single-shot Enforcement: Continuations in
kina-aiare linear/single-shot by default. Calling the same continuationktwice triggers a runtime safety panic. This matches the single-pass nature of automatic differentiation.
6. Verification and Invariant Auditing
Every compiler backend pass generating LLVM IR or MLIR containing effect row transformations must satisfy:
- Static Effect Safety: No compiled program can perform an effect that is not present in its declared effect signature.
- Unwind Integrity: Stack frames allocated during handler invocation must be cleanly unwound or fully resumed. Under no circumstances may a handler exit without reclaiming fiber frames.