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
Source: Groovy documentation: Comments
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 throughJAVA_OPTS) and mark the comment/**@. Without the option the example above still runs, but thegroovydoc.contentof the method is empty, which is why the first run printedfalse. -
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 |
|
Visibility |
|
Control flow |
|
Expressions and literals |
|
Reserved but unused |
|
Hyphenated |
|
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? |
|---|---|---|
|
type coercion and import aliases |
yes |
|
|
yes |
|
local variable with an inferred type |
yes |
|
final local variable with an inferred type (Groovy 6.0) |
yes, when the feature is off or outside a declaration |
|
record declaration |
yes |
|
sealed hierarchies (the third word, |
yes |
|
trait declaration |
yes |
|
value of a |
yes |
|
asynchronous programming (6.0, incubating) |
yes |
|
only after |
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.
Source: Groovy documentation: Keywords
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.classandfoo.assertwork.
// 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.
Source: Groovy documentation: Keywords
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 followstry(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: |
Terminal vocabulary |
Keywords and the four concentric name sets ( |
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 |
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 |
Intentionally excluded |
What is not syntax: AST transformations as language forms, type checking, the Groovy Truth, operator overloading,
default imports, the implicit closure parameter |
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 |
|---|---|---|
|
|
missing punctuation is named |
|
|
reported where the string opened |
|
|
reported at the comment opener |
|
|
a Windows path with backslashes is named as an illegal escape |
|
|
invisible pasted characters are shown as |
|
|
reserved words get the Groovy spelling |
|
|
a number that starts with 0 and contains 8 or 9 |
|
|
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,yieldand the new 6.0 wordsval,async,awaitanddeferdo 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
-
Groovy documentation: Syntax (Comments, Keywords, Identifiers)
-
Groovy Language Documentation: Grammar (Groovy 6.0)
-
Groovy 2.0 release notes (binary literals, underscores in numbers)
-
Groovy 4.0 release notes (decimal literals without a leading zero)
-
Groovy in Action (Manning, first edition) (publisher page; described in the section bibliography)