Viscosity
Review
Part III · A tree-walking interpreter · Day 7

27. Tokens and lexemes

A parser that worked on single characters would drown in detail. Before anything else, Drip cuts its source into words, and each word comes with a label and a note of where it was found.

About 15 minutes, including the exercises and quiz
By the end you will be able to
  • Explain what a token is, and tell its type, lexeme and literal apart.
  • Read Drip's TokenType union and say which type a piece of source gets.
  • Explain why tokens carry a source location, and turn a character offset into a line and column.

Characters are too small

Here is a line of Drip:

let speed = 10.5;

The parser’s job, coming up in chapter 30, is to recognise the shape of this line: “a let, then a name, then =, then a value, then ;”. Now imagine it had to do that one character at a time. Is l the start of let, or of a name like lemon? Is 1 a whole number, or the start of 10.5? Every rule of the grammar would have to worry about spelling.

So the pipeline splits the work. The scanner deals with spelling once, and hands the parser a list of whole words. Those words are called tokens.

Intuition

Think of a sentence as a teacher marks it up. Each word gets underlined and labelled: noun, verb, number. The word as written, say “cats”, is the exact spelling. The label, noun, is the category. And the teacher might note “line 3” in the margin so they can talk about it later. A token is that: a category, the exact spelling, and a place.

Cutting a line into tokens

Step through two lines being cut up. Watch the card: every token has the same four parts.

Step throughCutting two lines into tokens
The characters of two Drip lines, with the current token's characters highlighted, and a card showing the token's type, lexeme, literal and line.1let·speed·=·10.5;2print·"fast";·//·hiSO FAR0 of 9 tokens · spaces and the comment make none
Two lines of Drip: 37 characters, counting the newline between them. We want the tokens: the pieces the parser will care about.
1/10

In the companion code a token is a plain object. This is the interface from companion/ts/src/token.ts, with its comments removed:

interface Token {
  type: TokenType;
  lexeme: string;
  literal: string | number | null;
  line: number;
}
Definition

A token is one meaningful word of source code. Its lexeme is the exact characters it was made from, such as "10.5". Its token type is its category: a number, an identifier, a keyword, the + operator. For numbers and strings, the scanner also stores the literal: the actual value the characters stand for, such as the number 10.5.

Lexeme and literal are easy to mix up, and strings make the difference clearest. For the Drip source "fast", the lexeme is six characters, quote marks included. The literal is the four-letter text fast, which is what print will show.

The list of token types

Why bother with a type at all, if the lexeme already says everything? Because the parser thinks in categories. Its rule is “a let is followed by any name”, not “followed by speed, or x, or total…”. Thousands of different lexemes share the one type identifier.

Here is every token type Drip has, from token.ts, rewrapped to fit:

type TokenType =
  // single-character tokens
  | '(' | ')' | '{' | '}' | ',' | '.'
  | '-' | '+' | ';' | '/' | '*'
  // one or two character tokens
  | '!' | '!=' | '=' | '=='
  | '>' | '>=' | '<' | '<='
  // literals
  | 'identifier' | 'string' | 'number'
  // keywords
  | 'and' | 'class' | 'else' | 'false'
  | 'fn' | 'for' | 'if' | 'nil' | 'or'
  | 'print' | 'return' | 'super'
  | 'this' | 'true' | 'let' | 'while'
  | 'eof';

It is a union of string literal types, from chapter 14. That buys two things. TypeScript rejects a typo such as 'eqaul', because it isn’t in the union. And the names are easy to read: for punctuation and keywords, the type is the spelling, so the token for <= has type '<='. Only for names, numbers and strings do the type and the lexeme differ.

The last type, eof, means “end of file”. The scanner always finishes with one, so the parser can keep asking “what’s the next token?” and get a clear answer when the program has run out.

Definition

A token type is the category of a token. Drip has 39: one for each punctuation mark or pair, three for literals (identifier, string, number), one for each of its 16 keywords, and eof. Spaces, newlines and comments separate tokens but never become tokens.

Where did it come from?

A token also remembers where it was found. When something goes wrong much later, say in the parser, that is the only way to tell you where.

Try itAn error knows its line
print "one";
let x = 1;
print x +;

The parser reports line 3, which it read from the ; token. Notice also that one is never printed. The front end checks the whole program before any of it runs, so a mistake in line 3 stops line 1 too.

Definition

A token’s source location says where in the source it came from: its line, and often its column (its position within the line). Error messages use it to point at the problem.

Drip’s tokens record only the line, because Drip’s error messages, which follow the Lox test suite, name only the line[1]. Many compilers go further and print the offending line with a caret ^ under the exact spot. For that a token needs its column too, which is what you’ll compute in the exercise.

Under the hood

The line number survives the whole pipeline, even past the tree. When Drip’s VM in Part IV compiles to bytecode, its Chunk keeps a lines array beside the code array, one entry per byte of bytecode (see companion/ts/src/vm/chunk.ts). A runtime error deep inside the VM can still say which source line it came from. Viscosity’s front end will need the same care: when it rewrites your arrays, errors should still point at your code.

Your turn

The core exercise turns a position in the source, counted in characters, into a line and a column. The extra one uses that to draw a caret under an error.

ExerciseFrom offset to line and column

A scanner walks the source by offset: the index of a character in the whole string. Error messages want a line and column instead, both counted from 1.

Write locate(src, offset), returning { line, col }. In 'ab\ncd', offset 0 (a) is line 1, column 1, and offset 4 (d) is line 2, column 2. A newline character belongs to the end of its line.

function locate(src, offset) {
  // your code here
}

console.log(locate('ab\ncd', 4));
Extra practicePoint at the problem

Write pointAt(src, line, col). It returns two lines joined by \n: the source line number line, and under it a caret ^ in column col (columns count from 1, so col - 1 spaces come first).

For 'let x = 1;\nprint x +;' with line 2 and column 10 it returns 'print x +;\n ^'.

function pointAt(src, line, col) {
  // your code here
}

const src = 'let x = 1;\nprint x +;';
console.log(pointAt(src, 2, 10));

What you learned

  • A token is one meaningful word of source. Its lexeme is the exact characters, its token type is its category, and numbers and strings also carry a literal value.
  • Drip’s TokenType is a union of 39 string literal types. For punctuation and keywords the type is the spelling itself. The list always ends with eof.
  • Whitespace and comments separate tokens but never become tokens.
  • A source location (line, and often column) lets later stages report errors in the right place. Drip records the line.

Next: the scanner itself, the loop that makes all these tokens.

Check your understanding

0/6
  1. 1.

    How many tokens does Drip’s scanner make for print x;?

  2. 2.

    What is the lexeme of the token made from 12.5 in a Drip program?

  3. 3.

    Which pair of tokens has the same type but different lexemes?

  4. 4.

    Why does each Drip token store a line number?

  5. 5.

    In Drip’s TokenType, the token for while has which type?

  6. 6.

    This Drip program has a syntax error on line 2. What is printed?

    print "hello";
    print 1 +;

References

  1. [1] Robert Nystrom (2021). Crafting Interpreters. Free web edition at craftinginterpreters.com (© 2015–2021); source, text and test suite at github.com/munificent/craftinginterpreters. github.com/munificent/craftinginterpreters (checked 2026-10-31)