# `@quay/dsl` — Higher-level pricing-curve compiler (design draft)

A TypeScript library that lets MMs (and routers) author pricing curves in
ergonomic syntax, then compile them to the Quay VM bytecode defined in
[`SPEC.md`](./SPEC.md).

**Status:** design only. Not implemented.

---

## 1. Goals

- **One-line install** for any Solana toolchain: `npm install @quay/dsl`.
- **Same source of truth as on-chain.** Compiler emits the exact bytecode
  the on-chain VM executes; off-chain simulators (routers, dashboards)
  evaluate the same bytecode via `@quay/vm` (the wasm wrapper around the
  Rust VM crate).
- **Easy to author** — math expressions, named variables, no manual stack
  juggling.
- **Easy to inspect** — the bytecode is plain `Uint8Array`; the AST is
  introspectable for tooling (curve diff, gas profiling, MM dashboards).
- **Type-safe** — TypeScript catches mistakes (missing params, wrong arity,
  out-of-range immediates) at MM authoring time, before bytes hit chain.
- **Validated by construction** — the compiler only emits bytecode that
  passes the on-chain validator; if it would fail validation, compilation
  errors locally with a clear message.

## 2. Non-goals

- A general-purpose programming language. No loops, no functions calling
  themselves, no IO, no allocation.
- Cross-MM curve sharing. Each curve compiles standalone to bytes.
- Optimizing compiler. v1 emits straightforward bytecode; we add a peephole
  pass when a real curve hits the size limit.
- Solana-program client wrapper. That belongs in a separate `@quay/sdk`
  package; this lib just produces bytecode + params.

## 3. Surface area (v1 — Builder API)

Two-phase model:

1. **Author** — build an expression tree using helpers (`quote`, `inv`,
   `size`, `side`, `param`, `state`, plus arithmetic via method chaining
   or operator helpers).
2. **Compile** — call `.compile()` on the root expression. Returns
   `{ bytecode: Uint8Array, params: bigint[], stats: { stackDepth, steps,
   bytecodeBytes } }`. Throws `CompileError` with a message + AST node
   reference if the program would violate the VM spec.

### 3.1 Imports

```ts
import {
  // input registers
  quote, inv, size, side,
  // immediate / table accessors
  cnst, param, state, tierLookup,
  // ops
  add, sub, mul, div, muldiv, neg, abs,
  and, or, xor, not, shl, shr,
  min, max, clamp,
  ifLt, ifGt, ifLe, ifGe, ifEq,   // expressions, not statements
  // helpers
  BPS,                              // const = 10_000n, common in spread math
  Curve, ParamsBuilder,
} from "@quay/dsl";
```

Method-chaining sugar lives on the `Expr` base class so you can write
`a.mul(b).add(c)` as well as `add(mul(a, b), c)`.

### 3.2 Mental model

Every helper returns an `Expr`. An `Expr` represents *one value on the
VM stack at evaluation time*. The compiler walks the expression tree
post-order and emits the opcode sequence that produces that value, then
appends `HALT`. Stack-depth analysis is done bottom-up during emission
so it stays cheap.

Branching expressions (`ifGt(...)` etc.) are *expressions*, not
statements — they're emitted as `IF_*` + forward `JMP`, with the
"then" and "else" branches both producing one value at the same stack
depth. The compiler enforces both branches yield the same stack delta
(diagnostic: `branch arity mismatch`).

### 3.3 Params

Params are managed by a builder so MMs name them at authoring time and
the compiler assigns indices automatically:

```ts
const p = new ParamsBuilder();
const SKEW   = p.add("skew_bps",   100n);    // → params[0]
const UNIT   = p.add("inv_unit",   1_000_000n);
const MAXDEV = p.add("max_dev_bps", 50n);
const TIERS  = p.addTable("size_tiers", [
  [36_500_000n,    10n],
  [365_000_000n,   15n],
  [3_650_000_000n, 20n],
  [7_300_000_000n, 25n],
]);                                           // table starts at params[3]
```

`SKEW`, `UNIT`, `MAXDEV` are `ParamRef`s (typed `Expr`s). Tables return
a `TableRef` usable with `tierLookup(key, TIERS)`.

`p.toBytes()` and `p.toI64Array()` produce the wire-format userspace
bytes you stamp into a Strategy's userspace tail via `set_userspace`.

### 3.4 State (user region)

`state(offset)` and `state(offset).store(value)` for read/write of the
MM-controlled scratch region. Offset is in bytes from `BASIC_DATA_END`,
8-byte aligned. Compiler enforces alignment + non-overlapping with
basic-data at compile time (it knows the layout because `@quay/vm`
exports `BASIC_DATA_END_FOR_VERSION`).

```ts
const EMA_SLOT = 0;     // first 8 bytes of user region

const prev = state(EMA_SLOT);
const ema  = prev.mul(cnst(9n)).add(quote(0)).div(cnst(10n));
const out  = state(EMA_SLOT).store(ema).then(ema.mul(cnst(10_050n)).div(BPS));
```

`.store(x).then(y)` is a sequencing helper: emit the store opcode, leave
`y` on the stack as the result. (Internally: emit subexpr → STORE_STATE
→ emit `y`.)

### 3.5 Compile

```ts
const root = clamp(
  quote(0).mul(BPS.add(SKEW.mul(inv()).div(UNIT))).div(BPS),
  quote(0).mul(BPS.sub(MAXDEV)).div(BPS),
  quote(0).mul(BPS.add(MAXDEV)).div(BPS),
);

const { bytecode, stats } = root.compile({ params: p });

console.log(stats);
// { bytecodeBytes: 38, maxStackDepth: 4, worstCaseSteps: 12 }
```

Errors at this stage if:
- Stack depth would exceed 16
- Worst-case steps exceeds 1024
- A param/table/state ref is unused (warning, not error — but flagged)
- A branch arity mismatch
- Bytecode exceeds `MAX_BYTECODE_LEN`

## 4. Worked example end-to-end

Worked example curve: tier-laddered fee + linear inventory skew + clamp.

```ts
import { quote, inv, size, side, cnst, param, tierLookup,
         clamp, BPS, ParamsBuilder } from "@quay/dsl";

const p = new ParamsBuilder();
const SKEW    = p.add("skew_bps",    50n);
const UNIT    = p.add("inv_unit",    1_000_000n);
const MAXDEV  = p.add("max_dev_bps", 100n);
const TIERS   = p.addTable("tier_fee_bps", [
  [36_500_000n,    10n],
  [365_000_000n,   15n],
  [3_650_000_000n, 20n],
  [7_300_000_000n, 25n],
]);

// pieces
const mid       = quote(0);
const tierFee   = tierLookup(size(), TIERS);
const skewTerm  = SKEW.mul(inv()).div(UNIT);
const sideSign  = side().mul(cnst(2n)).sub(cnst(1n));        // 0→-1, 1→+1
const adjusted  = mid.muldiv(BPS.add(skewTerm).add(sideSign.mul(tierFee)), BPS);

const result = clamp(
  adjusted,
  mid.muldiv(BPS.sub(MAXDEV), BPS),
  mid.muldiv(BPS.add(MAXDEV), BPS),
);

const { bytecode, stats } = result.compile({ params: p });

// ready to submit:
//   bytecode               → Strategy.bytecode (via init_strategy
//                            or update_strategy_bytecode)
//   p.toUserspaceBytes()   → Strategy.userspace (via set_userspace,
//                            offset-addressed partial writes)
```

## 5. Future surface (v2 / nice-to-have)

### 5.1 String DSL on top of the builder

Same expressions, but parsed from text. Uses chevrotain or peggy.

```ts
import { compile } from "@quay/dsl/text";

const { bytecode, params } = compile(`
  param skew_bps   = 50
  param inv_unit   = 1_000_000
  param max_dev    = 100
  table size_tiers = [
    (36_500_000,    10),
    (365_000_000,   15),
    (3_650_000_000, 20),
    (7_300_000_000, 25),
  ]

  let mid  = quote(0)
  let fee  = tier_lookup(size, size_tiers)
  let skew = skew_bps * inv / inv_unit
  let sign = side * 2 - 1

  clamp(
    mid * (BPS + skew + sign * fee) / BPS,
    mid * (BPS - max_dev) / BPS,
    mid * (BPS + max_dev) / BPS,
  )
`);
```

Worth doing once a real MM tells you the builder is too verbose.
Useful side-effect: textual diffs in PRs when curves change.

### 5.2 Python binding

`quay-dsl-py` via pyO3 — same Rust core (compiler reused as a wasm/native
crate). Sugar uses Python's operator overloading (`__add__`, etc.) for
identical ergonomics:

```py
from quay_dsl import Curve, quote, inv, size, side, param, clamp, BPS

c = Curve()
SKEW = c.param("skew_bps", 50)
mid  = quote(0)
out  = clamp(
    mid * (BPS + SKEW * inv() / 1_000_000) / BPS,
    mid * (BPS - 100) / BPS,
    mid * (BPS + 100) / BPS,
)
bytecode, params = c.compile(out)
```

Worth doing once an MM team specifically asks. Backtesting frameworks
typically read Strategy via web3.js anyway.

### 5.3 Curve introspection / debugging tools

- `dump(expr) → string` — pretty-printed AST (for code review, MM dashboards).
- `disassemble(bytecode) → string` — opcode-level dump (for debugging
  on-chain behavior).
- `simulate(bytecode, params, { quote, inv, size, side, state }) →
  { result, trace }` — runs the VM via `@quay/vm`, returns trace of
  every opcode executed for visualization.
- `cuEstimate(bytecode) → number` — projected CU cost on Solana BPF
  (per-opcode cost table from real benchmarks).

These are tooling, not the core compiler. Build them when MMs are
debugging real curves.

## 6. Internals (rough)

**Tree:**
```
Expr
├── LeafExpr      // quote(i), inv(), size(), side(), cnst(i64), param(ref), state(off)
├── UnaryExpr    // neg, abs, not
├── BinaryExpr   // add, sub, mul, div, and, or, xor, shl, shr, min, max, store
├── TernaryExpr  // muldiv, clamp, ifXX
└── LookupExpr   // tierLookup(key, table)
```

**Compiler walks the tree post-order:**
- `LeafExpr` → emit single LOAD opcode
- `UnaryExpr(child)` → emit child, then unary op
- `BinaryExpr(l, r, op)` → emit l, emit r, emit op
- `TernaryExpr(a, b, c, op)` → emit a, b, c (in op-defined order), op
- `IfXX(cmp_l, cmp_r, then, else)` → emit cmp_l, cmp_r, IF_XX with offset
  computed after emitting the `else` branch first (so the IF jumps over
  it to land at `then`); then emit JMP at end of `then` to skip past.

**Stack-depth bookkeeping:**
- Maintained as `(current, max)` during emission.
- Each opcode declares its `(pops, pushes)`.
- After emission, validate `max ≤ 16`; otherwise compile error.

**Worst-case step counter:**
- Each opcode contributes 1 step.
- For branches, take `max(then, else)`.
- Validate ≤ 1024.

**Param table layout:**
- Scalars laid out in `add()` order: `params[0..n_scalars]`.
- Tables appended after scalars: `params[n_scalars..]`.
- `tierLookup(key, table)` emits `TIER_LOOKUP <table_start_index>`.

## 7. Open questions

- **Sequencing semantics** — `.store(x).then(y)` works for "side effect then
  return value." Multi-statement bodies (write three state slots, then return
  result) need either a sequencing primitive (`seq(a, b, c, ...)`) or block
  syntax. Probably `seq(...)` for v1.
- **Operator overloading via tagged template literals?** Could enable
  ``` math\`mid * (1 + skew_bps * inv / inv_unit)\` ``` as syntax sugar
  without a real parser. Cute but maybe too clever.
- **Constant folding** — should `cnst(10).add(cnst(5))` collapse to `cnst(15)`
  at compile time? Easy win for bytecode size but adds a pass.
- **Precision of compile-time math** — params are `bigint` (i64 wire). Compile
  errors if a `cnst(...)` overflows i64. Math ops on `Expr`s stay symbolic
  until runtime.
- **Multi-curve owner** — if an owner has many Strategies with similar curves
  parameterized differently, do we offer a `Curve.template(args)` factory?
  Easy to add later.
