Lexical Structure, Comments, Keywords & Identifiers

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Apache Groovy 6.0 (6.0.0, released 2026-09-22) and was checked on 2026-10-14. Every example was run with Groovy 6.0.0 on JDK 26. It covers the layer below expressions and statements: how Groovy source text is cut into tokens. It explains comments, the reserved and contextual keywords, normal and quoted identifiers, when a newline ends a statement, and the grammar specification that the official documentation gained in 6.0.

Strings and numbers are lexical elements too, but they have pages of their own: Strings and GStrings and Numbers and Numeric Literals. Operators are in Operators: Arithmetic, Relational and Logical.

Comments

Groovy has the same three comment forms as Java and one extra line form for Unix scripts:

Form Starts with Ends with Notes

Single-line

//

end of line

Can follow code on the same line.

Multiline

/*

*/

Can sit between tokens of a single statement. Does not nest.

Groovydoc

/**

*/

A multiline comment that documents the type, field, property or method that follows it.

Runtime Groovydoc

/**@

*/

Groovydoc that can be read back at run time (Groovy 3.0 and later, opt-in).

Shebang

#!

end of line

Only at the very start of a file.

The following script uses every form. It belongs to the greenhouse running scenario: a Sensor class in the greenhouse package.

#!/usr/bin/env groovy
// greenhouse/Sensor.groovy -- single-line comment after the shebang
package greenhouse

/* a multiline comment
   spanning two lines */

/**
 * Groovydoc: associated with the class that follows.
 */
class Sensor {
    /** the sensor identifier */
    String id
    /**@
     * Runtime Groovydoc: kept in the class file only when
     * -Dgroovy.attach.runtime.groovydoc=true is set.
     */
    double read() { 21.5 /* inline comment */ }
}

def s = new Sensor(id: 'bench-1')
println s.read() + 1 /* one */ + 2 /* two */
println Sensor.getMethod('read').groovydoc.content.contains('Runtime Groovydoc')

Run as groovy Comments.groovy the output is:

24.5
false

Run with the option that turns runtime Groovydoc on, the last line becomes true:

$ JAVA_OPTS=-Dgroovy.attach.runtime.groovydoc=true groovy Comments.groovy
24.5
true

Points worth knowing:

  • Groovydoc is a convention, not a check. The compiler accepts a /** ... */ comment anywhere. The Groovydoc tool and the IDEs attach it to the type, field, property or method that follows. It uses the Javadoc tags (@param, @return, …​). See GroovyDoc for generating HTML from it.

  • Runtime Groovydoc (3.0.0) is off by default. Start a script with -Dgroovy.attach.runtime.groovydoc=true (here through JAVA_OPTS) and mark the comment /**@. Without the option the example above still runs, but the groovydoc.content of the method is empty, which is why the first run printed false.

  • A comment can act as a separator. The 6.0 grammar rewrites // comments and the /* ... */ comments that stand alone at the end of a line to the same token as a newline, so they end a statement just like a line break (see Newlines and the grouping stack).

  • An unclosed /* is a compile error reported at the place where the comment opened (see Clearer syntax error messages in 6.0).

The shebang line

A first line starting with #! lets Unix systems run the file directly when groovy is on the PATH:

#!/usr/bin/env groovy
println "Hello from the shebang line"

# is not a comment character anywhere else; a #! line after the first token is a compile error:

Bad8.groovy: 2: Shebang comment should appear at the first line @ line 2, column 20.

The syntax tutorial says the # must be the first character and that indentation is a compilation error. The 6.0 grammar is more precise. The shebang is skipped as long as no token has been read before it, so leading spaces and tabs are tolerated. A script that begins with ` #!/usr/bin/env groovy` (note the leading space) ran correctly with 6.0.0. A preceding empty line or any statement is what makes it an error. Do not rely on the leniency; keep #! in column 1 so that the operating system also recognises it. The groovy command itself is described in Running Scripts and the groovy Command.

Keywords

Groovy has three families of words with special meaning. Reading them as three tables is more useful than a flat list: it tells you what you may still use as a name.

Reserved keywords

These cannot be used as variable, field or method names (with the exception described below):

Group Words

Declarations and types

abstract, class, def, enum, extends, final, implements, import, interface, native, package, static, strictfp, synchronized, transient, volatile

Visibility

public, protected, private

Control flow

assert, break, case, catch, continue, default, do, else, finally, for, if, return, switch, throw, throws, try, while

Expressions and literals

instanceof, new, super, this, null, true, false, the primitive types (boolean, char, byte, short, int, long, float, double) and void

Reserved but unused

const, goto, threadsafe (and strictfp, which is accepted)

Hyphenated

non-sealed

Two of the unused words are worth a note. const, goto and threadsafe are compile errors, and from 6.0 the message names the Groovy spelling to use instead (see below). volatile and transient are the Java modifiers and are accepted. This list follows the grammar of the language (Grammar, Terminal vocabulary); the syntax tutorial prints a slightly shorter table.

Contextual keywords

These words are keywords only where the grammar expects them. Everywhere else they are ordinary names, which keeps old code compiling when the language adds a word.

Word Meaning in context Still usable as a name?

as

type coercion and import aliases

yes

in

for (x in xs), membership test

yes

var

local variable with an inferred type

yes

val

final local variable with an inferred type (Groovy 6.0)

yes, when the feature is off or outside a declaration

record

record declaration

yes

sealed, permits

sealed hierarchies (the third word, non-sealed, is a single reserved token)

yes

trait

trait declaration

yes

yield

value of a switch expression branch

yes

async, await, defer

asynchronous programming (6.0, incubating)

yes

module

only after import, as in import module java.base

yes

The syntax tutorial lists yields; the grammar token is yield. The 6.0 grammar chapter states explicitly that it follows the ANTLR grammar files, and that its tables are "a user-facing cut" in the tutorial. When the two disagree the grammar wins, so write yield.

Because they are contextual, all of the following are legal:

// contextual keywords are ordinary names outside their context
def as = 'tomatoes'
def record = [id: 'bench-1', celsius: 21.5]
def sealed = false
def permits = ['north', 'south']
def trait = 'heat-loving'
assert as == 'tomatoes' && record.celsius == 21.5 && !sealed
assert permits.size() == 2 && trait.startsWith('heat')

// a method can be named like a contextual keyword; calls only need this. when ambiguous
def in() { 'inside the greenhouse' }
assert this.in() == 'inside the greenhouse'

Running the full script prints keywords ok after these assertions pass. Using such names is allowed but is rarely a good idea; a record called record reads poorly.

Reserved words as method or property names

Reserved words are not locked out of Groovy programs. There are two escape hatches, and both exist for Java integration and for DSLs, where "verbs" and "nouns" that collide with keywords are common:

  • A method may be named with a quoted name, and is then called with an explicit this. receiver.

  • After a dot, every keyword is a valid property name. This is how map.class and foo.assert work.

// reserved words as method names must be quoted, and calls are qualified with this.
def "abstract"() { 'not concrete' }
assert this.abstract() == 'not concrete'

// after a dot every keyword is a valid property name
def flags = [:]
flags.class = 'greenhouse.Zone'          // a map key named class
flags.assert = true
flags.if = 'unused'
assert flags.get('class') == 'greenhouse.Zone' && flags.assert && flags.if == 'unused'
println 'keywords ok'

Run output:

keywords ok

The flags.class line shows a pitfall that is dealt with on Maps. On a Map, property syntax reads an entry, so flags.class is null until an entry named class is stored and never gives the class object; call flags.getClass() for that. The example reads the entry back with flags.get('class'), which is unambiguous.

The same applies to null, true, false and the primitive type names: def "null"() { true } is legal and this.null() calls it, but this is strictly a trick for DSLs.

Reserved but unused words and the 6.0 diagnostics

const, goto, strictfp and threadsafe are reserved words that Groovy does not use. const and goto exist because Java reserves them. Using const, goto or threadsafe stops compilation, and 6.0 reports it with the spelling to use instead:

$ groovyc --error-format short Bad3.groovy
Bad3.groovy:1:1: error: 'const' is not supported; use 'val' or 'static final' instead

val declares a final local variable (new in 6.0, see Variables, Typing, def, var and val) and static final declares a class constant. The unused-keyword diagnostic was part of the "clearer syntax error messages" work described below.

Identifiers

Normal identifiers

An identifier starts with a letter, a dollar sign ($) or an underscore (_), and continues with letters, digits, $ and _. It cannot start with a digit. Letters are not limited to ASCII, so accented and non-Latin names work:

def name = 'x'; def item3 = 3; def with_underscore = 4; def $dollarStart = 5
def café = 'Ünïcode'        // letters beyond ASCII are fine
assert café == 'Ünïcode' && $dollarStart + item3 == 8

Invalid names are those with an operator or a leading digit: def 3tier, def a+b and def a#b are syntax errors.

The 6.0 grammar adds precision on two points. It separates CapitalizedIdentifier (starts with [A-Z], or an upper-case non-ASCII letter), used for class names, from the lower-case Identifier. A name starting with $ or _ is always a plain Identifier. And inside a GString a $identifier path cannot itself contain a $, so write "$a.b", not "$a$b" when you mean two interpolations in a row (see Strings and GStrings).

Quoted identifiers

Anything that may follow a dot can be quoted, so a property can have a name that the Java language forbids: with a dash, a space or an exclamation mark. Every string form is accepted after the dot, including a GString, which is evaluated first:

def sensor = [:]
sensor.'soil-moisture' = 0.31                   // quoted identifier, single quotes
sensor."air temperature" = 21.5                 // spaces allowed when quoted
sensor.'''triple'''  = 1
sensor./slashy/ = 2
sensor.$/dollar-slashy/$ = 3
def zone = 'north'
sensor."reading-${zone}" = 19.0                 // a GString is evaluated first
assert sensor.'soil-moisture' == 0.31
assert sensor.'reading-north' == 19.0
assert sensor.keySet() as List == ['soil-moisture', 'air temperature', 'triple', 'slashy', 'dollar-slashy', 'reading-north']
println sensor

Run output:

[soil-moisture:0.31, air temperature:21.5, triple:1, slashy:2, dollar-slashy:3, reading-north:19.0]

Quoted identifiers are what make map.'content-type', JSON field access and builder DSLs read naturally. The dynamic form object."$name" also looks up a member whose name is only known at run time, a technique used throughout Runtime Metaprogramming and the MOP.

Newlines and the grouping stack

Groovy needs no statement terminator: a newline ends a statement, unless the expression is clearly unfinished. A semicolon is optional (see Optionality). The 6.0 grammar describes exactly how this works, because it is not a pure context-free rule. The lexer keeps a grouping stack:

  • Opening (, [ and ?[ push a frame; the matching closer pops it. Inside a frame a newline is hidden from the parser, so arguments, list and index expressions can be wrapped freely.

  • Opening { also pushes a frame, but inside braces newlines remain significant. A closure or block is a sequence of statements.

  • The exception: a ( that directly follows try (try-with-resources) does not hide newlines.

  • < and > in generic types do not push a frame, which is why the grammar writes the optional newlines in type arguments explicitly.

  • A backslash immediately before the line terminator joins two lines (a line continuation) and is never a newline.

  • The interpolation ${ ... } in a GString also pushes a frame, and the stack top is that brace while the closure body is tokenised.

The practical consequences:

def a = 1
def b = a +
        2                    // newline AFTER an operator: statement continues
assert b == 3

def c = [1,
         2,
         3]                  // newlines inside [ ] are hidden
assert c == [1, 2, 3]

def d = Math.max(
    4,
    5
)                            // newlines inside ( ) are hidden
assert d == 5

def e = 10
+ 1                          // newline BEFORE + : two statements; "+ 1" is a unary plus
assert e == 10

def f = 10 \
      + 1                    // a backslash line continuation joins the lines
assert f == 11

def g = { x ->
    def y = x * 2            // inside { } newlines separate statements
    y + 1
}
assert g(4) == 9
println 'newlines ok'

Run output:

newlines ok

The rule of thumb for style is to put a binary operator at the end of a wrapped line, not at the start of the next one. A line that starts with +, - or * begins a new statement at script level; a line that starts with a comma, /, a comparison operator or = is allowed to continue because those cannot start a statement. Also, a newline before an opening parenthesis is not a call: foo\n(1) is two statements outside a grouping frame.

Comments take part in this. A // comment and a /* ... */ comment that is not inside a grouping frame and has only white space after it on its line count as a newline. A /* c */ b is therefore not two statements, but /* c */ followed by a line break and then b is.

The 6.0 grammar specification

Until 6.0 the way to find out what Groovy accepts was the two ANTLR 4 files in the compiler sources: src/antlr/GroovyLexer.g4 and src/antlr/GroovyParser.g4. The Groovy 6.0 user guide adds a Grammar chapter (section 1.2 of the single-page documentation, anchor groovy-grammar) which is a complete EBNF projection of those files, written "for readers who need a closed, citable grammar": language researchers, tool authors, implementers and the authors of syntax highlighters and formatters. It is not a tutorial; the informal chapters (Syntax, Operators, Program structure, Semantics) remain the place to learn the language.

What is in the chapter:

Section What it gives you

Status and scope

Three layers: a lexical context machine, a context-free skeleton, and a list of numbered restrictions (R1, R2, …​) that filter or disambiguate it. Groovy’s surface syntax is not a pure context-free grammar, so the restrictions are part of the specification, like the static restrictions in the Java Language Specification.

Notation

The EBNF conventions: N ::= ... for a production, a vertical bar for alternatives, parentheses for grouping, the suffixes ?, * and +, quoted terminals, fragment for lexer fragments, <MODE> for a lexer mode, and a comment such as /* R12 */ marks an alternative subject to restriction R12.

Terminal vocabulary

Keywords and the four concentric name sets (identifier, keywords, qualifiedNameElement, methodName), plus operators and separators.

Lexical structure

White space and line escapes, comments and the shebang, the grouping stack and newline rules above, identifiers, numeric literals, string literals, and the GString lexical modes.

Parser productions

Compilation unit, types and names, declarations, statements, expressions (with the operator precedence table), path expressions, primaries, command expressions, closures, lambdas and switch expressions.

Restrictions and diagnostics

The restrictions R1…​ and the inputs that are errors rather than language (unclosed strings and comments, invalid octal, a number ending in _, a late shebang).

Intentionally excluded

What is not syntax: AST transformations as language forms, type checking, the Groovy Truth, operator overloading, default imports, the implicit closure parameter it, runtime GString evaluation.

Production index

Every named parser rule and every lexer token, as a coverage checklist.

How to read the EBNF

A rule reads left to right. These two, copied in spirit from the chapter, define what a number and an identifier look like:

IntegerLiteral ::= (DecimalIntegerLiteral | HexIntegerLiteral
                    | OctalIntegerLiteral | BinaryIntegerLiteral) Underscore?
fragment IntegerTypeSuffix ::= [lLiIgG]
Identifier ::= [a-z$_] JavaLetterOrDigit*

Read them as: an integer literal is one of the four bases, optionally followed by a (diagnosed) trailing underscore; a suffix is one of l L i I g G; a lower-case identifier starts with a lower-case ASCII letter, $ or _, followed by any number of letters or digits. Rules for tokens (capitalised, like IntegerLiteral) belong to the lexer; the camel-case ones (pathExpression, closure) belong to the parser. A bare A | B in a parser rule is unordered alternation, but in a lexer rule alternatives are tried in order, which is what the /* R */ marks record.

The same chapter confirms behaviours that this page and the numbers page show. The suffix G is BigInteger or BigDecimal only at AST conversion, not a distinct token. Unicode escapes use exactly one u. A ** is left-associative. The tokens async, await, defer, module, val and yield exist, while the tutorial says yields. Each can be checked against the chapter’s production index, which is also the quickest way to learn whether a construct is syntax at all (for example it is not a token, it is a semantic convention, and ==> is the IMPLIES token).

Clearer syntax error messages in 6.0

The 6.0 release notes describe error messages for "the mistakes people actually make". They are visible whenever a script fails to compile: unclosed strings and comments are reported at the position where they opened, a bad escape is named, invisible characters are shown as escapes and missing punctuation is named. These are the messages that Groovy 6.0.0 printed for small broken files.

Source line Message What changed in 6.0

def readings = [21.5, 19.0
println "unclosed list"

Missing ']' @ line 2, column 1.

missing punctuation is named

def label = "bench-1

Unclosed string literal @ line 1, column 13.

reported where the string opened

def x = 1 /* never closed

Unclosed comment @ line 1, column 11.

reported at the comment opener

def path = "C:\Users\sensor\log.csv"

Illegal escape character: '\U' @ line 1, column 15.

a Windows path with backslashes is named as an illegal escape

def x followed by a no-break space, then = 1

Unexpected character: '\u00a0' @ line 1, column 6.

invisible pasted characters are shown as \u escapes

const int LIMIT = 40

'const' is not supported; use 'val' or 'static final' instead

reserved words get the Groovy spelling

def x = 08

Invalid octal number

a number that starts with 0 and contains 8 or 9

def y = 1_

Number ending with underscores is invalid

a trailing underscore in a literal

The messages above come from running the groovy command and groovyc on those files. For tools, 6.0 also adds groovyc --error-format short, which prints one file:line:column: severity: message line per diagnostic, a shape that editors, CI log parsers and coding agents handle without extra work:

$ groovyc --error-format short Bad2.groovy
Bad2.groovy:1:13: error: Unclosed string literal
1 error

A tip for Windows paths in string literals: either use forward slashes, which the JVM accepts, a slashy string (/C:\Users\sensor/ needs no escapes for backslashes) or double the backslashes. Slashy strings are described on Strings and GStrings.

Where older references are outdated

A book or tutorial written for Groovy 1.0 or 1.5 (the first edition of Groovy in Action covers Groovy 1.0, January 2007) still explains the lexical rules correctly in spirit, but several details have moved:

  • Binary literals and underscores in numbers (0b1010, 1_000) arrived with Groovy 2.0; older texts show only decimal, octal and hexadecimal.

  • Runtime Groovydoc (/**@ ... */) is a Groovy 3.0 feature; older texts treat all /** */ comments as discarded at compile time.

  • Contextual keywords such as var, record, sealed, permits, trait, yield and the new 6.0 words val, async, await and defer do not exist in older texts, and the list of reserved words they give is shorter.

  • Decimal literals without a leading zero (.5) arrived in Groovy 4.0; see Numbers and Numeric Literals.

  • Error messages in old texts differ from the 6.0 wording shown above; do not match programs against them.

  • The grammar for Groovy 1.x and 2.x was a different parser (Antlr 2); the 6.0 EBNF describes the Parrot parser (ANTLR 4) introduced with Groovy 3.0 and refined since.

References