# The Box-DSL — `toBox` syntax

The Box-DSL is a tiny declarative expression language pack authors write inside `toBox.outputs[port]` (and optionally `toBox.defs[name]`) strings. The Builder parses each expression at compile time, evaluates it against a substitution env (params + inputs + defs), and produces a **Box AST** that the Faust generator emits as DSL source. Same primitive the Builder uses internally — your pack composes Box ASTs the same way `faustGen` composes graphs.

## Why Box-DSL

The pre-Phase-4 surface used `{{params.X}}` / `{{inputs.Y}}` template strings — pure substitution into hand-written Faust strings. That's gone. Box-DSL replaced it because:

- **Box ASTs compose**. Wrapping a kind's expression in another transform (Sound-Quality Welle's drift / multi-voice / smoothing) means walking + rewriting structure, not regexing template strings.
- **The same primitives the graph-level generator uses**. `faustGen.ts` itself emits Box ASTs (`seq`, `par`, `withDefs`, `parLoop`) for FX-shape graphs + multi-voice unison. Pack authors and generator authors speak the same DSL.
- **Arity is checkable**. Every Box carries `inputs` / `outputs` counts — wire mismatches surface as build-time errors instead of opaque libfaust diagnostics.

Migration is complete: no remaining string-template Kinds in the bundled packs as of v0.64.x.

## The constructors

A `toBox` expression is a tree of these constructors. Whitespace + newlines are ignored — write them inline or pretty-print across lines.

### Leaves

| Constructor | Emits | Arity | Notes |
|---|---|---|---|
| `input('id')` | The input port's upstream wire expression, or `0` if unconnected | (0, 1) | Match the `id` field of an entry in `inputs[]` |
| `param('id')` | The param's resolved Faust source (hslider with smoothing / velocity / drift wraps applied) | (0, 1) | Match the `id` field of an entry in `params[]` |
| `def('name')` | A previously-declared `defs[name]` Box, in declaration order | (0, 1+) | Forward refs are an error |
| `prim('symbol', args...)` | A bare Faust function call: `symbol(a, b, …)` | depends | For primitives not aliased in stdfaust (e.g. `T1_12AX7` from tubes.lib) |
| `real(0.5)` / `int(2)` | A literal number | (0, 1) | Use `int` for indices, `real` for everything else |
| `wire()` | The identity wire `_` | (1, 1) | For `par(wire(), …)` route-passthrough patterns |
| `cut()` | The cut wire `!` | (1, 0) | For dropping unused parallel branches |
| `mem` and `mem(box)` | Faust `mem` (one-sample memory) | (1, 1) | Used in feedback loops; see ping_pong_delay |

### Composers

| Constructor | Emits | Notes |
|---|---|---|
| `seq(a, b, …)` | `a : b : …` (sequential composition) | Output arity of left must match input arity of right |
| `par(a, b, …)` | `a, b, …` (parallel composition) | Independent inputs + outputs |
| `parLoop('i', N, body)` | `par(i, N, body)` — bind `i` to 0..N-1 | Used by faustGen for unison multi-voice |
| `withDefs([{name, body}], outer)` | `outer with { name = body; … };` | Local definitions inside an expression |
| `merge(a, b)` | `a :> b` (multi-source merge) | Sum N-out into M-in |
| `split(a, b)` | `a <: b` (split + fan-out) | Fan one signal to many |

### Library calls

| Constructor | Emits | Notes |
|---|---|---|
| `lib('ns', 'fn', args...)` | `ns.fn(args)` | `ns` is a stdfaust alias (`os`, `fi`, `re`, `ef`, `pm`, `an`, `ma`, `ba`, `si`, `de`, `en`, `no`) — they're declared in stdfaust.lib |
| `prim('fn', args...)` | `fn(args)` (bare name) | For libraries NOT auto-imported by stdfaust — declare `imports: ["xyz.lib"]` on the kind so faustGen emits the explicit `import("xyz.lib")` (see § 3) |

### Arithmetic + comparators

Available as constructors for cleaner reading vs `prim('+')` / `prim('*')`:

`plus(a, b)`, `minus(a, b)`, `times(a, b)`, `divide(a, b)`, `over(a, b)` (same as divide), `intCast(box)`, `realCast(box)`.

These compose like any other Box and respect arity. `times(param('mix'), input('in'))` reads as `mix * in`.

## Worked examples — from the bundled packs

### 1. Lowpass filter — one library call, one param

```json
{
  "kind": "lp",
  "inputs": [
    { "id": "in", "label": "in", "kind": "audio" },
    { "id": "cutoff_in", "label": "cutoff", "kind": "control" }
  ],
  "outputs": [{ "id": "out", "label": "out", "kind": "audio" }],
  "params": [
    { "type": "hslider", "id": "cutoff", "label": "Cutoff", "init": 1000,
      "min": 20, "max": 20000, "step": 1, "unit": "Hz", "scale": "log",
      "style": "knob", "smoothing_class": "cutoff", "velocity_responds": 0.3 }
  ],
  "toBox": {
    "outputs": {
      "out": "lib('fi', 'lowpass', int(2), param('cutoff'), input('in'))"
    }
  }
}
```

Emits roughly: `fi.lowpass(2, (cutoff_hslider : si.smooth(...) : <velocity-wrap>), in)`. The `smoothing_class: "cutoff"` + `velocity_responds: 0.3` wraps automatically — see § 4.

### 2. Vintage tube stage — bare-name primitive + imports

```json
{
  "kind": "tube_12ax7_t1",
  "imports": ["tubes.lib"],
  "inputs": [{ "id": "in", "label": "in", "kind": "audio" }],
  "outputs": [{ "id": "out", "label": "out", "kind": "audio" }],
  "params": [],
  "toBox": {
    "outputs": { "out": "prim('T1_12AX7', input('in'))" }
  }
}
```

`prim` (not `lib`) because `T1_12AX7` lives in `tubes.lib`, which isn't aliased by stdfaust. The `imports: ["tubes.lib"]` field tells `faustGen` to emit `import("tubes.lib");` in the DSP header so the bare name resolves. See § 3 for the plumbing.

### 3. Ping-pong delay — shared `defs` + per-channel `wet_l` / `wet_r`

```json
{
  "kind": "ping_pong_delay",
  "inputs": [
    { "id": "in_l", "label": "L", "kind": "audio", "channels": 2 },
    { "id": "in_r", "label": "R", "kind": "audio", "channels": 2 }
  ],
  "outputs": [
    { "id": "out_l", "label": "L", "kind": "audio", "channels": 2 },
    { "id": "out_r", "label": "R", "kind": "audio", "channels": 2 }
  ],
  "params": [
    { "type": "hslider", "id": "time_ms", "label": "Time", "init": 250,
      "min": 1, "max": 2000, "step": 0.1, "unit": "ms", "style": "knob" },
    { "type": "hslider", "id": "feedback", "label": "Feedback", "init": 0.4,
      "min": 0, "max": 0.95, "step": 0.001, "style": "knob",
      "smoothing_class": "mix" },
    { "type": "hslider", "id": "mix", "label": "Mix", "init": 0.4,
      "min": 0, "max": 1, "step": 0.001, "style": "knob",
      "smoothing_class": "mix" }
  ],
  "toBox": {
    "defs": {
      "samples": "intCast(over(times(param('time_ms'), lib('ma', 'SR')), int(1000)))",
      "wet_l": "seq(plus(input('in_l'), times(mem(input('in_r')), param('feedback'))), lib('de', 'delay', int(96000), def('samples')))",
      "wet_r": "seq(plus(input('in_r'), times(mem(input('in_l')), param('feedback'))), lib('de', 'delay', int(96000), def('samples')))"
    },
    "outputs": {
      "out_l": "plus(times(input('in_l'), minus(real(1), param('mix'))), times(def('wet_l'), param('mix')))",
      "out_r": "plus(times(input('in_r'), minus(real(1), param('mix'))), times(def('wet_r'), param('mix')))"
    }
  }
}
```

`defs.samples` resolves first (referenced by both `wet_l` + `wet_r`), then the two wet paths, then the dry+wet mix per output. Notice `mem(input('in_r'))` in `wet_l` — that's the ping-pong feedback: L's delay reads from the previous-sample R, R reads from previous L. The `channels: 2` on every port + every paired pair flags this as a stereo-bundle Kind (see § 3).

## Substitution semantics — what fires when

When the Builder compiles a kind instance, the adapter (`packKindAdapter.ts`) walks `toBox`:

1. Resolve `params` — each `ParamDef` becomes a Faust source string with the Sound-Quality wraps applied (smoothing → velocity → drift in that order). Wrapped in a `raw` Box.
2. Resolve `inputs` — each input port becomes either the upstream's emitted expression (if wired) or the literal `0` (if unconnected).
3. Walk `defs` in declaration order — each def's expression parses against `{ params, inputs, defs-so-far, literals, faustId }`. Later defs can reference earlier ones via `def('name')`.
4. Walk `outputs` — same env, plus all defs in scope.
5. Each output's Box becomes the value for its port id in the kind's `toBox(ctx)` return.

The Builder's `faustGen` then plugs the per-kind output Boxes into the graph's wire structure, wraps multi-voice / FX-shape / vgroup as needed, and emits the final Faust source.

## Common patterns

- **Mono effect**: `seq(input('in'), lib(...))`. The classic shape — take input, transform, emit.
- **Param-controlled lib call**: `lib('fi', 'lowpass', int(2), param('cutoff'), input('in'))`. Standard 2nd-order Butterworth.
- **Mix dry/wet**: `plus(times(input('in'), minus(real(1), param('mix'))), times(def('wet'), param('mix')))`. The 1−mix on dry pattern, with the wet path in a `def` for reuse.
- **Feedback line**: `mem(...)` wraps a one-sample memory; combine with `plus` inside a `seq` for delay lines + comb filters.
- **Stereo splitter**: `par(input('in_l'), input('in_r'))`. Brings paired ports into a single `(2, 2)` Box for chaining through a stereo lib like `re.jpverb`.

> 🔘 **Tip**: when stuck, look at how the bundled packs do it. `packages/app/build-resources/bundled-node-packs/*.nodepack.json` are all Box-DSL — every category of `toBox` you'd want to write has a precedent there.
