
# Custom Types

User-defined type constructors. A custom type is defined using the `@type.` prefix, with an `@is` inner function that validates input, optionally transforms it, and returns a typed value. Custom types can be defined in `.op.sh` files (bash) or `.ops` files (DSL) — both compile to the same mechanism.

Once registered, custom types are automatically available in all `@verb.type` positions (`@in.type`, `@out.type`) via the dynamic dispatch system (`command_not_found_handle`).

## Usage

### Defining in `.op.sh` files (bash)

```bash
@type.port() {
    @is() {
        local value="$1"
        @int.is "$value"
        if (( value < 1 || value > 65535 )); then
            @throw "port-out-of-range" "port must be between 1 and 65535"
        fi
        @return "$value"
    }

    @default() {
        @return 8080
    }
}
```

**Type contract functions:**

| Function | Role | Required |
| --- | --- | --- |
| `@is` | Constructor/validator — validates input, optionally transforms, returns typed value | yes |
| `@default` | Zero value — returned when the type is constructed with no argument | no |

`load_ops` calls the outer function, discovers contract functions, and generates:
- `@port` — constructor. With args: calls `@is`. No args: calls `@default` (or throws if no `@default`).
- `@port.is` — validator (calls `@is`, discards output, surfaces throws)
- Dynamic dispatch for `@in.port` and `@out.port`, plus modifier variants (`@in.port?`, `@out.port[]`, etc.)

**Default values** work like zero values in typed languages:

```bash
$(@port)         # → 8080 (from @default)
$(@port 3000)    # → 3000 (from @is)
```

Built-in types have implicit defaults: `@int` → `0`, `@string` → `""`, `@bool` → `false`, `@array` → `[]`.

### Validate-and-transform types

Custom types can return structured output, not just pass-through strings:

```bash
@type.endpoint() {
    _parse_host() {
        echo "${1%%:*}"
    }

    _parse_port() {
        echo "${1##*:}"
    }

    @is() {
        local value="$1"
        local host=$(_parse_host "$value")
        local port=$(_parse_port "$value")
        @url.is "$host"
        @port.is "$port"
        @out.url host "$host"
        @out.port port "$port"
        @return.out
    }
}
```

### Using custom types in ops

Once loaded, custom types work identically to built-in types in the `@verb.type` pattern:

```bash
@operation.deploy.service() {
    @in.endpoint  target
    @in.port      health-port
    @out.port     health-port
    @out.string   status

    @do() {
        local target=$(@in target)
        local hp=$(@in health-port)

        @out.port   health-port "$hp"
        @out.string status      "deployed"
        @return.out
    }
}
```

Type modifiers compose with custom types:

```bash
@in.port?      override-port     # optional port
@out.port[]    ports             # array of ports
@out.port[]?   fallback-ports    # optional array of ports
```

### Defining in `.ops` files (DSL)

DSL shorthand for the same pattern. The emitter compiles this to an outer function + `@is`:

```ops
@type port = ({
  @int.is($value)
  if $value < 1 || $value > 65535 {
    throw port-out-of-range "port must be between 1 and 65535"
  }
})
```

### Using custom types in env

```ops
@type.port SERVICE_PORT: 8080
@type.endpoint API_ENDPOINT: "api.example.com:443"
```

### Referencing from imports

```ops
@use ./deploy

env = {
  @deploy.type.endpoint TARGET
}
```

### Composition via `.is()`

`.is()` is generated mechanically — it calls `@is`, discards the return value, and surfaces any throws. Works for both validate-only and validate-and-transform types:

```bash
@type.secure-connection() {
    @is() {
        local value="$1"
        @connection-string.is "$value"
        if [[ "$value" != https://* ]]; then
            @throw "invalid-connection" "connection must use https"
        fi
        @return "$value"
    }
}
```

## Regex Types

Regex types use `@regex` — a one-liner, no outer function needed:

```bash
@regex "^v[0-9]+\.[0-9]+\.[0-9]+$" "semver-tag"
# generates @semver-tag, @semver-tag.is, and all @verb.semver-tag variants
```

DSL inline regex types (`@/pattern/ VAR`) compile to anonymous `@regex` calls with generated names. Bash authors who want regex validation directly just use `[[ =~ ]]`.

## Grammar

```peg
TypeDecl        = '@type' IDENT '=' '(' '{' Statement* '}' ')'
```

The body is an imperative block with an implicit `$value` parameter (the raw string input).

## IR representation

The IR carries type names so the engine can verify references:

```json
{
  "_types": {
    "port": {},
    "endpoint": {},
    "semver-tag": { "regex": "^v[0-9]+\\.[0-9]+\\.[0-9]+$" }
  }
}
```

For DSL-defined types, the emitter generates the bash outer function + `@is`. For `.op.sh`-defined types, the registry captures them from `load_ops`.

## Runtime

- `@is` is the constructor/validator — validates input, optionally transforms, `@return`s the result.
- `@default` provides the zero value when the type is constructed with no argument.
- `@port` (constructor) and `@port.is` (validator) are generated by `load_ops`. Zero-arg `@port` calls `@default`; with-arg `@port value` calls `@is`.
- Dynamic dispatch via `command_not_found_handle` makes the type available as `@in.port`, `@out.port`, and all modifier variants (`?`, `[]`, `[]?`).
- Type constructors compose via `$()` command substitution and `.is()` calls, same as built-in types.
- `_`-prefixed inner functions are private helpers, ignored by `load_ops`.

## Implementation

The compiler only needs to know a custom type *exists* so it can verify references. Actual validation runs at runtime. No compile-time analysis of custom type logic.

| Tier | What ships |
| --- | --- |
| Tier 7 (Env Block) | Grammar for `@type` declarations alongside `@enum`. Parser registers type names. |
| Tier 15 (Resolver: Validation) | Verify `@type` references resolve. Cross-file resolution via `@use`. |
| Tier 22 (Engine Cutover) | `load_ops` structural contract discovery. `@is` inner function pattern. `.is()` generation. Dynamic dispatch registration. |

## Design decisions

- **Structural contract**: custom types follow the same outer-function-with-inner-contract pattern as operations. `@type.name` is the outer function, `@is` and `@default` are the contract functions. `load_ops` discovers and registers mechanically.
- **Constructors, not just validators**: custom types can validate-and-pass-through (like `@port`) or validate-and-transform (like `@endpoint` returning structured JSON). `.is()` works for both since it discards the return value.
- **Automatic `@verb.type` integration**: registering a custom type makes it available in all `@verb.type` positions via `command_not_found_handle`. No separate registration step per verb.
- **Modifier composability**: `?`, `[]`, `[]?` suffixes work with any type — built-in or custom — because the dynamic dispatch handler strips and interprets modifiers generically.
- **Composition over inheritance**: custom types compose by calling `.is()` on other types. No type hierarchy, no subtyping.
- **Runtime validation only**: the compiler verifies type *references*, not type *logic*. Custom validators can contain arbitrary bash — the engine can't statically analyze those.
- **Regex shorthand**: `@regex` generates a type without an outer function. Inline DSL regex compiles to anonymous `@regex` calls.

## Caveats

- Validation errors from custom types surface at runtime, not compile time. Error messages should include the type name and the failing value.
- Circular type composition (type A calls type B's `.is()`, type B calls type A's `.is()`) would cause infinite recursion. The resolver could detect this statically via the call graph.

## Open questions

None.
