A scanner needs a precise, machine-readable way to say what each token looks like. "An integer
is one or more digits" is fine for a human, but the scanner-generator needs a notation it can compile
into an automaton. That notation is the regular expression — the same
Over an alphabet
| Expression | Name | Language it denotes |
|---|---|---|
| union / alternation | a string matching | |
| concatenation | an | |
| Kleene star | zero or more | |
| positive closure | one or more | |
| optional | zero or one |
Precedence, highest to lowest, is star/plus/? > concatenation > alternation, and
parentheses override it. So
Real token patterns get unwieldy fast, so we build them up as a sequence of regular
definitions: named regular expressions, where each definition may use the names defined
before it. This is nothing more than "let-bindings for regexes", and it is exactly how a
lex specification reads. Here is the canonical set for identifiers and numbers:
Read 60, 3.14, 6.02E23 and 1E-9 —
every optional group and the two
A few token classes come up in nearly every language; committing their regular definitions to memory is worth the effort:
| Token | Regular definition | Matches |
|---|---|---|
| integer | 0, 60, 2048 | |
| identifier | x, rate2, _tmp | |
| float | 3.14, 0.5 | |
| line comment | // to end of line | |
| block comment | see the warning below |
The C-style line comment /* … */ is the famously awkward one: written naïvely as
*/ in the file, merging two separate comments into one. The correct
regular definition forbids the closing delimiter inside the body — roughly "any character that is not a
*, or a * not followed by /, repeated". It is expressible
as a regular language; it is just fiddly, which is why lex lets you write it as a small
start-condition sub-machine instead.
A scanner generator like lex/flex takes a file of rules, each
a pattern paired with an action — a fragment of code to run when that pattern wins the
match. The generator compiles all the patterns together into one finite automaton (the subject of the
next few pages) and wraps it in the longest-match, first-rule-wins driver. Conceptually a rule list
looks like this:
Every convention from the previous page is visible here: the keyword rule sits above the
identifier rule (priority), <= is a separate rule that longest-match prefers over
<, whitespace and comments have empty-emitting actions, and the final . is a
deliberate lexical-error catch-all. yytext is the matched lexeme; the action decides what
token — if any — to hand the parser.
You do not need a whole generator to feel how this works. Below, two token patterns — identifier and
number — are written as hand-rolled matchers that return the length of the longest prefix they match at
a position (or
Look at 9lives: matchIdentifier returns matchNumber returns 9) — so a driver would emit a num and then start a fresh match at
lives. And 007bond matches num for length 3, then
id for the rest: the patterns compose to tokenise the whole string.
Tokens are, by design, flat: a number, a name, an operator — none of them nest. Regular
languages are precisely the languages a finite-memory machine can recognise, and a finite automaton is
astonishingly cheap: constant memory, one pass, linear time. That is a perfect fit for token structure,
and it is why we deliberately stop at regular power for the scanner. The moment a construct
nests — balanced parentheses, { } blocks, matching if…else — you have
stepped beyond regular languages: no finite automaton can count the open brackets to match them against
the closed ones (that needs a stack, i.e. a context-free grammar and a parser). Splitting the front end
into "regular scanner + context-free parser" is not an accident of history; it is drawing the line
exactly where the cheap model runs out of power.
The /…/ regexes in Perl, Python, JavaScript and friends look like the formal object but
have quietly grown teeth that escape the regular languages entirely:
(a+)\1 ("some as, then the same run
again") match a non-regular language — no finite automaton can do it. The instant you use one, you are
no longer in regular-language land and the linear-time guarantee can collapse.
*? is
lazy and grabs as little as possible. For a comment body, /\*.*\*/ (greedy) eats
to the last */; /\*.*?\*/ (lazy) stops at the first. In the formal
model there is no such knob — a regular expression denotes a set of strings with no notion of
"how much" a repetition consumes; greediness is a property of the matcher's search strategy,
resolved for us by the scanner's longest-match rule.
For scanner specifications, stick to the honest regular subset (union, concat, star, plus, optional, classes). That is all a token needs, and it is all that compiles to a fast, backtracking-free automaton.