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 expressionscomptime fnhelpers- annotation compile-time hooks:
comptime pre(...)andcomptime post(...)
If you understand one, you understand all three.
Why Comptime Exists
Section titled “Why Comptime Exists”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)
Execution Model
Section titled “Execution Model”For annotated functions/types, the compiler pipeline is:
- parse + semantic setup
- run annotation
comptime prehooks in source order - run annotation
comptime posthooks in source order - apply emitted directives (
set param,set return,extend, …) - compile final runtime code
- 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).
Comptime Blocks
Section titled “Comptime Blocks”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 Scope Rules
Section titled “Comptime Scope Rules”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 fnhelpers - annotation
targetdescriptor + compile-time annotation arguments
comptime fn Helpers
Section titled “comptime fn Helpers”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 Hook Parameters: target and ctx
Section titled “Comptime Hook Parameters: target and ctx”comptime pre/post handlers are positional:
- first parameter:
target - second parameter:
ctx - 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.nametarget.fieldswith{ name, type, annotations }(for type targets)target.paramswith{ name, type, const }(for function targets)target.return_typetarget.annotationstarget.captures
ctx is currently a reserved structured object for future compile-time context
expansion. Keep it in signatures for forward compatibility.
Comptime Builtins
Section titled “Comptime Builtins”| Builtin | Purpose |
|---|---|
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.
Comptime Directives
Section titled “Comptime Directives”| Directive | Meaning | Allowed Targets |
|---|---|---|
set param name = expr | set the default value for an existing parameter | function |
set param name: Type | set parameter type | function |
set return Type | set return type | function |
set return (expr) | set return type from evaluated payload | function |
replace body { ... } | replace function body with inline statements | function |
replace body (expr) | replace function body from evaluated payload | function |
replace module (expr) | replace the module’s items with a generated module fragment | module |
extend TypeName { ... } | add methods/impls | type/function-driven generation |
extend target { ... } | add methods to current target | target-aware generation |
remove target | remove annotated target from output | annotation-driven pruning |
Constraints:
set paramcan only concretize existing parametersset returncannot override explicit source return annotationsreplace bodyreplaces the entire bodyreplace module (expr)is the module-target counterpart toreplace body (expr):exprmust evaluate to a module-source payload (Vec<Item>JSON or parseable module source) that the compiler installs in place of the original module items
set return (expr) Payload Contract
Section titled “set return (expr) Payload Contract”expr must evaluate to one of:
- serialized
TypeAnnotationJSON - textual type source parseable as a Shape type annotation
This is the core for connector-driven return typing.
replace body (expr) Payload Contract
Section titled “replace body (expr) Payload Contract”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:
- base function is a template
- each distinct const-argument call can produce a specialized clone
- comptime hooks run against each specialized clone
- final runtime calls dispatch to the specialized function
This is how URI-specific connector typing works ergonomically.
Connector-Driven Generated Types
Section titled “Connector-Driven Generated Types”// extern C declarations let comptime call native libraries directlyextern 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 fncallsextern Cdirectly at compile time- the native library is resolved through the same
[native-dependencies]pipeline used at runtime (projectshape.tomlor script frontmatter) set returnreceives the type string from the comptime function- this pattern works for any database or connector with a C API
Native C Interop
Section titled “Native C Interop”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.
Comptime Fields on Types
Section titled “Comptime Fields on Types”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
Field Annotations
Section titled “Field Annotations”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.
Tooling: Inspect Expanded Output
Section titled “Tooling: Inspect Expanded Output”shape expand-comptime path/to/file.shapeshape expand-comptime path/to/file.shape --module duckdbshape expand-comptime path/to/file.shape --function connectUse this to inspect generated wrappers/specializations and verify ergonomics.
Practical Guidance
Section titled “Practical Guidance”- 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