Vesta tiene un sistema de módulos completo que permite dividir código en múltiples archivos, importar símbolos, organizar paquetes (directorios) y distribuir librerías precompiladas con su interfaz binaria estable.
Documentos relacionados:
- [[CompilacionCondicional]] —
@Targetpara dependencias platform-specific - [[CargaDinamica]] —
loadmodule/unloadmoduleen runtime - [[Sandbox]] — capabilities sobre módulos cargados
// math_utils.vx
public i32 add(i32 a, i32 b) { return a + b; }
public const i32 PI_FIXED = 314;
public class Counter {
public i32 value;
public Counter(i32 init) { this.value = init; }
public i32 inc() { this.value = this.value + 1; return this.value; }
}
private i32 internal_helper() { return 42; } // NO se exporta// main.vx
import "math_utils"; // namespace cualificado
import "math_utils" as mu; // alias
import "math_utils" only add, PI_FIXED, Counter; // al scope local
i32 main() {
i32 a = math_utils.add(10, 20); // vía namespace
i32 b = mu.add(1, 2); // vía alias
i32 c = add(3, 4); // vía only (sin prefijo)
Counter ctr = new Counter(0);
return ctr.inc() + c;
}Compilación:
vm --vx main.vx -o programaEl compilador detecta los import automáticamente, resuelve las
dependencias en orden topológico, cachea la interfaz binaria de cada
una (archivos .vxi) y produce un único programa.velb con todo el
código necesario.
import "path"; // (1) plain: namespace = último segmento
import "path" as alias; // (2) plain con alias
import "path" only A, B; // (3) selectivo: A, B al scope local
import "path" only A as A2, B; // (4) selectivo con renombrado
public import "path"; // (5) re-export completo
public import "path" only A, B; // (6) re-export selectivo
@Target("os:windows") import "path"; // (7) condicional (ver CompilacionCondicional)Notas:
- El
pathes siempre un string literal. No se admite variable. - Los paths usan
/como separador (multiplataforma). - No se incluye la extensión
.vx— el resolvedor la añade automáticamente. - Para cargar un
.velben runtime, verloadmodule(path)en [[CargaDinamica]].
Para import "a/b/c"; el resolvedor busca en este orden:
- Directorio del importador:
<importer_dir>/a/b/c.vx(módulo single-file) o<importer_dir>/a/b/c/mod.vx(paquete-directorio). - Search paths: cada entrada de la variable de entorno
VX_PATH(separador:en POSIX,;en Windows) y cualquier--vx-path /diradicional. - Paquetes locales:
./vx_modules/a/b/c.vx(reservado para el futuro gestor de paquetes). - Standard library:
$VX_HOME/stdlib/a/b/c.vx(solo si el path empieza constd/).
El primer match gana. Si nada coincide, se reporta un error claro con la lista de paths probados.
Los símbolos son privados al módulo por defecto. El programador marca explícitamente lo que exporta:
public i32 add(i32 a, i32 b) { return a + b; } // exportado
public class Counter { ... } // exportado
public const i32 MAX_USERS = 100; // exportado (con valor inlineable)
private i32 internal_helper() { return 42; } // NO exportado
i32 also_internal() { return 0; } // sin keyword: tampoco exportadoAplicable a: class, struct, enum, typedef, using, funciones
libres y globales (const/var).
Si un consumidor escribe import "lib" only internal_helper;, el
compilador lo rechaza con un mensaje claro:
error: el modulo 'lib' no exporta 'internal_helper' (es privado o no existe)
Un paquete es un directorio con un fichero de entrada mod.vx más
otros .vx adicionales.
proyecto/
├── main.vx
└── mypkg/
├── mod.vx ; entry point del paquete
├── utils.vx
└── internal.vx
// mypkg/mod.vx
// Re-export plain: TODOS los símbolos públicos de utils son visibles
// como si estuvieran declarados directamente en mod.vx.
public import "utils";
// También se puede hacer re-export selectivo:
public import "internal" only helpful_function;
// Y declaraciones propias del entry:
public i32 mod_value() { return 100; }// mypkg/utils.vx
// El path es relativo al directorio del importador (mypkg/).
// Desde mod.vx, "import 'utils'" resuelve a mypkg/utils.vx.
public i32 utility_a() { return 1; }
public i32 utility_b() { return 2; }// proyecto/main.vx
import "mypkg" only utility_a, utility_b, mod_value, helpful_function;
i32 main() {
return utility_a() + utility_b() + mod_value() + helpful_function();
}Cuando el resolvedor encuentra mypkg/mod.vx, el nombre lógico del
módulo es mypkg (no mod). El paquete se identifica por su
directorio, no por el fichero entry.
Un módulo puede "re-exportar" símbolos de sus dependencias para que los consumidores no necesiten conocer la estructura interna del paquete.
// mid.vx
public import "base" only foo, bar; // foo y bar visibles como propios de midAhora import "mid" only foo; funciona aunque foo esté declarado en
base.vx.
// mid.vx
public import "base"; // TODOS los símbolos públicos de base son re-exportadosEl compilador itera los símbolos públicos de base, los inyecta en
mid y los marca como re-exportados. El .vxi de mid los expone
como propios.
Cuándo usar plain vs only:
public import "base";— el paquete entero forma parte de la API pública.public import "base" only A, B;— solo unos símbolos específicos se reexponen.
El compilador mantiene una caché transparente para evitar recompilar módulos cuyo source no haya cambiado. Activa por defecto.
Todo vive dentro de un único directorio .cache/, repartido en un cajón por
tipo de artefacto. Nada se escribe junto al fuente: el árbol de fuentes sólo
contiene fuentes, y limpiar la caché entera es borrar ese directorio.
Por cada módulo (excepto el root):
.cache/ir/<huella>_<módulo>.vxi— interfaz binaria del módulo (tipos y firmas públicas)..cache/ir/<huella>_<módulo>.vxir— IR SSA serializado para reutilizar al hacer el merge..cache/vel/<huella>_<módulo>.vel— bytecode standalone del módulo (formato distribuible)..cache/facts/y.cache/analysis/— lo que el ASA supo del módulo y los análisis ya calculados.
La huella delante del nombre es la de la ruta canónica del fuente: es lo que permite que dos módulos homónimos de carpetas distintas compartan cajón sin pisarse.
Adicionalmente:
.cache/projects/<root_hash>.vpc— el.velbfinal completo. Si todos los módulos del proyecto coinciden por source_hash, el.velbse copia directamente (omite compile y link, hit instantáneo).
La raíz sale de subir desde el directorio de trabajo hasta la primera marca:
VX_CACHE_DIR si está definida gana siempre; si no, el manifiesto de un paquete
(vx.toml / vx.json) o la raíz de la copia de trabajo (.git), lo que
aparezca antes; y sin ninguna, el propio directorio de trabajo. Con la variable
puesta, varios proyectos comparten la caché de las librerías comunes.
Que .git cuente como marca es lo que hace que un árbol no se llene de cachés:
la mayoría de los directorios desde los que se invoca al compilador no son un
paquete —una carpeta de pruebas, de herramientas, de ejemplos—, y sin ella cada
uno estrenaría la suya.
Cada cajón declara además su alcance, que dice si lo que guarda sirve en otra máquina:
| cajón | alcance | por qué |
|---|---|---|
ir/, facts/, analysis/, vel/, ctpe/, pkg/ |
portable | se deriva del fuente, de la versión del compilador y del objetivo, y los tres entran en la clave |
vxdbg/ |
mixto | packs/ va direccionado por contenido y viaja; roots/ son apuntadores con rutas absolutas y no |
projects/ |
local | sus entradas apuntan a los artefactos de este árbol por dónde están |
work/, tmp/ |
transitorio | trabajo a medias; se puede borrar en cualquier momento |
Publicar una librería precompilada es, por tanto, copiar el .vx con su cajón
ir/; y archivar o compartir una caché es quedarse con la mitad portable.
Tres niveles independientes:
- Source hash: si el
.vxdel módulo cambió (cualquier byte), su caché se invalida y se recompila. - Dependencia transitiva: si una dependencia DIRECTA del módulo cambió su
abi_hash(interfaz pública), el módulo también se recompila aunque su source no haya cambiado. - Compiler version hash: si el binario del compilador cambió (otro build, otra versión), todas las cachés se invalidan automáticamente.
Esto garantiza builds reproducibles bit-perfect: dos compilaciones del
mismo source con el mismo compilador producen el mismo .velb byte
por byte.
| Variable | Efecto |
|---|---|
VX_NO_CACHE=1 |
Desactiva toda la caché (compile y link siempre frescos). |
VX_NO_PROJECT_CACHE=1 |
Desactiva solo el .vpc (project cache); sigue cacheando .vxi/.vxir. |
VX_CACHE_DIR=/path |
Redirige la caché de .vxi/.vxir/.vel al directorio dado (por defecto: junto al source). Habilita compartir caché entre proyectos para librerías comunes. |
VX_VERBOSE_CACHE=1 |
Imprime [vx-cache] hit/miss por módulo. |
VX_VERBOSE_PROJECT_CACHE=1 |
Imprime información del project cache. |
VX_VERBOSE_COMPILE=1 |
Imprime el plan topológico y el progreso de compilación de cada módulo. |
Por defecto el compilador usa std::thread::hardware_concurrency()
limitado a 8 threads como máximo. Los módulos del MISMO nivel
topológico (sin dependencias entre sí) se compilan concurrentemente;
los de niveles distintos respetan el orden mediante un barrier por
nivel.
Override vía VX_PARALLEL_COMPILE=N:
VX_PARALLEL_COMPILE=1 vm --vx main.vx -o prog # secuencial forzado
VX_PARALLEL_COMPILE=4 vm --vx main.vx -o prog # exactamente 4 threads
# Sin la variable: auto (hardware_concurrency limitado a 8)Cuándo usar secuencial: para diagnóstico, o cuando se necesita output
determinista de VX_VERBOSE_COMPILE. En modo paralelo el orden de
las líneas [Lx] puede variar, pero un mutex interno garantiza que
cada línea sea atómica (no se entremezclan a nivel byte).
Cuándo subir el conteo: builds grandes con más de 10 módulos en un mismo nivel topológico. Por encima de 8 threads aparecen retornos decrecientes por contención.
El .vxi es un blob binario que describe la API pública de un módulo.
Es lo único que un consumidor necesita conocer de la dependencia — el
source .vx puede mantenerse privado, distribuyendo solo .vxi y
.velb (librería precompilada).
[+0 ] u32 magic = 'VXI'
[+4 ] u16 format_version
[+6 ] u16 reservado
[+8 ] u64 abi_hash (FNV-1a sobre símbolos y firmas públicas)
[+16 ] u64 source_hash (FNV-1a sobre el .vx fuente)
[+24 ] u64 compiler_version_hash (auto-invalidación cross-version)
[+32 ] u32 symbol_count
[+36 ] u32 string_pool_offset
[+40 ] u32 dep_count (dependencias directas para invalidación transitiva)
[+44 ] u32 dep_table_offset
[+48 ] SymbolEntry[symbol_count] (tamaño variable; ver tipos abajo)
[ +N ] DepEntry[dep_count] (16 bytes cada uno: name_off + name_len + abi_hash)
[ +M ] String pool (nombres y payload referenciados por offset)
| Kind | Payload |
|---|---|
FUNCTION |
nombre, lib (extern), mangled_label, return_type, param_types[], param_names[] |
STRUCT |
nombre, size, align, fields[] (con bit_offset/bit_width) |
CLASS |
nombre, super, interfaces[], size, fields[], methods[] (con vtable_index/flags) |
ENUM |
nombre, variants[] (con payload_types[]) |
TYPEDEF_ALIAS |
nombre, underlying_type_string |
TYPEDEF_NEW |
nombre, underlying, is_opaque, align_override, from_conversions[], to_conversions[] |
GLOBAL_VAR |
nombre, type, flags (is_const, is_public), has_init_value, init_value (i64) |
Los símbolos privados se filtran al emitir (no aparecen en el .vxi).
Soporte opcional vía OpenSSL EVP_DigestSign/Verify (RSA-SHA256 o
ECDSA-SHA256):
vm --sign-velb prog.velb --sign-key private.pem -o prog.signed.velb
vm --verify-velb prog.signed.velb --verify-key public.pem
# exit 0 si la firma es válida, 1 si es inválida o el archivo no está firmadoEl footer firmado se añade al final del .velb con magic VSIG. El
loader lo ignora al cargar; un .velb firmado se ejecuta normalmente
sin verificación explícita.
Sintaxis al estilo C++ para agrupar declaraciones dentro de un mismo fichero. Soporta anidamiento. El mangling automático evita colisiones.
namespace ui {
public class Button {
public string label;
public Button(string l) { this.label = l; }
}
namespace internal {
public i32 layout_helper() { return 0; }
}
}
namespace audio {
public class Button { // OK: distinto namespace, no colisiona
public f64 volume;
}
}
i32 main() {
ui.Button b = new ui.Button("OK");
audio.Button a = new audio.Button();
i32 x = ui.internal.layout_helper();
return 0;
}Internamente las clases se manglean a ui__Button, audio__Button,
ui__internal__layout_helper, etc. El acceso siempre se hace con .
(nunca con ::) para mantener consistencia con el acceso a campos.
Cuando se hace import "lib"; (plain), los TIPOS públicos del módulo
quedan accesibles como lib.TypeName:
// lib.vx
public class Counter { public i32 value; public Counter(i32 v) { this.value = v; } }
public struct Point { i32 x; i32 y; }
public typedef i64 UserId;// main.vx
import "lib";
i32 main() {
lib.Counter c = new lib.Counter(42);
lib.Point p = {.x = 1, .y = 2};
lib.UserId uid = 0xDEADBEEF;
return c.value;
}Diferencia con only Counter: el cualificado lib.Counter permite
acceder al tipo SIN traerlo al scope local. Útil cuando hay potencial
colisión con tipos del consumidor.
Permite reducir el .velb final eliminando dependencias cuyos símbolos
importados no se referencien. Desactivado por defecto.
VX_TREE_SHAKE=1 vm --vx main.vx -o progReglas:
- Solo se eliminan dependencias importadas con
onlyy con TODOS los símbolos sin usar. - Imports plain (
import "lib";) nunca se eliminan (referencia opaca). - Dependencias que declaran clases nunca se eliminan (
__module_initpuede tener efectos colaterales de registro).
Beneficio típico: en una librería con 3 funciones y ninguna usada, el
.velb baja del orden de 2.2 KB a 1.2 KB.
error: modulo 'mypkg/utils' no encontrado. Paths probados:
- main.vx/../mypkg/utils.vx
- main.vx/../mypkg/utils/mod.vx
- VX_HOME/stdlib/mypkg/utils.vx
Causas posibles:
- Typo en el path. Usa
/como separador y sin extensión. - El fichero existe pero está en otro directorio. Añadirlo vía
VX_PATH=/dir1:/dir2o--vx-path /dir. - Para stdlib, prefijar con
std/.
error: el modulo 'lib' no exporta 'helper' (es privado o no existe)
El símbolo helper está declarado en lib.vx pero sin public. O no
existe. Verificar con grep "public.*helper" lib.vx.
error: ciclo de herencia detectado: la clase 'A' aparece en su propia cadena de superclases
Una clase A : B cuya cadena de superclases vuelve a A (por ejemplo
A : B, B : C, C : A). El detector soporta hasta 256 niveles de
profundidad.
[vx-cache] miss (transitivo): dep 'lib' cambio (abi_hash old=0x... new=0x...)
Tras cambiar lib.vx modificando su interfaz pública, los
consumidores detectan el desajuste y se recompilan. Solo informativo;
el build continúa.
El linker actual fusiona los IRs de cada módulo en uno solo antes de
emitir el .velb final. No puede consumir directamente un .velb ya
compilado para incluirlo en otro .velb. Consecuencia práctica:
- Para distribuir una librería precompilada sin source, el consumidor
necesita el
.vxoriginal. El.vxipor sí solo no basta porque carece de los cuerpos IR necesarios para el merge. - Alternativa: usar
loadmodule("lib.velb")en runtime (ver [[CargaDinamica]]). El módulo se carga dinámicamente en el proceso vivo, sus clases se registran vía__module_inity el consumidor las usa por reflexión (forName,newInstance,invoke).
Cerrar esta limitación requiere un linker capaz de parsear .velbs
ya compilados y resolver símbolos cross-velb. Es trabajo significativo
que no afecta al uso normal del lenguaje.