03 / 06

Proyectos

Un solo archivo deja de alcanzar apenas quieres pruebas, dependencias o un binario que puedas copiar a otro lado. Un proyecto te da las tres cosas, y está a un comando de distancia.

nyx init

$ nyx init my-app
nyx v0.33.0 — init
CAPABILITIES.md generado desde ~/.nyx/std (59 archivos escaneados)
Initialized project: my-app
  Created: my-app/
  Next:    cd my-app && nyx run

Eso siembra nueve archivos:

my-app/
  nyx.toml
  .gitignore
  src/main.nx
  AGENTS.md
  CAPABILITIES.md
  docs/nyx/LLM.md
  docs/nyx/guides/write-a-program.md
  docs/nyx/guides/fix-a-compile-error.md
  docs/nyx/guides/report-friction.md

Los primeros tres son el proyecto. Los otros seis son el contexto que un asistente necesita para escribir Nyx acá sin adivinar — el paso 04 trata sobre ellos. Si quieres los documentos en español, nyx init my-app --lang es siembra el juego en español; --agent= y --sdd agregan más, y el paso 04 cubre los dos.

nyx.toml

[package]
name = "my-app"
version = "0.1.0"
main = "src/main.nx"

[dependencies]

name es además el nombre del binario que produce nyx build, y la línea que el .gitignore sembrado ya ignora. main es el punto de entrada al que vuelve cada herramienta cuando no le das un archivo.

Compilar y ejecutar

$ nyx build
nyx v0.33.0 — build
-> building my-app v0.1.0
   compiling src/main.nx
✓ Built: my-app
build complete

Eso deja ./my-app al lado de nyx.toml: un ejecutable nativo que puedes copiar a otra máquina de la misma arquitectura. nyx run hace el mismo build y después lo ejecuta.

Los argumentos después de nyx run van a tu programa, no a Nyx. Cuando un argumento se podría confundir con una bandera, antepón --:

fn main() {
    let args: Array = get_args()
    var i: int = 0
    while i < args.length() {
        let a: String = args[i]
        print(int_to_string(i) + ": " + a)
        i = i + 1
    }
}
$ nyx run -- --verbose one
nyx v0.33.0 — run
-> building my-app v0.1.0
   compiling src/main.nx
✓ Built: my-app
0: ./my-app
1: --verbose
2: one

El elemento 0 es la ruta del binario, así que tus argumentos empiezan en 1. Fíjate en la anotación de tipo de a: un elemento leído de un Array sin anotarlo se trata como int, e imprimirlo te da un número en vez del texto.

Pruebas

nyx test corre todos los tests/*.nx. Una prueba es un bloque test "nombre" { … } — no una función:

test "greeting is not empty" {
    let msg: String = "hello"
    assert(msg.length() > 0)
}

test "sum works" {
    assert(1 + 1 == 2)
}
$ nyx test
=== Nyx Test Runner ===
  Found 1 test file(s)

  PASS  tests/greet_test.nx (2 tests)

===================================
  Files:  1 passed, 0 failed (1 total)
  Tests:  2 passed, 0 failed (2 total)
  Status: ALL TESTS PASSED
===================================

La trampa que conviene conocer antes de pisarla. Un archivo lleno de funciones llamadas test_algo() se encuentra, se cuenta, y después se saltea — el corredor dice No files with test blocks found y sale contento. Nada falló, porque nada corrió. Si tu conteo de pruebas es cero y esperabas pruebas, es por esto: los bloques son test "nombre" { … }, siempre.

Ten en cuenta también que assert() aborta el proceso en el primer fallo. Dentro de nyx test eso está resuelto — reporta por prueba y sale al final — pero en un programa tuyo, una aserción que falla es el final del programa.

Dependencias

$ nyx add some-package
$ nyx add some-package --from https://example.com/some-package.git

La dependencia se escribe en [dependencies] dentro de nyx.toml, y nyx build clona lo que falte en packages/ — que el .gitignore sembrado ignora, asumiendo que prefieres traerlas a vendorizarlas. Borrar esa línea y commitear packages/ es la otra decisión razonable: hace el build reproducible sin red.

La biblioteca estándar no necesita nada de esto. Módulos como std/json, std/http o std/toml vienen con la toolchain y se usan con un import "std/json" y nada más.

Compilación separada: [lib] modules

Un proyecto puede declarar qué módulos de src/ se compilan como unidades aparte, una vez, y se reutilizan entre builds en vez de inlinearse de nuevo en cada nyx build:

[lib]
modules = ["src/util", "src/geo"]

Cada entrada se escribe como en el import (sin .nx), y nunca un módulo de std/ — el manifiesto rechaza las dos cosas. nyx build compila cada módulo declarado a target/nyx-lib/ y reutiliza el objeto mientras ni el módulo ni nada de su cierre de imports (el prelude y los std/ que use, incluidos) haya cambiado, y tampoco el compilador ni las flags de build. Tocar un módulo lo recompila y, en cascada, a los módulos de [lib] que lo importan — main.nx siempre se compila de nuevo, contra las interfaces vigentes. Esto solo aplica a targets nativos: con --target wasm32-wasi los módulos de [lib] se inlinean como cualquier import normal.

Lo que cruza la frontera es una firma, no un cuerpo — como un header de C: funciones exportadas (export fn/pub fn, con sus tipos de parámetros y de retorno) y struct/enum exportados. Una fn genérica, los métodos de un impl (con o sin trait) y un trait necesitan su cuerpo donde se los llama — la monomorfización y el despacho de métodos no pueden cruzar la frontera de un objeto compilado — así que un módulo de [lib] que declare cualquiera de estos se inlinea como siempre (el programa es el mismo), con el aviso NYX0302.

Límite conocido. El chequeo de tipos de los argumentos en la frontera todavía no existe: llamar a una función compilada aparte con un tipo de argumento equivocado no se detecta hoy en tiempo de compilación.

nyx test también usa [lib]: compila (o reutiliza) las bibliotecas una sola vez por suite y enlaza cada archivo de prueba contra ellas, en vez de volver a compilar todo lo que importa en cada uno. Un archivo de prueba que es él mismo un módulo de [lib] se compila entero, con sus bloques test. Con --coverage se inlinea todo, como antes.

Dos comandos que vas a querer después

$ nyx info
nyx v0.33.0 — info
Project: my-app
Version: 0.1.0
Main:    src/main.nx
Mode:    GC (default)

Y, después de actualizar la toolchain, nyx update --sync-docs — corrido dentro del proyecto — refresca los documentos sembrados a la versión que tienes ahora, guardando un .bak de todo lo que reemplaza. Qué refresca, y por qué importa, es el paso siguiente.