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:

Tags disponibles

TagTipoPayload
TAG_NIL (0)nulo
TAG_TRUE (1)booleano (verdadero)
TAG_FALSE (2)booleano (falso)
TAG_INT (3)entero i64Valor 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:

CampoTipoDescripción
ipusizeInstruction Pointer (puntero al bytecode actual)
stackVec<ValorFast>Stack principal de operandos
stack_top[ValorFast; 4]Top 4 registros cacheados del stack (stack caching, elimina accesos a Vec)
top_lenusizeContador 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_countusizeProfundidad actual de llamadas
flat_varsVec<ValorFast>Flat Var Stack — único Vec para todas las variables de todas las funciones
base_ptrusizePuntero base en flat_vars para la función actual
obj_heapVec<ObjVal>Heap de objetos (instancias de clase)
str_heapVec<Arc<str>>Heap de strings (Arcs para compartir entre hilos)
array_heapVec<Vec<ValorFast>>Heap de arreglos dinámicos
map_heapVec<HashMap<String, ValorFast>>Heap de mapas (diccionarios)
exacto_heapVec<ExactoVal>Heap de valores Exacto (BigDecimal: coeficiente i128 + escala u32)
chan_tx_heapVec<Sender<ValorFast>>Heap de canales de transmisión
chan_rx_heapVec<Receiver<ValorFast>>Heap de canales de recepción
thread_heapVec<Option<ValorFast>>Resultados de hilos ejecutados
native_registryNativeRegistryRegistro de funciones nativas (FFI)
sandboxSandboxRedSandbox de red (air-gapped por defecto)
sym_tableSymbolTableTabla de símbolos para interning (comparaciones O(1))

ErrFast — Errores en tiempo de ejecución

VarianteDescripció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
DivCeroDivisión por cero
OverflowAritOverflow aritmético
FnNoDef(String)Función no definida
LimiteLí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ónDescripción
new() -> SelfCrea una nueva VM Fast con valores por defecto (air-gapped, contratos activos, max_inst = 10⁷)
con_sandbox(sandbox) -> SelfConfigura el sandbox de red (builder pattern)
con_contratos(activo) -> SelfActiva/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ónDescripció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ónDescripción
alloc_obj(obj: ObjVal) -> u32Aloca un objeto en el heap, retorna índice
alloc_str(s: Arc<str>) -> u32Aloca un string, retorna índice
alloc_arr(arr: Vec<ValorFast>) -> u32Aloca un arreglo, retorna índice
alloc_map(m: HashMap) -> u32Aloca un mapa, retorna índice
alloc_exacto(e: ExactoVal) -> u32Aloca un BigDecimal, retorna índice
gc_collect()GC stop-the-world: mark & sweep. Mantiene free lists para reuso inmediato

Getters del heap

FunciónDescripción
get_obj(idx) -> &ObjValObtiene referencia a un objeto por índice
get_obj_mut(idx) -> &mut ObjValObtiene 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ónDescripció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) -> u32Obtiene 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ónDescripción
socket_alloc(state) -> u32Aloca un nuevo socket en el socket heap
socket_get(idx) -> &SocketStateObtiene estado de un socket
socket_get_mut(idx) -> &mut SocketStateObtiene estado mutable de un socket
socket_cerrar(idx)Cierra un socket

Utilidades

FunciónDescripció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.