Лексический анализ
Лексический анализатор (Lexer) - это первый этап обработки исходного кода. Он преобразует строку текста в последовательность токенов, которые затем обрабатываются парсером.
Основные концепции
Token - Токен
Токен - это элементарная единица синтаксиса. Каждый токен представляет логическую часть исходного кода (ключевое слово, идентификатор, оператор, литерал и т.д.).
public readonly struct Token
{
public int Type { get; init; } // Тип токена (пользовательское число)
public string Content { get; init; } // Исходное содержимое из текста
public TextPosition Position { get; init; } // Позиция в исходном коде
}
Пример:
var token = new Token
{
Type = TOKEN_IDENTIFIER,
Content = "myVariable",
Position = new TextPosition(Line: 1, Column: 5)
};
TextPosition - Позиция в коде
Структура TextPosition хранит информацию о месте токена в исходном файле:
public readonly record struct TextPosition(int Line, int Column);
Позиции используются для:
- Вывода информативных сообщений об ошибках
- Подсветки синтаксиса в редакторах
- Навигации по коду
LexerResult - Результат анализа
Все операции лексера возвращают LexerResult:
public class LexerResult
{
public bool IsSuccess { get; set; }
public Token[]? Tokens { get; set; }
public List<string> Errors { get; set; } = new();
}
Пример использования:
var result = lexer.Analyze("2 + 3");
if (result.IsSuccess)
{
foreach (var token in result.Tokens!)
{
Console.WriteLine($"{token.Type}: {token.Content}");
}
}
else
{
foreach (var error in result.Errors)
{
Console.WriteLine($"Ошибка: {error}");
}
}
Создание лексера
Базовая настройка
Лексер создается с использованием Fluent API:
using SynLex.LexicalAnalysis;
var lexer = new Lexer()
.AddRule(rule1)
.AddRule(rule2)
.AddRule(rule3);
ILexerRule - Интерфейс правила
Все правила лексера реализуют интерфейс ILexerRule:
public interface ILexerRule
{
bool Apply(
string text, // Исходный текст
int position, // Текущая позиция в тексте
out Token token, // Создаваемый токен
out int advance // На сколько символов продвинуться
);
}
Правило должно:
- Проверить, подходит ли текст с позиции
position - Если подходит - создать токен и вернуть
true, указавadvance - Если не подходит - вернуть
false
Встроенные правила
SimpleSublineLexerRule - Точное совпадение
Проверяет совпадение с заданными строками:
// Операторы
var operatorRule = new SimpleSublineLexerRule(
new[] { "+", "-", "*", "/", "==", "!=" },
TOKEN_OPERATOR
);
// Ключевые слова
var keywordRule = new SimpleSublineLexerRule(
new[] { "if", "else", "while", "return" },
TOKEN_KEYWORD
);
Особенности:
- Использует Trie для эффективного поиска
- Проверяет самое длинное совпадение первым
- Идеально для ключевых слов и операторов
RegexLexerRule - Регулярные выражения
Использует регулярные выражения для сопоставления:
using SynLex.LexicalAnalysis;
// Числа
var numberRule = new RegexLexerRule(@"\d+", TOKEN_NUMBER);
// Идентификаторы
var identifierRule = new RegexLexerRule(@"[a-zA-Z_]\w*", TOKEN_IDENTIFIER);
// Строки
var stringRule = new RegexLexerRule(@"""[^""]*""", TOKEN_STRING);
// Комментарии
var commentRule = new RegexLexerRule(@"//[^\n]*", TOKEN_COMMENT);
Особенности:
- Применяется с начала текста (автоматически добавляется
^) - Поддерживает захватывающие группы
- Гибкий, но медленнее SimpleSublineLexerRule
WhitespaceLexerRule - Пробельные символы
Пропускает пробелы, табуляцию, переносы строк:
var whitespaceRule = new WhitespaceLexerRule();
var lexer = new Lexer()
.AddRule(whitespaceRule) // Обычно добавляется первым
.AddRule(otherRules...);
Особенности:
- Не создает токены (возвращает
null) - Автоматически продвигает позицию
- Учитывает строки и столбцы
Пользовательские правила
Создание своего правила
public class HexNumberLexerRule : ILexerRule
{
private readonly int _tokenType;
public HexNumberLexerRule(int tokenType)
{
_tokenType = tokenType;
}
public bool Apply(string text, int position, out Token token, out int advance)
{
token = default;
advance = 0;
// Проверяем префикс 0x
if (position + 2 >= text.Length ||
text[position] != '0' ||
text[position + 1] != 'x')
{
return false;
}
// Считаем шестнадцатеричные цифры
int start = position;
int current = position + 2;
while (current < text.Length && IsHexDigit(text[current]))
{
current++;
}
if (current == position + 2) // Нет цифр после 0x
{
return false;
}
// Создаем токен
advance = current - start;
token = new Token
{
Type = _tokenType,
Content = text.Substring(start, advance),
Position = CalculatePosition(text, start)
};
return true;
}
private static bool IsHexDigit(char c)
{
return (c >= '0' && c <= '9') ||
(c >= 'a' && c <= 'f') ||
(c >= 'A' && c <= 'F');
}
private static TextPosition CalculatePosition(string text, int position)
{
int line = 1;
int column = 1;
for (int i = 0; i < position; i++)
{
if (text[i] == '\n')
{
line++;
column = 1;
}
else
{
column++;
}
}
return new TextPosition(line, column);
}
}
Использование:
var lexer = new Lexer()
.AddRule(new HexNumberLexerRule(TOKEN_HEX_NUMBER))
.AddRule(new RegexLexerRule(@"\d+", TOKEN_DECIMAL_NUMBER));
var result = lexer.Analyze("0xFF + 42");
// Токены: [0xFF (HEX), + (OPERATOR), 42 (DECIMAL)]
Порядок применения правил
Лексер применяет правила в порядке добавления. Первое успешное правило создает токен.
var lexer = new Lexer()
.AddRule(new SimpleSublineLexerRule(new[] { "if", "else" }, TOKEN_KEYWORD))
.AddRule(new RegexLexerRule(@"[a-zA-Z_]\w*", TOKEN_IDENTIFIER));
// "if" -> TOKEN_KEYWORD (первое правило сработало)
// "myVar" -> TOKEN_IDENTIFIER (второе правило сработало)
Важно: Более специфичные правила должны идти перед общими!
Обработка ошибок
Некорректный символ
Если ни одно правило не сработало, лексер генерирует ошибку:
var result = lexer.Analyze("2 + # 3"); // '#' - недопустимый символ
if (!result.IsSuccess)
{
Console.WriteLine(result.Errors[0]);
// "Unexpected character '#' at line 1, column 5"
}
Правило для неизвестных символов
Можно добавить "fallback" правило:
public class UnknownCharacterRule : ILexerRule
{
public bool Apply(string text, int position, out Token token, out int advance)
{
token = new Token
{
Type = TOKEN_ERROR,
Content = text[position].ToString(),
Position = CalculatePosition(text, position)
};
advance = 1;
return true;
}
}
// Добавить в конец списка правил
var lexer = new Lexer()
.AddRule(validRules...)
.AddRule(new UnknownCharacterRule()); // Последнее правило
Оптимизация производительности
1. Используйте Trie для ключевых слов
// ✅ Быстро - O(k), где k - длина ключевого слова
var keywordRule = new SimpleSublineLexerRule(
new[] { "if", "else", "while", "for", "return" },
TOKEN_KEYWORD
);
// ❌ Медленно - O(n*m), где n - количество слов, m - длина
var ifRule = new RegexLexerRule("if", TOKEN_IF);
var elseRule = new RegexLexerRule("else", TOKEN_ELSE);
// ... множество правил
2. Порядок правил
Размещайте часто встречающиеся правила раньше:
var lexer = new Lexer()
.AddRule(whitespaceRule) // Очень часто
.AddRule(numberRule) // Часто
.AddRule(identifierRule) // Часто
.AddRule(operatorRule) // Средне
.AddRule(stringRule) // Редко
.AddRule(commentRule); // Редко
3. Избегайте сложных регулярок
// ❌ Медленно - backtracking
var complexRule = new RegexLexerRule(@"[a-zA-Z_]([a-zA-Z0-9_]|::)*", TOKEN_ID);
// ✅ Быстро - простое совпадение
var simpleRule = new RegexLexerRule(@"[a-zA-Z_]\w*", TOKEN_ID);
Полный пример
Простой язык выражений
using SynLex;
using SynLex.LexicalAnalysis;
// Типы токенов
const int TOKEN_NUMBER = 1;
const int TOKEN_PLUS = 2;
const int TOKEN_MINUS = 3;
const int TOKEN_MULT = 4;
const int TOKEN_DIV = 5;
const int TOKEN_LPAREN = 6;
const int TOKEN_RPAREN = 7;
// Создаем лексер
var lexer = new Lexer()
.AddRule(new WhitespaceLexerRule())
.AddRule(new RegexLexerRule(@"\d+(\.\d+)?", TOKEN_NUMBER))
.AddRule(new SimpleSublineLexerRule(new[] { "+" }, TOKEN_PLUS))
.AddRule(new SimpleSublineLexerRule(new[] { "-" }, TOKEN_MINUS))
.AddRule(new SimpleSublineLexerRule(new[] { "*" }, TOKEN_MULT))
.AddRule(new SimpleSublineLexerRule(new[] { "/" }, TOKEN_DIV))
.AddRule(new SimpleSublineLexerRule(new[] { "(" }, TOKEN_LPAREN))
.AddRule(new SimpleSublineLexerRule(new[] { ")" }, TOKEN_RPAREN));
// Анализ выражения
var result = lexer.Analyze("(2 + 3) * 4.5");
if (result.IsSuccess)
{
foreach (var token in result.Tokens!)
{
Console.WriteLine($"Type: {token.Type}, Content: '{token.Content}', " +
$"Position: {token.Position.Line}:{token.Position.Column}");
}
}
/* Вывод:
Type: 6, Content: '(', Position: 1:1
Type: 1, Content: '2', Position: 1:2
Type: 2, Content: '+', Position: 1:4
Type: 1, Content: '3', Position: 1:6
Type: 7, Content: ')', Position: 1:7
Type: 4, Content: '*', Position: 1:9
Type: 1, Content: '4.5', Position: 1:11
*/
Лучшие практики
1. Группируйте похожие токены
const int TOKEN_OPERATOR = 100;
const int TOKEN_KEYWORD = 200;
const int TOKEN_LITERAL = 300;
// Операторы: 100-199
const int TOKEN_PLUS = 101;
const int TOKEN_MINUS = 102;
// Ключевые слова: 200-299
const int TOKEN_IF = 201;
const int TOKEN_ELSE = 202;
// Литералы: 300-399
const int TOKEN_NUMBER = 301;
const int TOKEN_STRING = 302;
2. Документируйте типы токенов
/// <summary>
/// Типы токенов для языка MiniLang
/// </summary>
public static class TokenTypes
{
/// <summary>Целое или дробное число: 42, 3.14</summary>
public const int Number = 1;
/// <summary>Идентификатор: myVar, count</summary>
public const int Identifier = 2;
/// <summary>Строковый литерал: "hello"</summary>
public const int String = 3;
}
3. Проверяйте результаты
var result = lexer.Analyze(sourceCode);
if (!result.IsSuccess)
{
foreach (var error in result.Errors)
{
Console.Error.WriteLine(error);
}
return;
}
// Продолжаем работу с токенами
var tokens = result.Tokens!;
4. Используйте константы
// ✅ Хорошо
const int TOKEN_CLASS = 10;
lexer.AddRule(new SimpleSublineLexerRule(new[] { "class" }, TOKEN_CLASS));
// ❌ Плохо
lexer.AddRule(new SimpleSublineLexerRule(new[] { "class" }, 10));
Следующие шаги
После создания лексера:
- Изучите Синтаксический анализ для построения AST
- См. Примеры для готовых решений
- Ознакомьтесь с Архитектурой для понимания внутреннего устройства
© 2026 Alexander Izmailov. Все права защищены.