Лексический анализ

Лексический анализатор (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           // На сколько символов продвинуться
    );
}

Правило должно:

  1. Проверить, подходит ли текст с позиции position
  2. Если подходит - создать токен и вернуть true, указав advance
  3. Если не подходит - вернуть false

Встроенные правила

SimpleSublineLexerRule - Точное совпадение

Проверяет совпадение с заданными строками:

// Операторы
var operatorRule = new SimpleSublineLexerRule(
    new[] { "+", "-", "*", "/", "==", "!=" },
    TOKEN_OPERATOR
);

// Ключевые слова
var keywordRule = new SimpleSublineLexerRule(
    new[] { "if", "else", "while", "return" },
    TOKEN_KEYWORD
);

Особенности:

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);

Особенности:

WhitespaceLexerRule - Пробельные символы

Пропускает пробелы, табуляцию, переносы строк:

var whitespaceRule = new WhitespaceLexerRule();

var lexer = new Lexer()
    .AddRule(whitespaceRule)  // Обычно добавляется первым
    .AddRule(otherRules...);

Особенности:

Пользовательские правила

Создание своего правила

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));

Следующие шаги

После создания лексера:

  1. Изучите Синтаксический анализ для построения AST
  2. См. Примеры для готовых решений
  3. Ознакомьтесь с Архитектурой для понимания внутреннего устройства

© 2026 Alexander Izmailov. Все права защищены.