@external propSource highlighting from "./highlight" @context trackScope from "./parserScopeContext" @skip { space | Comment } @top Program { item* } @external tokens operatorTokenizer from "./operatorTokenizer" { Star, Slash, Plus, Minus, And, Or, Eq, EqEq, Neq, Lt, Lte, Gt, Gte, Modulo, PlusEq, MinusEq, StarEq, SlashEq, ModuloEq, Band, Bor, Bxor, Shl, Shr, Ushr, NullishCoalesce, NullishEq } @tokens { @precedence { Number Regex } StringFragment { !['\\$]+ } DoubleQuote { '"' !["]* '"' } NamedArgPrefix { $[a-z] $[a-z0-9-]* "=" } Number { ("-" | "+")? "0x" $[0-9a-fA-F]+ | ("-" | "+")? "0b" $[01]+ | ("-" | "+")? "0o" $[0-7]+ | ("-" | "+")? $[0-9]+ ("_"? $[0-9]+)* ('.' $[0-9]+ ("_"? $[0-9]+)*)? } Boolean { "true" | "false" } semicolon { ";" } eof { @eof } space { " " | "\t" } Comment { "#" ![\n]* } leftParen { "(" } rightParen { ")" } colon[closedBy="end", @name="colon"] { ":" } Underscore { "_" } Dollar { "$" } Regex { "//" (![/\\\n[] | "\\" ![\n] | "[" (![\n\\\]] | "\\" ![\n])* "]")+ ("//" $[gimsuy]*)? } // Stolen from the lezer JavaScript grammar "|"[@name=operator] } newlineOrSemicolon { newline | semicolon } end { @specialize[@name=keyword] } while { @specialize[@name=keyword] } if { @specialize[@name=keyword] } else { @specialize[@name=keyword] } try { @specialize[@name=keyword] } catch { @specialize[@name=keyword] } finally { @specialize[@name=keyword] } throw { @specialize[@name=keyword] } not { @specialize[@name=keyword] } import { @specialize[@name=keyword] } null { @specialize[@name=Null] } @external tokens tokenizer from "./tokenizer" { Identifier, AssignableIdentifier, Word, IdentifierBeforeDot, CurlyString } @external tokens pipeStartsLineTokenizer from "./tokenizer" { newline, pipeStartsLine } @external specialize {Identifier} specializeKeyword from "./tokenizer" { Do } @precedence { pipe @left, or @left, and @left, nullish @left, comparison @left, multiplicative @left, additive @left, bitwise @left, call, functionWithNewlines } item { consumeToTerminator newlineOrSemicolon | consumeToTerminator eof | newlineOrSemicolon // allow blank lines } consumeToTerminator { PipeExpr | WhileExpr | FunctionCallWithBlock | ambiguousFunctionCall | TryExpr | Throw | Not | Import | IfExpr | FunctionDef | CompoundAssign | Assign | BinOp | ConditionalOp | expressionWithoutIdentifier } PipeExpr { pipeOperand (!pipe (pipeStartsLine? "|") newlineOrSemicolon* pipeOperand)+ } pipeOperand { consumeToTerminator } WhileExpr { while (ConditionalOp | expression) colon Block end } Block { consumeToTerminator | newlineOrSemicolon block } FunctionCallWithBlock { ambiguousFunctionCall colon Block CatchExpr? FinallyExpr? end } FunctionCallOrIdentifier { DotGet | Identifier } ambiguousFunctionCall { FunctionCall | FunctionCallOrIdentifier } FunctionCall { (DotGet | Identifier | ParenExpr) arg+ } arg { PositionalArg | NamedArg } PositionalArg { expression | FunctionDef | Underscore } NamedArg { NamedArgPrefix (expression | FunctionDef | Underscore) } FunctionDef { Do Params colon (consumeToTerminator | newlineOrSemicolon block) CatchExpr? FinallyExpr? end } ifTest { ConditionalOp | expression | FunctionCall } IfExpr { if ifTest colon Block ElseIfExpr* ElseExpr? end } ElseIfExpr { else if ifTest colon Block } ElseExpr { else colon Block } TryExpr { try colon Block CatchExpr? FinallyExpr? end } CatchExpr { catch Identifier colon Block } FinallyExpr { finally colon Block } Throw { throw (BinOp | ConditionalOp | expression) } Not { not (BinOp | ConditionalOp | expression) } // this has to be in the parse tree so the scope tracker can use it Import { import NamedArg* Identifier+ NamedArg* } ConditionalOp { expression !comparison EqEq expression | expression !comparison Neq expression | expression !comparison Lt expression | expression !comparison Lte expression | expression !comparison Gt expression | expression !comparison Gte expression | (expression | ConditionalOp) !and And (expression | ConditionalOp) | (expression | ConditionalOp) !or Or (expression | ConditionalOp) | (expression | ConditionalOp) !nullish NullishCoalesce (expression | ConditionalOp) } Params { Identifier* NamedParam* } NamedParam { NamedArgPrefix (String | Number | Boolean | null) } Assign { (AssignableIdentifier | Array) Eq consumeToTerminator } CompoundAssign { AssignableIdentifier (PlusEq | MinusEq | StarEq | SlashEq | ModuloEq | NullishEq) consumeToTerminator } BinOp { expression !multiplicative Modulo expression | (expression | BinOp) !multiplicative Star (expression | BinOp) | (expression | BinOp) !multiplicative Slash (expression | BinOp) | (expression | BinOp) !additive Plus (expression | BinOp) | (expression | BinOp) !additive Minus (expression | BinOp) | (expression | BinOp) !bitwise Band (expression | BinOp) | (expression | BinOp) !bitwise Bor (expression | BinOp) | (expression | BinOp) !bitwise Bxor (expression | BinOp) | (expression | BinOp) !bitwise Shl (expression | BinOp) | (expression | BinOp) !bitwise Shr (expression | BinOp) | (expression | BinOp) !bitwise Ushr (expression | BinOp) } ParenExpr { leftParen newlineOrSemicolon* ( FunctionCallWithNewlines | IfExpr | ambiguousFunctionCall | BinOp newlineOrSemicolon* | expressionWithoutIdentifier | ConditionalOp newlineOrSemicolon* | PipeExpr | FunctionDef ) rightParen } FunctionCallWithNewlines[@name=FunctionCall] { (DotGet | Identifier | ParenExpr) newlineOrSemicolon+ arg !functionWithNewlines (newlineOrSemicolon+ arg)* newlineOrSemicolon* } expression { expressionWithoutIdentifier | DotGet | Identifier } @local tokens { dot { "." } } @skip {} { DotGet { IdentifierBeforeDot dot (DotGet | Number | Identifier | ParenExpr) | Dollar dot (DotGet | Number | Identifier | ParenExpr) } String { "'" stringContent* "'" | CurlyString | DoubleQuote } } stringContent { StringFragment | Interpolation | EscapeSeq } Interpolation { "$" FunctionCallOrIdentifier | "$" ParenExpr } EscapeSeq { "\\" ("$" | "n" | "t" | "r" | "\\" | "'") } Dict { "[=]" | "[" newlineOrSemicolon* NamedArg (newlineOrSemicolon | NamedArg)* "]" } Array { "[" newlineOrSemicolon* (expression (newlineOrSemicolon | expression)*)? "]" } // We need expressionWithoutIdentifier to avoid conflicts in consumeToTerminator. // Without this, when parsing "my-var" at statement level, the parser can't decide: // - ambiguousFunctionCall → FunctionCallOrIdentifier → Identifier // - expression → Identifier // Both want the same Identifier token! So we use expressionWithoutIdentifier // to remove Identifier from the second path, forcing standalone identifiers // to go through ambiguousFunctionCall (which is what we want semantically). // Yes, it is annoying and I gave up trying to use GLR to fix it. expressionWithoutIdentifier { ParenExpr | Word | String | Number | Boolean | Regex | Dict | Array | null } block { (consumeToTerminator? newlineOrSemicolon)* }