# TreeScript Language Reference (LLM-Authoring Edition)

> Canonical, machine-readable specification of the TreeScript indicator
> language. This document is structured for both human developers and
> LLM agents that need to author or modify TreeScript indicators
> programmatically.
>
> **Companion artifacts** (single source of truth, kept in lock-step with
> the compiler):
> - JSON Schema: [`docs/whitepapers/treescript-language-schema.json`](./treescript-language-schema.json)
> - Stdlib registry (live): `src/lib/metachart/treescript/stdlib/registry.ts`
> - IR contract (live): `src/lib/metachart/treescript/ir.ts`
> - Compile pipeline: `src/lib/metachart/treescript/compile.ts`
>
> **Compiler & IR version:** `1.0.0` (IR) · compiler `1.1.0` (additive — Pine-compatible input UI metadata, see §5.2)
> **Status:** stable v1 surface

---

## 0. Scope & Audience

TreeScript is a small, sandboxed DSL for authoring chart indicators
that run inside the Treeova MetaChart Engine. It is **not** a general-
purpose language. It cannot allocate, loop, recurse, fetch, import, or
call JavaScript. Every program is a directed acyclic graph of typed
nodes that emits `plot`, `signal`, or `alert` records on each bar.

This reference is the **complete** authoring contract. If a feature is
not described here, it is not in the v1 surface. Agents must not
synthesize syntax, builtins, or stdlib symbols that are not listed.

---

## 1. Program Anatomy

Every TreeScript program is a sequence of statements in this fixed
canonical order (mixing is allowed but the order below is recommended):

```
study "Name", overlay = true|false   // exactly one, must be first

input <id> : <type> = <default> [, min=N] [, max=N] [, step=N] [, options=["a","b"]]
                              [, title="..."] [, group="..."] [, inline="..."] [, tooltip="..."]
                              [, display="all"|"data_window"|"none"] [, confirm=true|false] [, active=true|false]   // 0..64
...

let <id> = <expr>   // 0..512

plot(<expr>,   title = "...", color = #RRGGBB, pane = "price"|"oscillator"|"<name>")
signal(<expr>, title = "...", severity = "info"|"warning"|"critical")
alert(<expr>,  title = "...", message = "...")        // 0..64 emits total
```

### Hard limits (resource bounds)

| Limit                         | Value      | Error code               |
|-------------------------------|------------|--------------------------|
| Source size                   | 200 KB     | `resource_bounds_error`  |
| Inputs                        | 64         | `resource_bounds_error`  |
| `let` bindings                | 512        | `resource_bounds_error`  |
| Emits (plot+signal+alert)     | 64         | `resource_bounds_error`  |
| IR nodes                      | 2,000      | `resource_bounds_error`  |
| History lookback `expr[N]`    | N ≤ 5,000  | `history_bounds_error`   |

---

## 2. Lexical Structure

- **Comments:** `// line` and `/* block */` (block comments may span lines).
- **Identifiers:** `[A-Za-z_][A-Za-z0-9_]*`. Case-sensitive.
- **Numbers:** decimal integer or float (`14`, `2.5`). No scientific
  notation, no hex.
- **Strings:** double-quoted, with `\n`, `\t`, and `\"` escapes only.
- **Color literals:** `#RRGGBB` exactly (7 chars, hex). No 3-digit form.
- **Booleans:** `true`, `false`.
- **Whitespace:** insignificant. Newlines do not terminate statements.
- **Reserved keywords** (do **not** use as identifiers):
  `study`, `input`, `let`, `and`, `or`, `not`, `true`, `false`, `int`,
  `float`, `bool`, `color`, `string`, `series`, `overlay`, `min`, `max`,
  `default`, `plot`, `signal`, `alert`, `title`, `severity`, `pane`,
  `message`.

---

## 3. Type System

Five scalar types and two series types. Series carry one value per bar;
scalars are bar-invariant.

| Type           | Description                                  |
|----------------|----------------------------------------------|
| `int`          | Integer scalar.                              |
| `float`        | Floating-point scalar.                       |
| `bool`         | Boolean scalar.                              |
| `color`        | `#RRGGBB` color literal.                     |
| `string`       | UTF-8 string.                                |
| `series<float>`| Per-bar float (e.g. `close`, `ta.rsi(...)`). |
| `series<bool>` | Per-bar boolean (e.g. `close > open`).       |

**Promotion rules:**
- Arithmetic between `int` and `float` produces `float`.
- Arithmetic between any scalar and a `series<float>` produces `series<float>`.
- Comparisons (`>`, `<`, `>=`, `<=`, `==`, `!=`) lift to `series<bool>`
  if either operand is a series.
- Logical ops (`and`, `or`, `not`) require `bool` or `series<bool>` and
  return the same kind.
- The scalar ternary `cond ? a : b` requires `cond : bool`. For per-bar
  conditional series, use `ta.iif(cond_series, a_series, b_series)`.

---

## 4. Built-in Series

These identifiers are reserved series readable anywhere an expression
is allowed:

| Identifier   | Type            | Meaning                          |
|--------------|-----------------|----------------------------------|
| `open`       | `series<float>` | Bar open                         |
| `high`       | `series<float>` | Bar high                         |
| `low`        | `series<float>` | Bar low                          |
| `close`      | `series<float>` | Bar close                        |
| `volume`     | `series<float>` | Bar volume                       |
| `hl2`        | `series<float>` | (high + low) / 2                 |
| `hlc3`       | `series<float>` | (high + low + close) / 3         |
| `ohlc4`      | `series<float>` | (open + high + low + close) / 4  |
| `bar_index`  | `int` (per-bar) | Zero-based bar index             |
| `time`       | `series<float>` | Bar timestamp (UTC seconds)      |

---

## 5. Statements

### 5.1 `study`

Declares the indicator name and whether it overlays the price pane.
**Required**, exactly once, must precede everything else.

```
study "RSI With Bands", overlay = false
```

### 5.2 `input`

Declares a user-tweakable parameter rendered in the indicator settings
panel. Defaults are mandatory.

```
input length    : int    = 14, min = 2, max = 200, step = 1, group = "Trend", tooltip = "Lookback window in bars"
input mult      : float  = 2.0, min = 0.1, max = 10.0, step = 0.1
input src       : series<float> = close                          // default must be a built-in series
input show_band : bool   = true,  title = "Show bands", inline = "row1"
input up_color  : color  = #22c55e, inline = "row1"
input label     : string = "RSI", options = ["RSI","ARSI","Stoch RSI"], display = "data_window"
input mode      : string = "fast", options = ["fast","slow"], confirm = true
```

Rules:
- `series<float>` inputs default to a built-in series identifier
  (`open`, `high`, `low`, `close`, `volume`, `hl2`, `hlc3`, `ohlc4`).
- `min`/`max`/`step` are only valid for numeric (`int`, `float`) inputs.
  `step` must be `> 0`. If both `min` and `max` are present, `min ≤ max`.
- `options=[...]` is a string-only enum, valid only for `string` inputs.
  When present, the `default` must be one of the listed values.
- `title`, `group`, `inline`, `tooltip`, `display`, `confirm`, `active`
  are **non-normative UI hints** — the runtime ignores them entirely.
  They exist to (a) light up the host settings panel and (b) preserve
  semantics from converted Pine / Thinkscript / NinjaScript scripts.
- Each input id must be unique within the program.

> **Conversion mapping** (compiler v1.1.0):
>
> | TreeScript        | Pine v5             | Thinkscript                  | NinjaScript                 |
> |-------------------|---------------------|------------------------------|-----------------------------|
> | `min` / `max`     | `minval=` / `maxval=` | `(min=…, max=…)`           | `[Range(min,max)]`          |
> | `step`            | `step=`             | (n/a — declare in code)      | `[Range(min,max,step)]`     |
> | `options=[...]`   | `options=` / `input.enum` | `DefineGlobalEnum`     | enum + `[TypeConverter]`    |
> | `title`           | `title=`            | `(name="…")`                 | `[Display(Name=…)]`         |
> | `group`           | `group=`            | `(group="…")`                | `[Display(GroupName=…)]`    |
> | `inline`          | `inline=`           | (n/a)                        | `[Display(Order=…)]`        |
> | `tooltip`         | `tooltip=`          | `(description="…")`          | `[Display(Description=…)]`  |
> | `display`         | `display=`          | `Hide()`                     | `[Browsable(false)]`        |
> | `confirm`         | `confirm=`          | (n/a)                        | (n/a — host dialog)         |
> | `active`          | `active=`           | (n/a)                        | (n/a)                       |


### 5.3 `let`

Names an expression. Pure rebinding — `let` bindings cannot be
reassigned, and the right-hand side is evaluated in the order written.

```
let rsi_val = ta.rsi(src, length)
let upper   = 70
let lower   = 30
let oversold = rsi_val < lower
```

### 5.4 Emit statements

Three kinds, all using keyword arguments after the source expression:

```
plot(rsi_val,      title = "RSI",       color = #facc15, pane = "oscillator")
signal(oversold,   title = "Oversold",  severity = "warning")
alert(ta.crossover(rsi_val, lower), title = "RSI crossed up through 30",
      message = "RSI {rsi_val} crossed above {lower}")
```

| Emit     | Source type      | Required keys     | Optional keys      |
|----------|------------------|-------------------|--------------------|
| `plot`   | `series<float>`  | `title`           | `color`, `pane`    |
| `signal` | `series<bool>`   | `title`, `severity` | —                |
| `alert`  | `series<bool>`   | `title`           | `message`          |

`pane` accepted values:
- `"price"` — overlay on the main price chart.
- `"oscillator"` — render in the standard sub-pane below price.
- `"<custom name>"` — any other string routes to a named sub-pane;
  the renderer groups identical names into the same sub-pane.

If `pane` is omitted, the host falls back to the `study`'s `overlay`
flag: `overlay = true` → `"price"`, `overlay = false` → `"oscillator"`.

---

## 6. Expressions

### 6.1 Operator precedence (low → high)

| Level | Operators                  | Associativity |
|-------|----------------------------|---------------|
| 1     | `?:` (scalar ternary)      | right         |
| 2     | `or`                       | left          |
| 3     | `and`                      | left          |
| 4     | `not` (unary)              | —             |
| 5     | `==`, `!=`, `>`, `<`, `>=`, `<=` | left    |
| 6     | `+`, `-`                   | left          |
| 7     | `*`, `/`, `%`              | left          |
| 8     | unary `-`                  | —             |
| 9     | postfix `expr[N]`, `expr.field` | left     |
| 10    | primary (literal, ident, call, `(expr)`) | — |

### 6.2 History operator `expr[N]`

`expr[N]` reads the value of `expr` from `N` bars ago. **`N` must be
an integer literal**, `1 ≤ N ≤ 5000`. Dynamic lookback is not
supported in v1.

```
let prev_close = close[1]
let momentum   = close - close[10]
```

Out-of-range reads (early bars) return a sentinel that propagates
through arithmetic; downstream `plot`s simply skip those bars.

### 6.3 Member access `expr.field`

Used to extract a named field from a stdlib record return (e.g.
`ta.bbands`, `ta.macd`, `ta.stoch`).

```
let bb     = ta.bbands(close, 20, 2.0)
plot(bb.upper,  title = "BB Upper")
plot(bb.middle, title = "BB Mid")
plot(bb.lower,  title = "BB Lower")
```

Valid field names per stdlib symbol are listed in §7.

### 6.4 Function calls

Only stdlib symbols (§7) may be called. Calls are positional and
arity-checked at compile time.

```
ta.ema(close, 21)
math.max(a, b)
```

User-defined functions are **not** supported in v1.

---

## 7. Standard Library (v1)

Every symbol below is the complete set of callable functions. Unknown
symbols produce a `stdlib_unknown` compile error. `length`/`fast`/
`slow`/`signal`/`mult` parameters marked `literal_only` must be
integer/float **literals**, not expressions.

### 7.1 Moving averages

| Function                          | Returns         | Notes |
|-----------------------------------|-----------------|-------|
| `ta.sma(src, len)`                | `series<float>` | Simple MA. `len` literal int. |
| `ta.ema(src, len)`                | `series<float>` | Exponential MA. |
| `ta.rma(src, len)`                | `series<float>` | Wilder's smoothed MA. |

### 7.2 Momentum / oscillators

| Function                          | Returns                          |
|-----------------------------------|----------------------------------|
| `ta.rsi(src, len)`                | `series<float>` (0–100)          |
| `ta.macd(src, fast, slow, signal)`| `record:macd,signal,hist`        |
| `ta.stoch(len, smooth_k, smooth_d)`| `record:k,d`                    |

### 7.3 Volatility

| Function                          | Returns                                   |
|-----------------------------------|-------------------------------------------|
| `ta.atr(len)`                     | `series<float>` (uses high/low/close)     |
| `ta.bbands(src, len, mult)`       | `record:upper,middle,lower`               |

### 7.4 Range / extrema

| Function                          | Returns         |
|-----------------------------------|-----------------|
| `ta.highest(src, len)`            | `series<float>` |
| `ta.lowest(src, len)`             | `series<float>` |

### 7.5 Crossovers

| Function                          | Returns        |
|-----------------------------------|----------------|
| `ta.crossover(a, b)`              | `series<bool>` |
| `ta.crossunder(a, b)`             | `series<bool>` |

### 7.6 Series ternary

| Function                          | Returns         |
|-----------------------------------|-----------------|
| `ta.iif(cond, a, b)`              | `series<float>` (cond is `series<bool>`) |

### 7.7 Math (scalar)

`math.abs(x)`, `math.max(a,b)`, `math.min(a,b)`, `math.round(x)`,
`math.log(x)`, `math.sqrt(x)` — all return `float` and accept any
scalar arg.

---

## 8. Compile Pipeline & Errors

```
source ─► lex ─► parse ─► validate + build IR ─► attach sourceHash + compiledAt
```

Every stage returns structured errors of shape:

```ts
{ code: TreeScriptErrorCode, message: string, line?: number, column?: number }
```

### Error codes

| Code                     | Meaning                                            |
|--------------------------|----------------------------------------------------|
| `lex_error`              | Bad token (unterminated string, bad color, etc.)   |
| `parse_error`            | Syntax error or missing `study`                    |
| `validation_error`       | Generic semantic error                             |
| `type_error`             | Type mismatch in expression or emit                |
| `scope_error`            | Reference to undeclared input/let/series           |
| `history_bounds_error`   | `[N]` not literal int, or out of `1..5000`         |
| `resource_bounds_error`  | Exceeded source/input/let/emit/node limits         |
| `stdlib_unknown`         | Unknown function symbol                            |
| `stdlib_arity_error`     | Wrong number of args to stdlib call                |
| `ir_version_mismatch`    | IR `irVersion` major doesn't match runtime         |
| `runtime_error`          | Sandbox runtime failure                            |
| `budget_exceeded`        | Per-run watchdog tripped (≥ ~2s execution)         |

LLM authors should treat any error as an instruction to revise; do
**not** retry the same source unchanged.

---

## 9. Intermediate Representation (IR)

The IR is the persisted, runtime-canonical form. Source is for the
editor and audit log only; the IR is what executes in the sandbox.
Every expression becomes a typed node. Topologically sorted so a
consumer always appears after its producers.

### 9.1 IR root

```ts
interface TreeScriptIR {
  irVersion: "1.0.0";
  studyName: string;
  overlay: boolean;
  inputs: TreeScriptInputDecl[];
  nodes:  TreeScriptNode[];   // topo-sorted
  emits:  TreeScriptEmit[];
  sourceHash: string;          // SHA-256 of normalized source
  compiledAt: string;          // ISO-8601
  compilerVersion: "1.0.0";
}
```

### 9.2 Node kinds

| `kind`         | Fields                                                |
|----------------|--------------------------------------------------------|
| `input_ref`    | `input_id`                                             |
| `series_ref`   | `series` (one of the §4 builtins)                      |
| `literal`      | `value: number \| string \| bool`                      |
| `history`      | `source: nodeId`, `lookback: int (1..5000)`            |
| `binary`       | `op: add\|sub\|mul\|div\|mod\|gt\|lt\|gte\|lte\|eq\|neq`, `left`, `right` |
| `logical`      | `op: and\|or\|not`, `left`, `right?` (`not` uses `left`) |
| `ternary`      | `cond`, `then_branch`, `else_branch`                   |
| `stdlib_call`  | `fn: "ta.*\|math.*"`, `args: nodeId[]`                 |
| `record_get`   | `source: stdlib_call nodeId`, `field: "upper"\|...`    |

Each node carries `id: string` and `type: TreeScriptType`. The
validator fills `type` before the IR is frozen.

### 9.3 Emit kinds

```ts
type TreeScriptEmit =
  | { kind: "plot";   source: nodeId; title: string; color?: string; pane?: string }
  | { kind: "signal"; source: nodeId; title: string; severity: "info"|"warning"|"critical" }
  | { kind: "alert";  source: nodeId; title: string; message?: string }
```

### 9.4 Pane resolution

The renderer resolves `emit.pane` to a chart surface as follows:

1. If `pane` is `"price"` → main price overlay.
2. If `pane` is `"oscillator"` or any non-empty string → sub-pane below
   price. Identical strings group into the same sub-pane.
3. If `pane` is omitted → fall back to the `study.overlay` flag.

Authors rendering oscillators (RSI, MACD, Stoch, etc.) **must** set
`pane = "oscillator"` (or a custom sub-pane name) — these scales would
overwhelm a price overlay.

---

## 10. Sandbox Guarantees

- No `fetch`, no `importScripts`, no dynamic `import()`, no
  `WebAssembly.compile` of user bytes.
- Execution lives in a dedicated Web Worker; the only `postMessage`
  target is the parent port.
- Heartbeat every 250 ms; the host watchdog terminates after 2 s of
  silence and surfaces `budget_exceeded`.
- Source text never executes — only the IR (data, not code) is sent
  to the worker.

---

## 11. Worked Examples

### 11.1 RSI with overbought/oversold signals (sub-pane)

```
study "RSI With Bands", overlay = false

input length : int   = 14, min = 2, max = 200
input upper  : float = 70
input lower  : float = 30
input src    : series<float> = close

let r = ta.rsi(src, length)

plot(r,     title = "RSI",   color = #facc15, pane = "oscillator")
plot(upper, title = "Upper", color = #ef4444, pane = "oscillator")
plot(lower, title = "Lower", color = #22c55e, pane = "oscillator")

signal(r > upper, title = "Overbought", severity = "warning")
signal(r < lower, title = "Oversold",   severity = "warning")
alert(ta.crossunder(r, upper), title = "RSI crossed down through upper")
```

### 11.2 Bollinger Bands overlay on price

```
study "Bollinger Bands", overlay = true

input length : int   = 20, min = 2, max = 400
input mult   : float = 2.0, min = 0.1, max = 10.0
input src    : series<float> = close

let bb = ta.bbands(src, length, mult)

plot(bb.upper,  title = "BB Upper",  color = #60a5fa, pane = "price")
plot(bb.middle, title = "BB Middle", color = #94a3b8, pane = "price")
plot(bb.lower,  title = "BB Lower",  color = #60a5fa, pane = "price")
```

### 11.3 MACD across two panes (price + oscillator)

```
study "MACD Combo", overlay = false

input fast   : int = 12
input slow   : int = 26
input sigLen : int = 9
input src    : series<float> = close

let m = ta.macd(src, fast, slow, sigLen)

plot(ta.ema(src, fast), title = "EMA Fast", color = #22c55e, pane = "price")
plot(ta.ema(src, slow), title = "EMA Slow", color = #ef4444, pane = "price")

plot(m.macd,   title = "MACD",   color = #60a5fa, pane = "oscillator")
plot(m.signal, title = "Signal", color = #facc15, pane = "oscillator")
plot(m.hist,   title = "Hist",   color = #94a3b8, pane = "oscillator")

signal(ta.crossover(m.macd, m.signal),  title = "MACD bullish cross", severity = "info")
signal(ta.crossunder(m.macd, m.signal), title = "MACD bearish cross", severity = "info")
```

---

## 12. LLM Authoring Checklist

When an agent generates TreeScript source, it must verify **before
submitting**:

1. First non-comment statement is a `study "...", overlay = ...`.
2. Every identifier in an expression resolves to: a built-in series
   (§4), an `input` id, a `let` id, or a stdlib call (§7).
3. Every stdlib call uses the exact arity from §7 and respects
   `literal_only` parameters (lengths/multipliers must be literals).
4. Every `record_get` (`expr.field`) targets a record-returning stdlib
   and uses one of its declared field names.
5. Every `plot`/`signal`/`alert` source has the right type
   (`series<float>` for plots, `series<bool>` for signals/alerts).
6. Oscillator-scale series (RSI, MACD, Stoch, BB %, etc.) have
   `pane = "oscillator"` (or a named sub-pane). Price-scale series
   either omit `pane` (with `overlay = true`) or set `pane = "price"`.
7. No `expr[N]` uses a non-literal `N` or `N` outside `1..5000`.
8. Total emits ≤ 64; total `let`s ≤ 512; total inputs ≤ 64.
9. No reserved keyword (§2) is used as an identifier.

Agents should **not** invent: new stdlib symbols, alternate operator
spellings, dynamic history lookback, user-defined functions, loops,
`if`/`else` blocks, modules, or imports. None of these exist in v1.

---

## 13. Versioning

- `irVersion`: bumped on **breaking** IR shape changes. Runtime rejects
  mismatched majors with `ir_version_mismatch`.
- `compilerVersion`: bumped on any compiler change.
- The stdlib registry is additive within a major version. Removed or
  renamed symbols require a major bump.
- This document and the JSON schema are versioned to the IR. When the
  compiler advances, both update in the same change.

---

## 14. Roadmap V5–V6.1 emits (compiler 1.7.0)

§5.4 documents the original `plot` / `signal` / `alert` triad. As of
compiler **1.7.0**, three additional declarative emit families are
available. They are additive — the IR shape only grows; existing
indicators are unaffected.

### 14.1 `line(...)` — V5 price-pane segments

```
line(price1 = expr, time1 = expr, price2 = expr, time2 = expr,
     title = "..." , color = #RRGGBB, style = "solid" | "dashed")
```

- `price1` / `price2`: `series<float>` — endpoint prices per bar.
- `time1` / `time2`: `series<float>` (epoch ms) — endpoint timestamps.
- Each bar with all four values non-null contributes one segment.
- Hard caps: ≤ **200 items per emit** (`truncated: true` flag set
  when exceeded); ≤ 64 total emits per IR.
- Renderer (V5.1): one `LineSeries` per emit on pane 0; disjoint
  segments separated by `WhitespaceData` so they don't connect;
  `autoscaleInfoProvider: () => null` so drawings don't stretch the
  price scale.

### 14.2 `label(...)` — V5 price-pane markers

```
label(price = expr, time = expr, text = "..." | scalar,
      title = "...", color = #RRGGBB,
      position = "above" | "below" | "inline")
```

- `text` accepts a string literal or a scalar string source.
- Each bar with non-null `price` + `time` contributes one marker.
- Same 200-item cap as `line()`.
- Renderer (V5.1): invisible anchor `LineSeries` + `createSeriesMarkers`
  plugin (lightweight-charts v5.1 API). `position` maps to
  `aboveBar | belowBar | inBar`.

### 14.3 `cell(...)` / `hud(...)` — V6 HUD tables

```
hud(position = "top-right" | "top-left" | "bottom-right" | "bottom-left",
    title = "...")              ;; at most one per IR
cell(row = 0..15, col = 0..5,
     text = "..." | scalar,     ;; XOR with value
     value = series<float>,
     color = #RRGGBB, bg = #RRGGBB)
```

- `cell()` statements scatter freely through the program; the validator
  aggregates them into a single `HudEmit` IR node sorted by
  `(row, col)`.
- Caps: row < **16**, col < **6**, ≤ **96 cells**, ≤ **1 hud() config**.
  Cells are **excluded** from the 64-emit cap (they collapse to one).
- Exactly one of `text=` or `value=` per cell; `text` must resolve to a
  string scalar, `value` to numeric.
- Runtime (V6): **last-bar snapshot** — series sources are sampled at
  `len-1`. Null-bar values pass through as `null` so the renderer can
  show an em-dash placeholder.
- Renderer (V6.1): fixed-corner DOM overlay
  (`TreeScriptHudOverlay.tsx`) mounted on the chart container. Cards
  use a `repeat(6, minmax(0, auto))` CSS grid with explicit
  `gridRow` / `gridColumn` per cell. Semantic Tailwind tokens only
  (`bg-background`, `text-foreground`, `border-border`).

### 14.4 Drawing helpers in `TREESCRIPT_EXAMPLES`

Two starter scripts demonstrate the V5–V6.1 surface end-to-end:

- **`range-breakout-hud`** — `ta.vwap` (V1) + `ta.highest` / `ta.lowest`
  + 4-row HUD (V6 / V6.1).
- **`pivot-line-marker`** — `line()` + `label()` on a 10-bar pivot
  channel (V5 / V5.1).

Authors gate per-bar drawings/labels by nulling the source expression
(e.g. `ta.iif(cond, high, close[10000])`). There is no `na` literal in
v1.7; conditional emission is purely a sourcing concern.
