# Gotchas & common compile errors

The fastest way to stop fighting the compiler. Each row is a mistake that costs a
build round-trip, the diagnostic code you'll see (`banto.<code>`), and the fix.
Read this once before writing a game; skim it again the first time a build fails.

> Source of truth for the rules is `SYNTAX-AND-USAGE.md`; this page is the
> "things people actually trip on" subset. Diagnostic codes come from
> `packages/core/src/diagnostic-codes.ts`.

## Project & state structure

| Symptom | Code | Fix |
|---|---|---|
| Build says no start state | `missing-start-banto` | The project **must** have a `start.banto` whose `state` block is named `start`. |
| A state renders nothing / errors on entry | `default-host-missing`, `default-plyr-missing` | **Every** state needs one `host default { condition: true, … }` **and** one `plyr default { condition: true, … }`. These are the fallback screens. |
| "default must be last" / "default condition must be true" | `default-not-last`, `default-condition-not-true` | The `default` block goes **last** among its `host`/`plyr` siblings, and its `condition` must be literally `true`. Put conditional variants above it. |
| `lstn ... next` can't find a state | `state-not-found` | The `next.state` string must match a real state name. States are files: `foo.banto` → `state foo`. |
| `next` complains about inputs | `lstn-next-missing-inputs`, `lstn-next-inputs-mismatch`, `lstn-next-extraneous-inputs` | If the target state declares `params`, pass exactly those (names + types) in `next.inputs`. |

## Where blocks are allowed

| Symptom | Code | Fix |
|---|---|---|
| `cpnt` / `data` / `func` / global `var` rejected in a state file | `cpnt-only-in-globals`, `data-only-in-globals`, `func-only-in-globals`, `block-not-in-globals` | Declare custom components, datasets, custom funcs, and global vars in **`globals.banto`**. State files hold `state`, `host`, `plyr`, `lstn`, and state-local `var`/`cstate`. |
| `cstate` rejected | `cstate-only-in-states` | `cstate` (client-local draft state) lives inside a **state** block, not globals. |

## Actions, client vs server

| Symptom | Code | Fix |
|---|---|---|
| Action arrow rejected for too many params | `action-arrow-too-many-params` | Action arrows take **at most one** parameter: `arg => { … }`. Components pass a single value (`onChange: (v) => …`). |
| `client.X` rejected | `client-func-not-in-action`, `client-func-in-loop` | `client.*` (e.g. `client.playSound`) may appear **only** inside an action arrow body (`onClick`/`onChange`/`onSubmit`), and **never** inside a `.map`/`.forEach`/`.filter` body. |
| `cstate` write rejected | `cstate-write-not-top-level`, `cstate-write-interleaved`, `client-phase-mixed-branch` | `cstate.*` writes must sit at the top level of an action body and not be interleaved with server writes (`var.*`, `func.notify`, …). Do all `cstate` writes together; don't mix a `cstate` write and a `var` write in the same `if`/`forEach` branch. |
| Nothing happens when a button is clicked | *(no error)* | The **action must sit directly in a component's handler slot** (`onClick: () => …`). An action nested inside a plain-object data param is silently dropped — hoist it to the handler field. |
| Text/number input loses characters or won't submit | *(no error)* | Bind `value` to a **`cstate.<name>`**, write `cstate.<name> = arg` in `onChange`, and commit to `var.*` (and clear the cstate) in `onSubmit`. Binding `value` straight to a `var` fights the server round-trip. |

## Iterators & registry functions

| Symptom | Code | Fix |
|---|---|---|
| `.map(...)` callback rejected | `iterator-callback-statement-body` | `.map`/`.filter` callbacks must be a single **expression** (`x => x.score`), not a `{ … }` statement block. `.forEach` is the one that takes a statement body. |
| Passing an arrow to `func.mapValues` / `func.groupBy` fails | `call-arg-type-mismatch` | Registry funcs **don't accept arrow callbacks**. `mapValues`/`groupBy` take a `key` string (a property name), not a transform function. |
| `break` / `continue` rejected | `break-continue-outside-loop` | Only valid inside a `.forEach` body. |
| `Object.assign` / `Object.freeze` / etc. rejected | `unknown-func` | Banto recognizes only `Object.keys`, `Object.values`, `Object.entries`, `Object.fromEntries`. Everything else is a compile error. |
| A list of components renders empty for a non-named chain root | *(fixed in compiler)* | `[1,2,3].map(...)` etc. now compile to a `chain` IR node and iterate correctly — you don't need a named intermediate. |

## Custom `func.X`

Custom funcs are **pure helpers over their `params`** — think of them as
stateless utilities.

| Symptom | Code | Fix |
|---|---|---|
| Func body references `var`/`cstate`/`state`/`data`/`player` | `func-scope-forbidden` | Pass game state **in as an argument** (`func.score({players: var.players})`), don't read it from scope. |
| Writing to `params.x` inside a func | `func-param-readonly` | Params are read-only. Build a new value and `return` it. |
| Func calls itself | `func-recursion` | Recursion is disallowed. Flatten to a loop / `func.range(...).forEach(...)`. |
| Return-type errors | `func-missing-return`, `func-return-in-void`, `func-return-type-mismatch` | If `returnType` is set, every path must `return` an assignable value; if it's unset, don't `return` a value. |

## CSS

`styles.banto.css` is a **restricted** subset. `css-forbidden-property`,
`css-forbidden-function`, `css-forbidden-atrule`, and
`css-forbidden-element-selector` mean you used something outside the allowlist — styling has to be done via a `css` block reference (declared in `styles.banto.css`)

## Custom assets (`asst.X`)

| Symptom | Code | Fix |
|---|---|---|
| `asst.<name>` rejected as unknown | `unknown-asset` | The base name must match a file you've pushed. Add it under `assets/`, title it in `banto.config.json`, and run `banto assets push`. |
| Image asset in an audio slot (or vice versa) | *(type error)* | `asst.<name>` carries a kind. `cpnt.image` wants an `image`, `cpnt.audio`/`client.playSound` want `audio`. Push the right file and reference it. |

## Runtime (no compile error, but wrong on screen)

- **Host overflow is a bug.** The host screen shows on a TV and must never scroll
  in any direction. Size fonts/padding so the busiest phase (max players, longest
  text) still fits.
- **An empty dataset means you didn't seed one.** When you host a published
  game, `data.questionSet`/`promptSet`/`dynamic` come from the data source the
  host picks at game start — which is your `banto.config.json` `default.*`
  binding. If `default.*` is missing (and there's no `new.*` seed to bind
  `"this"` to), the dataset is empty. Ship a `new.*` seed or point `default.*`
  at a real data-source id.
- **A custom asset renders nothing.** If a `cpnt.image`/`cpnt.audio` shows
  nothing, the `asst.<name>` either wasn't pushed (`banto assets push`) or you
  passed a raw string id/URL that failed to load. `asst.<name>` refs are
  build-checked; raw strings are not.
- **`func.pickRandom` / `func.first` / `func.last` return `undefined` on an empty
  list.** Branch before use. Number aggregates (`sum`/`avg`/`minOf`/`maxOf`)
  return `0` on empty instead, so they render cleanly.