detect 0.3.0

Expression-based file search combining name, content, metadata, and structured data predicates
Documentation
WHITESPACE = _{ " " | "\t" | NEWLINE }

program = { SOI ~ expr ~ EOI }
expr = { prefix* ~ primary ~ (infix ~ prefix* ~ primary )* }
  infix = _{ and | or }
    and = { "&&" | ^"and" }
    or = { "||" | ^"or" }
  prefix = _{ neg }
    neg = { "!" | "\\!" | ^"not" }
  primary = _{ predicate | single_word | "(" ~ expr ~ ")" }

predicate = { selector ~ operator ~ value }
  selector = @{ (ASCII_ALPHANUMERIC | "." | "_" | "-" | "/" | "[" | "]" | "*" | ":")+ }
  // Parse operators flexibly - validate in typechecker
  // Start with symbols or letters, but not mix arbitrarily
  operator = @{
    // Symbol-based operators (can combine symbols)
    ("=" | "!" | ">" | "<" | "~")+ |
    // Word-based operators (alphanumeric with underscores)
    // But NOT the reserved infix/prefix operators
    !(^"and" | ^"or" | ^"not") ~ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")*
  }
  
  value = { value_content ~ trailing_quote? }

  value_content = _{ quoted_string | unterminated_string | raw_token }

  // Error detection: unterminated string literals
  // Matches opening quote without closing quote before EOI/newline
  unterminated_string = @{
    ("\"" ~ (!"\"" ~ (escaped | ANY))* ~ (EOI | &NEWLINE)) |
    ("'" ~ (!"'" ~ (escaped | ANY))* ~ (EOI | &NEWLINE))
  }

  // Error detection: trailing quote after value content
  trailing_quote = @{ "\"" | "'" }

  // Raw tokens with recursive balanced delimiter matching
  // Supports nested structures like ((pub|async)\s+)* or [[a-z]]
  raw_token = @{ raw_char+ }

  raw_char = _{
    "\\" ~ ANY |                                              // Escaped character
    balanced_paren |                                          // Recursive paren matching
    balanced_bracket |                                        // Recursive bracket matching
    balanced_curly |                                          // Recursive curly matching
    !WHITESPACE ~ !"&&" ~ !"||" ~ !")" ~ !"\"" ~ !"'" ~ ANY  // Regular character
  }

  balanced_paren   = { "(" ~ ( "\\" ~ ANY | balanced_paren | balanced_bracket | balanced_curly | !")" ~ ANY )* ~ ")" }
  balanced_bracket = { "[" ~ ( "\\" ~ ANY | balanced_paren | balanced_bracket | balanced_curly | !"]" ~ ANY )* ~ "]" }
  balanced_curly   = { "{" ~ ( "\\" ~ ANY | balanced_paren | balanced_bracket | balanced_curly | !"}" ~ ANY )* ~ "}" }
  
  quoted_string = ${ "\"" ~ inner_double ~ "\"" | "'" ~ inner_single ~ "'" }
  inner_double = @{ (!"\"" ~ (escaped | ANY))* }
  inner_single = @{ (!"'" ~ (escaped | ANY))* }
  
  escaped = { "\\" ~ ("\"" | "'" | "\\" | "n" | "t" | "r") }

single_word = @{
    // Structured data selectors: word:.path (validated at typecheck)
    (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* ~ ":" ~ (ASCII_ALPHANUMERIC | "." | "_" | "-" | "/" | "[" | "]" | "*")+ |
    // Regular word aliases
    (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")*
}

// Separate entry point for parsing set contents
// Used by typechecker when operator is 'in'
set_contents = { SOI ~ set_items? ~ EOI }
set_items = { set_item? ~ ("," ~ set_item?)* }
set_item = { quoted_string | bare_set_item }
// Bare items in sets: stop at comma, whitespace, or quotes
bare_set_item = @{ (!("," | "\"" | "'" | WHITESPACE) ~ ANY)+ }