Компиляция

Evaluator - это финальный этап конвейера SynLex, который преобразует абстрактное синтаксическое дерево (AST) в результирующий объект типа T. Это может быть вычисленное значение, промежуточное представление (IR), байт-код или любая другая структура данных.

Основные концепции

Evaluator - Компилятор с типом результата

Evaluator<T> - это generic-класс, который определяет тип результата компиляции:

var intEvaluator = new Evaluator<int>();           // Результат: int
var astEvaluator = new Evaluator<AstNode>();       // Результат: AstNode
var bytecodeEvaluator = new Evaluator<byte[]>();   // Результат: byte[]

Основной метод:

public EvaluatorResult<T> Compile(AbstractSyntaxTree ast);

EvaluatorResult - Результат компиляции

public class EvaluatorResult<T>
{
    public bool IsSuccess { get; set; }
    public T? Result { get; set; }
    public List<string> Errors { get; set; } = new();
    public List<T>? Variants { get; set; }  // Для вариативной компиляции
}

Использование:

var result = evaluator.Compile(ast);

if (result.IsSuccess)
{
    Console.WriteLine($"Result: {result.Result}");
}
else
{
    foreach (var error in result.Errors)
    {
        Console.Error.WriteLine(error);
    }
}

EvaluatorContext - Контекст компиляции

Контекст предоставляет доступ к структуре AST во время компиляции:

public class EvaluatorContext
{
    public AbstractSyntaxTree CurrentNode { get; init; }
    public Dictionary<string, object?> UserContext { get; init; }
}

Доступ к подузлам:

var node = context.CurrentNode;
var token = node.Token;                  // Токен узла
var branches = node.Branches;            // Дочерние узлы

// Обработка левого и правого операнда
var left = node.Branches[0];
var right = node.Branches[1];

Создание Evaluator

Базовая настройка

using SynLex.Compilation;

var evaluator = new Evaluator<int>()
    .AddRule(new NumberRule())
    .AddRule(new AdditionRule())
    .AddRule(new MultiplicationRule());

var result = evaluator.Compile(ast);

IEvaluatorRule - Интерфейс правила компиляции

public interface IEvaluatorRule<T>
{
    bool CanApply(Token token);  // Может ли правило обработать токен узла
    
    CompilerRuleResult<T> Apply(
        EvaluatorContext context,
        Func<AbstractSyntaxTree, EvaluatorResult<T>> compile  // Рекурсивная компиляция
    );
}

Структура результата:

public record CompilerRuleResult<T>(
    bool IsSuccess,
    T? Result,
    List<string>? Errors = null
);

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

SynLex не предоставляет универсальных встроенных правил для Evaluator, так как логика компиляции зависит от конкретного языка. Вместо этого вы создаете свои правила.

Пример: Простое правило для чисел

public class NumberEvaluatorRule : IEvaluatorRule<int>
{
    public bool CanApply(Token token)
    {
        return token.Type == TOKEN_NUMBER;
    }
    
    public CompilerRuleResult<int> Apply(
        EvaluatorContext context,
        Func<AbstractSyntaxTree, EvaluatorResult<int>> compile)
    {
        var content = context.CurrentNode.Token.Content;
        
        if (int.TryParse(content, out var value))
        {
            return new CompilerRuleResult<int>(
                IsSuccess: true,
                Result: value
            );
        }
        
        return new CompilerRuleResult<int>(
            IsSuccess: false,
            Result: 0,
            Errors: new List<string> { $"Invalid number: {content}" }
        );
    }
}

Рекурсивная компиляция

Evaluator обрабатывает дерево рекурсивно. Для обработки дочерних узлов используйте параметр compile:

Пример: Правило для бинарного оператора

public class AdditionRule : IEvaluatorRule<int>
{
    public bool CanApply(Token token)
    {
        return token.Type == TOKEN_PLUS;
    }
    
    public CompilerRuleResult<int> Apply(
        EvaluatorContext context,
        Func<AbstractSyntaxTree, EvaluatorResult<int>> compile)
    {
        var branches = context.CurrentNode.Branches;
        
        if (branches.Length != 2)
        {
            return new CompilerRuleResult<int>(
                IsSuccess: false,
                Result: 0,
                Errors: new List<string> { "Addition requires exactly 2 operands" }
            );
        }
        
        // Рекурсивно компилируем левый операнд
        var leftResult = compile(branches[0]);
        if (!leftResult.IsSuccess)
        {
            return new CompilerRuleResult<int>(
                IsSuccess: false,
                Result: 0,
                Errors: leftResult.Errors
            );
        }
        
        // Рекурсивно компилируем правый операнд
        var rightResult = compile(branches[1]);
        if (!rightResult.IsSuccess)
        {
            return new CompilerRuleResult<int>(
                IsSuccess: false,
                Result: 0,
                Errors: rightResult.Errors
            );
        }
        
        // Складываем результаты
        var sum = leftResult.Result!.Value + rightResult.Result!.Value;
        
        return new CompilerRuleResult<int>(
            IsSuccess: true,
            Result: sum
        );
    }
}

Пользовательский контекст

UserContext позволяет передавать дополнительные данные между правилами:

Пример: Таблица переменных

public class VariableEvaluator : IEvaluatorRule<int>
{
    public bool CanApply(Token token)
    {
        return token.Type == TOKEN_IDENTIFIER;
    }
    
    public CompilerRuleResult<int> Apply(
        EvaluatorContext context,
        Func<AbstractSyntaxTree, EvaluatorResult<int>> compile)
    {
        var varName = context.CurrentNode.Token.Content;
        
        // Получаем таблицу переменных из UserContext
        if (!context.UserContext.TryGetValue("variables", out var varsObj) ||
            varsObj is not Dictionary<string, int> variables)
        {
            return new CompilerRuleResult<int>(
                IsSuccess: false,
                Result: 0,
                Errors: new List<string> { "Variables table not initialized" }
            );
        }
        
        // Ищем переменную
        if (!variables.TryGetValue(varName, out var value))
        {
            return new CompilerRuleResult<int>(
                IsSuccess: false,
                Result: 0,
                Errors: new List<string> { $"Undefined variable: {varName}" }
            );
        }
        
        return new CompilerRuleResult<int>(
            IsSuccess: true,
            Result: value
        );
    }
}

// Использование
var evaluator = new Evaluator<int>()
    .AddRule(new VariableEvaluator());

var context = new EvaluatorContext
{
    CurrentNode = ast,
    UserContext = new Dictionary<string, object?>
    {
        ["variables"] = new Dictionary<string, int>
        {
            ["x"] = 10,
            ["y"] = 20
        }
    }
};

// Внутренне Evaluator использует этот контекст

Вариативная компиляция

Для неоднозначных AST можно генерировать множество результатов:

var evaluator = new Evaluator<string>()
    .AddRule(new AmbiguousRule())
    .EnableVariants();  // Включить вариативность

var result = evaluator.Compile(ast);

if (result.IsSuccess && result.Variants != null)
{
    Console.WriteLine($"Generated {result.Variants.Count} variants:");
    foreach (var variant in result.Variants)
    {
        Console.WriteLine($"  - {variant}");
    }
}

Когда использовать:

Сложные типы результатов

Компиляция в промежуточное представление

public class IRNode
{
    public string Operation { get; set; }
    public IRNode[] Operands { get; set; }
    public object? Value { get; set; }
}

public class IRCompiler : IEvaluatorRule<IRNode>
{
    public bool CanApply(Token token)
    {
        return token.Type == TOKEN_PLUS;
    }
    
    public CompilerRuleResult<IRNode> Apply(
        EvaluatorContext context,
        Func<AbstractSyntaxTree, EvaluatorResult<IRNode>> compile)
    {
        var branches = context.CurrentNode.Branches;
        
        var leftResult = compile(branches[0]);
        var rightResult = compile(branches[1]);
        
        if (!leftResult.IsSuccess || !rightResult.IsSuccess)
        {
            return new CompilerRuleResult<IRNode>(
                IsSuccess: false,
                Result: null,
                Errors: leftResult.Errors.Concat(rightResult.Errors).ToList()
            );
        }
        
        var irNode = new IRNode
        {
            Operation = "ADD",
            Operands = new[] { leftResult.Result!, rightResult.Result! }
        };
        
        return new CompilerRuleResult<IRNode>(
            IsSuccess: true,
            Result: irNode
        );
    }
}

Компиляция в байт-код

public class BytecodeCompiler : IEvaluatorRule<List<byte>>
{
    private const byte OP_PUSH = 0x01;
    private const byte OP_ADD = 0x02;
    
    public bool CanApply(Token token)
    {
        return token.Type == TOKEN_PLUS;
    }
    
    public CompilerRuleResult<List<byte>> Apply(
        EvaluatorContext context,
        Func<AbstractSyntaxTree, EvaluatorResult<List<byte>>> compile)
    {
        var bytecode = new List<byte>();
        var branches = context.CurrentNode.Branches;
        
        // Компилируем левый операнд
        var leftResult = compile(branches[0]);
        if (!leftResult.IsSuccess)
            return new CompilerRuleResult<List<byte>>(false, null, leftResult.Errors);
        
        bytecode.AddRange(leftResult.Result!);
        
        // Компилируем правый операнд
        var rightResult = compile(branches[1]);
        if (!rightResult.IsSuccess)
            return new CompilerRuleResult<List<byte>>(false, null, rightResult.Errors);
        
        bytecode.AddRange(rightResult.Result!);
        
        // Добавляем инструкцию ADD
        bytecode.Add(OP_ADD);
        
        return new CompilerRuleResult<List<byte>>(
            IsSuccess: true,
            Result: bytecode
        );
    }
}

// Правило для чисел
public class NumberBytecodeRule : IEvaluatorRule<List<byte>>
{
    public bool CanApply(Token token) => token.Type == TOKEN_NUMBER;
    
    public CompilerRuleResult<List<byte>> Apply(
        EvaluatorContext context,
        Func<AbstractSyntaxTree, EvaluatorResult<List<byte>>> compile)
    {
        var value = int.Parse(context.CurrentNode.Token.Content);
        var bytecode = new List<byte>
        {
            OP_PUSH,
            (byte)(value & 0xFF),
            (byte)((value >> 8) & 0xFF),
            (byte)((value >> 16) & 0xFF),
            (byte)((value >> 24) & 0xFF)
        };
        
        return new CompilerRuleResult<List<byte>>(true, bytecode);
    }
}

Обработка ошибок

Накопление ошибок

public CompilerRuleResult<int> Apply(
    EvaluatorContext context,
    Func<AbstractSyntaxTree, EvaluatorResult<int>> compile)
{
    var errors = new List<string>();
    var branches = context.CurrentNode.Branches;
    
    // Компилируем все операнды, собирая ошибки
    var operands = new List<int>();
    
    foreach (var branch in branches)
    {
        var result = compile(branch);
        
        if (result.IsSuccess)
        {
            operands.Add(result.Result!.Value);
        }
        else
        {
            errors.AddRange(result.Errors);
        }
    }
    
    // Если есть ошибки - возвращаем их все
    if (errors.Any())
    {
        return new CompilerRuleResult<int>(
            IsSuccess: false,
            Result: 0,
            Errors: errors
        );
    }
    
    // Иначе - продолжаем компиляцию
    var sum = operands.Sum();
    return new CompilerRuleResult<int>(true, sum);
}

Информативные сообщения

var token = context.CurrentNode.Token;
var position = token.Position;

return new CompilerRuleResult<int>(
    IsSuccess: false,
    Result: 0,
    Errors: new List<string>
    {
        $"Type mismatch at {position.Line}:{position.Column}: " +
        $"Cannot apply operator '{token.Content}' to operands"
    }
);

Оптимизация

Кэширование результатов

public class CachingEvaluator : IEvaluatorRule<int>
{
    private readonly Dictionary<AbstractSyntaxTree, int> _cache = new();
    
    public CompilerRuleResult<int> Apply(
        EvaluatorContext context,
        Func<AbstractSyntaxTree, EvaluatorResult<int>> compile)
    {
        var node = context.CurrentNode;
        
        // Проверяем кэш
        if (_cache.TryGetValue(node, out var cached))
        {
            return new CompilerRuleResult<int>(true, cached);
        }
        
        // Компилируем
        var result = PerformCompilation(context, compile);
        
        // Сохраняем в кэш
        if (result.IsSuccess)
        {
            _cache[node] = result.Result!.Value;
        }
        
        return result;
    }
}

Ленивое вычисление

public class LazyValue<T>
{
    private readonly Func<T> _factory;
    private T? _value;
    private bool _evaluated;
    
    public LazyValue(Func<T> factory)
    {
        _factory = factory;
    }
    
    public T Value
    {
        get
        {
            if (!_evaluated)
            {
                _value = _factory();
                _evaluated = true;
            }
            return _value!;
        }
    }
}

// Использование в Evaluator
var evaluator = new Evaluator<LazyValue<int>>()
    .AddRule(new LazyRule());

Полный пример

Калькулятор с переменными

using SynLex;
using SynLex.LexicalAnalysis;
using SynLex.SyntacticAnalysis;
using SynLex.Compilation;

// Типы токенов
const int TOKEN_NUMBER = 1;
const int TOKEN_IDENTIFIER = 2;
const int TOKEN_PLUS = 3;
const int TOKEN_MULT = 4;
const int TOKEN_ASSIGN = 5;

// Правила компиляции
public class NumberRule : IEvaluatorRule<int>
{
    public bool CanApply(Token token) => token.Type == TOKEN_NUMBER;
    
    public CompilerRuleResult<int> Apply(EvaluatorContext context, 
                                         Func<AbstractSyntaxTree, EvaluatorResult<int>> compile)
    {
        var value = int.Parse(context.CurrentNode.Token.Content);
        return new CompilerRuleResult<int>(true, value);
    }
}

public class VariableRule : IEvaluatorRule<int>
{
    public bool CanApply(Token token) => token.Type == TOKEN_IDENTIFIER;
    
    public CompilerRuleResult<int> Apply(EvaluatorContext context,
                                         Func<AbstractSyntaxTree, EvaluatorResult<int>> compile)
    {
        var varName = context.CurrentNode.Token.Content;
        var vars = (Dictionary<string, int>)context.UserContext["variables"]!;
        
        if (vars.TryGetValue(varName, out var value))
            return new CompilerRuleResult<int>(true, value);
        
        return new CompilerRuleResult<int>(false, 0, 
            new List<string> { $"Undefined variable: {varName}" });
    }
}

public class AddRule : IEvaluatorRule<int>
{
    public bool CanApply(Token token) => token.Type == TOKEN_PLUS;
    
    public CompilerRuleResult<int> Apply(EvaluatorContext context,
                                         Func<AbstractSyntaxTree, EvaluatorResult<int>> compile)
    {
        var left = compile(context.CurrentNode.Branches[0]);
        var right = compile(context.CurrentNode.Branches[1]);
        
        if (!left.IsSuccess || !right.IsSuccess)
            return new CompilerRuleResult<int>(false, 0);
        
        return new CompilerRuleResult<int>(true, left.Result!.Value + right.Result!.Value);
    }
}

// Использование
var variables = new Dictionary<string, int> { ["x"] = 10, ["y"] = 5 };

var evaluator = new Evaluator<int>()
    .AddRule(new NumberRule())
    .AddRule(new VariableRule())
    .AddRule(new AddRule());

// AST для выражения "x + y + 3"
var result = evaluator.Compile(ast, new Dictionary<string, object?> 
{ 
    ["variables"] = variables 
});

Console.WriteLine($"Result: {result.Result}"); // Result: 18

Лучшие практики

1. Проверяйте структуру AST

if (context.CurrentNode.Branches.Length != 2)
{
    return new CompilerRuleResult<T>(
        false, default,
        new List<string> { "Binary operator requires 2 operands" }
    );
}

2. Используйте паттерн Result

// ✅ Возвращайте успех/ошибку явно
return new CompilerRuleResult<int>(
    IsSuccess: false,
    Result: 0,
    Errors: new List<string> { "Error message" }
);

// ❌ Не бросайте исключения
throw new Exception("Compilation error");

3. Разделяйте правила

// ✅ Одно правило - одна операция
var evaluator = new Evaluator<int>()
    .AddRule(new AddRule())
    .AddRule(new SubtractRule())
    .AddRule(new MultiplyRule());

// ❌ Не объединяйте всё в одном правиле
var evaluator = new Evaluator<int>()
    .AddRule(new AllOperatorsRule());

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

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

  1. Изучите Виртуальную машину для интерпретации байт-кода
  2. См. Примеры для готовых компиляторов

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