Виртуальная машина

VirtualMachine - это интерпретатор байт-кода, который выполняет последовательность инструкций. Виртуальная машина использует стековую архитектуру и полностью настраивается через пользовательские правила.

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

VirtualMachine - Интерпретатор инструкций

Виртуальная машина выполняет массив инструкций:

var vm = new VirtualMachine()
    .AddRule(new PushRule())
    .AddRule(new AddRule())
    .AddRule(new PrintRule());

var result = vm.Execute(instructions);

Instruction - Инструкция

Инструкция представляет одну операцию виртуальной машины:

public readonly struct Instruction
{
    public byte OpCode { get; init; }        // Код операции
    public object?[] Operands { get; init; } // Операнды инструкции
}

Пример:

var pushInst = new Instruction
{
    OpCode = OP_PUSH,
    Operands = new object[] { 42 }
};

var addInst = new Instruction
{
    OpCode = OP_ADD,
    Operands = Array.Empty<object>()
};

VMExecutionContext - Контекст выполнения

Контекст хранит состояние виртуальной машины во время выполнения:

public class VMExecutionContext
{
    public Stack<object> Stack { get; init; }                    // Стек операндов
    public int InstructionPointer { get; set; }                  // Указатель на текущую инструкцию
    public Dictionary<string, object?> Memory { get; init; }     // Память (переменные)
    public Dictionary<string, object?> UserContext { get; init; } // Пользовательский контекст
}

Основные операции:

// Работа со стеком
context.Stack.Push(value);
var value = context.Stack.Pop();
var top = context.Stack.Peek();

// Работа с памятью
context.Memory["x"] = 42;
var x = (int)context.Memory["x"]!;

// Управление потоком
context.InstructionPointer = 10;  // Переход к инструкции #10

VMExecutionResult - Результат выполнения

public class VMExecutionResult
{
    public bool IsSuccess { get; set; }
    public object? Result { get; set; }             // Результат на вершине стека
    public List<string> Errors { get; set; } = new();
}

Создание виртуальной машины

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

using SynLex.VirtualMachine;

var vm = new VirtualMachine()
    .AddRule(rule1)
    .AddRule(rule2)
    .AddRule(rule3);

var result = vm.Execute(instructions);

IVMExecutionRule - Интерфейс правила выполнения

public interface IVMExecutionRule
{
    bool CanHandle(byte opCode);  // Может ли правило обработать OpCode
    
    bool Execute(
        Instruction instruction,
        VMExecutionContext context,
        out string? error
    );
}

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

  1. Проверить, может ли обработать opCode
  2. Выполнить операцию, изменяя context
  3. Вернуть true при успехе, false при ошибке

Стандартные коды операций

Определите константы для OpCode:

public static class OpCodes
{
    // Стек
    public const byte PUSH = 0x01;   // Поместить значение на стек
    public const byte POP = 0x02;    // Удалить значение из стека
    public const byte DUP = 0x03;    // Дублировать верхнее значение
    
    // Арифметика
    public const byte ADD = 0x10;    // Сложение
    public const byte SUB = 0x11;    // Вычитание
    public const byte MUL = 0x12;    // Умножение
    public const byte DIV = 0x13;    // Деление
    
    // Переменные
    public const byte LOAD = 0x20;   // Загрузить переменную на стек
    public const byte STORE = 0x21;  // Сохранить стек в переменную
    
    // Управление потоком
    public const byte JMP = 0x30;    // Безусловный переход
    public const byte JZ = 0x31;     // Переход если 0
    public const byte CALL = 0x32;   // Вызов функции
    public const byte RET = 0x33;    // Возврат из функции
    
    // Ввод/вывод
    public const byte PRINT = 0x40;  // Вывести значение
    public const byte READ = 0x41;   // Прочитать значение
}

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

Пример: Правило PUSH

using SynLex.VirtualMachine;

public class PushRule : IVMExecutionRule
{
    public bool CanHandle(byte opCode)
    {
        return opCode == OpCodes.PUSH;
    }
    
    public bool Execute(Instruction instruction, VMExecutionContext context, out string? error)
    {
        error = null;
        
        // Проверяем наличие операнда
        if (instruction.Operands.Length == 0)
        {
            error = "PUSH requires 1 operand";
            return false;
        }
        
        // Помещаем значение на стек
        context.Stack.Push(instruction.Operands[0]!);
        
        // Переходим к следующей инструкции
        context.InstructionPointer++;
        
        return true;
    }
}

Пример: Правило ADD

public class AddRule : IVMExecutionRule
{
    public bool CanHandle(byte opCode)
    {
        return opCode == OpCodes.ADD;
    }
    
    public bool Execute(Instruction instruction, VMExecutionContext context, out string? error)
    {
        error = null;
        
        // Проверяем, что на стеке минимум 2 значения
        if (context.Stack.Count < 2)
        {
            error = "ADD requires 2 values on stack";
            return false;
        }
        
        // Извлекаем операнды
        var right = context.Stack.Pop();
        var left = context.Stack.Pop();
        
        // Выполняем операцию
        if (left is int leftInt && right is int rightInt)
        {
            context.Stack.Push(leftInt + rightInt);
        }
        else if (left is double leftDbl && right is double rightDbl)
        {
            context.Stack.Push(leftDbl + rightDbl);
        }
        else
        {
            error = $"Cannot add {left?.GetType().Name} and {right?.GetType().Name}";
            return false;
        }
        
        context.InstructionPointer++;
        return true;
    }
}

Пример: Правило STORE (переменные)

public class StoreRule : IVMExecutionRule
{
    public bool CanHandle(byte opCode)
    {
        return opCode == OpCodes.STORE;
    }
    
    public bool Execute(Instruction instruction, VMExecutionContext context, out string? error)
    {
        error = null;
        
        // Получаем имя переменной из операнда
        if (instruction.Operands.Length == 0 || instruction.Operands[0] is not string varName)
        {
            error = "STORE requires variable name as operand";
            return false;
        }
        
        // Проверяем стек
        if (context.Stack.Count == 0)
        {
            error = "STORE requires value on stack";
            return false;
        }
        
        // Сохраняем значение в память
        var value = context.Stack.Pop();
        context.Memory[varName] = value;
        
        context.InstructionPointer++;
        return true;
    }
}

Пример: Правило JMP (переход)

public class JumpRule : IVMExecutionRule
{
    public bool CanHandle(byte opCode)
    {
        return opCode == OpCodes.JMP;
    }
    
    public bool Execute(Instruction instruction, VMExecutionContext context, out string? error)
    {
        error = null;
        
        // Получаем адрес перехода
        if (instruction.Operands.Length == 0 || instruction.Operands[0] is not int address)
        {
            error = "JMP requires address as operand";
            return false;
        }
        
        // Проверяем корректность адреса
        if (address < 0)
        {
            error = $"Invalid jump address: {address}";
            return false;
        }
        
        // Изменяем указатель инструкций
        context.InstructionPointer = address;
        
        return true;
    }
}

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

BytecodeCompiler - Компилятор AST → Instruction[]

using SynLex.Compilation;

public class BytecodeCompiler
{
    private readonly List<Instruction> _instructions = new();
    
    public Instruction[] Compile(AbstractSyntaxTree ast)
    {
        _instructions.Clear();
        CompileNode(ast);
        return _instructions.ToArray();
    }
    
    private void CompileNode(AbstractSyntaxTree node)
    {
        switch (node.Token.Type)
        {
            case TOKEN_NUMBER:
                // Число -> PUSH
                var value = int.Parse(node.Token.Content);
                _instructions.Add(new Instruction
                {
                    OpCode = OpCodes.PUSH,
                    Operands = new object[] { value }
                });
                break;
            
            case TOKEN_PLUS:
                // Бинарный оператор: компилируем операнды, затем операцию
                CompileNode(node.Branches[0]);
                CompileNode(node.Branches[1]);
                _instructions.Add(new Instruction
                {
                    OpCode = OpCodes.ADD,
                    Operands = Array.Empty<object>()
                });
                break;
            
            case TOKEN_MULT:
                CompileNode(node.Branches[0]);
                CompileNode(node.Branches[1]);
                _instructions.Add(new Instruction
                {
                    OpCode = OpCodes.MUL,
                    Operands = Array.Empty<object>()
                });
                break;
            
            case TOKEN_IDENTIFIER:
                // Переменная -> LOAD
                _instructions.Add(new Instruction
                {
                    OpCode = OpCodes.LOAD,
                    Operands = new object[] { node.Token.Content }
                });
                break;
            
            case TOKEN_ASSIGN:
                // Присваивание: значение, затем STORE
                CompileNode(node.Branches[1]); // Правый операнд (значение)
                _instructions.Add(new Instruction
                {
                    OpCode = OpCodes.STORE,
                    Operands = new object[] { node.Branches[0].Token.Content }
                });
                break;
            
            default:
                throw new InvalidOperationException($"Unknown token type: {node.Token.Type}");
        }
    }
}

Использование компилятора

// 1. Парсим код в AST
var lexer = new Lexer().AddRules(...);
var parser = new Parser().AddRules(...);

var tokens = lexer.Analyze("x = 2 + 3").Tokens!;
var ast = parser.Analyze(tokens).Ast!;

// 2. Компилируем AST в байт-код
var compiler = new BytecodeCompiler();
var bytecode = compiler.Compile(ast);

// 3. Выполняем байт-код на VM
var vm = new VirtualMachine()
    .AddRule(new PushRule())
    .AddRule(new AddRule())
    .AddRule(new StoreRule())
    .AddRule(new LoadRule());

var result = vm.Execute(bytecode);

// 4. Результат
Console.WriteLine($"x = {result.Context.Memory["x"]}"); // x = 5

Расширенные возможности

1. Вызов функций

public class CallRule : IVMExecutionRule
{
    public bool CanHandle(byte opCode) => opCode == OpCodes.CALL;
    
    public bool Execute(Instruction instruction, VMExecutionContext context, out string? error)
    {
        error = null;
        
        if (instruction.Operands[0] is not string funcName)
        {
            error = "CALL requires function name";
            return false;
        }
        
        // Сохраняем адрес возврата на стек
        context.Stack.Push(context.InstructionPointer + 1);
        
        // Переходим к функции
        if (!context.Memory.TryGetValue($"func_{funcName}", out var funcAddress))
        {
            error = $"Undefined function: {funcName}";
            return false;
        }
        
        context.InstructionPointer = (int)funcAddress!;
        return true;
    }
}

public class ReturnRule : IVMExecutionRule
{
    public bool CanHandle(byte opCode) => opCode == OpCodes.RET;
    
    public bool Execute(Instruction instruction, VMExecutionContext context, out string? error)
    {
        error = null;
        
        // Извлекаем адрес возврата
        if (context.Stack.Count == 0)
        {
            error = "RET requires return address on stack";
            return false;
        }
        
        var returnAddress = (int)context.Stack.Pop();
        context.InstructionPointer = returnAddress;
        
        return true;
    }
}

2. Условные переходы

public class JumpIfZeroRule : IVMExecutionRule
{
    public bool CanHandle(byte opCode) => opCode == OpCodes.JZ;
    
    public bool Execute(Instruction instruction, VMExecutionContext context, out string? error)
    {
        error = null;
        
        if (context.Stack.Count == 0)
        {
            error = "JZ requires value on stack";
            return false;
        }
        
        var value = context.Stack.Pop();
        var address = (int)instruction.Operands[0]!;
        
        // Переход если значение == 0
        if (value is int intVal && intVal == 0)
        {
            context.InstructionPointer = address;
        }
        else
        {
            context.InstructionPointer++;
        }
        
        return true;
    }
}

3. Отладка и трассировка

public class TraceRule : IVMExecutionRule
{
    public bool CanHandle(byte opCode) => opCode == OpCodes.PRINT;
    
    public bool Execute(Instruction instruction, VMExecutionContext context, out string? error)
    {
        error = null;
        
        if (context.Stack.Count == 0)
        {
            error = "PRINT requires value on stack";
            return false;
        }
        
        var value = context.Stack.Peek(); // Не удаляем со стека
        
        // Вывод для отладки
        Console.WriteLine($"[TRACE] IP={context.InstructionPointer}, Stack.Top={value}");
        
        context.InstructionPointer++;
        return true;
    }
}

Оптимизация байт-кода

Constant folding (свертка констант)

public class ConstantFolder
{
    public Instruction[] Optimize(Instruction[] instructions)
    {
        var optimized = new List<Instruction>();
        
        for (int i = 0; i < instructions.Length; i++)
        {
            var inst = instructions[i];
            
            // Проверяем паттерн: PUSH a, PUSH b, ADD
            if (inst.OpCode == OpCodes.ADD &&
                i >= 2 &&
                instructions[i - 1].OpCode == OpCodes.PUSH &&
                instructions[i - 2].OpCode == OpCodes.PUSH)
            {
                // Вычисляем результат во время компиляции
                var a = (int)instructions[i - 2].Operands[0]!;
                var b = (int)instructions[i - 1].Operands[0]!;
                var result = a + b;
                
                // Заменяем три инструкции на одну
                optimized.RemoveRange(optimized.Count - 2, 2);
                optimized.Add(new Instruction
                {
                    OpCode = OpCodes.PUSH,
                    Operands = new object[] { result }
                });
            }
            else
            {
                optimized.Add(inst);
            }
        }
        
        return optimized.ToArray();
    }
}

Dead code elimination (удаление мертвого кода)

public class DeadCodeEliminator
{
    public Instruction[] Optimize(Instruction[] instructions)
    {
        var reachable = new HashSet<int>();
        var queue = new Queue<int>();
        
        // Начинаем с первой инструкции
        queue.Enqueue(0);
        
        while (queue.Count > 0)
        {
            var ip = queue.Dequeue();
            
            if (ip >= instructions.Length || reachable.Contains(ip))
                continue;
            
            reachable.Add(ip);
            var inst = instructions[ip];
            
            // Добавляем следующие инструкции
            if (inst.OpCode != OpCodes.JMP && inst.OpCode != OpCodes.RET)
            {
                queue.Enqueue(ip + 1);
            }
            
            // Добавляем цели переходов
            if (inst.OpCode == OpCodes.JMP || inst.OpCode == OpCodes.JZ)
            {
                var target = (int)inst.Operands[0]!;
                queue.Enqueue(target);
            }
        }
        
        // Удаляем недостижимые инструкции
        return instructions.Where((_, i) => reachable.Contains(i)).ToArray();
    }
}

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

Простой язык с переменными и арифметикой

using SynLex;
using SynLex.VirtualMachine;

// Коды операций
public static class OpCodes
{
    public const byte PUSH = 0x01;
    public const byte ADD = 0x10;
    public const byte MUL = 0x12;
    public const byte LOAD = 0x20;
    public const byte STORE = 0x21;
}

// Виртуальная машина
var vm = new VirtualMachine()
    .AddRule(new PushRule())
    .AddRule(new AddRule())
    .AddRule(new MulRule())
    .AddRule(new LoadRule())
    .AddRule(new StoreRule());

// Программа: x = 2 + 3; y = x * 4
var program = new[]
{
    // x = 2 + 3
    new Instruction { OpCode = OpCodes.PUSH, Operands = new object[] { 2 } },
    new Instruction { OpCode = OpCodes.PUSH, Operands = new object[] { 3 } },
    new Instruction { OpCode = OpCodes.ADD, Operands = Array.Empty<object>() },
    new Instruction { OpCode = OpCodes.STORE, Operands = new object[] { "x" } },
    
    // y = x * 4
    new Instruction { OpCode = OpCodes.LOAD, Operands = new object[] { "x" } },
    new Instruction { OpCode = OpCodes.PUSH, Operands = new object[] { 4 } },
    new Instruction { OpCode = OpCodes.MUL, Operands = Array.Empty<object>() },
    new Instruction { OpCode = OpCodes.STORE, Operands = new object[] { "y" } }
};

// Выполнение
var result = vm.Execute(program);

if (result.IsSuccess)
{
    Console.WriteLine($"x = {result.Context.Memory["x"]}"); // x = 5
    Console.WriteLine($"y = {result.Context.Memory["y"]}"); // y = 20
}

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

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

if (context.Stack.Count < requiredCount)
{
    error = $"{opName} requires {requiredCount} values on stack";
    return false;
}

2. Документируйте OpCode

/// <summary>
/// OpCode: 0x10 - ADD
/// Stack: [a, b] -> [a + b]
/// </summary>
public const byte ADD = 0x10;

3. Используйте типобезопасность

// ✅ Проверяем типы
if (left is int leftInt && right is int rightInt)
{
    result = leftInt + rightInt;
}
else
{
    error = "Type mismatch";
    return false;
}

4. Логируйте состояние для отладки

public bool Execute(Instruction instruction, VMExecutionContext context, out string? error)
{
    Console.WriteLine($"[{context.InstructionPointer}] {GetOpName(instruction.OpCode)}");
    Console.WriteLine($"Stack: [{string.Join(", ", context.Stack)}]");
    
    // ... выполнение
}

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

После создания виртуальной машины:

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

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