Lexical Grammar

Grove source files are UTF-8 encoded text with the .grove extension. The lexer breaks source text into a stream of tokens before parsing. This page defines every token class the Grove lexer recognizes, including keywords, identifiers, literals, operators, and comments.

Prerequisites: Familiarity with EBNF notation and general programming-language concepts. What you'll learn: The complete set of lexical elements that make up a valid Grove source file.


Source Encoding

Grove source files must be valid UTF-8. A byte-order mark (BOM) at the start of a file is permitted but ignored. Line endings may be LF (\n) or CRLF (\r\n); the lexer normalizes both to LF internally.

Comments

Grove supports two comment forms:

LineComment  = "//" { any-char-except-newline } newline .
BlockComment = "/*" { any-char | newline } "*/" .

Block comments do not nest. Comments are treated as whitespace by the parser and carry no semantic meaning.

// This is a line comment

/* This is a
   block comment */

Whitespace

Whitespace characters (space U+0020, horizontal tab U+0009, newline U+000A, carriage return U+000D) separate tokens but are otherwise insignificant. Grove is not indentation-sensitive.

Keywords

The following identifiers are reserved as keywords and cannot be used as user-defined names:

CategoryKeywords
Declarationsimport, type, enum, record, event, action, validate, upcast, function, workflow, activity, service, route, query, trigger, schedule, webhook, test
Statementslet, if, else, for, in, return, emit, fail, publish
TypesInt, Decimal, Bool, String, Id, Date, Time, DateTime, Duration, Epoch, Value, Blob, List, Map
Literalstrue, false, null
Operatorsand, or, not, is
Modifiersmut
Annotations@create, @delete, @pii, @confidential

Identifiers

Identifier   = Letter { Letter | Digit | "_" } .
Letter       = "a"..."z" | "A"..."Z" | "_" .
Digit        = "0"..."9" .

Identifiers are case-sensitive. By convention:

Examples of valid identifiers: order_total, OrderCreated, _internal, x2.

Literals

Integer Literals

IntLiteral     = DecimalInt .
DecimalInt     = "0" | NonZeroDigit { Digit } .
NonZeroDigit   = "1"..."9" .

Negative integers are expressed with the unary - operator applied to an integer literal: -42.

Examples: 0, 42, 1000.

Decimal Literals

DecimalLiteral = DecimalInt "." Digit { Digit } .

At least one digit must appear after the decimal point.

Examples: 3.14, 0.5, 100.00.

Boolean Literals

BoolLiteral = "true" | "false" .

String Literals

StringLiteral   = '"' { StringChar } '"' .
StringChar      = any-char-except-quote-or-backslash | EscapeSeq .
EscapeSeq       = "\\" | '\"' | "\n" | "\t" | "\r" .

String literals are delimited by double quotes. Grove does not support single-quoted strings or raw string literals.

"hello world"
"line one\nline two"
"a quote: \"here\""

String Interpolation

String literals support interpolation with ${} syntax:

InterpolatedString = '"' { StringChar | "${" Expression "}" } '"' .
let name = "Grove"
let greeting = "Hello, ${name}!"

Null Literal

NullLiteral = "null" .

The null literal represents the absence of a value and is only valid where an optional type (T?) is expected.

Operators and Punctuation

Arithmetic Operators

OperatorMeaning
+Addition / string concatenation
-Subtraction / unary negation
*Multiplication
/Division
%Modulo (remainder)

Comparison Operators

OperatorMeaning
==Equal
!=Not equal
<Less than
>Greater than
<=Less than or equal
>=Greater than or equal

Logical Operators

OperatorMeaning
andLogical AND (short-circuit)
orLogical OR (short-circuit)
notLogical NOT (prefix)
isType / null check

Assignment and Access

TokenMeaning
=Assignment (in let)
.Member access
?.Optional chaining
??Null coalescing

Delimiters

TokenUsage
( )Grouping, function arguments
{ }Block bodies, object literals
[ ]List literals, index access
< >Type parameters
,Separator in lists, arguments, fields
:Type annotation, key-value separator
->Return type annotation
=>Route / trigger / schedule handler arrow
..Spread operator

Token Precedence

When ambiguity arises, the lexer applies longest-match: it consumes the longest sequence of characters that forms a valid token. For example, >= is lexed as a single operator, not > followed by =.

Semicolons

Grove does not use semicolons as statement terminators. The lint command will warn if semicolons are present. Newlines serve as implicit statement separators where syntactically required.

See Also