← back to lessons
lesson 01 — beginner

How the lexer works

Learn how first reads source code one character at a time and emits tokens without loading the file into memory.

~15 min · lexer.h lexer.c

A lexer (also called a scanner or tokenizer) is the first stage of any interpreter. Its job is simple: take a stream of characters and produce a stream of tokens — meaningful units like numbers, operators, and keywords.

In first, the lexer is implemented in lexer.c. The key design decision is that it operates on a FILE* and calls fgetc() one character at a time. It never loads the whole file.

The main function: lex_next_token

Every call to lex_next_token() returns exactly one token. It reads the first character and uses a switch for the small set of single-character tokens that need no lookahead (%, ,, {, }, (, ), EOF), falling through to a default case that's an if/else chain for everything that might need to peek at a second character:

int32_t lex_next_token(Lexer *self, Token *token) {
  int32_t c = fgetc(self->file);
  switch (c) {
  case ',':
    *token = create_ctoken(c, TT_COMMA); break;
  default:
    if (isdigit(c)) {
      ungetc(c, self->file);        // put it back
      char *str = get_while(self, isdigit);
      *token = create_stoken(str, TT_NUMBER);
      free(str);
    }
    // ... plus a branch per compound operator, string, identifier, etc.
  }
  return 0;
}

Notice ungetc(): when we peek ahead and find a digit, we push the character back into the stream before calling get_while(). Every helper function always reads from a consistent starting position.

Bonus: the lexer also handles line comments, even though there's no dedicated TT_COMMENT token type. A # character makes it read (and discard) everything up to the next newline via get_until(self, isnewline), then emit a single TT_NEWLINE — as far as the parser is concerned, a comment line simply disappears and leaves behind the statement terminator it would have had anyway.

get_while and get_until

Two helpers handle multi-character tokens:

exercise

Trace what happens when lex_next_token() encounters the input 123 + 456. How many calls does it take? What does each call return? Draw the FILE* cursor position after each call.

Memory ownership

Every token's value field is a heap-allocated char * created by either char_to_string() or strdup(). The caller owns this memory and must call free(token.value) after use.

Key insight: The lexer knows nothing about grammar. It does not know that let is a keyword or that + should come between two numbers. That is the parser's job. The lexer only answers: "what is this next chunk of text?"