# Imperative blocks

## Usage

### Pipeline groups

```ops
@configure {
  git.config()
  git.auth()
}

@release needs [@configure] {
  git.bump() as bump
}

main @ship {
  release()
  deploy-all()
}

@deploy-all needs [$VERSION] wants [@release] {
  ...
}
```

### Block ops

Block ops are registry-driven ops that wrap a body of statements. They always use `op(args) { body }` syntax. All ops that were previously "decorators" or "imperative ops" use this uniform form.

```ops
@release {
  parallel.all() {
    lint()
    test()
  }

  retry(3) {
    deploy-single(datacenter: $dc)
  }

  optional() {
    timeout(30) {
      bash.shellcheck()
    }
  }
}
```

Block ops nest naturally — each wraps its body:

```ops
optional() {
  timeout(600) {
    retry(3) {
      infra.deploy(datacenter: $dc)
    }
  }
}
```

## Grammar

```
PipelineDecl    = PipelineMarkers? 'main'? '@' BlockName BlockMods? ImperBlock
ImperBlock      = '{' Statement* '}'
BlockName       = IDENT ('-' IDENT)*
PipelineMarkers = ('remote' | 'dangerous' | 'deprecated')+
BlockMods       = (NeedsClause | WantsClause)*
NeedsClause     = 'needs' '[' NeedRef* ']'
WantsClause     = 'wants' '[' NeedRef* ']'
NeedRef         = '$' IDENT | '@' IDENT | IDENT

BlockOpStmt     = BLOCK_OP Invocation '{' Statement* '}'
BLOCK_OP        = (registry-driven: ops with --block flag)
```

## Future IR Representation

Canonical pipeline, step, and block-op shapes live in [ir_schema](../ir_schema.md) and [pipelines-and-steps](../ir/pipelines-and-steps.md). The examples below are feature-level sketches.

### Pipeline groups

```json
{
  "role": "pipeline",
  "name": "group_name",
  "payload": [
    { "role": "entrypoint", "payload": true },
    ...
  ]
}
```

### Block ops

```json
{
  "role": "call",
  "payload": [
    { "role": "target", "payload": "std.retry" },
    { "role": "args", "payload": [{ "role": "positional", "payload": "3" }] },
    { "role": "block", "payload": ["bash.bats"] }
  ]
}

{
  "role": "call",
  "payload": [
    { "role": "target", "payload": "parallel.all" },
    { "role": "args", "payload": [] },
    { "role": "block", "payload": [...] }
  ]
}

// Nesting: block ops inside block ops
{
  "role": "call",
  "payload": [
    { "role": "target", "payload": "std.optional" },
    { "role": "args", "payload": [] },
    { "role": "block", "payload": [
      {
        "role": "call",
        "payload": [
          { "role": "target", "payload": "std.timeout" },
          { "role": "args", "payload": [{ "role": "positional", "payload": "600" }] },
          { "role": "block", "payload": [
            {
              "role": "call",
              "payload": [
                { "role": "target", "payload": "std.retry" },
                { "role": "args", "payload": [{ "role": "positional", "payload": "3" }] },
                { "role": "block", "payload": ["infra.deploy"] }
              ]
            }
          ]}
        ]
      }
    ]}
  ]
}
```

## Runtime

- `@operation` — registers an op in the bash runtime.
- `--block` — registry flag indicating the op wraps a body of statements.

## Implementation

### Build tiers

- **Tier 2 (Minimal parser):** `ast.rs`: `File`, `TopLevel::Pipeline`, `PipelineDecl`, `ImperBlock`, `Step`. `parser.rs`: `@name {` blocks; step = `op.path()`. IR lowering waits on a defined schema.
- **Tier 8 (Main keyword):** `PipelineDecl` gains `is_main: bool`. `main` before `@name` sets the flag.
- **Tier 11 (Block ops):** Registry enters the parser. `--block` flag on ops triggers `op(args) { body }` parsing. `ast.rs`: `Statement::BlockOp { op, args, body }`. Nesting handled by recursive parse.

### Data flow

- Variable naming: `$UPPER_CASE` for env/exports; `$lower_case.key.path` for `as` captures.
- All data passing is explicit at the call site: `op(arg: $VAR)`. No implicit env consumption.
- Parallel branches: subshells use named pipes (FIFOs) to pass exports and JSON blobs back to the parent after `wait`.

### Behavioral reference

- Top-level block names regex: `[a-zA-Z_][a-zA-Z0-9_/.-]*`
- Name mangling: `/` → `__`, `.` → `__`, `-` → `_`. Registry `safe_name` overrides.

### Validation

Diff against bash compile output; main keyword tests; multiple-main error; same-file reference error. Block ops: bats tests for `parallel.all`/`parallel.any`, `retry`, `optional`, `timeout`, nesting.

## Design Decisions

- Imperative blocks use `@`-prefixed names **without** parentheses for definition. Parentheses are reserved for invocation.
- Block modifiers — `needs` and `wants` — follow the block name. They accept `@`-prefixed group references, `$`-prefixed variable references, or plain identifiers.
- **`main` modifier:** Declares the entry point. Exactly one per file (compile error otherwise). Only affects direct invocation (`./ops <pipeline>`); no effect on imports. No same-file group may reference `main` via `needs` or direct invocation — both are compile errors. Without `main`: falls back to terminal-group detection with a deprecation warning.
- **Pipelines as functions:** Pipelines, ops, and CLIs are all functions. A group can be invoked as a step in the same file — the compiler emits `.op.sh` per group, so same-file groups are indistinguishable from external ops at resolve time. Pipelines do not declare parameter lists; the resolver discovers their interface bottom-up from consumed env vars and deps.
- **Block ops are uniform:** All ops that wrap a body use `op(args) { body }`. No decorator/descriptor distinction. The registry `--block` flag is the only classification needed. This replaces the former `--decorator`, `--descriptor`, and `--imperative` flags.
- **Metadata annotations** (e.g. deprecated, timed) that were previously descriptors are now handled via doc-comment `@keyword` tags (see [doc-comments.md](doc-comments.md)).

## Caveats

- Deprecation path when `main` is omitted: terminal-group detection is transitional, not the long-term default.
- Registry `--block` flag must be set correctly; an op without `--block` used with `{ body }` is a parse error.
