Compiler design / 05. Tokenization
Lexical analysis cuts character streams into located tokens and prepares document source kinds for shared later stages.
A parser that stares at an undifferentiated character stream spends its life guessing. Where does the identifier end? Is = part of == or a lonely assignment? Did a string already open?
Tokenization (lexical analysis) cuts the stream into tokens: small classified chunks with spelling and source locations. Later stages stop arguing about character boundaries and start arguing about meaning.
A token carries at least:
Take the page import:
import @greeting { greet }
A lexer might emit a sequence in this spirit:
Spelling | Kind family |
|---|---|
| keyword or identifier reserved by grammar |
| package or module path atom |
| delimiter |
| identifier |
| delimiter |
Exact kind names live in the tokenizer. The teaching point stays fixed: boundaries enter here, meaning does not.
Identifiers, delimiters, operators and literals each become discrete pieces. The parser will arrange them. The type checker will judge them. The lexer only marks the boundaries.
Diagram placeholder: Source characters become located token kinds
Caption: simplified lexer view. Question: where do boundaries enter the pipeline?
Every useful diagnostic eventually points at text the author wrote. Locations make that possible without forcing every later stage to keep the entire file string beside every node forever.
A span says "these characters in this source identity." When interface binding later fails to find greet, the diagnostic can underline the import name rather than shrug at "somewhere in the module."
Tokenization attaches locations. It does not decide that greet is a function. That decision waits for binding and AST.
Diagram placeholder: Token spans point back to exact source ranges
Caption: exact diagnostic principle, simplified data shape. Question: how can later stages point to authored text without reparsing?
Not every file starts in the same lexical mode.
.moth starts in code mode..mtf starts in an implicit template body while preserving original source locations..md is prepared before tokenization and does not use a tokenizer entry mode in the same way.Those paths converge. Moth template preparation synthesises an ordinary private content #String declaration whose initializer is a Markdown template over the body tokens. Markdown preparation can contribute the same declaration shape with a synthetic string literal. Later stages see ordinary declarations, not a forever-special document dialect in HIR.
The greeting project uses .moth files. Document kinds still matter because the same compiler owners serve docs sites and content imports without a second semantic pipeline.
Diagram placeholder: moth, mtf and md use different entry preparation then converge
Caption: teaching simplification. Question: why avoid duplicate pipelines for documents and code?
Some mistakes die at the first cut:
numeric_text owns shared numeric grammar, normalisation, separators and exponents. Lexical numeric failure is not yet a semantic Number versus Byte debate. Semantic numeric types arrive later. Keep those layers separate when you read diagnostics.
Tokenization recognises spelling, locations and delimiter context. It may reject unsupported directive forms at the syntax level through the merged directive registry.
It does not:
If a stage after the lexer rediscovers token boundaries from raw text, something has slipped.
Defer semantic errors to their owning stages. A good lexer stays humble.
Alternative: give each source kind a separate parser and backend pipeline.
Choice: adapt source kinds early, then share declaration and semantic stages.
Benefit: one semantic owner for ordinary declarations and runtime lowering. Document content becomes data the rest of the compiler already understands.
Cost: source-kind adapters must preserve locations and keep boundaries crisp.
You leave with located tokens and source-kind prepared payloads. Next: which declarations and import relationships those tokens describe.