VM Fast – Variante Optimizada (v6, NaN Tagging)
Archivo: src/vm_fast.rs (8694 líneas)
¿Por qué existe?
La VM Fast (v6) es la tercera generación de máquinas virtuales de Forja, diseñada para ofrecer el máximo rendimiento en la interpretación de bytecode. Junto con la VM Direct Threading (vm_jit.rs) es uno de los motores de ejecución de Forja (la VM original vm.rs fue removida en 9.0.0). Utiliza técnicas avanzadas como NaN Tagging, Stack Caching, Flat Var Stack, Quickening adaptativo (PEP 659), y GC generacional stop-the-world para lograr ejecución de alto rendimiento sin perder la seguridad del tipado dinámico.
Estructuras clave
ValorFast — Representación unificada con NaN Tagging
Es la representación de cualquier valor de Forja en 8 bytes (un u64). Aprovecha que IEEE 754 reserva 2^53−1 valores NaN para codificar tipos no-flotante:
- Bits 63-52 = 0x7FF → patrón NaN (quiet NaN)
- Bits 51 → quiet bit
- Bits 50-48 → TAG (3 bits: 0-7)
- Bits 47-0 → payload (48 bits para índices en heaps)
- Si NO es NaN pattern → es un
f64directo (flotante sin conversión)
Tags disponibles
| Tag | Tipo | Payload |
|---|---|---|
TAG_NIL (0) | nulo | — |
TAG_TRUE (1) | booleano (verdadero) | — |
TAG_FALSE (2) | booleano (falso) | — |
TAG_INT (3) | entero i64 | Valor entero (48 bits con signo) |
TAG_OBJ (4) | objeto | Índice en obj_heap |
TAG_STR (5) | texto (string) | Índice en str_heap |
TAG_ARR (6) | arreglo | Índice en array_heap |
TAG_MAP (7) | mapa | Índice en map_heap |
Los flotantes se almacenan como su representación IEEE 754 directa (sin NaN tagging). Cuando se detecta que los bits son NaN, se interpreta como un valor etiquetado.
ForjaFast — Estructura principal de la VM
Es el núcleo de la máquina virtual. Administra el estado completo de ejecución:
| Campo | Tipo | Descripción |
|---|---|---|
ip | usize | Instruction Pointer (puntero al bytecode actual) |
stack | Vec<ValorFast> | Stack principal de operandos |
stack_top | [ValorFast; 4] | Top 4 registros cacheados del stack (stack caching, elimina accesos a Vec) |
top_len | usize | Contador de cuántos registros del cache están ocupados (0..4) |
frame_buffer | [FrmFast; 2048] | Frame buffer pre-asignado para llamadas a funciones (evita alocaciones) |
frame_count | usize | Profundidad actual de llamadas |
flat_vars | Vec<ValorFast> | Flat Var Stack — único Vec para todas las variables de todas las funciones |
base_ptr | usize | Puntero base en flat_vars para la función actual |
obj_heap | Vec<ObjVal> | Heap de objetos (instancias de clase) |
str_heap | Vec<Arc<str>> | Heap de strings (Arcs para compartir entre hilos) |
array_heap | Vec<Vec<ValorFast>> | Heap de arreglos dinámicos |
map_heap | Vec<HashMap<String, ValorFast>> | Heap de mapas (diccionarios) |
exacto_heap | Vec<ExactoVal> | Heap de valores Exacto (BigDecimal: coeficiente i128 + escala u32) |
chan_tx_heap | Vec<Sender<ValorFast>> | Heap de canales de transmisión |
chan_rx_heap | Vec<Receiver<ValorFast>> | Heap de canales de recepción |
thread_heap | Vec<Option<ValorFast>> | Resultados de hilos ejecutados |
native_registry | NativeRegistry | Registro de funciones nativas (FFI) |
sandbox | SandboxRed | Sandbox de red (air-gapped por defecto) |
sym_table | SymbolTable | Tabla de símbolos para interning (comparaciones O(1)) |
ErrFast — Errores en tiempo de ejecución
| Variante | Descripción |
|---|---|
StackUnder(String) | Stack vacío cuando se esperaba un valor |
VarNoDecl(String) | Variable no declarada |
TipoInv(String) | Tipo inválido para la operación |
DivCero | División por cero |
OverflowArit | Overflow aritmético |
FnNoDef(String) | Función no definida |
Limite | Límite de instrucciones excedido (max_inst) |
IdxOut(String) | Índice fuera de rango |
ErrorPropagado(ValorFast) | Error propagado por el operador ? |
API principal de ForjaFast
Constructor y configuración
| Función | Descripción |
|---|---|
new() -> Self | Crea una nueva VM Fast con valores por defecto (air-gapped, contratos activos, max_inst = 10⁷) |
con_sandbox(sandbox) -> Self | Configura el sandbox de red (builder pattern) |
con_contratos(activo) -> Self | Activa/desactiva la verificación de contratos (design by contract) |
set_max_inst(n) | Establece el límite máximo de instrucciones a ejecutar |
Carga y ejecución de bytecode
| Función | Descripción |
|---|---|
cargar_bytecode(bc: Vec<Opcode>) | Carga bytecode, indexa labels, registra funciones, inicializa inline caches y contadores de especialización |
ejecutar() -> Result<(), ErrFast> | Bucle principal de ejecución. Despacha cada opcode mediante match. Soporta especialización adaptativa y fusión de opcodes |
ejecutar_uops() -> Result<(), ErrFast> | Alternativa que expande a micro-opcodes (Uops), optimiza y ejecuta el pipeline de uops para mayor rendimiento |
reset_ejecucion() | Resetea el estado de ejecución (stack, frames, output) pero mantiene el bytecode cargado |
reset() | Reseteo completo incluyendo conteo de instrucciones y output |
Gestión de memoria (Heap Allocators)
| Función | Descripción |
|---|---|
alloc_obj(obj: ObjVal) -> u32 | Aloca un objeto en el heap, retorna índice |
alloc_str(s: Arc<str>) -> u32 | Aloca un string, retorna índice |
alloc_arr(arr: Vec<ValorFast>) -> u32 | Aloca un arreglo, retorna índice |
alloc_map(m: HashMap) -> u32 | Aloca un mapa, retorna índice |
alloc_exacto(e: ExactoVal) -> u32 | Aloca un BigDecimal, retorna índice |
gc_collect() | GC stop-the-world: mark & sweep. Mantiene free lists para reuso inmediato |
Getters del heap
| Función | Descripción |
|---|---|
get_obj(idx) -> &ObjVal | Obtiene referencia a un objeto por índice |
get_obj_mut(idx) -> &mut ObjVal | Obtiene referencia mutable a un objeto |
get_str(idx) -> &Arc<str> | Obtiene referencia a un string |
get_arr(idx) -> &Vec<ValorFast> | Obtiene referencia a un arreglo |
get_arr_mut(idx) -> &mut Vec<ValorFast> | Obtiene referencia mutable a un arreglo |
Gestión de funciones y hot-reload
| Función | Descripción |
|---|---|
registrar_funcion(sym_id, ip, vars_size, version) | Registra una función en la tabla de indirección (FunctionTable) |
reemplazar_funcion(sym_id, ip, vars_size) | Reemplaza una función en caliente (hot reload) |
version_funcion(sym) -> u32 | Obtiene la versión actual de una función |
lookup_func_entry(sym) -> Option<FuncVersion> | Busca entrada de función. Si no existe, cae a funciones nativas |
hot_swap_module(module_id) | Recarga en caliente un módulo completo (solo non-WASM) |
Gestión de sockets
| Función | Descripción |
|---|---|
socket_alloc(state) -> u32 | Aloca un nuevo socket en el socket heap |
socket_get(idx) -> &SocketState | Obtiene estado de un socket |
socket_get_mut(idx) -> &mut SocketState | Obtiene estado mutable de un socket |
socket_cerrar(idx) | Cierra un socket |
Utilidades
| Función | Descripción |
|---|---|
obtener_output() -> &[String] | Devuelve el output acumulado de la ejecución (prints) |
Técnicas de optimización
1. Stack Caching
En lugar de hacer push/pop directamente sobre Vec<ValorFast> (que implica bounds checking y posibles reallocations), la VM mantiene un array fijo de 4 registros (stack_top: [ValorFast; 4]) y un contador (top_len). Las operaciones que afectan los primeros 4 elementos del stack operan sobre este array sin branches impredecibles. Cuando el stack crece más allá de 4, se hace flush a Vec.
2. Flat Var Stack
En lugar de que cada función tenga su propio Vec<ValorFast> para variables locales (causando alocaciones en cada llamada), hay un único Vec global (flat_vars). Cada función recibe un rango [base_ptr, base_ptr + num_vars) dentro de este Vec. En Call se extiende flat_vars (sin alocar un Vec nuevo), y en Return se trunca. Acceso O(1) a cualquier variable.
3. Quickening adaptativo (estilo PEP 659)
La VM monitorea los tipos que fluyen por cada opcode usando contador_especializacion. Cuando un opcode genérico (ej: Add) ve consistentemente los mismos tipos (entero+entero o flotante+flotante), se parchea a sí mismo en bytecode a una versión especializada (ej: AddInt o AddFloat), eliminando los checks de tipo dinámicos. Si los tipos cambian, se des-especializa (fallback al genérico).
4. Inline Caches para GetField/SetField/CallMethod
Cachean la clase del objeto y el índice del campo/método para evitar la búsqueda en la tabla de clases. Indexados por IP del bytecode. Cuando fallan (miss), incrementan un contador; si fallan muchas veces, se des-especializan.
5. NaN Tagging
Los flotantes se almacenan como f64 nativos sin wrapping. Los enteros, booleanos, nulos y referencias a heap (objetos, strings, arreglos, mapas) se codifican en los 2^53−1 valores NaN no utilizados. Esto elimina la necesidad de un enum Rust para Valor (que tendría tag + payload + padding), reduciendo cada valor de 16-24 bytes a exactamente 8 bytes.
6. GC generacional stop-the-world
El GC usa mark & sweep con free lists. Los objetos, strings, arreglos, mapas, valores Exacto, canales e hilos tienen cada uno su propio heap y free list. Se ejecuta cada N alocaciones (gc_threshold). Las marcas se almacenan en vectores paralelos (obj_marked, str_marked, etc.) para no contaminar los datos.
7. Pipeline de Uops
El método ejecutar_uops() expande opcodes compuestos en secuencias de micro-opcodes (Uops), optimiza patrones comunes mediante fusión (peephole optimization), y ejecuta el pipeline resultante. Esto permite que el compilador JIT nativo (jit.rs) pueda traducir Uops directamente a código máquina.
8. Small Integer Cache
Los enteros en el rango [-5, 256] se sirven desde un cache estático thread-local (OnceCell), evitando alocar o construir ValorFast repetidamente para constantes pequeñas muy frecuentes.
¿Cuándo se usa?
Se selecciona automáticamente como el motor de ejecución por defecto. Es el sucesor de la VM original (vm.rs, removida en 9.0.0) y de la VM JIT (vm_jit.rs). La VM Fast ofrece rendimiento cercano al JIT sin la complejidad de generar código máquina en tiempo de ejecución, haciendo de ella la opción óptima para la mayoría de los casos de uso.
Para programas que requieren aún más rendimiento, existe el backend uops + JIT nativo (jit.rs + jit_engine.rs) que compila directamente a código x86-64/ARM64, y los compiladores AOT (aot.rs) que generan binarios nativos estáticos vía ASM o LLVM.