Skip to content

Comptime & Annotations Cookbook

This chapter focuses on concrete patterns you can ship.

It assumes:

  • compile-time generation with comptime pre/post
  • runtime wrapping with before/after
  • extension-provided runtime primitives (RPC clients, retries, breaker state, queues)

The language provides composition mechanics. Extensions provide infrastructure.

Use this split consistently:

  • @annotation: declare policy and attachment point
  • comptime pre/post: shape code and types before runtime
  • before/after: enforce runtime behavior
  • comptime fn: share compile-time helper logic

If code feels confusing, one of these roles is usually mixed with another.

Recipe 1: Connector-Driven Return Types (DuckDB/Postgres/CSV)

Section titled “Recipe 1: Connector-Driven Return Types (DuckDB/Postgres/CSV)”
// extern C declarations for DuckDB's C API
extern C fn duckdb_open(path: string, out_db: ptr) -> i32 from "duckdb";
extern C fn duckdb_query(conn: ptr, sql: string, out_result: ptr) -> i32 from "duckdb";
extern C fn duckdb_value_varchar(result: ptr, col: i64, row: i64) -> string from "duckdb";
// ... more extern C declarations
comptime fn describe_schema(path: string, sql: string) -> string {
// open database at compile time, run DESCRIBE, map types
// returns e.g. "Result<Table<{id: i64, name: string}>, AnyError>"
}
annotation infer_schema() {
targets: [function]
comptime post(target, ctx) {
set param path: string
set param sql: string
set return (describe_schema(path, sql))
}
}
@infer_schema()
pub fn query(const path: string, const sql: string) {
// runtime: execute query via Arrow C interface
}

Why this is ergonomic:

  • callsite remains duckdb.query("path", "SELECT ...")
  • comptime calls extern C directly at compile time
  • return type is generated before runtime via set return
  • LSP/autocomplete sees concrete generated shape
annotation serializable() {
targets: [type]
comptime pre(target, ctx) {
comptime for field in target.fields {
if field.type == "function" {
error("Serializable type cannot include function fields")
}
}
}
comptime post(target, ctx) {
extend target {
method to_json() {
// generated serializer
}
method from_json(payload) {
// generated deserializer
}
}
}
}

This is the canonical derive pattern in Shape.

Recipe 2b: Schema Generation from Field Annotations

Section titled “Recipe 2b: Schema Generation from Field Annotations”
type Prompt {
@description("The user's query")
query: string
@description("Creativity level")
@range(0.0, 1.0)
temperature: float
}
annotation json_schema() {
targets: [type]
comptime post(target, ctx) {
comptime for field in target.fields {
comptime for ann in field.annotations {
if ann.name == "description" {
// use ann.args[0] as JSON Schema "description"
}
if ann.name == "range" {
// use ann.args[0], ann.args[1] as "minimum", "maximum"
}
}
}
extend target {
method schema() {
// return generated JSON Schema object
}
}
}
}

Field annotations ({ name, args }) let comptime hooks inspect per-field metadata for validation, serialization, or API schema generation.

Native binding syntax and behavior are defined in the canonical Native C Interop chapter.

In cookbook usage, treat comptime as a declaration generator only:

  • generate extern C declarations from metadata
  • generate type C companions and ergonomic wrappers
  • keep all ABI/marshalling rules aligned to the canonical chapter

Reliability Policy Recipes (Recipes 4–10)

Section titled “Reliability Policy Recipes (Recipes 4–10)”

Recipes 4 through 10 show annotation composition patterns for reliability policies: routing, timeout, retry, circuit-breaker, fallback, shadow execution, and how to compose them. Each pattern wires the same annotation mechanics (before/after on await_expr, the { args, state } contract, and ctx.state for correlation) around a small set of policy primitives.

Status of the policy primitives: the wrapper functions referenced below (route, with_timeout, with_retry, with_circuit, with_fallback, with_shadow, outcome reporting) are extension-provided at runtime, not first-party stdlib. The first-party extensions today are python and typescript; no reliability extension ships with Shape. Treat these recipes as a guide to the annotation surface — readers wiring real reliability policies will either supply their own extension implementing these primitives, or replace the calls with equivalent in-process logic.

The annotation mechanics shown in each recipe (target kind, before rewrite, after post-processing, ctx.state correlation, composition order) are correct against the language and are exercised by the test suite using in-tree primitives.

annotation host(name) {
targets: [await_expr]
before(args, ctx) {
// `route` is extension-provided: convert awaitable to remote awaitable
{ args: [route(name, args[0])], state: { host: name } }
}
after(args, result, ctx) {
result
}
}
let res = await @host("eu-west") fetch_order(id)

Mechanically, this works because await annotations can rewrite the awaited subject in before and inspect resolved value in after. The route primitive is extension code.

annotation timeout(ms) {
targets: [await_expr]
before(args, ctx) {
{ args: [with_timeout(args[0], ms)], state: { timeout_ms: ms } }
}
after(args, result, ctx) {
result
}
}

with_timeout is extension-provided. Annotation mechanics are native.

annotation retry(max_attempts, backoff_ms) {
targets: [await_expr]
before(args, ctx) {
{
args: [with_retry(args[0], max_attempts, backoff_ms)],
state: { attempts: max_attempts, backoff_ms: backoff_ms }
}
}
after(args, result, ctx) {
result
}
}

Use this as a pure wrapper; keep retry state in extension/runtime infra.

annotation circuit(key, fail_threshold, reset_ms) {
targets: [await_expr]
before(args, ctx) {
{
args: [with_circuit(args[0], key, fail_threshold, reset_ms)],
state: { key: key }
}
}
after(args, result, ctx) {
record_outcome(ctx.state.key, result)
result
}
}

Use ctx.state only for structured correlation data.

annotation fallback(policy) {
targets: [await_expr]
before(args, ctx) {
{ args: [with_fallback(args[0], policy)], state: { policy: policy } }
}
after(args, result, ctx) {
result
}
}

Fallback can be backup host, stale cache, synthetic default, or queue handoff.

annotation shadow(shadow_target) {
targets: [await_expr]
before(args, ctx) {
{
args: [with_shadow(args[0], shadow_target)],
state: { shadow: shadow_target }
}
}
after(args, result, ctx) {
// extension can expose primary/shadow envelope; return primary to caller
report_shadow(ctx.state.shadow, result)
result
}
}

Shadow is ideal for safe migration/validation of new backends.

let out = await @fallback("cache")
@circuit("orders", 5, 30000)
@retry(3, 100)
@timeout(500)
@host("api-east")
fetch_order(id)

Use this order as a baseline:

  1. routing (@host)
  2. timeout
  3. retry
  4. breaker
  5. fallback

Adjust per SLO and failure mode.

from std::core::snapshot use { Snapshot }
fn step(name, f) {
match snapshot() {
Snapshot::Hash(id) => {
print("checkpoint " + name + ": " + id)
exit(0)
}
Snapshot::Resumed => {
f()
}
}
}
step("ingest", || ingest())
step("normalize", || normalize())
step("publish", || publish())

This gives explicit resumable checkpoints between critical stages.

Recipe 12: Remote Resume Handoff (Conceptual)

Section titled “Recipe 12: Remote Resume Handoff (Conceptual)”

Goal: annotate await so local host snapshots and another worker resumes.

Conceptual flow:

  1. before captures a checkpoint via snapshot() and obtains Snapshot::Hash
  2. extension enqueues {snapshot_hash, target_host, payload} to remote worker
  3. local execution can park or return control marker
  4. remote worker runs shape --resume <hash> (or shape --resume <hash> script.shape)
  5. resumed path continues from checkpoint and returns result through extension transport

Sketch:

annotation host(name) {
targets: [await_expr]
before(args, ctx) {
match snapshot() {
Snapshot::Hash(id) => {
enqueue_resume(name, id, args[0])
{ args: [await_ticket(id)], state: { snapshot: id, host: name } }
}
Snapshot::Resumed => {
{ args: args, state: { resumed: true, host: name } }
}
}
}
after(args, result, ctx) {
result
}
}

This recipe depends on extension/runtime infrastructure (enqueue_resume / await_ticket are extension-provided). The annotation and snapshot mechanics are available in language/VM.

Recipe 13: Compile-Time Guardrails for Reliability Policies

Section titled “Recipe 13: Compile-Time Guardrails for Reliability Policies”
comptime fn require_const_host(target) {
if target.params.length == 0 || target.params[0].const != true {
error("First parameter must be const for host policy")
}
}
annotation host_typed() {
targets: [function]
comptime pre(target, ctx) {
require_const_host(target)
}
}

Use this to enforce constraints before runtime wrappers are even emitted.

For the resilience recipes above, extension APIs typically provide:

  • awaitable wrappers (with_timeout, with_retry, with_circuit, …)
  • remote dispatch/enqueue primitives
  • outcome recording/reporting sinks
  • optional ticket/join abstractions for remote completion

Shape annotations provide deterministic composition and static structure around those primitives.

These core mechanics are implemented:

  • await annotation before wraps the awaited input
  • await annotation after receives resolved result
  • { args, state } contract in before for runtime wrappers
  • compile-time hooks (comptime pre/post) for function/type/expression/await/binding targets
  • definition-time lifecycle hooks (on_define/metadata) enforced to function/type targets
  • snapshot resume marker correctness for Snapshot::Resumed matching
  1. keep targets: [...] explicit on every annotation
  2. keep runtime hooks side-effect minimal and structured
  3. move compile-time logic into comptime fn
  4. add tests for hook order and policy composition
  5. test resume paths separately from first-run paths