Skip to content

Comptime System

comptime runs Shape code during compilation and can transform the final program before runtime execution starts.

This is one system with multiple entrypoints:

  • comptime { ... } blocks and expressions
  • comptime fn helpers
  • annotation compile-time hooks: comptime pre(...) and comptime post(...)

If you understand one, you understand all three.

Use comptime when runtime is too late:

  • validate contracts early (error(...), warning(...))
  • generate or extend APIs (extend, replace body)
  • concretize function signatures (set param, set return)
  • build connector-driven types from static inputs (CSV/DuckDB/Postgres-style)

For annotated functions/types, the compiler pipeline is:

  1. parse + semantic setup
  2. run annotation comptime pre hooks in source order
  3. run annotation comptime post hooks in source order
  4. apply emitted directives (set param, set return, extend, …)
  5. compile final runtime code
  6. emit definition-time lifecycle hooks (on_define, metadata) for supported targets

For plain comptime { ... }, the block is executed during compile and replaced with its value (expression form) or discarded (top-level side-effect form).

Expression form:

const BUILD_TAG = comptime {
"dev"
}

Top-level side-effect form:

comptime {
warning("Compiling with generated query helpers")
}

Behavior:

  • expression form: evaluate now, embed literal result
  • top-level form: run for compile-time side effects only
  • directives inside the block are consumed by the compiler

Comptime runs in its own compile-time environment.

It cannot read runtime locals from surrounding code:

let marker = 42
comptime {
marker // compile-time error
}

Comptime code can use:

  • comptime builtins (warning, error, implements, build_config)
  • imported extension namespaces (if export visibility allows comptime)
  • top-level comptime fn helpers
  • annotation target descriptor + compile-time annotation arguments

Define reusable compile-time helpers at module scope:

comptime fn normalize_uri(uri: string) {
uri.trim()
}
comptime {
let u = normalize_uri(" duckdb://analytics.db ")
}

Rules:

  • callable only from compile-time contexts
  • calling from runtime code is a compile-time error
  • ideal for sharing logic between multiple annotation handlers

comptime pre/post handlers are positional:

  1. first parameter: target
  2. second parameter: ctx
  3. remaining parameters: annotation call arguments

Example:

annotation typed(name) {
comptime post(target, ctx, type_name) {
set return (type_name)
}
}

target is a structured descriptor (LSP-friendly, fixed fields):

  • target.kind: "function" | "type" | "module" | "expression" | "block" | "await_expr" | "binding"
  • target.name
  • target.fields with { name, type, annotations } (for type targets)
  • target.params with { name, type, const } (for function targets)
  • target.return_type
  • target.annotations
  • target.captures

ctx is currently a reserved structured object for future compile-time context expansion. Keep it in signatures for forward compatibility.

BuiltinPurpose
implements(type_name, trait_name)compile-time trait implementation check
warning(msg)compile-time warning
error(msg)compile-time hard failure
build_config()returns an object { debug: bool, version: string, target_os: string, target_arch: string } — the host build target and debug flag

These are compile-time-only.

DirectiveMeaningAllowed Targets
set param name = exprset the default value for an existing parameterfunction
set param name: Typeset parameter typefunction
set return Typeset return typefunction
set return (expr)set return type from evaluated payloadfunction
replace body { ... }replace function body with inline statementsfunction
replace body (expr)replace function body from evaluated payloadfunction
replace module (expr)replace the module’s items with a generated module fragmentmodule
extend TypeName { ... }add methods/implstype/function-driven generation
extend target { ... }add methods to current targettarget-aware generation
remove targetremove annotated target from outputannotation-driven pruning

Constraints:

  • set param can only concretize existing parameters
  • set return cannot override explicit source return annotations
  • replace body replaces the entire body
  • replace module (expr) is the module-target counterpart to replace body (expr): expr must evaluate to a module-source payload (Vec<Item> JSON or parseable module source) that the compiler installs in place of the original module items

expr must evaluate to one of:

  • serialized TypeAnnotation JSON
  • textual type source parseable as a Shape type annotation

This is the core for connector-driven return typing.

expr must evaluate to one of:

  • serialized Vec<Statement> JSON
  • statement source text parseable as a function body fragment

This is the core for extension-driven wrapper synthesis where the body shape depends on compile-time metadata (for example generated FFI calls).

Specialization and Monomorphization over const

Section titled “Specialization and Monomorphization over const”

When a function has const parameters and compile-time handlers, Shape can specialize per distinct const callsite values.

Model:

  1. base function is a template
  2. each distinct const-argument call can produce a specialized clone
  3. comptime hooks run against each specialized clone
  4. final runtime calls dispatch to the specialized function

This is how URI-specific connector typing works ergonomically.

// extern C declarations let comptime call native libraries directly
extern C fn duckdb_open(path: string, out_db: ptr) -> i32 from "duckdb";
extern C fn duckdb_connect(db: ptr, out_conn: ptr) -> i32 from "duckdb";
extern C fn duckdb_query(conn: ptr, sql: string, out_result: ptr) -> i32 from "duckdb";
extern C fn duckdb_row_count(result: ptr) -> i64 from "duckdb";
extern C fn duckdb_value_varchar(result: ptr, col: i64, row: i64) -> string from "duckdb";
// ... more extern C declarations for cleanup
comptime fn describe_schema(path: string, sql: string) -> string {
// open database, run DESCRIBE, map DuckDB types to Shape 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 query via Arrow C interface
}

Notes:

  • comptime fn calls extern C directly at compile time
  • the native library is resolved through the same [native-dependencies] pipeline used at runtime (project shape.toml or script frontmatter)
  • set return receives the type string from the comptime function
  • this pattern works for any database or connector with a C API

Use comptime to generate declaration sets and wrappers, but native ABI syntax, marshalling, layout, dependency, and lock contracts are defined in the canonical Native C Interop chapter.

type Unit {
comptime symbol: string = "m"
comptime precision: int = 2
}
type Celsius = Unit { symbol: "°C" }

Comptime fields are type-level metadata:

  • available at compile time
  • excluded from runtime object storage/layout
  • not writable via runtime object updates

Fields on type targets carry their annotations in target.fields. Each field has { name, type, annotations } where annotations is a list of { name, args } objects.

type Prompt {
@description("The user's query")
query: string
@description("Creativity level")
@range(0.0, 1.0)
temperature: float
}

In a comptime hook, inspect field annotations:

annotation ai() {
targets: [type]
comptime post(target, ctx) {
comptime for field in target.fields {
comptime for ann in field.annotations {
if ann.name == "description" {
// ann.args[0] is "The user's query" etc.
}
}
}
}
}

This enables schema generation patterns (JSON Schema, OpenAPI, validation) driven by field-level metadata.

Terminal window
shape expand-comptime path/to/file.shape
shape expand-comptime path/to/file.shape --module duckdb
shape expand-comptime path/to/file.shape --function connect

Use this to inspect generated wrappers/specializations and verify ergonomics.

  • keep comptime deterministic where possible
  • move repeated compile-time logic into comptime fn
  • reserve runtime wrappers (before/after) for runtime policy only
  • prefer explicit target restrictions in annotations for better LSP behavior
  • treat extension codegen APIs as normal functions: typed inputs, typed outputs