The lexer (lexical analyzer) is the first stage of the language processing pipeline, converting source code into a stream of tokens.
Source Code (String)
│
▼
┌───────────────────┐
│ Lexer │
│ ┌─────────────┐ │
│ │ Scanner │ │ Character-by-character processing
│ └──────┬──────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Tokenizer │ │ Group characters into tokens
│ └──────┬──────┘ │
│ ▼ │
│ ┌─────────────┐ │
│ │ Filter │ │ Remove whitespace/comments
│ └─────────────┘ │
└────────┬──────────┘
▼
Token Stream
enum TokenKind {
// Literals
IntLiteral, // 42, 0xFF, 0b1010
FloatLiteral, // 3.14, 1e-10
StringLiteral, // "hello"
CharLiteral, // 'a'
BoolLiteral, // true, false
// Identifiers
Identifier, // foo, myVar, _private
TypeIdentifier, // Int, String, MyType
// Keywords (language-specific)
Keyword, // let, fn, if, while, ...
// Operators
Plus, Minus, Star, Slash, // + - * /
Eq, Ne, Lt, Le, Gt, Ge, // == != < <= > >=
And, Or, Not, // && || !
Assign, // =
Arrow, // ->
FatArrow, // =>
// Delimiters
LParen, RParen, // ( )
LBrace, RBrace, // { }
LBracket, RBracket, // [ ]
Comma, Semicolon, Colon, Dot,
// Special
Newline,
Indent, Dedent, // For indentation-sensitive languages
Eof,
Error,
}struct Token {
kind: TokenKind,
lexeme: String, // The actual text
span: Span, // Source location
value: Option<Value>, // Parsed literal value
}
struct Span {
file: FileId,
start: Position,
end: Position,
}
struct Position {
offset: usize, // Byte offset
line: u32,
column: u32,
}Simple tokenizer with minimal special characters:
Tokens: words, numbers, quoted strings
Keywords: say, ask, remember, repeat, if, then, else, end
No operators - uses words: plus, minus, times, divided-by
Standard imperative tokenizer:
let count = 0
repeat 5 times
count = count + 1
say count
end
Tokens:
- Keywords:
let,repeat,times,say,end - Identifiers:
count - Literals:
0,5,1 - Operators:
=,+
Adds AI-related tokens:
@synth: function to sort list
@verify: sorted output
intent("find the maximum")
Special tokens:
- Annotations:
@synth,@verify - Intent strings:
intent("...")
Full-featured lexer with advanced constructs:
ai_model GPT4 {
temperature: 0.7,
max_tokens: 1000,
}
prompt analyze_code {
system: "You are a code reviewer.",
user: "{code}",
}Racket-based S-expression lexer:
(ternary-bet 'option-a 'option-b 'option-c)
(weighted-bet ((0.5 . heads) (0.5 . tails)))Tokens: parentheses, symbols, numbers, quoted atoms
Dual-channel lexer (Control + Data):
CONTROL: if input > 0 then output = process(input)
DATA: [name: "John", age: 30]
Separate token streams for each channel.
Advanced token set for type annotations:
let f : forall a. (Linear a -> a) -> Effect[IO] a =
fun x -> consume xTokens for: forall, Linear, Effect, type brackets
Linear/ephemeral markers:
(once x (use x)) ; x can only be used once
(ephemeral (create-resource))Special tokens: once, ephemeral, consume
Best for: Simple languages, educational purposes
impl Lexer {
fn next_token(&mut self) -> Token {
self.skip_whitespace();
match self.current_char() {
None => Token::eof(self.position()),
Some(c) if c.is_alphabetic() => self.identifier(),
Some(c) if c.is_digit(10) => self.number(),
Some('"') => self.string(),
Some('+') => self.single_char(TokenKind::Plus),
// ... more patterns
}
}
}Pros: Full control, excellent error messages Cons: More code to maintain
Best for: Production languages
use logos::Logos;
#[derive(Logos, Debug, PartialEq)]
enum Token {
#[token("let")]
Let,
#[token("fn")]
Fn,
#[regex("[a-zA-Z_][a-zA-Z0-9_]*")]
Identifier,
#[regex("[0-9]+")]
Number,
#[error]
Error,
}Pros: Fast, declarative, less bugs Cons: Less control over error recovery
Best for: Complex token patterns
fn identifier(input: &str) -> IResult<&str, Token> {
map(
recognize(pair(
alt((alpha1, tag("_"))),
many0(alt((alphanumeric1, tag("_"))))
)),
|s| Token::Identifier(s.to_string())
)(input)
}Essential for error messages:
fn error_at(&self, span: Span, message: &str) -> Diagnostic {
Diagnostic::error()
.with_message(message)
.with_label(Label::primary(span.file, span.start..span.end))
}
// Output:
// error: unexpected character '©'
// --> src/main.solo:5:10
// |
// 5 | let x = ©
// | ^ unexpected characterHandle international identifiers:
fn is_identifier_start(c: char) -> bool {
c.is_alphabetic() || c == '_'
// Or use Unicode categories:
// unicode_xid::UnicodeXID::is_xid_start(c)
}For IDE support, re-lex only changed portions:
struct IncrementalLexer {
tokens: Vec<Token>,
dirty_range: Option<Range<usize>>,
}
impl IncrementalLexer {
fn update(&mut self, edit: TextEdit) {
// Only re-lex affected region
let start = self.find_token_at(edit.start);
let end = self.find_token_at(edit.end);
self.relex_range(start, end, &edit.new_text);
}
}Continue lexing after errors:
fn recover_from_error(&mut self) -> Token {
let start = self.position();
// Skip until we find something recognizable
while let Some(c) = self.current_char() {
if c.is_whitespace() || self.is_delimiter(c) {
break;
}
self.advance();
}
Token::error("unrecognized token", Span::new(start, self.position()))
}#[test]
fn test_keywords() {
let tokens: Vec<_> = Lexer::new("let fn if").collect();
assert_eq!(tokens[0].kind, TokenKind::Let);
assert_eq!(tokens[1].kind, TokenKind::Fn);
assert_eq!(tokens[2].kind, TokenKind::If);
}
#[test]
fn test_numbers() {
let tokens: Vec<_> = Lexer::new("42 3.14 0xFF").collect();
assert_eq!(tokens[0].value, Some(Value::Int(42)));
assert_eq!(tokens[1].value, Some(Value::Float(3.14)));
assert_eq!(tokens[2].value, Some(Value::Int(255)));
}#[test]
fn fuzz_lexer() {
arbtest::arbtest(|u| {
let input: String = u.arbitrary()?;
// Lexer should never panic
let _: Vec<_> = Lexer::new(&input).collect();
Ok(())
});
}(verify-property lexer-complete
"Every valid program can be tokenized"
(forall (prog : ValidProgram)
(exists (tokens : TokenStream)
(tokenize prog) = tokens)))
- [[Parser Architecture]]
- [[Error Messages]]
- [[Unicode Handling]]
- [[IDE Support]]