# Control flow

`if` / `elif` / `else`, `match`, and `each`.

## Usage

```ops
# if/elif/else
if $BRANCH == "main" && $RELEASE_ON_MAIN {
  tag()
  deploy(deploy-token: $DEPLOY_TOKEN)
} elif $BRANCH == "dev" {
  tag()
  log.info("dev build — skipping deploy")
} else {
  log.info("feature branch — validation only")
}

# if with step condition and export
if git.bump() export VERSION {
  bash.shellcheck()
} elif git.config() {
  bash.bats()
} else {
  log.warn()
}

# if with as capture
if $info.pending > 0 {
  log.warn(`{$info.pending} pending migrations`)
} elif $info.failed > 0 {
  log.error(`{$info.failed} failed migrations`)
  throw migration-failed `database has {$info.failed} failed migrations`
} else {
  log.info("database is up to date")
}

# match
match $BRANCH {
  "main" { ci.deploy() }
  * { ci.validate() }
}

match $DATACENTER {
  production {
    deploy-production(version: $VERSION)
  }
  * {
    deploy-staging(version: $VERSION, dc: $DATACENTER)
  }
}

# each
each $target in "linux darwin" {
  bash.shellcheck()
}

each $dc in $DEPLOY_TARGETS {
  retry(3) {
    deploy-single(datacenter: $dc)
  }
}
```

## Grammar

```
IfBlock         = 'if' Condition '{' Statement* '}'
                  ('elif' Condition '{' Statement* '}')*
                  ('else' '{' Statement* '}')?
Condition       = '!'? (InfixExpr | Step | VarRef)

MatchBlock      = 'match' '$' IDENT '{' MatchArm* '}'
MatchArm        = MatchPattern '{' Statement* '}'
MatchPattern    = Literal | IDENT | '*' | '_'

EachBlock       = 'each' '$' IDENT 'in' Iterable '{' Statement* '}'
Iterable        = STRING_LIT | TEMPLATE_LIT | '$' IDENT | Step
```

## IR representation

Canonical control-flow node shapes live in [control-flow](../ir/control-flow.md). The examples here are feature-level sketches for parser and lowering behavior.

**If (step condition)**

```json
{
  "role": "branch",
  "payload": [
    {
      "role": "case",
      "payload": [
        { "role": "when", "payload": [
          { "role": "call", "payload": [
            { "role": "target", "payload": "git.bump" },
            { "role": "args", "payload": [] }
          ]}
        ]},
        { "role": "export", "payload": "VERSION" },
        ...
      ]
    },
    {
      "role": "case",
      "payload": [
        { "role": "when", "payload": [
          { "role": "expr", "payload": [
            { "role": "op", "payload": "equals" },
            { "role": "left", "payload": [{ "role": "var", "payload": "BRANCH" }] },
            { "role": "right", "payload": "main" }
          ]}
        ]},
        ...
      ]
    },
    { "role": "default", "payload": [...] }
  ]
}
```

**If (infix condition)**

```json
{
  "role": "branch",
  "payload": [
    {
      "role": "case",
      "payload": [
        { "role": "when", "payload": [
          { "role": "expr", "payload": [
            { "role": "op", "payload": "condition.and" },
            { "role": "left", "payload": [...] },
            { "role": "right", "payload": [...] }
          ]}
        ]},
        ...
      ]
    }
  ]
}
```

**Match**

```json
{
  "role": "branch",
  "payload": [
    { "role": "input", "payload": [{ "role": "var", "payload": "BRANCH" }] },
    { "role": "case", "payload": [
      { "role": "when", "payload": "main" },
      ...
    ]},
    { "role": "case", "payload": [
      { "role": "when", "payload": "dev" },
      ...
    ]},
    { "role": "default", "payload": [...] }
  ]
}
```

**Each**

```json
{
  "role": "loop",
  "payload": [
    { "role": "binding", "payload": "target" },
    { "role": "source", "payload": "linux darwin" },
    { "role": "body", "payload": [...] }
  ]
}
```

## Types

**Match expression types (settled):** scalar types with meaningful equality — string, int, number, bool, enum, semver. Excludes `time-*` types and structured/opaque types (json, glob, command, secret).

## Implementation

**Tier 9** (`if` / `try` / `with`): `ast.rs` — `Statement::If`. `parser.rs` — parse `if` / `elif` / `else`. `lower.rs` — emit nested IR, `ExportAssign` desugaring.

**Tier 10** (`match` / `each`): `ast.rs` — `Statement::Match { expr, arms }`, `Statement::Each { var, list_expr, body }`. `parser.rs` — `match $VAR {` → arms with pattern/wildcard; `each $VAR in EXPR {`. `lower.rs` — `match` → `arms`/`default` IR; `each` → `each` / `as` / `do` structure.

## Design decisions

- Structural control flow is explicit in the AST as block statements, not as ad-hoc shell fragments.

## Caveats

- **Bash:** `elif` conditions do not support infix expressions; the Rust implementation fixes this.
- **Bash:** `if` drops prior steps — entering `if` resets `block_body`, dropping preceding steps. The Rust AST models blocks as `Vec<Statement>`.

### Newline rules for control flow

Newlines are significant statement terminators. Between these rules, the parser
enforces one-statement-per-line while still allowing readable multi-line layouts:

1. **Newlines terminate statements.** After a step, `@use`, or compound statement,
   the next token on the same line must be a newline, `}`, comment, or EOF.
2. **`()` absorbs newlines.** Multi-line conditions and invocation arguments are
   fine when wrapped in parens — newlines inside balanced `()` are whitespace.
3. **`{` can be on its own line.** The opening brace does not have to share a line
   with the keyword or condition.
4. **`elif` / `else` / `catch` / `finally` can follow `}` across newlines.** After
   the closing `}` of a branch, the parser peeks past trivia for a continuation
   keyword. If found, the compound statement continues; otherwise it ends.

```ops
# valid: multi-line condition in parens, elif/else on own lines
if (
  $BRANCH == "main"
  &&
  $RELEASE_ON_MAIN
) {
  tag()
}
elif $BRANCH == "dev"
{
  tag()
}
else
{
  log.info("feature branch — validation only")
}
```

## Open questions

- **`each` list sources** (settled): only list-like types are valid after `in`. Strings, scalars, and other non-list types are compile errors — if you need to iterate a string, split it explicitly first.
- **Iterator indexing** (settled): zero-based. `$bump[0].version` for the first element. Matches bash array semantics — no hidden offset in emitted code.

## Validation

Bats tests should cover all `if` / `elif` / `else` variants, `match` arms and wildcard default, `each` blocks, and malformed-syntax errors.
