Dependent Types Tutorial
This tutorial walks through refinement types in three parts: basic parameter validation, financial proof obligations, and the MCP-guided agent repair loop. Each part shows where Calor's type-level constraints go beyond what plain C# can express.
Part 1 — API Parameter Validation
The C# Problem
In C#, parameter constraints live in scattered runtime checks:
public void ConfigureServer(int port, string hostname, int maxConnections)
{
if (port < 1 || port > 65535)
throw new ArgumentOutOfRangeException(nameof(port));
if (string.IsNullOrEmpty(hostname))
throw new ArgumentException("Hostname required", nameof(hostname));
if (maxConnections <= 0)
throw new ArgumentOutOfRangeException(nameof(maxConnections));
// ... actual logic buried after validation
}Every caller must remember to pass valid values. Every callee must remember to validate. Nothing in the type system enforces this — a port parameter typed int accepts any integer.
Calor With Inline Refinements
In Calor, constraints are part of the parameter declaration:
§F{f001:ConfigureServer:pub}
§I{i32:port} | (&& (>= # INT:1) (<= # INT:65535))
§I{str:hostname} | (> (len #) INT:0)
§I{i32:maxConnections} | (> # INT:0)
§O{void}
// ... actual logic, no validation boilerplateWith Named Refinement Types
For reusable constraints, define named refinement types:
§M{m001:Server}
§RTYPE{r1:Port:i32} (&& (>= # INT:1) (<= # INT:65535))
§RTYPE{r2:NonEmpty:str} (> (len #) INT:0)
§RTYPE{r3:Positive:i32} (> # INT:0)
§F{f001:ConfigureServer:pub}
§I{Port:port}
§I{NonEmpty:hostname}
§I{Positive:maxConnections}
§O{void}
// ...Now Port, NonEmpty, and Positive are reusable across the entire module.
What Happens in the Emitted C#
Refinement types are erased — the C# output uses base types with runtime guards for public functions:
public void ConfigureServer(int port, string hostname, int maxConnections)
{
// Boundary obligation guards (auto-generated)
if (!(port >= 1 && port <= 65535))
throw new InvalidOperationException("Obligation failed: port must satisfy Port");
if (!(hostname.Length > 0))
throw new InvalidOperationException("Obligation failed: hostname must satisfy NonEmpty");
if (!(maxConnections > 0))
throw new InvalidOperationException("Obligation failed: maxConnections must satisfy Positive");
// ...
}The key difference: in Calor the constraint is declared once in the type, and the compiler generates the guards. In C# you write them by hand in every function.
Part 2 — Financial Calculations with Proof Obligations
The Setup
A transfer function where:
- Amounts must be positive
- Balances must be non-negative
- After a transfer, the balance must remain non-negative
§M{m001:Banking}
§RTYPE{r1:PositiveAmount:i32} (> # INT:0)
§RTYPE{r2:NonNegBalance:i32} (>= # INT:0)
§F{f001:Transfer:pub}
§I{NonNegBalance:balance}
§I{PositiveAmount:amount}
§O{i32}
§Q (>= balance amount) // sufficient funds
§B{newBalance:i32} (- balance amount)
§PROOF{p1:balance-safe} (>= newBalance INT:0)
§R newBalanceZ3 Proves the Obligation
The obligation engine runs this verification:
- Assume:
balance >= 0(fromNonNegBalance),amount > 0(fromPositiveAmount),balance >= amount(precondition) - Compute:
newBalance = balance - amount - Check: Is
newBalance >= 0?
Z3 proves it: given balance >= amount and amount > 0, then balance - amount >= 0. The proof obligation is discharged — no runtime guard needed.
When Z3 Finds a Counterexample
Remove the precondition:
§F{f002:UnsafeTransfer:pub}
§I{NonNegBalance:balance}
§I{PositiveAmount:amount}
§O{i32}
// No §Q — missing sufficient funds check!
§B{newBalance:i32} (- balance amount)
§PROOF{p1:balance-safe} (>= newBalance INT:0)
§R newBalanceZ3 finds: balance = 0, amount = 1 → newBalance = -1. The obligation fails with a concrete counterexample. With the default policy, this is a compilation error.
The Fix Cycle
The counterexample tells you exactly what's wrong: the function needs (>= balance amount) as a precondition. Add it, recompile, and the obligation is discharged.
Part 3 — The MCP-Guided Agent Repair Loop
This is where refinement types and MCP tools combine. Instead of manually diagnosing obligation failures, an AI agent uses five specialized tools to navigate the repair loop with structured data.
Starting Point
An agent has written code with unconstrained parameters:
§M{m001:Calculator}
§F{f001:Divide:pub}
§I{i32:a}
§I{i32:b}
§O{i32}
§R (/ a b)
§F{f002:IndexArray:pub}
§I{i32:index}
§I{i32:size}
§O{i32}
§R indexStep 1: Discover Missing Refinements
The agent calls calor_refine with action: "types":
{
"source": "§M{m001:Calculator} ..."
}Response:
{
"success": true,
"suggestions": [
{
"function_id": "f001",
"parameter_name": "b",
"current_type": "i32",
"suggested_predicate": "(!= # INT:0)",
"reason": "used_as_divisor",
"confidence": "high",
"calor_syntax": "§I{i32:b} | (!= # INT:0)"
},
{
"function_id": "f002",
"parameter_name": "index",
"current_type": "i32",
"suggested_predicate": "(>= # INT:0)",
"reason": "used_as_index",
"confidence": "high",
"calor_syntax": "§I{i32:index} | (>= # INT:0)"
}
]
}The tool analyzed the function bodies and detected that b is used as a divisor (must be non-zero) and index is used as an array index (must be non-negative).
Step 2: Add Refinements and Check Obligations
The agent applies the suggestions and calls calor_refine with
action: "obligations":
{
"source": "§M{Calculator}\n§F{Divide:pub}\n §I{i32:a}\n §I{i32:b} | (!= # INT:0)\n §O{i32}\n §R (/ a b)\n..."
}Response:
{
"success": true,
"summary": {
"total": 2,
"discharged": 0,
"failed": 0,
"boundary": 2
},
"obligations": [
{
"id": "obl_0",
"kind": "RefinementEntry",
"function_id": "f001",
"description": "b must satisfy (!= # INT:0)",
"status": "Boundary"
},
{
"id": "obl_1",
"kind": "RefinementEntry",
"function_id": "f002",
"description": "index must satisfy (>= # INT:0)",
"status": "Boundary"
}
]
}Both are Boundary — the functions are public, so the compiler can't verify what callers pass. Runtime guards will be emitted automatically.
Step 3: Discover Guards for Failed Obligations
If the agent had private functions with failed obligations, it would call
calor_refine with action: "guards":
{
"source": "...",
"obligation_id": "obl_0"
}Response:
{
"success": true,
"guards": [
{
"obligation_id": "obl_0",
"description": "Add precondition requiring b != 0",
"calor_expression": "§Q (!= b INT:0)",
"insertion_kind": "precondition",
"confidence": "high",
"validated": true
},
{
"obligation_id": "obl_0",
"description": "Add if-guard returning error",
"calor_expression": "§IF{g1} (== b INT:0) → §R (err \"division by zero\")",
"insertion_kind": "if_guard",
"confidence": "medium"
}
]
}Guards are ranked by confidence. The validated: true flag means Z3 confirmed that adding this guard would discharge the obligation.
Step 4: The All-in-One Repair Tool
For the full repair loop in a single call, the agent uses calor_refine with
action: "diagnose":
{
"source": "...",
"policy": "default"
}Response:
{
"success": true,
"policy": "default",
"summary": {
"total": 2,
"discharged": 0,
"boundary": 2
},
"patches": [
{
"obligation_id": "obl_0",
"obligation_status": "Boundary",
"policy_action": "AlwaysGuard",
"patch_kind": "precondition",
"calor_code": "§Q (!= b INT:0)",
"description": "Add precondition for b",
"confidence": "high",
"discharges_obligations": ["obl_0"]
}
]
}The patches array gives the agent exactly what to insert and where, with obligation IDs showing which problems each patch resolves.
Why This Matters for Agent Workflows
The traditional agent approach to fixing a runtime error:
- Read error message → guess the fix → try it → compile → read new error → repeat
The MCP-guided approach:
calor_refine/types→ identify parameters that may need constraintscalor_refine/obligations→ inspect obligation statuses and counterexamplescalor_refine/guards→ request guard candidates for failed obligationscalor_refine/diagnose→ get the combined repair analysis
The tools prune the agent's search space. Instead of guessing, the agent gets structured data about what's wrong and exactly how to fix it.
MCP Tool Reference
| Tool | Purpose | When to Use |
|---|---|---|
calor_refine / types | Detect parameters needing refinements | Before adding refinements |
calor_refine / obligations | Generate and verify obligations | After adding refinements |
calor_refine / guards | Find guards for failed obligations | When obligations fail |
calor_refine / fixes | Get ranked fix strategies | When alternatives are needed |
calor_refine / diagnose | Combined repair analysis | For the full loop |
See Also
- Refinement Types Reference — Complete syntax reference
- Contracts — Preconditions and postconditions
- Static Verification — Z3-based verification
- MCP Server — All MCP tools