Sigil by Example

Learning CSSL through practical code
effects · linear types · autodiff · SDF · DCU
7 sections · basics → advanced · consistent with spec · Rust-hybrid surface
Note Compiler in development. Examples use the Rust-hybrid surface syntax (see dual surface). All features are spec-complete (v1); compiler support varies by feature — see Sigil reference for current status.

Overview

Each example teaches one concept. They build on each other — by the final section you will have seen the full feature set that makes Sigil different: effects verified at compile time, linear ownership without a borrow checker, source-to-source autodiff, and a type system that refuses to compile bugs that other languages let through at runtime.

Sigil has two parseable surfaces. These examples use the Rust-hybrid surface — familiar keywords, good for learning. Both surfaces parse to the same HIR; a formatter round-trips between them losslessly. The effect row sits after / in the function signature: fn f(x: T) -> R / {Effect1, Effect2} { ... }. A pure function omits the / entirely.

01 · Basics

Module declarations, let bindings, primitive types, vector types, and pure functions. A Sigil source file begins with a module declaration; everything else follows from there.

01-A · Hello World

Module, IO, and a pure string

The simplest Sigil program. main requires the {IO} effect to write to stdout — the compiler will reject a call to print from a function that does not declare it. The / {IO} syntax is the effect row.

module com.apocky.examples.hello

fn greet(name : str) -> str {
    // Pure function: no effect row needed.
    str::concat("hello, ", name)
}

fn main() -> () / {IO} {
    let msg = greet("Sigil")
    print(msg)           // print requires {IO} — compiler enforced
}
Demonstrates: module declaration · pure fn (no effect row) · IO-effectful fn · let binding · str literal

01-B · Primitive Types and Vectors

Scalars, vectors, and dimensional units

Primitive types: i32 i64 u32 u64 f32 f64 bool str. Vector types (vec2 vec3 vec4 mat4) are compiler built-ins, not structs. Dimensional suffixes like f32'pos (refined to positive reals) are part of the type, not annotations — mixing them is a compile error caught during type checking.

module com.apocky.examples.types

fn demo_types() -> () / {IO} {
    let count   : u32      = 42
    let ratio   : f64      = 3.14159265358979
    let ok      : bool     = true
    let label   : str      = "density = sovereignty"

    // Refined type: compiler guarantees radius > 0 at every use site.
    let radius  : f32'pos  = 0.5

    // Vector built-ins: swizzle, arithmetic, length all work out of the box.
    let origin  : vec3     = vec3(0.0, 0.0, 0.0)
    let point   : vec3     = vec3(1.0, 2.0, 3.0)
    let dist    : f32      = length(point - origin)   // = sqrt(14)

    print_f32(dist)
}

// Struct with annotated memory layout (F2 — layout refinement).
@layout(std430)
struct Vertex {
    pos    : vec3,   // 12 bytes, std430 aligned
    normal : vec3,   // 12 bytes
    uv     : vec2,   //  8 bytes
}
Demonstrates: primitive types · f32'pos refinement suffix · vec3 built-in · @layout struct annotation · type inference

01-C · Generics and Traits

Row-polymorphic functions

Sigil supports ML-style parametric polymorphism plus type-class style traits. The effect row is itself polymorphic — μ is the tail variable that lets callers extend a function's effects without changing its signature.

trait Blend {
    fn lerp(self, other : Self, t : f32) -> Self
}

impl Blend for f32 {
    fn lerp(self, other : f32, t : f32) -> f32 {
        self + (other - self) * t
    }
}

impl Blend for vec3 {
    fn lerp(self, other : vec3, t : f32) -> vec3 {
        self + (other - self) * vec3(t, t, t)
    }
}

// Effect-polymorphic: μ is the inferred tail.
// Any effect the body needs is propagated to callers automatically.
fn blend_pair<T : Blend>(a : T, b : T, t : f32) -> T {
    a.lerp(b, t)
}
Demonstrates: trait definitions · impl blocks · generic type parameters · effect-row polymorphism (implicit tail)

02 · Memory & Capabilities

CSSL uses Pony-6 reference capabilities instead of a borrow checker. Six keywords — iso trn ref val box tag — control aliasing and mutability. The compiler statically tracks ownership transitions; there is no runtime overhead.

02-A · GPU Buffer Lifecycle

iso → write → val: build, fill, freeze

A GPU buffer starts as iso (exclusively owned, writable). After uploading vertex data, it is consumed and returned as val (immutable-shared), safe for concurrent reads from multiple shader stages. The capability transition is tracked in the type — not at runtime.

module com.apocky.examples.gpu_buffer

// iso = exclusively owned, linear-mutable.
// Caller cannot alias this — moving it is the only option.
fn alloc_vertex_buffer(capacity : u64) -> iso GpuBuffer
    / {GPU, NoRecurse}
{
    GpuBuffer::alloc(capacity)
}

// Consumes iso, uploads verts, returns val (immutable-shared).
// box [Vertex] = read-only view; no ownership transfer.
fn upload_mesh(
    buf   : iso GpuBuffer,
    verts : box [Vertex],
) -> val GpuBuffer
    / {GPU}
{
    buf.write(verts)    // mutable write, iso required
    buf.freeze()        // iso → val: now safe to share
}

// val GpuBuffer can be read by multiple callers simultaneously.
fn bind_and_draw(
    buf    : val GpuBuffer,
    shader : val ShaderPipeline,
) -> ()
    / {GPU, Deadline<16ms>}
{
    shader.bind(buf)
    shader.draw_indexed(0, buf.vertex_count())
}
Demonstrates: iso exclusive ownership · val immutable-shared · box read-only view · {GPU} and {Deadline} effects · linear consumption

02-B · trn: Transition Capability

Build phase → immutable scene

trn is write-unique: only one writable reference exists, but read-only (box) aliases are permitted. Once construction is complete, trn transitions to val. This models the classic "builder → frozen object" pattern with static safety.

// trn = write-unique: one writer, many readers allowed.
fn build_scene() -> val Scene / {Alloc} {
    let s : trn Scene = Scene::new()

    // box aliasing is fine during build:
    let aabb = compute_aabb(s as box)
    log_bounds(aabb)   // does not consume s

    s.add_mesh(load_mesh("terrain.cssl_mesh"))
    s.add_light(DirectionalLight { dir: vec3(0.0, -1.0, -0.5), intensity: 1.0 })

    s.freeze()          // trn → val: builder pattern complete
}

// val Scene: freely shared across threads, render passes, frames.
fn render_pass(scene : val Scene, cam : Camera) -> Image
    / {GPU, Deadline<16ms>, NoAlloc}
{
    rasterize(scene, cam)
}
Demonstrates: trn transition cap · box aliasing during build · .freeze() seal · capability promotion to val

02-C · Entity Handles

Generational references — stale handle = compile guard

Handle<T> is an opaque tag-capability generational reference packed into a single u64. The generation counter prevents use-after-free. The compiler refuses direct dereference of a tag — you must go through World::get which checks liveness.

// Handle = tag + generation packed into u64.
// Solves the stale-entity-reference bug from the Labyrinth of Apocalypse retrospective.

fn spawn_enemy(world : ref World, pos : vec3) -> Handle<Enemy>
    / {State<World>}
{
    world.alloc(Enemy { pos, hp: 100 })    // returns tag-cap Handle
}

fn damage_enemy(
    world  : ref World,
    handle : Handle<Enemy>,
    amount : u32,
) -> Option<()>
    / {State<World>}
{
    // World::get checks the generation; returns None if entity was freed.
    let enemy = world.get<Enemy>(handle)?
    enemy.hp -= amount.min(enemy.hp)
    Some(())
}
Demonstrates: Handle<T> generational ref · tag cap (opaque, no deref) · ref shared-mutable · {State<S>} effect · ? propagation

03 · Effect System

Sigil's effect system is row-polymorphic (Koka lineage). A function declares its observable side-effects in the signature; the compiler unions them up the call stack. Effects compile to evidence-passing — zero runtime overhead. Handlers let you define the semantics of user-defined effects.

03-A · Declaring and Using Effects

Custom effect: Logger

effect declares a set of operations. Functions that call those operations must declare the effect in their row. At the call site you provide a handle ... with block that gives the concrete implementation — separating the what from the how.

module com.apocky.examples.logger

// Declare the effect interface.
effect Log {
    fn emit(level : LogLevel, msg : str) -> ()
    fn span(name : str) -> SpanId
}

// This function uses Log but doesn't define it.
fn load_asset(path : str) -> Asset / {Log, IO} {
    let span = Log::span("load_asset")
    Log::emit(LogLevel::Debug, str::concat("loading: ", path))
    let data = read_file(path)?
    Log::emit(LogLevel::Debug, "loaded ok")
    Asset::parse(data)
}

// Provide the Log handler: writes to stdout.
fn main() -> () / {IO} {
    handle load_asset("player.mesh") with {
        Log::emit(level, msg) => {
            write_stdout(str::format("[{}] {}", level, msg))
            resume(())
        }
        Log::span(name) => {
            resume(SpanId::new(name))
        }
    }
}
Demonstrates: effect declaration · effect rows propagated up the call stack · handle ... with algebraic handler · resume continuation

03-B · Resource and Timing Effects

NoAlloc + Deadline: audio-safe function

{NoAlloc} forbids heap allocation anywhere in the call tree — the compiler walks every callee and rejects if any path reaches an allocator. {Deadline<1ms>} adds a cost-model check at compile time and a wallclock guard at runtime. Together they express audio-callback safety without a separate linting pass.

// Audio callback: must complete in under 1ms, no heap allocation.
// The compiler refuses to compile this if any callee allocates.
fn audio_tick(
    buf  : iso AudioBuffer,   // exclusive ownership of the output buffer
    freq : f32'pos,            // frequency > 0 — refinement enforced
    sr   : f32'pos,            // sample rate > 0
) -> iso AudioBuffer
    / {NoAlloc, Deadline<1ms>, DetRNG}
{
    let step = 2.0 * PI * freq / sr
    let n    = buf.len()

    // Stack-allocated phase state; no heap involved.
    let mut phase : f32 = 0.0
    for i in 0..n {
        buf[i]  = sin(phase) * 0.5
        phase  += step
    }
    phase = phase % (2.0 * PI)
    buf
}

// Calling a heap-allocating function from audio_tick is a type error:
//   fn bad_tick(...) -> ... / {NoAlloc} {
//       let v = Vec::new()  // error: {Alloc} ∉ {NoAlloc} context
//   }
Demonstrates: {NoAlloc} compile-time enforcement · {Deadline<N>} latency gate · {DetRNG} determinism · iso buffer ownership · refinement types

03-C · IFC: Information-Flow Effects

Sensitive data — declassification required

{Sensitive<dom>} tags data with a confidentiality domain. The compiler threads this label through every SSA value and refuses to let sensitive data escape without an explicit declassify operation carrying a {Privilege<lvl>} gate.

// player_id is sensitive: not allowed to leak to GPU telemetry.
fn compute_score(
    player_id : u64,
    raw_score : i32,
) -> i32
    / {Sensitive<PlayerData>}
{
    let bonus = lookup_bonus(player_id)   // tainted: Sensitive<PlayerData>
    raw_score + bonus
}

// Only a Privilege function may declassify the result.
fn broadcast_score(
    player_id : u64,
    raw_score : i32,
) -> ()
    / {Privilege<GameServer>, IO}
{
    let s = compute_score(player_id, raw_score)
    // declassify is an explicit privilege exercise — auditable.
    let pub_score = declassify<PlayerData, GameServer>(s)
    network::broadcast(pub_score)
}
Demonstrates: F5 IFC labels · {Sensitive<dom>} taint propagation · {Privilege<lvl>} gate · declassify explicit escape hatch

04 · Manifold & SDF Geometry

Signed Distance Functions (SDFs) and manifold operations are compiler-aware primitives in Sigil, not library wrappers. The SDF'L<k≤1> refinement type tracks the Lipschitz constant at compile time. IFS (Iterated Function System) rules compose into fractal geometry without special syntax.

04-A · SDF Algebra

Union, intersection, smooth blend

SDF combinators obey known Lipschitz bounds. The compiler tracks these through f32'L<k> refinements and rejects combinations that would violate the bound required by the ray marcher. smin (smooth minimum) introduces a blend radius; the compiler widens the Lipschitz estimate accordingly.

@differentiable
@lipschitz(k = 1.0)
fn sphere_sdf(p : vec3, r : f32'pos) -> f32 {
    length(p) - r
}

@differentiable
@lipschitz(k = 1.0)
fn box_sdf(p : vec3, half : vec3) -> f32 {
    let q = abs(p) - half
    length(max(q, 0.0)) + min(max(q.x, max(q.y, q.z)), 0.0)
}

// SDF union: min of two Lipschitz-1 SDFs is still Lipschitz-1.
@differentiable
fn scene_sdf(p : vec3) -> f32 {
    let d_sphere = sphere_sdf(p, 0.5)
    let d_box    = box_sdf(p - vec3(1.2, 0.0, 0.0), vec3(0.3, 0.3, 0.3))
    min(d_sphere, d_box)       // union combinator
}

// smin: smooth union with blend radius k.
// Lipschitz bound widens to 1 + k/blend; compiler checks ray-march steps.
@differentiable
fn melted_scene(p : vec3) -> f32 {
    let k   = 0.1
    let d1  = sphere_sdf(p, 0.5)
    let d2  = sphere_sdf(p - vec3(0.8, 0.0, 0.0), 0.4)
    smin(d1, d2, k)            // smooth minimum — organic blend
}

// Normal via autodiff. The compiler inserts the reverse-mode AD pass.
fn surface_normal(hit : vec3) -> vec3 {
    normalize(bwd_diff(scene_sdf)(hit).d_p)
}
Demonstrates: @differentiable · @lipschitz refinement tracking · SDF union/smin combinators · bwd_diff surface-normal derivation

04-B · IFS: Iterated Function System

Fractal geometry via chaos game

An IFS is a set of affine contractions whose attractor forms a fractal. The chaos game iteratively applies a randomly-chosen contraction — with {DetRNG} ensuring bit-exact replay across machines. {NoAlloc} keeps it audio/GPU safe.

struct IFSRule {
    scale  : f32'pos,   // contraction factor in (0, 1)
    offset : vec2,
    rotate : f32,       // rotation angle, radians
}

// Chaos game: iterate IFS rules to sample the attractor.
// DetRNG: bit-exact cross-machine reproducibility.
fn ifs_chaos_game(
    rules  : box [IFSRule],
    iters  : u32,
    seed   : vec2,
    out    : iso [vec2],
) -> iso [vec2]
    / {DetRNG, NoAlloc}
{
    let mut p = seed
    let n     = rules.len() as f32

    for i in 0..iters {
        // DetRNG: seeded RNG, same sequence on every machine.
        let r   = (DetRNG::next_f32() * n) as u32
        let rul = rules[r % rules.len()]
        let cos_a = cos(rul.rotate)
        let sin_a = sin(rul.rotate)
        p = vec2(
            rul.scale * (cos_a * p.x - sin_a * p.y) + rul.offset.x,
            rul.scale * (sin_a * p.x + cos_a * p.y) + rul.offset.y,
        )
        out[i] = p
    }
    out
}
Demonstrates: IFS geometry · {DetRNG} deterministic RNG · {NoAlloc} + iso output buffer · fractal attractor iteration pattern

05 · Autodiff (F1)

Source-to-source automatic differentiation is a non-negotiable feature (F1). The Slang.D interface — @differentiable, fwd_diff, bwd_diff, IDifferentiable — is lowered by source-to-source transformation on the structured MIR. No LLVM-Enzyme; no runtime AD library. Zero overhead on non-differentiable paths.

05-A · Forward-Mode AD

fwd_diff: derivative of a scalar function

fwd_diff(f) returns a function that takes a dual number (x, dx) and returns (f(x), f'(x)·dx). This is forward-mode (Jacobian-vector product). The transformed function is emitted by the compiler — no closures, no tape.

@differentiable
fn sigmoid(x : f32) -> f32 {
    1.0 / (1.0 + exp(-x))
}

@differentiable
fn activation(x : f32, w : f32, b : f32) -> f32 {
    sigmoid(w * x + b)
}

fn demo_fwd() -> () / {IO} {
    // fwd_diff returns a function that computes (value, d/dx) simultaneously.
    let x      = 1.5
    let dx     = 1.0           // tangent seed
    let result = fwd_diff(activation)(x, dx, w=2.0, b=-0.5)
    print_f32(result.val)    // σ(2·1.5 - 0.5)   = σ(2.5)
    print_f32(result.d_x)   // ∂σ/∂x at x=1.5
}
Demonstrates: @differentiable annotation · fwd_diff forward-mode JVP · dual-number result .val and .d_x · compiler-emitted AD transform

05-B · Reverse-Mode AD

bwd_diff: gradient of a loss over many parameters

bwd_diff(f) returns the transpose of the Jacobian — a vector-Jacobian product. This is reverse-mode AD, the basis of backpropagation. The compiler emits a reversed computation graph by source-to-source transformation on the structured MIR, before SPIR-V emission.

@differentiable
fn linear_layer(
    x      : box [f32],
    w      : box [[f32]],
    b      : box [f32],
    n_in   : u32,
    n_out  : u32,
    out    : iso [f32],
) -> iso [f32] / {NoAlloc} {
    for j in 0..n_out {
        let mut acc = b[j]
        for i in 0..n_in { acc += w[j][i] * x[i] }
        out[j] = acc
    }
    out
}

@differentiable
fn mse_loss(pred : box [f32], target : box [f32], n : u32) -> f32 {
    let mut acc = 0.0
    for i in 0..n {
        let diff = pred[i] - target[i]
        acc += diff * diff
    }
    acc / (n as f32)
}

fn backprop_step(
    x       : box [f32],
    w       : ref [[f32]],
    b       : ref [f32],
    target  : box [f32],
    lr      : f32'pos,
    n_in    : u32,
    n_out   : u32,
) -> f32
    / {NoAlloc, State<[[f32]]>}
{
    // bwd_diff emits the VJP; .d_w and .d_b are the weight/bias gradients.
    let scratch = alloc_stack([f32; n_out])
    let pred    = linear_layer(x, w, b, n_in, n_out, scratch)
    let loss_fn = |p| mse_loss(p, target, n_out)
    let grads   = bwd_diff(loss_fn)(pred)

    // SGD update step.
    for j in 0..n_out {
        for i in 0..n_in { w[j][i] -= lr * grads.d_w[j][i] }
        b[j] -= lr * grads.d_b[j]
    }
    mse_loss(pred, target, n_out)
}
Demonstrates: bwd_diff reverse-mode VJP · gradient struct .d_w / .d_b · SGD update · {NoAlloc} + stack allocation · {State<S>} mutable weight update

05-C · Jet: Higher-Order AD

Jet<T,N>: up to N-th order derivatives

Jet<T,N> carries the value and its first N derivatives simultaneously — useful for curvature (N=2) in physics simulations and Hessian approximations. The Jet type is a compiler built-in; no special syntax needed to compute with it.

// Jet: value + 1st + 2nd derivative.
fn potential_energy(r : Jet<f32, 2>) -> Jet<f32, 2> {
    // Lennard-Jones potential: V(r) = ε[(σ/r)¹² - 2(σ/r)⁶]
    let eps   = jet_const(1.0)
    let sigma = jet_const(1.0)
    let s6    = (sigma / r).powi(6)
    eps * (s6 * s6 - 2.0 * s6)
}

fn force_and_curvature(r : f32'pos) -> (f32, f32) {
    // Seed with r + 1st tangent 1.0, 2nd tangent 0.0.
    let r_jet   = Jet::seed(r, 1.0, 0.0)
    let v_jet   = potential_energy(r_jet)
    // v_jet.d[0] = -∂V/∂r (force), v_jet.d[1] = ∂²V/∂r² (curvature).
    (-v_jet.d[0], v_jet.d[1])
}
Demonstrates: Jet<T,N> higher-order AD · Jet::seed tangent seeding · multi-order derivative access via .d[k] · physics simulation pattern

06 · Quantum-Inspired & DCU Operations

Sigil's determinism and numerical precision guarantees make it a natural substrate for quantum-inspired classical computation and DCU (Digital Cognitive Unit) synchronization. These examples use {PureDet} for bit-exact cross-machine reproducibility and {NoAlloc} to keep operations bounded for real-time use.

06-A · Krylov Subspace Evolution

Sparse Hamiltonian: |ψ'⟩ = exp(−iHt)|ψ⟩

Time evolution of a quantum state under a sparse Hamiltonian via Arnoldi iteration — a classical simulation technique used in quantum chemistry and quantum-inspired optimization. {PureDet} guarantees the same floating-point result on every machine, enabling deterministic simulation replay.

struct SparseH {
    row_ptr : box [u32],    // CSR format
    col_idx : box [u32],
    vals    : box [f32],    // real-valued Hamiltonian elements
    dim     : u32,
}

// Sparse matrix-vector product H|ψ⟩.
fn sparse_matvec(
    H   : box SparseH,
    psi : box [f32],
    out : iso [f32],
) -> iso [f32] / {NoAlloc} {
    for i in 0..H.dim {
        let mut acc = 0.0
        for j in H.row_ptr[i]..H.row_ptr[i+1] {
            acc += H.vals[j] * psi[H.col_idx[j]]
        }
        out[i] = acc
    }
    out
}

// k-step Arnoldi: build orthonormal Krylov basis V_k.
// PureDet: bit-exact across architectures — replay-safe simulation.
fn krylov_evolve(
    H    : box SparseH,
    psi0 : box [f32],   // initial state vector, length H.dim
    t    : f32'pos,      // evolution time
    k    : u32,           // Krylov dimension (k ≪ H.dim)
    out  : iso [f32],
) -> iso [f32]
    / {PureDet, NoAlloc, Deadline<1ms>}
{
    // V[0..k]: orthonormal Krylov basis vectors (stack-allocated).
    let V    = alloc_stack([[f32; H.dim]; k])
    let h    = alloc_stack([[f32; k  ]; k])   // Hessenberg matrix
    let tmp  = alloc_stack([f32; H.dim])

    V[0] = normalized(psi0)

    for j in 0..k-1 {
        // w = H · V[j]
        let w = sparse_matvec(H, V[j], tmp)
        // Modified Gram-Schmidt orthogonalization.
        for i in 0..=j {
            h[i][j] = dot(w, V[i])
            w      -= h[i][j] * V[i]
        }
        h[j+1][j] = norm(w)
        V[j+1]   = w / h[j+1][j]
    }

    // exp(-i·h·t) via Padé approximant; project back: out = V · exp_h · e₀.
    let exp_h = pade_expm_real(h, t, k)
    matvec_t(V, exp_h[0..k], out, k, H.dim)
}
Demonstrates: sparse CSR Hamiltonian · Krylov-Arnoldi iteration · Padé matrix exponent · {PureDet} bit-exact replay · {NoAlloc} + stack-allocated workspace

06-B · DCU Kuramoto Synchronization

Phase-lock detection via order parameter

The Kuramoto model describes synchronization of coupled oscillators — here applied to DCU (Digital Cognitive Unit) agents synchronizing their internal phase. The order parameter r ∈ [0,1] measures coherence: r≈1 is phase-locked, r≈0 is incoherent. {PureDet} ensures the entire simulation is deterministically replayable.

struct DCUOscillator {
    phi    : f32,       // phase ∈ [0, 2π)
    omega  : f32'pos,   // natural frequency, rad/s
    kappa  : f32'pos,   // coupling strength
}

// Single Euler step: dφ/dt = ω + (κ/N)·Σ sin(φⱼ - φᵢ)
fn kuramoto_step(
    agents : ref [DCUOscillator],
    idx    : u32,
    dt     : f32'pos,
) -> ()
    / {State<[DCUOscillator]>, PureDet}
{
    let n     = agents.len() as f32
    let osc   = agents[idx]
    let mut coupling_sum = 0.0

    for j in 0..agents.len() {
        if j != idx {
            coupling_sum += sin(agents[j].phi - osc.phi)
        }
    }

    agents[idx].phi += dt * (osc.omega + (osc.kappa / n) * coupling_sum)
    agents[idx].phi  = agents[idx].phi % (2.0 * PI)
}

// Order parameter r = |Σ exp(iφⱼ)| / N — measures phase coherence.
// r ≈ 1.0: phase-locked (consensus). r ≈ 0.0: incoherent (independence).
fn order_parameter(agents : box [DCUOscillator]) -> f32
    / {PureDet}
{
    let n = agents.len() as f32
    let (mut re, mut im) = (0.0, 0.0)
    for osc in agents {
        re += cos(osc.phi)
        im += sin(osc.phi)
    }
    sqrt(re * re + im * im) / n
}

// Simulate until phase-lock (r > threshold) or max_steps reached.
fn run_until_sync(
    agents    : ref [DCUOscillator],
    dt        : f32'pos,
    threshold : f32'pos,   // e.g. 0.95 for near-perfect sync
    max_steps : u32,
) -> u32
    / {State<[DCUOscillator]>, PureDet}
{
    for step in 0..max_steps {
        for i in 0..agents.len() {
            kuramoto_step(agents, i, dt)
        }
        if order_parameter(agents) > threshold {
            return step
        }
    }
    max_steps
}
Demonstrates: DCU agent model · Kuramoto coupled-oscillator dynamics · order-parameter coherence measure · {State<S>} mutable agent array · {PureDet} deterministic simulation

07 · Complete Program

A self-contained additive synthesizer tying together capabilities, effects, refinement types, and algebraic handlers. The audio callback is hard-real-time safe: {NoAlloc} forbids all heap allocation, {Deadline<1ms>} bounds latency, {DetRNG} keeps the vibrato LFO reproducible, and {Telemetry} captures per-callback timing into the signed audit ring without any user-visible code.

07 · Additive Synth

A complete mini-program: all features in one

This program defines a multi-voice additive synthesizer: each voice is a SineVoice with frequency, phase, amplitude, and a vibrato LFO. The fill_buffer callback is safe for audio threads. A Log effect handler captures telemetry through the application layer without changing the audio callback's signature.

module com.apocky.examples.synth

use std::math::{sin, cos, PI}
use std::audio::{AudioBuffer, AudioConfig}

// F2: Refined types. Amplitude ∈ [0,1]. Frequency > 0. Phase ∈ [0,2π).
type Amplitude = f32 where v >= 0.0 && v <= 1.0
type Phase     = f32 where v >= 0.0 && v < 2.0 * PI

struct SineVoice {
    freq    : f32'pos,   // fundamental Hz
    phase   : Phase,      // current phase (refined)
    amp     : Amplitude,  // loudness (refined)
    lfo_hz  : f32'pos,   // vibrato rate Hz
    lfo_dep : f32'pos,   // vibrato depth, semitones
    lfo_phi : Phase,      // LFO phase
}

// F3: Effect declaration — audio thread uses this to report metrics.
effect AudioMetrics {
    fn report_cpu_us(micros : u32) -> ()
}

// F1: @differentiable — vibrato depth can be auto-differentiated for
// inverse synthesis (finding parameters that match a target spectrum).
@differentiable
fn vibrato_freq(base : f32'pos, depth : f32'pos, phi : f32) -> f32'pos {
    // depth in semitones → multiplicative pitch deviation.
    base * exp2(depth * sin(phi) / 12.0)
}

// F3: NoAlloc + Deadline — hard real-time audio callback.
// F6: Telemetry — CPU time emitted into signed audit ring.
fn fill_buffer(
    voices : ref [SineVoice],
    cfg    : box AudioConfig,
    buf    : iso AudioBuffer,
) -> iso AudioBuffer
    / {NoAlloc, Deadline<1ms>, DetRNG,
       AudioMetrics, Telemetry<AudioCPU>}
{
    let sr   = cfg.sample_rate as f32
    let n    = buf.frames()
    let nv   = voices.len()

    // Zero the mix buffer (stack allocation only).
    let mix = alloc_stack([f32; n])

    for v_idx in 0..nv {
        let v    = &voices[v_idx]
        let step = 2.0 * PI * v.freq / sr
        let lfo  = 2.0 * PI * v.lfo_hz / sr

        let mut phi     = v.phase
        let mut lfo_phi = v.lfo_phi

        for i in 0..n {
            let mod_freq = vibrato_freq(v.freq, v.lfo_dep, lfo_phi)
            let sample   = v.amp * sin(phi)
            mix[i]      += sample
            phi          = (phi + 2.0 * PI * mod_freq / sr) % (2.0 * PI)
            lfo_phi      = (lfo_phi + lfo) % (2.0 * PI)
        }

        // Write back updated phases (ref cap permits mutation).
        voices[v_idx].phase   = phi
        voices[v_idx].lfo_phi = lfo_phi
    }

    // Normalize and write to output buffer.
    let scale = 1.0 / (nv as f32).max(1.0)
    for i in 0..n { buf.write(i, mix[i] * scale) }
    buf
}

fn main() -> () / {IO} {
    // Build three voices: A4, E5, A5 (major chord).
    let mut voices : ref [SineVoice] = [
        SineVoice { freq: 440.0, phase: 0.0, amp: 0.6, lfo_hz: 5.0, lfo_dep: 0.15, lfo_phi: 0.0 },
        SineVoice { freq: 659.2, phase: 0.0, amp: 0.5, lfo_hz: 5.2, lfo_dep: 0.10, lfo_phi: 0.5 },
        SineVoice { freq: 880.0, phase: 0.0, amp: 0.4, lfo_hz: 4.8, lfo_dep: 0.12, lfo_phi: 1.0 },
    ]
    let cfg = AudioConfig { sample_rate: 48000, frames: 256 }

    // handle wires the AudioMetrics effect to stdout.
    let buf = AudioBuffer::alloc_stack(cfg.frames)
    handle fill_buffer(voices, cfg, buf) with {
        AudioMetrics::report_cpu_us(us) => {
            print_u32(us)
            resume(())
        }
    }
}
Demonstrates: F1 @differentiable on vibrato function · F2 refined types Amplitude / Phase / f32'pos · F3 {NoAlloc} + {Deadline<1ms>} + {DetRNG} audio safety · F3 AudioMetrics custom effect + handler · F6 {Telemetry<AudioCPU>} · iso/ref/box caps in one function

Next Steps

FEEDBACK