Компиляция
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:
- Изучите Виртуальную машину для интерпретации байт-кода
- См. Примеры для готовых компиляторов
© 2026 Alexander Izmailov. Все права защищены.