expl3 / LaTeX3 layer

Writing \usepackage{expl3} on a current LaTeX loads nothing at all. expl3 — the programming layer that grew out of the effort once called “LaTeX3” — is already part of the format. Line 1125 of the kernel source latex.ltx logs exactly that: Skipping: expl3 code already part of the format, and expl3.sty checks at line 53 whether the code is already present and, if it is, throws the whole load away. This page measures what \ExplSyntaxOn really does to the reading of characters, then works through how to read a name like \module_function:nn, what the argument specifiers mean, and the data types — tl, seq, prop, int, fp and the rest.

expl3 is already in the kernel — \fpeval is the proof

TeX began as a macro processor, a tool for defining commands with \def and \newcommand. But writing a large package exposed how inconsistent the raw primitives are: controlling expansion and handling variables became a craft. expl3 gives the TeX and e-TeX primitives new names, names functions and variables systematically, and writes each argument’s type into the name itself. Built up over many years by the LaTeX Project, it amounts to both a standard library and a programming language for LaTeX — xparse, siunitx, fontspec and l3keys2e all stand on it.

And that layer is no longer sold separately. Open expl3.sty and lines 53 to 59 test whether \tex_let:D is already defined; if it is, \@gobble eats the \input expl3-code.tex whole. What survives as a package is a name-only shell — it even announces itself as L3 programming layer (loader) — while the substance sits in the format. Sure enough, in a bare article with nothing loaded, \ExplSyntaxOn, \tl_new:N and \ProvidesExplPackage are all already defined. The clearest proof is four lines starting at latex.ltx line 1167: \fpeval, \inteval, \dimeval and \skipeval are defined with \cs_new_eq:NN as aliases of \fp_eval:n, \int_eval:n, \dim_eval:n and \skip_eval:n. The document-level \fpeval{sqrt(2)} is therefore the expl3 function itself, called by another name (see the counters and lengths page).

What \ExplSyntaxOn actually changes: space and ~ swap roles

expl3 code lives between \ExplSyntaxOn and \ExplSyntaxOff, and inside that region exactly four characters change category code. Ask the kernel to print \the\catcode before and after and you get this: space goes from 10 (space) to 9 (ignored character), _ from 8 (subscript) to 11 (letter), : from 12 (other) to 11 (letter), and ~ from 13 (active) to 10 (space). \ExplSyntaxOff restores 10, 8, 12 and 13 exactly. Category codes themselves are the subject of the catcode page, but what is delightful here is that space and ~ have swapped seats. Space gives up catcode 10 and moves to 9, the “ignored” slot, and ~ sits down in the vacated 10. Which is why, inside expl3, ~ is not a command that produces a space — it literally is a space character.

CharacterOrdinary LaTeXInside ExplSyntaxOn
(space)10 — space9 — ignored character. Indentation and line breaks never reach the output
~13 — active (a non-breaking space)10 — a space itself. This is what you write when the output needs a gap
_8 — subscript11 — letter. It may appear inside a command name
:12 — other11 — letter. It can separate the argument signature

The practical consequence of that swap is a single rule: a bare space inside expl3 never reaches the output. To typeset “Fruit: apple” you must write Fruit:~#1; replace the ~ with a plain space and you get the run-together “Fruit:apple”. Turned around, it means you can indent your code as much as you like and put gaps between tokens for readability without changing the output at all. Given how much attention ordinary LaTeX demands about spaces, that is quite a liberation.

document.tex
% no \usepackage{expl3} is needed: it is in the format
\ExplSyntaxOn
  % spaces and newlines here are catcode 9 (ignored); _ and : are letters
  \tl_new:N  \l_greeting_tl
  \tl_set:Nn \l_greeting_tl { Hello,~world! }   % ~ is the real space
  \tl_use:N  \l_greeting_tl
\ExplSyntaxOff

How to read \seq_put_right:Nn — the name states the types

An expl3 function name has the form \⟨module⟩_⟨description⟩:⟨arg-signature⟩. Everything up to the first _ is the module (a data type or area of work); everything up to the : is the descriptive name; what follows the : is the argument signature. In \seq_put_right:Nn the module is seq (sequences), the description is put_right (append on the right), and the signature is Nn. Each letter of the signature says how that argument is processed before it is passed on. Read the name and you know how many arguments it takes and what happens to each — reading the types without consulting the manual is precisely what the scheme is for.

SpecifierMeaning
NNo manipulation; a single token (usually one control sequence).
nNo manipulation; a braced group of tokens.
cTurns the argument into a control sequence via \csname before use.
V / vPasses the value of a variable (V from a single token, v builds the name first).
oExpands the argument once before use.
x / eFull expansion (x is like \edef and is not expandable; e uses \expanded).
fExpands left to right, up to the first unexpandable token.
pA TeX parameter text (#1#2…); used when defining a function.
T / FCode to run when the test is true / false. Usually paired as TF, as in \tl_if_empty:nTF, with T-only and F-only variants also provided.

Variables follow the same style but begin with a single letter for scope. l_ is local (changed only within the current TeX group), g_ is global, c_ is a constant. A type identifier ends the name: _tl (token list), _int (integer), _seq (sequence), _prop (property list), _clist (comma list), _fp (floating point), _str (string), _bool (boolean), and so on. So \l_my_name_tl reads at a glance as “a local token-list variable” and \g_counter_int as “a global integer.” Each module also provides scratch variables — disposable temporaries — such as \l_tmpa_tl and \l_tmpb_int.

Seeing the difference between :Nn and :Nx

n stores what you wrote; x burns in the value as it stands right now. Rather than explain that abstractly, dump the contents to the log with \tl_show:N. Put the same { [ \l_src_tl ] } into two variables, one with \tl_set:Nn and one with \tl_set:Nx, and the log reads: > \l_a_tl=[\l_src_tl ]. and > \l_b_tl=[FIRST]. The n version holds the token \l_src_tl itself; the x version has already become FIRST, the value at assignment time. Now change \l_src_tl to SECOND and typeset both: the n version prints “[SECOND]”, the x version “[FIRST]”. In other words, n keeps a reference and x keeps a snapshot. Most of the situations people describe as “expansion control is hard” are really this choice made the wrong way round. \tl_show:N is worth remembering as the first move when you get stuck.

document.tex
\ExplSyntaxOn
  \tl_new:N \l_src_tl  \tl_set:Nn \l_src_tl { FIRST }
  \tl_new:N \l_a_tl    \tl_set:Nn \l_a_tl { [ \l_src_tl ] }   % reference
  \tl_new:N \l_b_tl    \tl_set:Nx \l_b_tl { [ \l_src_tl ] }   % snapshot

  \tl_show:N \l_a_tl   % log: > \l_a_tl=[\l_src_tl ].
  \tl_show:N \l_b_tl   % log: > \l_b_tl=[FIRST].

  \tl_set:Nn \l_src_tl { SECOND }
  Nn~stored:~\tl_use:N \l_a_tl \par   % prints [SECOND]
  Nx~stored:~\tl_use:N \l_b_tl \par   % prints [FIRST]
\ExplSyntaxOff

The data types: create, set, use

expl3 is split into modules by data type, and they all share the same rhythm: create, set, use. Declare with \⟨type⟩_new:N, put a value in with \⟨type⟩_set:Nn, get it out with \⟨type⟩_use:N or a sibling. The gestures do not change as the type changes, so learning one lets you guess the rest. Only function definition looks a little different: \cs_new:Npn defines a new function and raises an error if one of that name already exists (cs is “control sequence”). \cs_set:Npn also defines one, but only within the current TeX group, and does not complain about redefinition. Both use :Npn, where N is the function being defined, p its parameter text (#1#2…) and n the body, the replacement text.

ModuleWhat it holdsCommon functions, with measured results
tlToken list — the most basic variable, usable much like a string\tl_new:N / \tl_set:Nn (the old contents go) / \tl_use:N / \tl_show:N
strStrings — for comparing as characters rather than tokens\str_if_eq:nnTF { a~b } { a~b } returns same
intIntegers — integer arithmetic with the usual operators\int_eval:n { 2 + 3 * 4 } is 14 (\inteval is its alias)
fpFloating point — with scientific functions such as sin, sqrt, pi\fp_eval:n { sqrt(2) } is 1.414213562373095; \fp_eval:n { 2 * pi } is 6.283185307179586
seqSequences — lists with access at both ends (also usable as stacks)\seq_put_right:Nn appends, \seq_map_inline:Nn walks every item as #1, \seq_use:Nn joins with a separator
propProperty lists — dictionaries, mapping keys to values\prop_put:Nnn stores variable, key, value; \prop_item:Nn retrieves
clistA comma-separated list of valuesAfter \clist_set:Nn \l_c_clist { 1,2,3 }, \clist_use:Nn \l_c_clist { ~+~ } gives “1 + 2 + 3”
boolBooleans — what you use instead of \newif\bool_new:N / \bool_set_true:N / \bool_if:NTF to branch
document.tex
\documentclass{article}
\begin{document}
\ExplSyntaxOn
  \seq_new:N \l_example_fruits_seq

  % define a function that appends one item
  \cs_new:Npn \example_add:n #1
    { \seq_put_right:Nn \l_example_fruits_seq {#1} }

  \example_add:n { apple }
  \example_add:n { banana }
  \example_add:n { cherry }

  % each item arrives as #1; ~ is a real space
  \seq_map_inline:Nn \l_example_fruits_seq
    { Fruit:~#1 \par }
\ExplSyntaxOff
\end{document}

Compile that and “Fruit: apple”, “Fruit: banana” and “Fruit: cherry” come out on three separate lines. The #1 in \cs_new:Npn is the defined function’s argument, while the #1 in \seq_map_inline:Nn is each item being mapped over — both arrive as n-type, the braced contents. The _ and : in the command names cause no trouble precisely because this is an \ExplSyntaxOn region.

l3keys, and how expl3 is used when writing a package

When you want key = value options on your own commands or package, the standard tool is l3keys. Declare keys with \keys_define:nn { module } { ... } and set them with \keys_set:nn { module } { key = value }. A key declaration carries a trailing property such as .tl_set:N (store into a token-list variable), .bool_set:N (make it a boolean), .code:n (run arbitrary code) or .initial:n (a default value). This machinery was originally wired to package options through a package called l3keys2e, but its core has since been absorbed into the LaTeX2ε kernel and is available without any package as \DeclareKeys and \ProcessKeyOptions (the package-authoring page writes one out).

For ordinary document writing you will hardly ever need to write expl3 directly. But the moment you set out to write a package or a class, expl3 is now effectively the standard. A common pairing is to take the user-facing command with \NewDocumentCommand (xparse) and implement its body in expl3. Keep two things apart: xparse’s argument specification (document-level arguments such as m, O{...}, s) and expl3’s argument signature (programming-level processing such as N, n) are not the same thing. The former is covered in detail on the xparse page. And if the first line of your .sty is \ProvidesExplPackage, expl3 syntax switches on automatically from that point — you do not write \ExplSyntaxOn at all.

  • Wrap code in \ExplSyntaxOn\ExplSyntaxOff. Inside, a bare space becomes category code 9 (ignored); use ~ when the output needs a gap.
  • No \usepackage{expl3} is needed — it is in the kernel, and expl3.sty discards the load itself. In a .sty, \ProvidesExplPackage removes the need for \ExplSyntaxOn too.
  • Functions are \⟨module⟩_⟨description⟩:⟨signature⟩; variables are \⟨scope⟩_⟨name⟩_⟨type⟩, with scope l_ / g_ / c_.
  • Do not mix up n and x. Choose according to whether you want to keep a reference or burn in the current value. When unsure, print it with \tl_show:N and look.
  • Do not invent command names. The naming is strict; the official interface3 manual (texdoc interface3) is the primary source.