Syntax and Lexical Structure
|
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. |
Checked against R 4.6.1 (2026-06-24) on 2026-10-20; R 4.6.2 is scheduled for 2026-10-29. This page is about the
smallest unit of R that a programmer reads and writes: the text of the language itself. It follows the parser from the
characters in a file to the tokens, the expressions and the function calls that the evaluator finally runs, and it shows
what R says when that text is wrong. Every example uses only base R (the base and utils packages that ship with R),
so no add-on package version needs to be stated; the lintr 3.4.0 default linters were used only to check the scripts.
Every example below is a script that was run with Rscript on R 4.6.1 with warnings printed as they happen
(options(warn = 1)), and the lines starting with #> are what it printed. The scripts also pass the default lintr linters; where an idiom being shown is exactly what a default
linter dislikes (T, x<-7, a non-snake-case name, a semicolon), the line carries a # nolint: comment so that the
linter does not hide the lesson. The names books, loans and loan_days come from the running library-shelf
scenario described on the section index: a script either reads books.csv
with read.csv() or starts with source("scenario.R"). The first page,
Getting Started, shows how to install R and run a script.
From text to value: what the parser does
The R Language Definition puts the whole mechanism in one chapter. The parser converts the textual representation of R code into an internal form, and that internal form is itself an R object that can be saved and manipulated (R-lang: Parser). Reading a program therefore has three stages, which map onto the sections of this page.
| Stage | What happens | Where it is covered below |
|---|---|---|
Lexical analysis |
The characters are split into tokens: constants, identifiers, operators, brackets and separators. Whitespace and comments are discarded. |
Tokens, names, reserved words, literals |
Parsing |
The tokens are grouped into expressions by the grammar. The result is a tree of function calls, held in an object
of type |
Expressions, newlines and blocks; the parse tree |
Evaluation |
The evaluator walks the tree. This is the stage that can fail with an "object not found" or "non-numeric argument" error. |
Not covered here; see Lazy Evaluation and Promises |
A syntax error belongs to the second stage and is therefore found before any code runs; an evaluation error belongs to the
third. The manual lists three modes of parsing. The read-eval-print loop reads input until a complete expression is
available (the continuation prompt, + by default, means the parser expects the rest of an incomplete expression).
Text files are parsed by parse() and, through it, by source(), which parses and checks the entire file before
evaluating anything. Character strings are parsed by parse(text = ...), treating each element as a line of a file
(R-lang: Modes of parsing). The first
consequence is shown by the following script: the valid first line of a file does not run if a later line has a syntax
error, while the console, which parses one line at a time, runs it.
# source() and parse() read the whole text before anything is evaluated, so a
# syntax error on the last line stops the first line from running.
script <- c(
"cat(\"step one ran\\n\")",
"step_two <- 2",
"oops oops"
)
tryCatch(
source(textConnection(script)),
error = \(e) writeLines(conditionMessage(e))
)
#> textConnection(script):3:6: unexpected symbol
#> 2: step_two <- 2
#> 3: oops oops
#> ^
exists("step_two")
#> [1] FALSE
# Parsing the lines one at a time, as the console does, runs the valid ones.
for (line in script) {
tryCatch(
eval(parse(text = line)),
error = \(e) writeLines("syntax error on this line")
)
}
#> step one ran
#> syntax error on this line
exists("step_two")
#> [1] TRUE
Source: R documentation: source
This is also why a long script that fails with "unexpected symbol" on its last line has not done any work, whereas a script that fails with "object not found" has done everything before that line. When a script misbehaves the first question is which of the two stages produced the message; the next sections describe each token class so that the message can be read.
Tokens
Tokens are the elementary building blocks, recognised during lexical analysis, which conceptually happens before the syntactic analysis done by the parser itself (R-lang: Tokens). The manual distinguishes the following classes.
| Token class | Examples | Section of the manual |
|---|---|---|
Constants |
|
|
Identifiers |
|
|
Reserved words |
|
|
Special operators |
|
|
Separators |
whitespace, newlines, |
|
Operator tokens |
|
|
Grouping |
|
|
Indexing tokens |
|
The operator list of the manual predates a few operators. The help page ?Syntax lists the current set in precedence
order, adds ::, :::, @, |>, &&, ||, ->>, <<- and the help operator ?, and warns that there are
substantial precedence differences between R and S
(R documentation: Syntax). The precedence table itself, and
the reasons for it, are the subject of Operators and
Precedence; this page only needs the fact that every one of those operators is a token that the lexer recognises.
Whitespace as a separator
Whitespace (spaces, tabs and form feeds; on Windows and in UTF-8 locales also other Unicode whitespace characters) serves
to delimit tokens when there would otherwise be an ambiguity. The manual’s own example compares x<-5 and
x < -5: the first is an assignment of 5 to x, the second is a comparison with minus five
(R-lang: Separators). Beyond that, extra
whitespace is not significant: R accepts a space before the opening parenthesis of a call or the opening bracket of an
index, and indentation carries no meaning, unlike Python.
# Whitespace separates tokens only when the tokens would otherwise run together.
x <- 5
x
#> [1] 5
x < -5 # nolint: infix_spaces_linter.
#> [1] FALSE
x<-7 # nolint: infix_spaces_linter.
x
#> [1] 7
# Spaces before "(" and "[" are allowed, though unusual.
sum (1, 2) # nolint: function_left_parentheses_linter.
#> [1] 3
x [1] # nolint: brace_linter, spaces_inside_linter.
#> [1] 7
# A comment runs from "#" to the end of the line,
# unless the "#" is inside a string.
y <- 1 # This is a comment...
z <- " #... but this is not."
z
#> [1] " #... but this is not."
# The parser reports tokens and their kinds: getParseData() lists them.
exprs <- parse(text = "total <- price * 2L # double", keep.source = TRUE)
parse_data <- getParseData(exprs)
terminals <- parse_data[parse_data$terminal, c("token", "text")]
terminals
#> token text
#> 1 SYMBOL total
#> 2 LEFT_ASSIGN <-
#> 4 SYMBOL price
#> 5 '*' *
#> 7 NUM_CONST 2L
#> 9 COMMENT # double
# The "#line" directive changes the line number the parser reports.
code <- c(
"#line 100 \"shelf.R\"",
"a <- 1",
"b b"
)
tryCatch(
parse(text = code, keep.source = FALSE),
error = \(e) writeLines(conditionMessage(e))
)
#> shelf.R:101:3: unexpected symbol
#> 100: a <- 1
#> 101: b b
#> ^
getParseData() is the window onto the lexer: it returns one row per token, with the token kind (SYMBOL,
LEFT_ASSIGN, NUM_CONST, COMMENT, and so on) and its text. It needs the source to be kept, which is why the call
passes keep.source = TRUE. The COMMENT row shows that comments are tokens for tooling such as styler and
lintr, even though the evaluator never sees them. The options() entry keep.parse.data controls whether parse data
is stored with the source (default TRUE), and keep.source defaults to interactive(), so it is TRUE in a console
session and FALSE under Rscript
(R documentation: options).
Comments and the #line directive
A comment starts at a # outside a quoted string and runs to the end of the line. The parser ignores it. R has no
block-comment syntax, so a block is commented line by line (editors do this with one shortcut), and the first line
#!/usr/bin/env Rscript of an executable script is just a comment for the parser. The roxygen2 documentation comments
that start with #' are comments too: they matter only to the roxygen2 package, which reads the file as text.
The only directive the parser supports is #line. It is the same idea as the C preprocessor directive: it tells the
parser that the following line is line nn of the file "filename", so that diagnostics point to the original source
when a preprocessor generated the R code. Unlike C, the directive must be the first five characters of the line, the
file name needs double quotes, and any text after it is a comment
(R-lang: Directives). The last chunk of the
script above shows it: after #line 100 "shelf.R" the error is reported at line 101 of shelf.R. It is rarely used by
people, but it explains why an error message can name a file that does not exist on disk.
Identifiers and syntactic names
An identifier is a sequence of letters, digits, the period and the underscore. It must not start with a digit or an
underscore, nor with a period followed by a digit. What counts as a letter depends on the locale: the manual gives the C
expression isalnum(c) || c == '.' || c == '_', which includes accented letters in many Western European locales, while
only the ASCII digits 0-9 are digits
(R-lang: Identifiers). An identifier that is
also not a reserved word is a syntactic name and can be used directly in code. Names that start with a period are not
listed by ls() unless all.names = TRUE.
The five meanings of "name"
The R FAQ spends an entry on the word, because the language uses it in five related senses, and only some of them are restricted to syntactic names (R FAQ 7.14: What are valid names?).
| Kind of name | Rule |
|---|---|
Syntactic name |
What the parser reads as a symbol: letters, numbers, |
Object name |
A string associated with an object by assignment or by |
Argument name |
What appears left of |
Element name |
Identifies a piece of an object, for example a list component. After |
File name |
A string for the operating system. Nothing to do with names in the language. |
The practical consequence is the pair of rules that the next script exercises: a non-syntactic name is reached with
backticks (or, in a few positions, with a string), and make.names() turns arbitrary text into a syntactic name.
make.names and backticks
make.names(names, unique = FALSE, allow_ = TRUE) makes syntactically valid names out of character vectors. It prepends
X when the first character is not allowed, translates every invalid character to ., translates a missing value to
"NA", appends a dot to names that match R keywords, and (with unique = TRUE) makes duplicates distinct through
make.unique(). The allow_ argument exists only for compatibility with R before 1.9.0, when underscores were not
valid in names; allow_ = FALSE is still useful when exporting names to an application that rejects underscores
(R documentation: make.names).
data.frame() and read.table()/read.csv() call it through their check.names = TRUE argument, which is why a
column read as Loan ID is called Loan.ID and one called 1st loan becomes X1st.loan.
The preferred way to quote a non-syntactic name is the backtick; deparse() normally uses it, and under many
circumstances single or double quotes can be used instead because a character constant is often converted to a name.
Backticks may be essential to delimit variable names in formulae
(R documentation: Quotes).
books <- read.csv("books.csv")
# make.names() repairs strings that are not syntactic names.
raw_names <- c("title", "pages in hundreds", "1st edition", "if", "_id",
".2way", "Author's", NA, "", "page.count", "ISBN-13")
make.names(raw_names)
#> [1] "title" "pages.in.hundreds" "X1st.edition"
#> [4] "if." "X_id" "X.2way"
#> [7] "Author.s" "NA." "X"
#> [10] "page.count" "ISBN.13"
raw_names == make.names(raw_names)
#> [1] TRUE FALSE FALSE FALSE FALSE FALSE FALSE NA FALSE TRUE FALSE
# unique = TRUE makes the results distinct; allow_ = FALSE turns "_" into ".".
make.names(c("a and b", "a-and-b"), unique = TRUE)
#> [1] "a.and.b" "a.and.b.1"
make.names(c("a and b", "a_and_b"), unique = TRUE, allow_ = FALSE)
#> [1] "a.and.b" "a.and.b.1"
make.names(c("", "X"), unique = TRUE)
#> [1] "X.1" "X"
# Every reserved word gets a dot appended; non-ASCII letters survive in a
# UTF-8 locale.
make.names(c("TRUE", "NULL", "NA_integer_", "function", "Inf", "next"))
#> [1] "TRUE." "NULL." "NA_integer_." "function." "Inf."
#> [6] "next."
make.names(c("café", "naïve name"))
#> [1] "café" "naïve.name"
# data.frame() and read.csv() call make.names() through check.names = TRUE.
csv_text <- c("Loan ID,member name,1st loan,if", "L001,Ana Ruiz,2025-01-07,yes")
read.csv(text = csv_text)
#> Loan.ID member.name X1st.loan if.
#> 1 L001 Ana Ruiz 2025-01-07 yes
names(read.csv(text = csv_text, check.names = FALSE))
#> [1] "Loan ID" "member name" "1st loan" "if"
# Backticks (the preferred quote) make any non-empty string usable as a name.
loans_raw <- read.csv(text = csv_text, check.names = FALSE)
loans_raw$`Loan ID`
#> [1] "L001"
loans_raw[["member name"]]
#> [1] "Ana Ruiz"
`my total` <- sum(books$pages, na.rm = TRUE) # nolint: object_name_linter.
`my total`
#> [1] 3755
# A string works on the left of "<-" and as an argument tag, too.
"shelf name" <- "Main hall" # nolint: object_name_linter.
`shelf name`
#> [1] "Main hall"
list(`a b` = 1, "c d" = 2)
#> $`a b`
#> [1] 1
#>
#> $`c d`
#> [1] 2
#>
# get() and assign() accept any name, but they do not understand syntax.
assign("books$title", "not a column") # nolint: object_name_linter.
get("books$title")
#> [1] "not a column"
exists("books$title")
#> [1] TRUE
identical(books$title, get("books$title"))
#> [1] FALSE
# Names that start with a dot are hidden from ls() unless all.names = TRUE.
shelf_env <- new.env()
assign(".hidden", 1, envir = shelf_env)
assign("shown", 2, envir = shelf_env)
ls(shelf_env)
#> [1] "shown"
ls(shelf_env, all.names = TRUE)
#> [1] ".hidden" "shown"
# A syntactic name survives make.names() unchanged.
is_syntactic <- function(x) x == make.names(x)
vapply(c("x1", ".x", "x_y", "x.y", "1x", ".1x", "_x", "x y"),
is_syntactic, logical(1L))
#> x1 .x x_y x.y 1x .1x _x x y
#> TRUE TRUE TRUE TRUE FALSE FALSE FALSE FALSE
Source: R documentation: make.names
Four details in that output are worth remembering. First, make.names("") is "X" and not an error, and with
unique = TRUE the call make.names(c("", "X"), unique = TRUE) gives "X.1" and "X" (R up to 3.0.2 gave them in the
other order). Second, a reserved word in a data frame column becomes if., which is why a CSV with a column named if
reads as if. and why check.names = FALSE is needed to keep it. Third, get("books$title") looks for an object
literally named books$title; get() and assign() do not recognise subscripting operators or replacement functions, so
x$a <- 1 and assign("x$a", 1) are different things, as are x[[1]] and get("x[[1]]"), and names(x) <- nm and
assign("names(x)", nm) (R-lang: Identifiers). Fourth, make.names() leaves ... and ..1 alone, although they are
reserved words: they are not valid object names, but they are valid tokens, so the function does not treat them as
clashing keywords (checked on R 4.6.1). The helper is_syntactic() at the end of the script is therefore only a first
approximation; quote() or parse() is the authority.
Names of functions follow the same rules. A function called overdue days can be defined and called with backticks or
with a string in the function position, but the parameters of a function expression must be real names:
# "..." and "..1", "..2" are reserved words that refer to the arguments passed
# on from the calling function.
second_of <- function(...) ..2
second_of("a", "b", "c")
#> [1] "b"
count_args <- function(...) ...length()
count_args(1, 2, 3)
#> [1] 3
label_args <- function(...) ...names()
label_args(first = 1, 2, third = 3)
#> [1] "first" "" "third"
nth_arg <- function(n, ...) ...elt(n)
nth_arg(3L, "x", "y", "z")
#> [1] "z"
# Using ..2 when there is only one argument is an error with its own message.
tryCatch(
second_of("only one"),
error = \(e) writeLines(conditionMessage(e))
)
#> the ... list contains fewer than 2 elements
tryCatch(
eval(parse(text = "..1")),
error = \(e) writeLines(conditionMessage(e))
)
#> ..1 used in an incorrect context, no ... to look in
# A function may have a non-syntactic name, but its parameters must be names.
`overdue days` <- function(loaned, returned) { # nolint: object_name_linter.
as.numeric(returned - loaned)
}
`overdue days`(as.Date("2025-01-07"), as.Date("2025-02-01"))
#> [1] 25
"overdue days"(as.Date("2025-01-07"), as.Date("2025-02-01"))
#> [1] 25
tryCatch(
parse(text = "function(\"x\") 1"),
error = \(e) writeLines(conditionMessage(e))
)
#> <text>:1:10: unexpected string constant
#> 1: function("x"
#> ^
# A call can use a quoted tag; a string and a name are equivalent there.
list("odd tag" = 1, plain = 2)
#> $`odd tag`
#> [1] 1
#>
#> $plain
#> [1] 2
#>
identical(list("a" = 1), list(a = 1))
#> [1] TRUE
The same script shows the three special tokens ..., ..1, ..2, and the functions that go with them: ...length(),
...names() (R 4.1.0) and ...elt(n). They refer to the arguments passed down from a calling function
(R documentation: dots). Using ..2 when fewer than two
arguments exist is an evaluation error with its own message, and ..1 outside a function with dots is another one; both
are shown above. Functions describes how dots are matched.
A note on style
Because both the dot and the underscore are legal, R code comes in several naming dialects. The dot has a historical
role in S3: print.book is the print method for class book, so a function whose name contains a dot may look like a
method to the dispatcher. That is the reason most current style guides, and the default lintr object_name_linter,
prefer snake_case for new names. Older tutorials use dotted names such as my.data.frame everywhere; both work, and
base R itself has dotted names (read.csv, is.na, Sys.Date) that are not S3 methods. Names are case sensitive:
Books and books are different objects
(R-intro: R commands, case sensitivity, etc.).
Reserved words
The parser reserves a short list of words. They have a special meaning and cannot be used as object names (R-lang: Reserved words).
| Group | Words |
|---|---|
Control flow |
|
Constants |
|
Missing values |
|
Dots |
|
Source: R documentation: Reserved
The help page adds the important detail: reserved words outside quotes are always parsed to be references to the objects
they stand for, hence they are not allowed as syntactic names, but they are allowed as non-syntactic names, for example
inside backticks. The next script shows the two failure modes. The control-flow words fail in the parser, because the
grammar has no rule that puts if on the left of <-. The constants parse as constants, so the failure comes from
the evaluator, which says that the left-hand side of the assignment is invalid.
# Run a piece of text through the parser and evaluator; report the outcome.
outcome <- function(text) {
tryCatch(
{
eval(parse(text = text))
"no error"
},
error = \(e) conditionMessage(e)
)
}
# The reserved words are recognised by the parser, so they cannot be names.
outcome("if <- 1")
#> [1] "<text>:1:4: unexpected assignment\n1: if <-\n ^"
outcome("function <- 1")
#> [1] "<text>:1:10: unexpected assignment\n1: function <-\n ^"
outcome("repeat <- 1")
#> [1] "<text>:1:8: unexpected assignment\n1: repeat <-\n ^"
# TRUE, FALSE, NULL, Inf and NaN parse as constants, so the
# parser accepts the assignment but the evaluator refuses it.
outcome("TRUE <- 0")
#> [1] "invalid (do_set) left-hand side to assignment"
outcome("NULL <- 1")
#> [1] "invalid (do_set) left-hand side to assignment"
outcome("Inf <- 1")
#> [1] "invalid (do_set) left-hand side to assignment"
# Backticks turn a reserved word into an ordinary, non-syntactic name.
`if` <- "just a variable" # nolint: object_name_linter.
`if`
#> [1] "just a variable"
rm(`if`)
# As a list name or an argument tag, a reserved word needs the same quoting.
outcome("list(if = 1)")
#> [1] "<text>:1:9: unexpected '='\n1: list(if =\n ^"
outcome("list(`if` = 1)")
#> [1] "no error"
shelf_item <- list(`if` = "interlibrary", `in` = "shelf 4")
outcome("shelf_item$if")
#> [1] "<text>:1:12: unexpected 'if'\n1: shelf_item$if\n ^"
shelf_item$`if`
#> [1] "interlibrary"
shelf_item$"in"
#> [1] "shelf 4"
# Words that merely look special are ordinary variables and can be shadowed:
# "c" is a function, but assigning to it does not stop c() from working.
c <- 3 # nolint: object_name_linter.
c(c, c)
#> [1] 3 3
rm(c)
# Types of the constants.
vapply(
list(TRUE, 1L, 1, 1i, "a", NULL, NA, NA_integer_, NA_real_, NA_character_),
typeof, character(1L)
)
#> [1] "logical" "integer" "double" "complex" "character" "NULL"
#> [7] "logical" "integer" "double" "character"
typeof(Inf)
#> [1] "double"
typeof(NaN)
#> [1] "double"
Source: R documentation: Reserved
The last section of the script is the common beginner’s worry about shadowing: c <- 3 is allowed because c is not a
reserved word, and c(c, c) still works because when R looks for a function called c it skips non-function
objects. Naming a variable c, t, df or data is legal, if confusing. The words that cannot be shadowed are
exactly the ones in the table; T and F, as the next sections show, are not among them.
Two quirks are worth knowing but not worth relying on. Assigning to a string constant assigns to the variable of that name
("shelf name" <- "Main hall" creates shelf name), so NA_character_ <- 1 does not fail: it silently creates a variable
called NA. And the parser accepts nonsense on the left of an assignment: the manual points out that 2 + 2 <- 5 is a
valid expression as far as the parser is concerned, and the evaluator is the one that objects
(R-lang: Infix and prefix operators).
Numeric literals
Numeric constants follow a syntax similar to C. The manual and the help page ?NumericConstants describe the grammar:
an integer part, an optional fractional part, and an optional exponent with e or E; hexadecimal constants start with
0x or 0X and may end in a binary exponent introduced by p or P; the suffix L makes an integer when possible,
and the suffix i makes an imaginary number
(R documentation: NumericConstants).
| Literal | Meaning | Type |
|---|---|---|
|
Decimal; the fractional or the integer part may be empty, not both |
|
|
Hexadecimal |
|
|
Hexadecimal with a binary exponent (a power of two), as in C99 |
|
|
Integer; a hex or scientific form is allowed if the value is a whole number in range |
|
|
Not a valid integer: a warning and a |
|
|
A purely imaginary number; other complex numbers are arithmetic on a number and an imaginary |
|
|
Infinity and not-a-number |
|
|
Missing value of each type |
|
# Decimal constants are doubles, even when they look like integers.
vapply(list(1, 10, 0.1, .2, 1e-7, 1.2e+7, 5.), typeof, character(1L))
#> [1] "double" "double" "double" "double" "double" "double" "double"
class(2)
#> [1] "numeric"
2 == 2L
#> [1] TRUE
identical(2, 2L)
#> [1] FALSE
# The suffix L makes an integer when the value allows it.
vapply(list(1L, 0x10L, 1000000L, 1e6L, 1e3L), typeof, character(1L))
#> [1] "integer" "integer" "integer" "integer" "integer"
identical(0xFL, 15L)
#> [1] TRUE
# Hexadecimal constants, with an optional binary exponent (p or P).
0x1F
#> [1] 31
0xA0
#> [1] 160
0x1.8p1
#> [1] 3
0x123p4
#> [1] 4656
0x1.1 # accepted since R 4.4.3, as 0x1.1p0
#> [1] 1.0625
# Too large for an integer, or with a fractional part: a warning and a double.
catch_warning <- function(expr) {
tryCatch(expr, warning = \(w) paste("warning:", conditionMessage(w)))
}
catch_warning(eval(parse(text = "1.5L")))
#> [1] "warning: integer literal 1.5L contains decimal; using numeric value"
catch_warning(eval(parse(text = "1.L")))
#> [1] "warning: integer literal 1.L contains unnecessary decimal point"
catch_warning(eval(parse(text = "3000000000L")))
#> [1] "warning: non-integer value 3000000000L qualified with L; using numeric value"
catch_warning(eval(parse(text = "1e-3L")))
#> [1] "warning: non-integer value 1e-3L qualified with L; using numeric value"
.Machine$integer.max
#> [1] 2147483647
catch_warning(.Machine$integer.max + 1L)
#> [1] "warning: NAs produced by integer overflow"
# Overflow becomes Inf, underflow becomes 0.
1e308 * 10
#> [1] Inf
1e400
#> [1] Inf
1e-400
#> [1] 0
# Complex constants: only the imaginary part is a constant.
typeof(2i)
#> [1] "complex"
3 + 2i
#> [1] 3+2i
class(quote(3 + 2i))
#> [1] "call"
sqrt(-1 + 0i)
#> [1] 0+1i
# A sign is a unary operator, not part of the constant.
-2^2
#> [1] -4
(-2)^2
#> [1] 4
class(quote(-1))
#> [1] "call"
class(quote(1))
#> [1] "numeric"
# There is no digit grouping, no binary or octal prefix and no "d" exponent.
parse_message <- function(text) {
tryCatch(
{
parse(text = text)
"parsed"
},
error = \(e) conditionMessage(e)
)
}
for (text in c("1_000", "0b101", "1d3", "12iL", "1e")) {
writeLines(parse_message(text))
}
#> <text>:1:2: unexpected input
#> 1: 1_
#> ^
#> <text>:1:2: unexpected symbol
#> 1: 0b101
#> ^
#> <text>:1:2: unexpected symbol
#> 1: 1d3
#> ^
#> <text>:1:4: unexpected symbol
#> 1: 12iL
#> ^
#> <text>:1:1: unexpected input
#> 1: 1e
#> ^
Points that regularly surprise newcomers, each visible in the output above:
-
A number that looks like an integer is a
double.class(2)is"numeric",2 == 2LisTRUE, andidentical(2, 2L)isFALSE. The scenario constantloan_daysis written21L, an integer literal, so it is an integer and not a double. Whole-number doubles print without a decimal point, so the difference is invisible until a function such asidentical()orvapply(..., integer(1L))cares. -
The warning text depends on the form:
1.5Lgives "integer literal 1.5L contains decimal; using numeric value",1.Lgives "integer literal 1.L contains unnecessary decimal point", and an integer that is too large, such as3000000000L, gives "non-integer value 3000000000L qualified with L; using numeric value". The largest integer is.Machine$integer.max, 2147483647. Arithmetic that overflows the integer type givesNAwith the warning "NAs produced by integer overflow", but a literal never overflows; it falls back to a double. -
A leading sign is not part of the constant. The parser sees a unary operator applied to a constant, so
-2^2is-4(the exponent binds tighter than the unary minus) andquote(-1)is a call, not a number. -
Values too large or too small overflow to
Infor underflow to0.InfandNaNare constants of typedouble; in text input (scan(),as.double()) they are also recognised ignoring case, withinfinityas an alternative. -
Only the ASCII digits are digits and the decimal separator is always a period, never a comma. There is no digit grouping (
1_000is a syntax error), no binary (0b101) or octal prefix, and nodexponent as in Fortran. -
The manual’s text says
0x1.1is a syntax error, because a decimal point in a hexadecimal constant needs a binary exponent. R 4.4.3 changed this: the parser now accepts hexadecimal constants with a decimal point and no exponent, taken asp0, as?NumericConstantsdocuments (R NEWS, Changes in R 4.4.3). On R 4.6.10x1.1is 1.0625. Code that must run on older versions should write the exponent.
Source: R NEWS: Changes in R 4.4.3
Floating-point representation (why 0.1 + 0.2 == 0.3 is FALSE) is not a parsing matter and belongs to
Numbers and Arithmetic.
String literals and escapes
A string constant is delimited by a pair of single or double quotes. They are interchangeable; double quotes are preferred, and R always prints strings with double quotes, so single quotes are normally used only to delimit a string that contains double quotes (R documentation: Quotes). A backslash starts an escape sequence. The following table is the one in the help page.
| Escape | Character |
|---|---|
|
newline (line feed), carriage return, tab |
|
backspace, alert (bell), form feed, vertical tab |
|
a backslash |
|
apostrophe, quotation mark and grave accent (backtick) |
|
octal code, one to three digits (maximum |
|
hexadecimal code, one or two digits |
|
Unicode character, one to four hex digits |
|
Unicode character, one to eight hex digits |
Source: R documentation: Quotes
Escaping a character that is not in the table is an error, so "\q" is rejected when the code is parsed, and so is
mixing octal or hexadecimal escapes with Unicode escapes in the same string. A NUL character cannot be part of a string:
\0 is rejected. The text of ?Quotes says that a NUL escape truncates the string at that point (usually with a
warning), but its own examples list both "foo\0bar" and "foo\u0000bar" as parse errors, and R 4.6.1 reports "nul
character not allowed". A string may contain a literal newline, in which case the newline
is part of the value. Unicode escapes can enter characters that are not in the current locale’s character set, in which
case the string is stored in UTF-8. UTF-16 surrogate pairs written as \uD834\uDD1E become a single code point, and
an unpaired surrogate gives a warning from the parser. As from R 4.1.0 the largest allowed \U value is \U10FFFF,
the largest Unicode code point; a bigger value is an error because it cannot be represented in valid UTF-8 (R NEWS,
Changes in R 4.1.0).
# Single and double quotes are interchangeable; printing uses double quotes.
'single quotes work too' # nolint: quotes_linter.
#> [1] "single quotes work too"
identical('"It\'s alive!", he screamed.', "\"It's alive!\", he screamed.")
#> [1] TRUE
# print() shows the string as R source; writeLines() shows its characters.
path <- "C:\\shelf\\books.csv"
print(path)
#> [1] "C:\\shelf\\books.csv"
writeLines(path)
#> C:\shelf\books.csv
nchar(path)
#> [1] 18
# Escape sequences.
line <- "title\tauthor\nThe Long Table\tIris Fenwick"
print(line)
#> [1] "title\tauthor\nThe Long Table\tIris Fenwick"
writeLines(line)
#> title author
#> The Long Table Iris Fenwick
# Octal, hexadecimal and Unicode escapes can spell the same text.
hello_octal <- "\110\145\154\154\157"
hello_hex <- "\x48\x65\x6c\x6c\x6f"
hello_unicode <- "\u48\u65\u6c\u6c\u6f"
c(hello_octal, hello_hex, hello_unicode)
#> [1] "Hello" "Hello" "Hello"
identical(hello_octal, hello_hex) && identical(hello_hex, hello_unicode)
#> [1] TRUE
"\u00e9\u0126\U0001F4DA"
#> [1] "éĦ📚"
"\u{e9}\U{1F4DA}"
#> [1] "é📚"
nchar("\U0001F4DA")
#> [1] 1
nchar("\U0001F4DA", type = "bytes")
#> [1] 4
# A character constant may span lines: the newline is part of the string.
two_lines <- "first line
second line"
two_lines
#> [1] "first line\nsecond line"
nchar(two_lines)
#> [1] 22
# Bad escapes and embedded NULs are errors when the code is parsed.
parse_message <- function(text) {
tryCatch(
{
parse(text = text)
"parsed"
},
error = \(e) conditionMessage(e)
)
}
parse_message('"\\q"')
#> [1] "'\\q' is an unrecognized escape in character string (<input>:1:3)"
parse_message('"\\x48\\u65"')
#> [1] "mixing Unicode and octal/hex escapes in a string is not allowed (<input>:1:10)"
parse_message('"foo\\0bar"')
#> [1] "nul character not allowed (<input>:1:6)"
parse_message('"\\U110000"')
#> [1] "invalid \\Uxxxxxxxx value 110000 (<input>:1:9)"
parse_message("'unterminated")
#> [1] "<text>:1:1: unexpected INCOMPLETE_STRING\n1: 'unterminated\n ^"
# Strings do not interpolate: use paste0(), sprintf() or format().
member <- "Ana Ruiz"
"Hello, member"
#> [1] "Hello, member"
paste0("Hello, ", member)
#> [1] "Hello, Ana Ruiz"
sprintf("Hello, %s", member)
#> [1] "Hello, Ana Ruiz"
Source: R documentation: Quotes
The first lesson of the script is the difference between a string and its display. print() shows the string as source
code, with backslashes doubled and non-printing characters escaped; writeLines() and cat() show the characters. The
string "C:\\shelf\\books.csv" has 18 characters, not 20: each doubled backslash is one character. nchar() counts
characters by default, so nchar("\U0001F4DA") is 1 while nchar(..., type = "bytes") is 4
(R documentation: nchar). Encodings and locales are the
subject of Encodings, Locales and
Internationalization; what matters for the lexer is that the same source file can be read differently in a different
locale unless the file is UTF-8 and the Unicode escapes are used for the non-ASCII characters in portable code.
Strings are never interpolated. "Hello, member" is literal text; a value is inserted with paste0(), sprintf() or
format(). Interpolation with {name} exists in the glue and cli packages, which are not part of base R and are not
used here. The last practical warning is about curly quotes: text pasted from a web page or a word processor often
contains typographic quotes such as the left double quotation mark, which are not quote characters to the lexer and give
"unexpected input" (see the next sections).
Raw strings
R 4.0.0 added a second syntax for character constants, modelled on C++: r"(...)", where ... is any character
sequence that does not contain the closing sequence )". The NEWS entry says it makes it easier to write strings that
contain backslashes or both single and double quotes. The delimiter pair can also be [] or {}, the r can be an
upper-case R, and any number of dashes can sit between the quote and the opening delimiter provided the same number
appears between the closing delimiter and the closing quote, which lets the text contain )" itself
(R documentation: Quotes and
R NEWS: Changes in R 4.0.0).
books <- read.csv("books.csv")
# A raw string needs no escaping: r"(...)" ends at the first )" sequence.
windows_path <- r"(C:\Program files\R)"
windows_path
#> [1] "C:\\Program files\\R"
writeLines(windows_path)
#> C:\Program files\R
nchar(windows_path)
#> [1] 18
identical(windows_path, "C:\\Program files\\R")
#> [1] TRUE
# It can hold both kinds of quotes.
r"(use both "double" and 'single' quotes)"
#> [1] "use both \"double\" and 'single' quotes"
# [] and {} are alternative delimiters, and R"..." works as well as r"...".
r"[a (parenthesised) note]"
#> [1] "a (parenthesised) note"
R"{a [bracketed] note}"
#> [1] "a [bracketed] note"
# Dashes between the quote and the delimiter let the text contain )".
r"---(\1--)-)---"
#> [1] "\\1--)-"
writeLines(r"---(a string containing )" inside)---")
#> a string containing )" inside
# Raw strings can span lines.
raw_lines <- r"(first
second)"
raw_lines
#> [1] "first\nsecond"
# They are most useful for regular expressions: no doubled backslashes.
hyphenated <- r"(^\d{3}(-\d+){4}$)"
hyphenated
#> [1] "^\\d{3}(-\\d+){4}$"
grepl(hyphenated, c("978-0-306-40615-7", "9780306406157"))
#> [1] TRUE FALSE
sum(grepl(hyphenated, books$isbn))
#> [1] 11
sum(grepl(r"(^\d{13}$)", books$isbn))
#> [1] 4
identical(grepl(r"(^\d{13}$)", books$isbn), grepl("^\\d{13}$", books$isbn))
#> [1] TRUE
# A raw string is an ordinary character constant once parsed.
class(quote(r"(\d)"))
#> [1] "character"
quote(r"(\d)")
#> [1] "\\d"
Source: R NEWS: Changes in R 4.0.0
A raw string has no escapes at all: the characters between the delimiters are the value, including backslashes and
newlines. It is only a way to write the string in source; once parsed it is an ordinary character constant (the script
ends by showing that quote(r"(\d)") is a plain "\\d"). The two uses that justify it are Windows paths and regular
expressions, where doubling every backslash is a recurring source of mistakes: r"(^\d{13}$)" and "^\\d{13}$" are the
same pattern, and the script confirms that they select the same ISBNs. Raw strings do not exist before R 4.0.0, so code
for older versions has to keep the doubled backslashes (or forward slashes in paths, which Windows accepts in R).
Logical constants, NULL and the NA family
TRUE and FALSE are reserved words and therefore true constants. T and F are not: they are ordinary variables
defined in the base package, with the values TRUE and FALSE. The R FAQ lists the difference between R and S here: in
S they are reserved words, while in R a user can overwrite them, and the FAQ’s advice is to always write TRUE and
FALSE (R FAQ 3.3.3: Others). find("T") reports "package:base",
and find("TRUE") finds nothing because there is no object to find.
# TRUE and FALSE are reserved words (constants); T and F are ordinary variables
# defined in the base environment.
T # nolint: T_and_F_symbol_linter.
#> [1] TRUE
F # nolint: T_and_F_symbol_linter.
#> [1] FALSE
find("T")
#> [1] "package:base"
find("TRUE")
#> character(0)
tryCatch(
eval(parse(text = "TRUE <- 0")),
error = \(e) conditionMessage(e)
)
#> [1] "invalid (do_set) left-hand side to assignment"
# Because T is a variable, any code can change what it means.
local({
T <- 0 # nolint: T_and_F_symbol_linter, object_name_linter.
c(spelled_out = mean(c(1, NA, 3), na.rm = TRUE),
shortcut = mean(c(1, NA, 3), na.rm = T)) # nolint: T_and_F_symbol_linter.
})
#> spelled_out shortcut
#> 2 NA
# NULL is the empty object; NA marks a missing value; they are different things.
length(NULL)
#> [1] 0
length(NA)
#> [1] 1
is.null(NA)
#> [1] FALSE
is.na(NULL)
#> logical(0)
c(1, NULL, 3)
#> [1] 1 3
c(1, NA, 3)
#> [1] 1 NA 3
# The NA family: one constant per atomic type.
vapply(
list(NA, NA_integer_, NA_real_, NA_complex_, NA_character_),
typeof, character(1L)
)
#> [1] "logical" "integer" "double" "complex" "character"
c(NA, "text")
#> [1] NA "text"
c(NA, 1L)
#> [1] NA 1
identical(NA_real_, NA_integer_)
#> [1] FALSE
# Inf and NaN are numeric constants (type double).
1 / 0
#> [1] Inf
-1 / 0
#> [1] -Inf
0 / 0
#> [1] NaN
is.nan(c(NaN, NA, 1))
#> [1] TRUE FALSE FALSE
is.na(c(NaN, NA, 1))
#> [1] TRUE TRUE FALSE
is.finite(c(Inf, NaN, NA, 1))
#> [1] FALSE FALSE FALSE TRUE
Inf - Inf
#> [1] NaN
NA > 1
#> [1] NA
NA & FALSE
#> [1] FALSE
NA || TRUE
#> [1] TRUE
Source: R FAQ 3.3.3: Others
The first half of the output is the argument against T: T <- 0 is legal, and after it na.rm = T means
na.rm = 0, which is FALSE, so mean() returns NA instead of 2. Nothing warns about it. The script wraps the
assignment in local() so that the change does not leak, but the same thing happens at the top level of a script or
inside a package that attaches another package defining T. There is no equivalent trap with TRUE: TRUE <- 0 is an
error. The default lintr linter T_and_F_symbol_linter flags T and F for this reason, and the examples in this
reference never use them except in the contrast that carries a # nolint: comment.
The remaining constants of the second half of the script:
-
NULLis the empty object, of type"NULL"and length zero. It is what a function without a result returns. It vanishes insidec(), which is whyc(1, NULL, 3)has length two.is.null(NA)isFALSE,is.na(NULL)islogical(0). -
NAis the logical missing value; each atomic type has its own constant (NA_integer_,NA_real_,NA_complex_,NA_character_), andNAis coerced to the type of the other elements inc().identical(NA_real_, NA_integer_)isFALSE. -
NaNis a double constant:0 / 0gives it.is.na(NaN)isTRUE, butis.nan(NA)isFALSE.Infand-Infcome from1 / 0and-1 / 0, andis.finite()isFALSEfor all ofInf,NaNandNA. -
Logical operators follow three-valued logic:
NA & FALSEisFALSEandNA || TRUEisTRUE, because the answer does not depend on the unknown value;NA > 1isNA.
Source: R documentation: NA
The behaviour of missing values in computation, in subsetting and in data frames is covered in Missing and Special Values; from the point of view of the lexer, what matters is that all of them are constants written with reserved words.
Expressions, statements and newlines
An R program is a sequence of expressions. An expression can be a simple one (a constant or an identifier) or a compound
one built from other expressions
(R-lang: Expressions). R makes no distinction
between expressions and statements: an assignment, an if, a for loop and a block are all expressions with a value, and
the value of a for is NULL.
Where a line ends
Newlines have a function that combines token separator and expression terminator: if an expression can end at the end
of the line, the parser assumes it does; otherwise the newline is treated as whitespace
(R-lang: Separators). This rule is the reason R
needs no line-continuation character and no terminating semicolon. It also determines where a long expression may be
broken: after something that leaves the expression unfinished. The next script lets the parser itself classify a set of
cases: "complete" means the console would run it, "incomplete" means the console would show the + prompt.
# Ask the parser what the console would do with a line (or several lines):
# "complete" means it can be run, "incomplete" means the console would show
# the "+" continuation prompt (the parser reports "unexpected end of input" or
# an unfinished string), anything else is a syntax error.
classify <- function(...) {
text <- c(...)
tryCatch(
{
parse(text = text)
"complete"
},
error = \(e) {
msg <- conditionMessage(e)
if (grepl("end of input|INCOMPLETE_STRING", msg)) {
"incomplete"
} else {
"syntax error"
}
}
)
}
classify("x <- 1 + 2")
#> [1] "complete"
classify("x <- 1 +")
#> [1] "incomplete"
classify("x <- (1 + 2")
#> [1] "incomplete"
classify("f(a,")
#> [1] "incomplete"
classify("title <- 'Dune")
#> [1] "incomplete"
classify("if (TRUE)")
#> [1] "incomplete"
classify("function(x)")
#> [1] "incomplete"
classify("for (i in 1:3)")
#> [1] "incomplete"
classify("shelf |>")
#> [1] "incomplete"
classify("2 %in%")
#> [1] "incomplete"
classify("x <-", "5")
#> [1] "complete"
classify("x", "+ 5")
#> [1] "complete"
classify("if (TRUE) 1", "else 2")
#> [1] "syntax error"
classify("{", "if (TRUE) 1", "else 2", "}")
#> [1] "complete"
classify("if (TRUE) {", "1", "} else {", "2", "}")
#> [1] "complete"
classify("if (TRUE) {", "1", "}", "else {", "2", "}")
#> [1] "syntax error"
| The line (or lines) | Result | Reason |
|---|---|---|
|
ends here |
the expression is complete |
|
continues |
a binary operator needs a right operand |
|
continues |
an open bracket, brace or parenthesis is not closed |
|
continues |
a string constant is not closed (it may contain newlines) |
|
continues |
the construct still needs its body |
|
two expressions |
the first line is complete; the second starts with a unary plus |
|
syntax error |
at the top level the first line is already complete |
The consequences for style are one rule and one trap. The rule: put the operator at the end of the line and indent the
continuation (total <- first + and then second), as the first block of the next script does. The trap: an
operator at the start of the next line does not continue the previous line. broken <- 1 followed by a line + 2 is two
expressions, and the second one is the harmless unary plus applied to 2, which is printed at the console and leaves
broken at 1. Chains of the native pipe therefore put |> at the end of each line, not at the beginning.
The same-line separators are the semicolon, which separates elementary expressions on one line (a <- 1; b <- 2), and
the comma, which separates arguments and indices. The semicolon is legal but unusual style; lintr flags compound
semicolons.
The else rule
The R Language Definition singles out one exception: inside a compound expression a newline before else is discarded,
whereas at the outermost level the newline ends the if and a following else is a syntax error. The anomaly exists
because R must decide whether the input is complete, incomplete or invalid as soon as RET is pressed. The final rows of
the previous script show it: the same text is accepted inside braces and rejected without them. Write the else on the
same line as the closing brace (} else {), which is correct everywhere. The full treatment of if, else, loops and
switch() is in Control Flow.
Blocks and parentheses
A compound expression has the form { expr1 ; expr2 ; ... ; exprn }, with newlines allowed in place of the semicolons,
and it is stored as a call to the function named { with the expressions as arguments. It evaluates its arguments in
order and returns the value of the last one; the empty block {} is NULL
(R-lang: Compound expressions).
Braces do not create a scope in R: variables assigned inside a block belong to the enclosing environment, which is a major
difference from C, Java and Rust (see Scoping,
Environments and Closures). Ordinary parentheses group an expression and delimit argument lists, and they are
recorded as a unary operator with the name ( even when precedence would make them redundant
(R documentation: Paren).
# A newline ends an expression when the expression is complete.
total <- 1 + 2
total
#> [1] 3
# It is only whitespace when the expression is still open: a trailing
# operator, an open bracket or an unfinished string all continue the line.
longer <- 1 +
2 +
3
longer
#> [1] 6
joined <- c(1,
2,
3)
joined
#> [1] 1 2 3
# A leading operator on the next line does NOT continue the previous line.
broken <- 1
+ 2
#> [1] 2
broken
#> [1] 1
# The same text, seen by the parser: two separate expressions.
exprs <- parse(text = c("broken <- 1", "+ 2"))
length(exprs)
#> [1] 2
exprs[[2L]]
#> +2
# Semicolons separate expressions on one line (rarely needed).
a <- 1; b <- 2 # nolint: semicolon_linter.
a + b
#> [1] 3
length(parse(text = "a <- 1; b <- 2;"))
#> [1] 2
# A block is a call to "{": it evaluates its expressions in order and
# returns the value of the last one.
value <- {
first <- 10
second <- 20
first + second
}
value
#> [1] 30
is.null(eval(parse(text = "{}")[[1L]])) # an empty block is NULL
#> [1] TRUE
block <- quote({
x
y
})
class(block)
#> [1] "{"
as.list(block)
#> [[1]]
#> `{`
#>
#> [[2]]
#> x
#>
#> [[3]]
#> y
#>
# Parentheses group, and are recorded as a call to "(".
class(quote((1 + 2)))
#> [1] "("
quote((1 + 2))[[1L]]
#> `(`
(1 + 2) * 3
#> [1] 9
x <- (y <- 5) # an assignment is an expression with a value
c(x, y)
#> [1] 5 5
The assignment x <- (y <- 5) at the end shows that an assignment is an expression with a value, which is what allows
chains such as a <- b <- 0. Parenthesising a whole assignment also makes R print its value, because the
parenthesised form is visible: (x <- 5) assigns and prints.
Syntax errors: the messages and what they mean
When the text violates the grammar, the parser stops and reports an error whose first line has the form
<source>:<line>:<column>: unexpected <token>, followed by the offending line (and the line before it) and a caret under
the token where the parser gave up. Where the source is a character vector the source is <text>; for a file it is the
file name. The token named is the first one that cannot continue the expression, which is often one token after the
real mistake: in a b the parser accepted a as a complete expression and objected to the second symbol. Two things
follow. The column points at the place where the parser could not go on, not at the cause; and an error at the very
end of the input (unexpected end of input) means that something opened earlier was never closed.
The next script captures real messages with tryCatch(parse(text = ...)), so that none of them stops the script. This
works because a syntax error from parse() is an ordinary R error condition (a plain simpleError). The R manual
describes the interactive behaviour: if the parser finds a state incompatible with the grammar, a "Syntax Error" is
flagged and it resumes at the beginning of the next input line.
# parse() reports a syntax error as an ordinary error condition, so a text
# can be checked without running it. The message has the form
# <source>:<line>:<column>: unexpected <token>, then the offending line.
check <- function(text) {
tryCatch(
{
parse(text = text)
writeLines("parsed")
},
error = \(e) writeLines(conditionMessage(e))
)
}
check("books <- read.csv(\"books.csv\") loans") # unexpected symbol
#> <text>:1:32: unexpected symbol
#> 1: books <- read.csv("books.csv") loans
#> ^
check("x <- c(1, 2))") # unexpected ')'
#> <text>:1:13: unexpected ')'
#> 1: x <- c(1, 2))
#> ^
check("total <- 5 5") # unexpected numeric constant
#> <text>:1:12: unexpected numeric constant
#> 1: total <- 5 5
#> ^
check("title <- \"Dune\" \"Emma\"") # unexpected string constant
#> <text>:1:17: unexpected string constant
#> 1: title <- "Dune" "Emma"
#> ^
check("x <- 1 \\ 2") # unexpected '\\'
#> <text>:1:8: unexpected '\\'
#> 1: x <- 1 \
#> ^
check("price <- “10”") # unexpected input
#> <text>:1:10: unexpected input
#> 1: price <- “
#> ^
check("rate <- 5 %% % 2") # unexpected input
#> <text>:1:14: unexpected input
#> 1: rate <- 5 %% % 2
#> ^
check("if (TRUE) {\n 1\n}}") # unexpected '}'
#> <text>:3:2: unexpected '}'
#> 2: 1
#> 3: }}
#> ^
check("f <- function(x) {\n x + 1\n") # unexpected end of input
#> <text>:4:0: unexpected end of input
#> 2: x + 1
#> 3:
#> ^
check("x <- 'unterminated") # INCOMPLETE_STRING
#> <text>:1:6: unexpected INCOMPLETE_STRING
#> 1: x <- 'unterminated
#> ^
check("if <- 1") # unexpected assignment
#> <text>:1:4: unexpected assignment
#> 1: if <-
#> ^
check("a = = 2") # unexpected '='
#> <text>:1:5: unexpected '='
#> 1: a = =
#> ^
# The top-level "else" rule: a newline before else ends the if at the top
# level of a script, but not inside braces (see Control Flow).
check("if (TRUE) 1\nelse 2")
#> <text>:2:1: unexpected 'else'
#> 1: if (TRUE) 1
#> 2: else
#> ^
check("{\n if (TRUE) 1\n else 2\n}")
#> parsed
# Since R 4.3.0 some syntax errors, notably those of the native pipe, are
# classed conditions with line and column fields; "unexpected ..." errors are
# still plain simpleError conditions.
class(tryCatch(parse(text = "a b"), error = identity))
#> [1] "simpleError" "error" "condition"
pipe_err <- tryCatch(parse(text = "a |> b"), error = identity)
class(pipe_err)
#> [1] "RHSnotFnCall" "parseError" "error" "condition"
conditionMessage(pipe_err)
#> [1] "The pipe operator requires a function call as RHS (<input>:1:6)"
c(line = pipe_err$lineno, column = pipe_err$colno)
#> line column
#> 1 6
# Text that parses but cannot be evaluated is not a syntax error.
check("2 + 2 <- 5")
#> parsed
tryCatch(
eval(parse(text = "2 + 2 <- 5")),
error = \(e) writeLines(conditionMessage(e))
)
#> target of assignment expands to non-language object
# An incomplete expression is how the console knows to show the "+" prompt:
# parse() reports it as "unexpected end of input".
check("sum(1,")
#> <text>:2:0: unexpected end of input
#> 1: sum(1,
#> ^
Source: R documentation: parse
| Message | Typical cause | Fix |
|---|---|---|
|
two names or a name after a complete expression: a missing operator, comma, or newline |
insert the operator or comma |
|
two numbers in a row, such as |
insert the comma |
|
two strings in a row, or a string after a name |
insert |
|
a closing bracket with no opener, or one too many |
match the brackets |
|
an opened bracket, brace, call, or operator that never closed |
close it |
|
a character the lexer cannot use: a curly quote, a stray |
retype
the character; use |
|
a quote that is never closed |
close the string |
|
an assignment where an expression is expected, such as |
do not use a reserved word as a name |
|
a second |
remove it |
|
|
put the |
|
a backslash outside a string, except in the lambda |
remove it |
Source: R documentation: parse
Three notes on the output. First, the text of the messages changed over time. R 4.4.0 updated the parser to bison 3.8.2
for the pre-generated parsers, and the NEWS entry warns that a few parser error messages have changed, which may affect
code that relies on exact messages (R NEWS, Changes in R 4.4.0): match error classes or locations, not strings. Second,
R 4.3.0 made the parser signal classed errors, notably for the native pipe |>; the error object then carries a class
(RHSnotFnCall, placeholderNotNamed, unsupportedInPipe, all inheriting from parseError) and the line and column as
fields, and the message gives them as (<input>:1:6). Ordinary "unexpected …" errors remain plain simpleError
conditions (R NEWS, Changes in R 4.3.0). Third, a syntax error cannot be caught inside the file that contains it, because
the whole file fails to parse: that is why the examples above pass the bad code as text.
Source: R NEWS: Changes in R 4.3.0
The rows for 2 + 2 <- 5 close the section from the other side. That text parses without complaint and fails when it is
evaluated, with the message "target of assignment expands to non-language object". A parse error is therefore about the
shape of the code and an evaluation error about its meaning; which of the two occurs is the first thing to determine
when a script fails.
The parse tree, parse() and deparse()
The manual states the central fact of the parser: parsed expressions are stored in an object that holds the parse tree,
and every elementary R expression is stored in function-call form, as a list whose first element names the function and
whose remaining elements are the arguments, which may in turn be expressions. All syntax elements are treated this way:
the assignment x <- 1 is encoded as "<-"(x, 1)
(R-lang: Internal representation).
parse() returns the parsed but unevaluated expressions as an object of type expression, a list of calls. Its arguments
are file, n, text, prompt, keep.source, srcfile and encoding; text takes precedence over file; the
default for keep.source is getOption("keep.source"). str2lang(s) and str2expression(s) are the short forms for a
single string: str2lang(s) is parse(text = s, keep.source = FALSE)[[1]] plus a check that there is exactly one
expression, and it returns "a call or simpler" (a call, a symbol, NULL or an atomic constant)
(R documentation: parse).
# parse() turns text into an expression vector: one element per expression.
exprs <- parse(text = "total <- price * 2L\nprice")
class(exprs)
#> [1] "expression"
length(exprs)
#> [1] 2
exprs[[1L]]
#> total <- price * 2L
class(exprs[[1L]]) # a call
#> [1] "<-"
class(exprs[[2L]]) # a symbol
#> [1] "name"
# quote() builds the same objects from code that is already parsed.
call_obj <- quote(total <- price * 2L)
identical(call_obj, exprs[[1L]])
#> [1] TRUE
as.list(call_obj)
#> [[1]]
#> `<-`
#>
#> [[2]]
#> total
#>
#> [[3]]
#> price * 2L
#>
as.list(call_obj[[3L]])
#> [[1]]
#> `*`
#>
#> [[2]]
#> price
#>
#> [[3]]
#> [1] 2
#>
# Every part of a call is addressable: function first, then arguments.
call_obj[[1L]]
#> `<-`
call_obj[[3L]][[1L]]
#> `*`
call_obj[[3L]][[3L]]
#> [1] 2
typeof(call_obj[[3L]][[3L]])
#> [1] "integer"
# deparse() goes the other way, from an object back to text.
deparse(call_obj)
#> [1] "total <- price * 2L"
deparse(c(1, 2, 3))
#> [1] "c(1, 2, 3)"
deparse(1:5)
#> [1] "1:5"
deparse(quote(if (x > 1) "big" else "small"))
#> [1] "if (x > 1) \"big\" else \"small\""
cat(deparse(function(x, y = 2) x + y), sep = "\n")
#> function (x, y = 2)
#> x + y
# Parsing the deparsed text of an expression gives back the same expression.
identical(parse(text = deparse(call_obj))[[1L]], call_obj)
#> [1] TRUE
identical(eval(parse(text = deparse(1:5))), 1:5)
#> [1] TRUE
deparse(1 / 3)
#> [1] "0.333333333333333"
identical(eval(parse(text = deparse(1 / 3))), 1 / 3)
#> [1] FALSE
identical(eval(parse(text = deparse(1 / 3, control = "digits17"))), 1 / 3)
#> [1] TRUE
# str2lang() and str2expression() parse a single expression from a string.
str2lang("loan_days * 2")
#> loan_days * 2
class(str2lang("loan_days"))
#> [1] "name"
str2expression("loan_days * 2")
#> expression(loan_days * 2)
# With keep.source = TRUE the parser keeps the original text, comments included.
getOption("keep.source") # FALSE under Rscript, TRUE in an interactive session
#> [1] FALSE
src_text <- "function(x) {\n x + 1 # add one\n}"
kept <- eval(parse(text = src_text, keep.source = TRUE))
kept
#> function(x) {
#> x + 1 # add one
#> }
dropped <- eval(parse(text = src_text, keep.source = FALSE))
dropped
#> function (x)
#> {
#> x + 1
#> }
Source: R documentation: parse
The conversion in the other direction is deparse(), which turns an R object back into text. The manual warns that only
objects of mode expression can be expected to be unchanged by reparsing the output of deparsing, and that evaluating
the deparsed and reparsed expression gives the same result as the original only "as far as possible"
(R-lang: Deparsing and
R documentation: deparse). The script shows both
sides. For calls the round trip is exact. For numbers the default deparse() uses 15 significant digits, so
1 / 3 becomes "0.333333333333333" and does not come back equal; control = "digits17" makes it exact. The manual’s
own illustration, that 1:5 deparses as c(1, 2, 3, 4, 5), is out of date: R 4.6.1 gives "1:5", which does
round-trip. Deparsing is also what R uses to print a function when the source was not kept: comments and the original
layout disappear, as the last two prints of the script show. In an interactive session keep.source is TRUE and the
function prints exactly as typed.
The deparser has its own bug-fix history. R 4.2.0 made it wrap sub-expressions such as if (A) ... in parentheses when
the precedence requires it (R NEWS, Changes in R 4.2.0), so output of deparse() from earlier versions can differ. For
the purposes of serialising data, dput() is built on the same machinery.
Source: R NEWS: Changes in R 4.2.0
Programs that manipulate code (macros, domain-specific languages, linters) use these objects as data. That topic,
metaprogramming with quote(), substitute(), bquote() and eval(), is beyond this page, but the knowledge needed to
start is already here: a call is a list with the function first, call[[1]] is the function and as.list(call) shows
all the parts.
Every call is a function call
The R Language Definition says that a function call has the form function_reference followed by a comma-separated
argument list in parentheses, and that the function reference can be an identifier, a text string (handy when the name is
not a valid identifier) or any expression that evaluates to a function object
(R-lang: Function calls).
Its examples include f(x), g(tag = value, , 5), the string form "odd name"("strange tag" = 5, y) and the call of a
function expression (function(x) x^2)(5). The second example shows that an argument can be empty.
What makes R unusual is that this single form is also what every other piece of syntax turns into. The manual says it three times, in three sections:
-
operators: "the parsed form of a unary or binary operation is completely equivalent to a function call with the operator as the function name and the operands as the function arguments" (R-lang: Infix and prefix operators);
-
indexing:
object[args],object[[args]]andobject$tagare stored as calls to[,[[and$(R-lang: Index constructions); -
compound expressions and flow control: a block is a call to
{, andif,while,repeat,for,breakandnextare calls to functions of those names (R-lang: Flow control elements), and a function definition is a call tofunctionwith the argument list and the body (R-lang: Function definitions).
The slogan, popularised by John Chambers, is that everything that happens in R is a function call. The consequences are practical, not just philosophical, as the next script shows. The first block calls operators and control flow by their function names; the second prints the parse tree of an expression, which is what "operator precedence" means in R: the shape of the tree.
books <- read.csv("books.csv")
# Operators, indexing and control flow are function calls with backquoted names.
`+`(1, 2)
#> [1] 3
`-`(10)
#> [1] -10
`[`(books$year, 2L)
#> [1] 1813
`[[`(books, "title")[1L:2L]
#> [1] "The Dispossessed" "Pride and Prejudice"
`$`(books, "pages")[1L:3L]
#> [1] 387 432 214
`if`(nrow(books) > 10L, "long shelf", "short shelf")
#> [1] "long shelf"
`(`(1 + 2) * 3
#> [1] 9
`{`(1, 2, 3)
#> [1] 3
x <- `<-`(y, 5) # nolint: object_name_linter.
c(x, y)
#> [1] 5 5
# Calling a function by its name as a string works the same way.
"sum"(1, 2, 3)
#> [1] 6
(function(n) n^2)(5)
#> [1] 25
`%in%`(2, 1:3)
#> [1] TRUE
# The parsed form of an expression is a call: the function is the first element.
call_plus <- quote(1 + 2)
class(call_plus)
#> [1] "call"
call_plus[[1L]]
#> `+`
as.list(call_plus)
#> [[1]]
#> `+`
#>
#> [[2]]
#> [1] 1
#>
#> [[3]]
#> [1] 2
#>
eval(call_plus)
#> [1] 3
identical(call_plus, as.call(list(as.name("+"), 1, 2)))
#> [1] TRUE
# Precedence is just the shape of the tree.
show_tree <- function(x, indent = "") {
if (is.call(x)) {
cat(indent, "call: ", deparse(x[[1L]]), "\n", sep = "")
for (arg in as.list(x)[-1L]) {
show_tree(arg, paste0(indent, " "))
}
} else {
cat(indent, deparse(x), "\n", sep = "")
}
}
show_tree(quote(1 + 2 * 3))
#> call: +
#> 1
#> call: *
#> 2
#> 3
show_tree(quote((1 + 2) * 3))
#> call: *
#> call: (
#> call: +
#> 1
#> 2
#> 3
show_tree(quote(-2^2))
#> call: -
#> call: ^
#> 2
#> 2
# Control flow and function definitions are calls too.
as.list(quote(if (a) b else c))
#> [[1]]
#> `if`
#>
#> [[2]]
#> a
#>
#> [[3]]
#> b
#>
#> [[4]]
#> c
#>
as.list(quote(for (i in 1:3) print(i)))[[1L]]
#> `for`
as.list(quote(while (TRUE) break))[[1L]]
#> `while`
quote(function(x, y = 2) x + y)[[1L]]
#> `function`
quote(\(x) x + 1)
#> function(x) x + 1
as.list(quote(x[1]))
#> [[1]]
#> `[`
#>
#> [[2]]
#> x
#>
#> [[3]]
#> [1] 1
#>
as.list(quote(x$name))
#> [[1]]
#> `$`
#>
#> [[2]]
#> x
#>
#> [[3]]
#> name
#>
as.list(quote(x[[2]]))
#> [[1]]
#> `[[`
#>
#> [[2]]
#> x
#>
#> [[3]]
#> [1] 2
#>
# A for loop is a call that returns NULL (invisibly); if returns its branch.
result <- for (i in 1L:3L) i
is.null(result)
#> [1] TRUE
value <- if (nrow(books) > 100L) "big" else "small"
value
#> [1] "small"
Reading the output from the top:
-
`+`(1, 2),`[`(books$year, 2L)and`if`(cond, a, b)do exactly what the operator forms do. The backticks are needed because these names are not syntactic; a string in the function position ("sum"(1, 2, 3)) is the alternative the grammar allows. -
quote(1 + 2)is a call object.call_plus[[1]]is the symbol+, andas.list()shows the function and the two arguments. Building the same call withas.call(list(as.name("+"), 1, 2))gives an identical object. -
show_tree()prints1 + 2 * 3with*nested inside+, and(1 + 2) * 3with an explicit(call in between, as the manual says. In-2^2the tree has-outside and^inside, which is why the result is -4. -
quote(if (a) b else c)has the functionifand three arguments.quote(for (i in 1:3) print(i))[[1]]isfor,whileiswhile, and a function definition starts withfunction. The lambda\(x) x + 1, introduced in R 4.1.0, is parsed into the very samefunctioncall; it deparses asfunction(x) x + 1. -
Control flow has values:
ifreturns the value of the branch it takes (value <- if (...) "big" else "small"), andforreturnsNULLinvisibly, soresult <- for (...)is legal and useless.
Pipes, replacement functions and operators as arguments
Two syntactic forms are not calls to a function with the name that appears in the source. The native pipe |> (R 4.1.0)
is a parse-time rewrite: the expression x |> f(y) becomes f(x, y) before it is evaluated, so no function named |>
exists (exists("|>") is FALSE) and quote(books |> head(2L) |> nrow()) prints as nrow(head(books, 2L)). This is a
difference from the magrittr pipe %>%, which is an ordinary function. The placeholder _ of R 4.2.0 is part of the
same rewrite; Pipes and the Native Pipe covers it in depth.
Assignment with a function on the left, as in names(x)[2] <- "last", is expanded into nested replacement-function calls:
the left side is evaluated into a temporary, the function named names<- receives the modified value as its last
argument (which must be called value), and the result is assigned back. The second script writes the expansion by
hand and checks that it gives an identical result
(R-lang: Function calls).
books <- read.csv("books.csv")
# The native pipe is rewritten by the parser: no function "|>" ever exists.
quote(books |> head(2L) |> nrow())
#> nrow(head(books, 2L))
exists("|>")
#> [1] FALSE
# Replacement syntax expands into replacement-function calls.
shelf_names <- c(first = "A", second = "B")
names(shelf_names)[2L] <- "last"
shelf_names
#> first last
#> "A" "B"
other_names <- c(first = "A", second = "B")
other_names <- `names<-`(other_names, `[<-`(names(other_names), 2L, "last"))
identical(shelf_names, other_names)
#> [1] TRUE
# Operators can be passed as arguments, which replaces many small functions.
Reduce(`+`, 1:5)
#> [1] 15
vapply(strsplit(books$author[1L:4L], " "), `[`, character(1L), 1L)
#> [1] "Ursula" "Jane" "Mara" "Mara"
do.call("+", list(3, 4))
#> [1] 7
Map(`*`, 1:3, 4:6)[[3L]]
#> [1] 18
# Since they are functions, operators can be redefined locally; do so rarely.
local({
`+` <- function(e1, e2) paste(e1, e2) # nolint: object_name_linter.
"shelf" + "list"
})
#> [1] "shelf list"
1 + 2
#> [1] 3
# User-defined infix operators are functions with %name% names.
`%+%` <- function(lhs, rhs) paste0(lhs, rhs) # nolint: object_name_linter.
"978" %+% "0306406157"
#> [1] "9780306406157"
# Most operators are primitives; "if" and "for" are special primitives.
vapply(list(`+`, `[`, `if`, `for`, sum, mean), typeof, character(1L))
#> [1] "builtin" "special" "special" "special" "builtin" "closure"
`if`
#> .Primitive("if")
Source: R documentation: funprog
Because operators are ordinary functions, they can be passed as arguments: Reduce(`+`, 1:5) is a sum,
vapply(parts, `[`, character(1L), 1L) takes the first element of each part, do.call("+", list(3, 4)) calls by
name. These replace the anonymous function that other languages need. They can also be redefined (inside local() the
script makes + paste its operands), which is why packages such as Matrix and ggplot2 can give operators new
meaning for their classes through S3 and S4 dispatch, a topic of
S3 Group Generics and Operator Overloading.
The special operators %name% are the user-extensible part: any function whose name starts and ends with % is an infix
operator, so the %+% defined at the end of the script is an ordinary function that happens to be callable infix. The
last lines of that script show the types of these functions: + is a builtin primitive, if, for and [ are
special primitives that receive their arguments unevaluated, and sum is a builtin, while mean is a closure,
a function written in R. Printing `if` shows .Primitive("if"), which is as far as R lets you look.
Reading code as data: a shelf example
The running scenario ends with a small use of these pieces. A check that the column names of the shelf tables are all
syntactic (and so can be used with $ without backticks), and a function that takes a filter expression typed by a
user as text, parses it first so that a syntax error becomes a message and not a crash, and only then evaluates it
against a table.
books <- read.csv("books.csv")
source("scenario.R")
# The columns of both tables are already syntactic names.
all(names(books) == make.names(names(books)))
#> [1] TRUE
all(names(loans) == make.names(names(loans)))
#> [1] TRUE
names(loans)
#> [1] "loan_id" "isbn" "member" "loan_date" "return_date"
# A filter typed by a user is only text: parse it first, run it afterwards.
filter_titles <- function(expr_text, data) {
expr <- tryCatch(str2lang(expr_text), error = \(e) NULL)
if (is.null(expr)) {
return(paste("not a valid R expression:", expr_text))
}
keep <- eval(expr, data, baseenv())
data$title[which(keep)]
}
filter_titles("pages > 400", books)
#> [1] "Pride and Prejudice" "Notes on Gravity"
filter_titles("genre == 'Poetry'", books)
#> [1] "Garden of Small Fires" "Paper Moons"
filter_titles("pages >> 400", books)
#> [1] "not a valid R expression: pages >> 400"
filter_titles("genre ==", books)
#> [1] "not a valid R expression: genre =="
# loan_days is an integer constant (the literal 21L) from the scenario file.
loan_days
#> [1] 21
class(loan_days)
#> [1] "integer"
Source: R documentation: parse
The function evaluates the parsed expression with eval(expr, data, baseenv()): names are looked up in the data frame
first and then in the base environment only, so the user text cannot see the caller’s variables. That is a limited
safeguard, not security. Parsing text and evaluating it runs arbitrary code, and str2lang() followed by eval() of text
from an untrusted source is a code-injection vector. Older tutorials use eval(parse(text = paste0("x", i))) to
build variable names; lists, [[, get() and assign() do the same job without evaluating a string.
Version notes
The syntax of R is stable, and the changes of the last twenty years are few. This table collects the ones that this page mentions, so that code which has to run on an older R can be checked against it.
| Version | Change |
|---|---|
R 1.9.0 |
Underscores became valid in names ( |
R 4.0.0 |
Raw character constants |
R 4.1.0 |
The native pipe |
R 4.2.0 |
The |
R 4.3.0 |
Classed parse errors ( |
R 4.4.0 |
The parser is regenerated with bison 3.8.2 and a few messages change; the base operator |
R 4.4.3 |
Hexadecimal constants with a decimal point and no binary exponent are accepted. |
Where older tutorials differ
Many pages on the web predate R 4.0.0, and some predate R 1.9.0. When one of them disagrees with this page, the table says why.
| Older tutorial | Current practice | Why | Version |
|---|---|---|---|
|
Write |
|
always |
Doubled backslashes in regular expressions and Windows paths |
Raw strings |
no escaping to get wrong; the string has the same value |
R 4.0.0 |
|
|
the lambda is parsed into the same |
R 4.1.0 |
|
the native pipe |
it is rewritten by the parser, so it needs no package and adds no function of its own |
R 4.1.0, |
"A hexadecimal constant needs a |
|
the parser was changed to match |
R 4.4.3 |
|
it is |
the deparser keeps compact sequences |
current |
Names with underscores cannot be used, |
underscores are valid in names;
|
underscores became valid in R 1.9.0 |
R 1.9.0 |
Dotted names such as |
|
a dot in a function name may be mistaken for an S3
method; the |
style, always valid |
|
|
the newline before |
always |
Pasting code with typographic quotes |
Use straight quotes |
a curly quote is "unexpected input" |
always |
|
lists, |
running a string runs arbitrary code |
always |
|
backticks and |
|
always |
|
|
covered in other pages |
always |
For the full list of changes by release see What’s New and Migrating Older Code.
Where to go next
-
Assignment, Names and Values for assignment forms (
<-,=,<<-,->) and copy-on-modify. -
Operators and Precedence for the precedence table that the parse trees above illustrate.
-
Control Flow for
if/else(including the top-levelelserule), loops andswitch(). -
Functions for function definitions, argument matching and replacement functions.
-
Strings and Regular Expressions and Encodings, Locales and Internationalization for what happens to string values after the lexer has read them.
-
Pipes and the Native Pipe for
|>and the_placeholder.
References
-
The R Language Definition: Parser (The parsing process, Modes of parsing, Internal representation, Deparsing, Comments, Tokens, Constants, Identifiers, Reserved words, Special operators, Separators, Operator tokens, Grouping, Indexing tokens, Expressions, Function calls, Infix and prefix operators, Index constructions, Compound expressions, Flow control elements, Function definitions, Directives)
-
An Introduction to R: R commands, case sensitivity, etc.
-
R FAQ 7.14: What are valid names?
-
R FAQ 3.3.3: Others (differences between R and S)
-
R NEWS: Changes in R 4.0.0, 4.1.0, 4.2.0, 4.3.0, 4.4.0 and 4.4.3