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 expression (a vector of expressions) or call.

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

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

1, 1L, 0x1F, 2i, "text", TRUE, NULL, NA_integer_, Inf

Constants

Identifiers

books, .hidden, loan_days, my.var

Identifiers

Reserved words

if, else, repeat, while, function, for, in, next, break

Reserved words

Special operators

%in%, %%, %/%, %*%, %o%, %x%, %||%, and any %name% you define

Special operators

Separators

whitespace, newlines, ;, ,

Separators

Operator tokens

+ - * / ^, ==, !=, <, <=, &, |, ~, $, :, <-, ->

Operator tokens

Grouping

( ) for grouping and argument lists, { } for blocks

Grouping

Indexing tokens

[ ], [[ ]], $

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, . and _, starting with a letter or a dot not followed by a number. Reserved words are not syntactic names.

Object name

A string associated with an object by assignment or by assign(). Usually syntactic, but any non-empty string works when quoted (and it is always quoted in assign()).

Argument name

What appears left of = in a call such as f(trim = .5). Usually syntactic, but anything when quoted.

Element name

Identifies a piece of an object, for example a list component. After $ it must be syntactic or quoted; otherwise any string is allowed.

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

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

if, else, repeat, while, function, for, in, next, break

Constants

TRUE, FALSE, NULL, Inf, NaN

Missing values

NA, NA_integer_, NA_real_, NA_complex_, NA_character_

Dots

..., ..1, ..2, and so on

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"

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

1, 10, 0.1, .2, 5., 1e-7, 1.2e+7

Decimal; the fractional or the integer part may be empty, not both

double

0x1F, 0xA0, 0X1f

Hexadecimal

double

0x1.8p1, 0x123p4

Hexadecimal with a binary exponent (a power of two), as in C99

double

1L, 0x10L, 1000000L, 1e6L

Integer; a hex or scientific form is allowed if the value is a whole number in range

integer

1.5L, 1e-3L, 3000000000L

Not a valid integer: a warning and a double

double

2i, 4.1i, 1e-2i

A purely imaginary number; other complex numbers are arithmetic on a number and an imaginary

complex

Inf, NaN

Infinity and not-a-number

double

NA, NA_integer_, NA_real_, NA_complex_, NA_character_

Missing value of each type

logical, integer, double, complex, character

# 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 == 2L is TRUE, and identical(2, 2L) is FALSE. The scenario constant loan_days is written 21L, 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 as identical() or vapply(..., integer(1L)) cares.

  • The warning text depends on the form: 1.5L gives "integer literal 1.5L contains decimal; using numeric value", 1.L gives "integer literal 1.L contains unnecessary decimal point", and an integer that is too large, such as 3000000000L, 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 gives NA with 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^2 is -4 (the exponent binds tighter than the unary minus) and quote(-1) is a call, not a number.

  • Values too large or too small overflow to Inf or underflow to 0. Inf and NaN are constants of type double; in text input (scan(), as.double()) they are also recognised ignoring case, with infinity as 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_000 is a syntax error), no binary (0b101) or octal prefix, and no d exponent as in Fortran.

  • The manual’s text says 0x1.1 is 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 as p0, as ?NumericConstants documents (R NEWS, Changes in R 4.4.3). On R 4.6.1 0x1.1 is 1.0625. Code that must run on older versions should write the exponent.

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

\n, \r, \t

newline (line feed), carriage return, tab

\b, \a, \f, \v

backspace, alert (bell), form feed, vertical tab

\\

a backslash

\', \", \`

apostrophe, quotation mark and grave accent (backtick)

\nnn

octal code, one to three digits (maximum \377)

\xnn

hexadecimal code, one or two digits

\unnnn, \u{nnnn}

Unicode character, one to four hex digits

\Unnnnnnnn, \U{nnnnnnnn}

Unicode character, one to eight hex digits

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"

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"

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

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:

  • NULL is the empty object, of type "NULL" and length zero. It is what a function without a result returns. It vanishes inside c(), which is why c(1, NULL, 3) has length two. is.null(NA) is FALSE, is.na(NULL) is logical(0).

  • NA is the logical missing value; each atomic type has its own constant (NA_integer_, NA_real_, NA_complex_, NA_character_), and NA is coerced to the type of the other elements in c(). identical(NA_real_, NA_integer_) is FALSE.

  • NaN is a double constant: 0 / 0 gives it. is.na(NaN) is TRUE, but is.nan(NA) is FALSE. Inf and -Inf come from 1 / 0 and -1 / 0, and is.finite() is FALSE for all of Inf, NaN and NA.

  • Logical operators follow three-valued logic: NA & FALSE is FALSE and NA || TRUE is TRUE, because the answer does not depend on the unknown value; NA > 1 is 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

x <- 1 + 2

ends here

the expression is complete

x <- 1 +, shelf |>, 2 %in%

continues

a binary operator needs a right operand

x <- (1 + 2, f(a,, {

continues

an open bracket, brace or parenthesis is not closed

title <- 'Dune

continues

a string constant is not closed (it may contain newlines)

if (TRUE), for (i in 1:3), function(x)

continues

the construct still needs its body

x then + 5

two expressions

the first line is complete; the second starts with a unary plus

if (TRUE) 1 then else 2

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,
#>    ^
Message Typical cause Fix

unexpected symbol

two names or a name after a complete expression: a missing operator, comma, or newline

insert the operator or comma

unexpected numeric constant

two numbers in a row, such as 5 5 or f(1 2)

insert the comma

unexpected string constant

two strings in a row, or a string after a name

insert , or paste0()

unexpected ')', unexpected '}'

a closing bracket with no opener, or one too many

match the brackets

unexpected end of input

an opened bracket, brace, call, or operator that never closed

close it

unexpected input

a character the lexer cannot use: a curly quote, a stray %, a Unicode symbol, 0x, 1e

retype the character; use " not a typographic quote

unexpected INCOMPLETE_STRING

a quote that is never closed

close the string

unexpected assignment

an assignment where an expression is expected, such as if <- 1

do not use a reserved word as a name

unexpected '='

a second =, or = where an expression was expected (f(TRUE = 1))

remove it

unexpected 'else'

else after a complete top-level if

put the else on the closing-brace line

unexpected '\\'

a backslash outside a string, except in the lambda \(x)

remove it

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.

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
#> }

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.

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]] and object$tag are stored as calls to [, [[ and $ (R-lang: Index constructions);

  • compound expressions and flow control: a block is a call to {, and if, while, repeat, for, break and next are calls to functions of those names (R-lang: Flow control elements), and a function definition is a call to function with 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 +, and as.list() shows the function and the two arguments. Building the same call with as.call(list(as.name("+"), 1, 2)) gives an identical object.

  • show_tree() prints 1 + 2 * 3 with * nested inside +, and (1 + 2) * 3 with an explicit ( call in between, as the manual says. In -2^2 the tree has - outside and ^ inside, which is why the result is -4.

  • quote(if (a) b else c) has the function if and three arguments. quote(for (i in 1:3) print(i))[[1]] is for, while is while, and a function definition starts with function. The lambda \(x) x + 1, introduced in R 4.1.0, is parsed into the very same function call; it deparses as function(x) x + 1.

  • Control flow has values: if returns the value of the branch it takes (value <- if (...) "big" else "small"), and for returns NULL invisibly, so result <- 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")

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"

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 (make.names(allow_ = FALSE) keeps the old conversion).

R 4.0.0

Raw character constants r"(...)".

R 4.1.0

The native pipe |>, the lambda \(x), ...names(); a \U escape above \U10FFFF is an error.

R 4.2.0

The _ placeholder of the pipe; the deparser wraps sub-expressions in parentheses when needed.

R 4.3.0

Classed parse errors (parseError) with line and column, notably for the pipe.

R 4.4.0

The parser is regenerated with bison 3.8.2 and a few messages change; the base operator %||% is added.

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

T and F for logical values

Write TRUE and FALSE

T and F are variables that can be overwritten, so mean(x, na.rm = T) can silently change meaning (R FAQ 3.3.3)

always

Doubled backslashes in regular expressions and Windows paths

Raw strings r"(...)"

no escaping to get wrong; the string has the same value

R 4.0.0

function(x) x + 1 only

\(x) x + 1 is the same call

the lambda is parsed into the same function call

R 4.1.0

%>% from magrittr for every chain

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, _ placeholder R 4.2.0

"A hexadecimal constant needs a p exponent": 0x1.1 is a syntax error (R-lang text)

0x1.1 is accepted and means 0x1.1p0

the parser was changed to match ?NumericConstants

R 4.4.3

deparse(1:5) is "c(1, 2, 3, 4, 5)" (R-lang text)

it is "1:5"

the deparser keeps compact sequences

current

Names with underscores cannot be used, _ converted to . by make.names()

underscores are valid in names; allow_ = FALSE restores the old conversion

underscores became valid in R 1.9.0

R 1.9.0

Dotted names such as my.data everywhere

snake_case for new names

a dot in a function name may be mistaken for an S3 method; the lintr default prefers snake_case

style, always valid

else on its own line at the top level of a script

} else { on the closing-brace line

the newline before else ends the if at the top level (see Control Flow)

always

Pasting code with typographic quotes

Use straight quotes " and ' in code

a curly quote is "unexpected input"

always

eval(parse(text = ...)) to build names

lists, [[, get(), assign(), str2lang() for a single known expression

running a string runs arbitrary code

always

assign() and get() for names that look like expressions

backticks and [[; assign("x$a", 1) creates an object named x$a

assign() does not recognise subscripting

always

sapply() and 1:length(x) in teaching code

vapply(), seq_along()

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

References