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.
Quick Mental Model
Section titled “Quick Mental Model”Use this split consistently:
@annotation: declare policy and attachment pointcomptime pre/post: shape code and types before runtimebefore/after: enforce runtime behaviorcomptime 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 APIextern 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 Cdirectly at compile time - return type is generated before runtime via
set return - LSP/autocomplete sees concrete generated shape
Recipe 2: Serde-Style @serializable
Section titled “Recipe 2: Serde-Style @serializable”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.
Recipe 3: Native C Bindings
Section titled “Recipe 3: Native C Bindings”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 Cdeclarations from metadata - generate
type Ccompanions 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.
Recipe 4: Await Remote Routing (@host)
Section titled “Recipe 4: Await Remote Routing (@host)”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.
Recipe 5: Timeout Wrapper
Section titled “Recipe 5: Timeout Wrapper”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.
Recipe 6: Retry with Backoff
Section titled “Recipe 6: Retry with Backoff”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.
Recipe 7: Circuit Breaker
Section titled “Recipe 7: Circuit Breaker”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.
Recipe 8: Fallback Strategy
Section titled “Recipe 8: Fallback Strategy”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.
Recipe 9: Shadow Execution
Section titled “Recipe 9: Shadow Execution”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.
Recipe 10: Compose Reliability Policies
Section titled “Recipe 10: Compose Reliability Policies”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:
- routing (
@host) - timeout
- retry
- breaker
- fallback
Adjust per SLO and failure mode.
Recipe 11: Checkpointed Workflow Steps
Section titled “Recipe 11: Checkpointed Workflow Steps”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:
beforecaptures a checkpoint viasnapshot()and obtainsSnapshot::Hash- extension enqueues
{snapshot_hash, target_host, payload}to remote worker - local execution can park or return control marker
- remote worker runs
shape --resume <hash>(orshape --resume <hash> script.shape) - 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.
What Must Exist on the Extension Side
Section titled “What Must Exist on the Extension Side”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.
Mechanics Status (Current)
Section titled “Mechanics Status (Current)”These core mechanics are implemented:
- await annotation
beforewraps the awaited input - await annotation
afterreceives resolved result { args, state }contract inbeforefor 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::Resumedmatching
Production Checklist
Section titled “Production Checklist”- keep
targets: [...]explicit on every annotation - keep runtime hooks side-effect minimal and structured
- move compile-time logic into
comptime fn - add tests for hook order and policy composition
- test resume paths separately from first-run paths