rill-lang 0.5.0

rill-lang — a Faust-style functional streaming DSL compiled to rill Algorithm<T>
Documentation

rill-lang

A Faust-style functional streaming DSL that compiles to a rill_core::Algorithm. Programs describe the internal mathematical structure of a signal-graph node as a compact block diagram; rill-lang compiles that source at runtime into a value you can drop straight into the rill graph.

The first backend is a safe, allocation-free interpreter. A Cranelift JIT backend is planned behind a future jit feature; both share the same linear IR, so the language front-end is unaffected when the JIT lands.

Example

use rill_lang::compile;
use rill_core::traits::Algorithm;

// A half-gain: y[n] = x[n] * 0.5
let mut prog = compile::<f32>("process = _ * 0.5;").unwrap();

let mut out = [0.0f32; 4];
prog.process(Some(&[1.0, 2.0, 4.0, 8.0]), &mut out).unwrap();
assert_eq!(out, [0.5, 1.0, 2.0, 4.0]);

The language in one screen

A program is a list of definitions ending in ;. One must be named process, and it must reduce to arity (0 or 1) → 1 so it fits the single-input, single-output Algorithm::process contract.

gain(x) = x * 0.5;          // a one-argument function
process  = gain(_);          // apply it to the input wire

Primitives

Syntax Meaning Arity
_ identity wire 1 → 1
! cut (discard a wire) 1 → 0
3, 3.5 int / float literal 0 → 1
+ - * / % arithmetic (as a block) 2 → 1
sin cos tan sqrt exp ln tanh abs math builtins 1 → 1
min max selection 2 → 1

Combinators (block-diagram algebra)

Operator Name Constraint Result arity
A : B sequential out(A) = in(B) (in A, out B)
A , B parallel (in A + in B, out A + out B)
A <: B split / fan-out in(B) multiple of out(A) (in A, out B)
A :> B merge / fan-in (sums) out(A) multiple of in(B) (in A, out B)
A ~ B feedback (1-sample delay) in(B) ≤ out(A), out(B) ≤ in(A) (in A − out B, out A)
A @ n integer delay (n const) A is _ → 1 same as A

Precedence, loosest → tightest: ~ < : < :> < <: < , < + - < * / % < @ < unary - < atoms.

Idioms

process = + ~ _;            // integrator:     y[n] = x[n] + y[n-1]
process = + ~ (_ * 0.5);    // leaky integrator: y[n] = x[n] + 0.5·y[n-1]
process = _ @ 1;            // one-sample delay
process = _ <: (_ , _) :> +; // fan-out then sum = 2·x

Type system

Types are inferred with a Hindley-Milner core: scalar types (int, float, type variables) are unified with an occurs check and let-generalized for named functions; wire arities are synthesized bottom-up and checked against the combinator algebra. Any mismatch is a compile error with a source span, and code generation is blocked — so an ill-formed diagram never reaches the runtime.

Serialization

With the serde feature, RillLangDef carries a program as its source string (the canonical, human-editable form) and compile_def turns it back into a runnable program:

use rill_lang::{RillLangDef, compile_def};

let def = RillLangDef::new("gain", "process = _ * 0.5;");
let prog = compile_def::<f32>(&def).unwrap();

Graph integration

The rill-adrift umbrella crate exposes rill-lang behind its lang feature, including a rill/lang factory node that reads a source parameter — so a serialized graph can embed a rill-lang block directly.

Execution model

The interpreter compiles the linear IR into a hybrid schedule via SCC analysis: feedforward regions run whole-buffer through the rill_core::math::vector SIMD eDSL, while feedback (~) and delay (@) recurrences run per-sample. The block path computes in T with zero heap allocation on the hot path. A Cranelift JIT backend is still planned and will reuse the same IR.

Built-in functions

rill-lang supports calling stateful DSP/model built-ins from rill-core-dsp/rill-core-model via compile_with(src, &registry, sample_rate). Parameters are compile-time constants (_ : lowpass(1000.0, 0.7)), signals flow via combinators. Per-sample built-ins (onepole, moog) are feedback-legal; whole-buffer built-ins (lowpass, highpass, analog_moog) are opaque block steps and cannot appear inside ~. Bindings live in rill-adrift (lang_builtins::full_registry, analog_moog behind the analog feature).

Parameters

rill-lang programs can expose named control-rate parameter slots with param("name", default[, min, max]). Parameters are RT-safe — stored in a flat array indexed by integer handle — and settable via RillProgram::set_param / param_index(name). On a rill/lang graph node they are advertised in NodeMetadata and writable by name via Node::set_parameter, so servos, LFOs, and MIDI mappings can automate them directly. The native smooth(x, ms) one-pole provides zipper-free interpolation when parameters change at block boundaries.

Status

MVP. Deferred to follow-on work: the Cranelift jit feature, foreign references to existing rill DSP primitives, and a SIMD-aware IR.

License

Apache-2.0. See the workspace LICENSE.md.