Виртуальная машина
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
);
}
Правило должно:
- Проверить, может ли обработать
opCode - Выполнить операцию, изменяя
context - Вернуть
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)}]");
// ... выполнение
}
Следующие шаги
После создания виртуальной машины:
- Изучите Примеры для готовых VM
- Ознакомьтесь с Компиляцией для генерации байт-кода
- См. Архитектуру для понимания внутреннего устройства
© 2026 Alexander Izmailov. Все права защищены.