@@[target("rust")]

// Attribute scanner — Frame-generated state machine.
//
// Source:    attribute_scanner.frs (Frame specification)
// Generated: attribute_scanner.gen.rs (via framec --target rust)
//
// Recognizes Frame's two pragma surface forms:
//
//   1. Bracket form (RFC-0013): `@@[name]` or `@@[name(args)]`
//      Examples: `@@[persist]`, `@@[target("python_3")]`, `@@[main]`,
//                `@@[save]`, `@@[load]`, `@@[no_persist]`,
//                `@@[migrate(from=1, to=2)]`
//
//   2. Bare form (legacy + still-supported keywords): `@@<name> <value?>`
//      Examples: `@@run-expect 42`, `@@skip-if windows`
//      RFC-0013 hard-cut: bare `@@persist` and `@@target` now error
//      (E803 / E804). RFC-0032 hard-cut: `@@codegen { ... }` now errors
//      (E824). (`@@import` was removed by RFC-0024 but reinstated by
//      RFC-0040 as an analysis-only directive — it is accepted again and
//      handled in `do_parse`, not an error here.)
//      The bare error-forms pass through this scanner as Other and the
//      pipeline reports the migration error downstream.
//
// The scanner returns: name span (`name_start`/`name_end`), optional
// args span (bracket form only — `args_start`/`args_end`), optional
// value span (bare form only — `value_start`/`value_end` post-trim),
// and `is_bracket_form`. Translation from name string to `PragmaKind`
// happens in the wrapper module — keeps the FSM decoupled from the
// growing list of recognized attribute names.
//
// State machine flow:
//
//   $Init.scan(start) → $AfterAt
//                          ├→ $BracketName → $BracketAfterName
//                          │                    ├→ $BracketArgs → $BracketBeforeClose → done
//                          │                    └→ $BracketBeforeClose → done
//                          └→ $BareName → $BareValue → done
//
// Why FSM: bounded, single-pass byte walk over a small lookahead
// window. Same shape as `body_closer/*.frs` and `multisys_assembler.frs`.
// Replacing the existing Rust `identify_pragma` puts attribute parsing
// on the same Frame-native footing as the rest of the scanner work.

@@system AttributeScannerFsm {
    interface:
        scan(start: usize)

    machine:
        $Init {
            scan(start: usize) {
                self.pos = start + 2;  // skip @@
                if self.pos < self.bytes.len() && self.bytes[self.pos] == 0x5B {
                    self.is_bracket_form = true;
                    self.pos = self.pos + 1;
                    -> $BracketName
                } else {
                    self.is_bracket_form = false;
                    -> $BareName
                }
            }
        }

        // --- Bracket form: @@[name] / @@[name(args)] ----------------

        // Read the attribute name. Name chars: alphanumeric, `_`, `-`.
        // Stop at the first non-name char (`(`, `]`, ws).
        $BracketName {
            $>() {
                self.name_start = self.pos;
                let n = self.bytes.len();
                while self.pos < n && is_attr_name_char(self.bytes[self.pos]) {
                    self.pos = self.pos + 1;
                }
                self.name_end = self.pos;
                -> $BracketAfterName
            }
        }

        // After the name: either `(args)` follows, or the closing `]`
        // (with possible intervening whitespace).
        $BracketAfterName {
            $>() {
                let n = self.bytes.len();
                if self.pos < n && self.bytes[self.pos] == 0x28 {
                    self.has_args = true;
                    self.args_start = self.pos;
                    self.paren_depth = 1;
                    self.pos = self.pos + 1;
                    -> $BracketArgs
                } else {
                    -> $BracketBeforeClose
                }
            }
        }

        // Inside `(args)` — track paren depth so nested calls within
        // arg expressions don't terminate the args block early.
        // E.g. `@@[migrate(from=foo(1), to=2)]` has nested parens that
        // depth-tracking keeps as a single args span.
        $BracketArgs {
            $>() {
                let n = self.bytes.len();
                while self.pos < n && self.paren_depth > 0 {
                    let b = self.bytes[self.pos];
                    if b == 0x28 {
                        self.paren_depth = self.paren_depth + 1;
                    } else if b == 0x29 {
                        self.paren_depth = self.paren_depth - 1;
                    }
                    self.pos = self.pos + 1;
                }
                self.args_end = self.pos;
                -> $BracketBeforeClose
            }
        }

        // Skip whitespace, expect closing `]`. Tolerates whitespace
        // between `)` and `]` (`@@[name(args) ]`) for source-style
        // flexibility.
        $BracketBeforeClose {
            $>() {
                let n = self.bytes.len();
                while self.pos < n && (self.bytes[self.pos] == 0x20 || self.bytes[self.pos] == 0x09) {
                    self.pos = self.pos + 1;
                }
                if self.pos < n && self.bytes[self.pos] == 0x5D {
                    self.pos = self.pos + 1;
                }
            }
        }

        // --- Bare form: @@<name> <value?> ---------------------------

        // Read the bare keyword name (same char class as bracket).
        $BareName {
            $>() {
                self.name_start = self.pos;
                let n = self.bytes.len();
                while self.pos < n && is_attr_name_char(self.bytes[self.pos]) {
                    self.pos = self.pos + 1;
                }
                self.name_end = self.pos;
                -> $BareValue
            }
        }

        // Skip whitespace after the keyword, then read the rest of
        // the line as the value. Trim trailing ws / `\r` so CRLF
        // sources round-trip clean.
        $BareValue {
            $>() {
                let n = self.bytes.len();
                while self.pos < n && (self.bytes[self.pos] == 0x20 || self.bytes[self.pos] == 0x09) {
                    self.pos = self.pos + 1;
                }
                let vs = self.pos;
                while self.pos < n && self.bytes[self.pos] != 0x0A {
                    self.pos = self.pos + 1;
                }
                let mut ve = self.pos;
                while ve > vs && (
                    self.bytes[ve - 1] == 0x20 ||
                    self.bytes[ve - 1] == 0x09 ||
                    self.bytes[ve - 1] == 0x0D
                ) {
                    ve = ve - 1;
                }
                if ve > vs {
                    self.has_value = true;
                    self.value_start = vs;
                    self.value_end = ve;
                }
            }
        }

    domain:
        // Input: the source bytes; the scan starting position is
        // passed via the `scan(start)` interface call.
        bytes: Vec<u8> = Vec::new()

        // Cursor — advances through the bytes during the walk;
        // accessible after the FSM finishes via `pos()`.
        pos: usize = 0

        // Surface form: true for `@@[name]`, false for bare `@@name`.
        is_bracket_form: bool = false

        // Name span — always set after a successful scan.
        name_start: usize = 0
        name_end: usize = 0

        // Args span — only valid when `has_args` is true (bracket
        // form with `(...)`). The span covers the parens and their
        // contents, e.g. for `@@[target("python_3")]` it points at
        // `("python_3")` inclusive.
        has_args: bool = false
        args_start: usize = 0
        args_end: usize = 0
        paren_depth: i32 = 0

        // Value span — only valid when `has_value` is true (bare
        // form with non-empty trailing text). Points at the trimmed
        // rest-of-line.
        has_value: bool = false
        value_start: usize = 0
        value_end: usize = 0
}
