How to parse operators of 2 words?

Hello! Lezer is awesome, very cool parser.

I’m trying to add support for operator with two words:

foo or bar
index not in array

But my operators, and “not” gets splashed into identifier.

So next expressions not parsed correctly:

notor // should be indentifier. 
notin // also

Here is my grammar:

@precedence {
  member,
  call,
  pipe,
  power @right,
  times @left,
  plus @left,
  rel @left,
  wordOp @left,
  and @left,
  or @left,
  ternary @right
}

@top Program { expression* }

expression[@isGroup=Expression] {
  Number |
  String |
  TemplateString |
  Identifier |
  @specialize[@name=Boolean]<Identifier, "true" | "false"> |
  kw<"nil"> |
  ArrayExpression {
    "[" commaSep<expression> "]"
  } |
  ObjectExpression {
    "{" commaSep<Property> "}"
  } |
  UnaryExpression |
  ParenthesizedExpression |
  MemberExpression |
  BinaryExpression |
  ConditionalExpression {
    expression !ternary LogicOp<"?"> expression LogicOp<":"> expression
  } |
  BuiltinExpression |
  CallExpression |
  VariableDeclaration |
  PointerExpression |
  PipeExpression {
    expression !pipe "|" (BuiltinExpression | CallExpression)
  }
}

BuiltinExpression {
  Builtin !call ArgList
}

CallExpression {
  expression !call ArgList
}

VariableDeclaration {
  kw<"let"> Identifier "=" expression semi
}

ParenthesizedExpression { "(" expression ")" }

ArgList { "(" commaSep<expression> ")" }

propName { PropertyName | ParenthesizedExpression | Number | String }

Property {
  propName ":" expression
}

UnaryExpression {
  (LogicOp<"not"> | LogicOp<"!"> | ArithOp<"+" | "-">)
  expression
}

BinaryExpression {
  expression !power (ArithOp<"**"> | ArithOp<"^">) expression |
  expression !times (ArithOp<"/"> | ArithOp<"%"> | ArithOp<"*">) expression |
  expression !plus ArithOp<"+" | "-"> expression |
  expression !rel CompareOp expression |
  expression !wordOp WordOp expression |
  expression !and LogicOp<"and" | "&&"> expression |
  expression !or LogicOp<"or" | "||" | "??"> expression
}

WordOp {
  ckw<"in"> |
  kw<"matches"> |
  kw<"contains"> |
  kw<"startsWith"> |
  kw<"endsWith">
}

MemberExpression {
  expression !member ( ("?." | ".") PropertyName | "[" expression "]")
}

PointerExpression {
  Pointer |
  "." PropertyName
}

commaSep<content> {
  (content ("," content)*)?
}

 Builtin {
    kw<"all"> |
    kw<"none"> |
    kw<"any"> |
    kw<"one"> |
    kw<"filter"> |
    kw<"map"> |
    kw<"find"> |
    kw<"findIndex"> |
    kw<"findLast"> |
    kw<"findLastIndex"> |
    kw<"count"> |
    kw<"sum"> |
    kw<"groupBy"> |
    kw<"sortBy"> |
    kw<"reduce"> |
    kw<"len"> |
    kw<"type"> |
    kw<"abs"> |
    kw<"ceil"> |
    kw<"floor"> |
    kw<"round"> |
    kw<"int"> |
    kw<"float"> |
    kw<"string"> |
    kw<"trim"> |
    kw<"trimPrefix"> |
    kw<"trimSuffix"> |
    kw<"upper"> |
    kw<"lower"> |
    kw<"split"> |
    kw<"splitAfter"> |
    kw<"replace"> |
    kw<"repeat"> |
    kw<"join"> |
    kw<"indexOf"> |
    kw<"lastIndexOf"> |
    kw<"hasPrefix"> |
    kw<"hasSuffix"> |
    kw<"max"> |
    kw<"min"> |
    kw<"mean"> |
    kw<"median"> |
    kw<"toJSON"> |
    kw<"fromJSON"> |
    kw<"toBase64"> |
    kw<"fromBase64"> |
    kw<"now"> |
    kw<"duration"> |
    kw<"date"> |
    kw<"timezone"> |
    kw<"first"> |
    kw<"last"> |
    kw<"get"> |
    kw<"take"> |
    kw<"keys"> |
    kw<"values"> |
    kw<"toPairs"> |
    kw<"fromPairs"> |
    kw<"reverse"> |
    kw<"concat"> |
    kw<"sort"> |
    kw<"bitand"> |
    kw<"bitor"> |
    kw<"bitxor"> |
    kw<"bitnand"> |
    kw<"bitshl"> |
    kw<"bitshr"> |
    kw<"bitushr"> |
    kw<"bitnot">
  }

kw<term> { @specialize[@name={term}]<Identifier, term> }

ckw<term> { @extend[@name={term}]<Identifier, term> }

semi { ";" }

@skip { spaces | newline | LineComment | BlockComment }

@skip {} {
  TemplateString {
    "`" (templateEscape | templateContent | Interpolation)* templateEnd
  }
}

Interpolation { InterpolationStart expression "}" }

@local tokens {
  InterpolationStart[@name="${"] { "${" }
  templateEnd { "`" }
  templateEscape { "\\" _ }
  @else templateContent
}

@skip {} {
  BlockComment { "/*" (blockCommentContent | blockCommentNewline)* blockCommentEnd }
}

@local tokens {
  blockCommentEnd { "*/" }
  blockCommentNewline { "\n" }
  @else blockCommentContent
}

@tokens {
  spaces[@export] { $[ \t]+ }

  newline[@export] { $[\r\n] }

  LineComment { "//" ![\n]* }

  @precedence { "/*", LineComment, ArithOp<"/"> }

  @precedence { "/*", LineComment, RegExp }

  identifierChar { @asciiLetter | $[_$\u{a1}-\u{10ffff}] }

  word { identifierChar (identifierChar | @digit)* }

  Identifier { word }

  PropertyName { word }

  Pointer { "#" word? }

  hex { @digit | $[a-fA-F] }

  Number {
    (@digit ("_" | @digit)* ("." ("_" | @digit)*)? | "." @digit ("_" | @digit)*)
      (("e" | "E") ("+" | "-")? ("_" | @digit)+)? |
    @digit ("_" | @digit)* "n" |
    "0x" (hex | "_")+ "n"? |
    "0b" $[01_]+ "n"? |
    "0o" $[0-7_]+ "n"?
  }

  @precedence { Number "." }

  String {
    '"' (![\\\n"] | "\\" _)* '"'? |
    "'" (![\\\n'] | "\\" _)* "'"?
  }

  ArithOp<expr> { expr }
  LogicOp<expr> { expr }

  CompareOp { "<" | ">" | "==" | "!=" }

  @precedence { LogicOp, Identifier }

  "=" "|"
  "(" ")" "[" "]" "{" "}"
  "." "?." "," ";" ":"
}

@detectDelim

I think I figured it out. I needed to create additional “inner” node for it to work.