Renderizado de SPEC.md, el documento normativo del repositorio: ante cualquier duda, manda el archivo fuente de la versión etiquetada.

Especificación del lenguaje raylang

Versión del lenguaje: 1.0.0 (esta especificación versiona con el lenguaje; ver §12).

Este documento es normativo: define qué es un programa raylang válido y qué hace. Los otros dos documentos del proyecto no lo son: DESIGN.md es la crónica de diseño (el porqué de cada decisión, fase a fase) y el libro (book/) es la pedagogía (cómo se construyó). Ante un conflicto, manda esta SPEC; un conflicto entre la SPEC y la implementación es un bug de una de las dos y debe resolverse explícitamente.

Conformidad. raylang tiene tres motores que deben producir comportamiento observable idéntico (stdout, errores, código de salida) para todo programa determinista:

  1. la máquina virtual de bytecode — el motor de producto (ray run);
  2. el binario nativo (ray build --native), que transpila el programa a Rust y lo compila a código máquina — su salida es byte-idéntica a la de la VM (corpus de paridad en la suite);
  3. el intérprete de árbol (ray run --interp), que es el oráculo secuencial de desarrollo.

La suite lo verifica por oráculo cruzado en las tres direcciones. Las excepciones están acotadas y listadas: la concurrencia (§9) y la E/S asíncrona no existen en el intérprete (da un error limpio "requires the VM"), y los subsistemas que un binario nativo excluye a propósito (--without …, ver REFERENCE.md §14) responden con un error de ejecución explícito en vez de silencio.

Notación de gramática: EBNF con { x } = cero o más, [ x ] = opcional, | = alternativa, 'x' = literal. Las producciones léxicas (§1) operan sobre caracteres; las sintácticas (§2, §4– §6) sobre tokens.


1. Léxico

La entrada es texto UTF-8 (una entrada con UTF-8 inválido se rechaza al leer el archivo). El lexer produce tokens; cada token lleva posición 1-basada (línea, columna) y su longitud en caracteres. Ningún token cruza líneas.

2. Programas, módulos e ítems

programa    = { item } ;
item        = import | from_import | [ anotaciones ] [ 'pub' ] declaracion ;
declaracion = funcion | struct | enum | trait | impl | const ;
import      = 'import' ruta_modulo [ 'as' IDENT ] ';' ;
from_import = [ 'pub' ] 'from' ruta_modulo 'import' nombre [ 'as' IDENT ]
              { ',' nombre [ 'as' IDENT ] } [ ',' ] ';' ;
ruta_modulo = IDENT { '/' IDENT } ;
anotaciones = { '@' IDENT [ '(' IDENT { ',' IDENT } ')' ] } ;

3. Tipos

tipo = 'int' | 'float' | 'bool' | 'string' | 'char' | 'bytes' | 'ptr'
     | 'u8' | 'u32' | 'u64'
     | '[' tipo ']'
     | '(' tipo ',' tipo { ',' tipo } ')'
     | 'fn' '(' [ tipo { ',' tipo } ] ')' [ '->' tipo ]
     | 'dyn' IDENT { '+' IDENT }
     | IDENT [ '<' tipo { ',' tipo } '>' ]        (* struct/enum/Map/Channel/Task/param de tipo *)
     | IDENT '.' IDENT [ '<' … '>' ] ;            (* tipo calificado por módulo: M.Punto *)

4. Declaraciones

funcion  = 'fn' IDENT [ genericos ] '(' [ param { ',' param } ] ')' [ '->' tipo ] bloque ;
param    = IDENT ':' tipo ;
genericos= '<' IDENT [ ':' IDENT { '+' IDENT } ] { ',' … } '>' ;
struct   = 'struct' IDENT [ genericos ] '{' { IDENT ':' tipo ',' } '}' ;
enum     = 'enum' IDENT [ genericos ] '{' variante { ',' variante } [ ',' ] '}' ;
variante = IDENT [ '(' tipo { ',' tipo } ')' ] ;
trait    = 'trait' IDENT [ '<' IDENT { ',' IDENT } '>' ] '{' { firma_metodo } '}' ;
firma_metodo = 'fn' IDENT '(' 'self' { ',' param } ')' [ '->' tipo ] ( ';' | bloque ) ;
impl     = 'impl' [ genericos ] IDENT [ '<' tipo … '>' ] 'for' tipo '{' { metodo } '}' ;
const    = 'const' IDENT ':' tipo '=' literal ';' ;
extern   = 'extern' STRING [ 'blocking' ] '{' { firma_extern } '}' ;
firma_extern = 'fn' IDENT '(' [ param { ',' param } ] ')' [ '->' tipo ] ';' ;

5. Sentencias

bloque    = '{' { sentencia } [ expresion ] '}' ;   (* la expresión final sin ';' es el valor *)
sentencia = 'let' ( IDENT | '(' IDENT ',' IDENT { ',' IDENT } ')' ) [ ':' tipo ] '=' expresion ';'
          | 'var' IDENT [ ':' tipo ] '=' expresion ';'
          | destino '=' expresion ';'
          | 'return' [ expresion ] ';'
          | 'while' '(' expresion ')' bloque
          | 'for' patron_for 'in' iterable bloque
          | expresion ';'
          | expresion_con_bloque ;                   (* if/match/bloque como sentencia, sin ';' *)
destino   = IDENT | expresion_postfija '.' IDENT | expresion_postfija '[' expresion ']' ;
patron_for= IDENT | '(' IDENT ',' IDENT ')' ;
iterable  = expresion [ '..' expresion ] ;

6. Expresiones

6.1 Precedencia (de menor a mayor)

NivelOperadoresAsociatividad
1|> (pipeline)izquierda
2||izquierda
3&&izquierda
4| (OR bit a bit)izquierda
5^ (XOR)izquierda
6& (AND)izquierda
7== !=izquierda
8< <= > >=izquierda
9<< >>izquierda
10+ -izquierda
11* / %izquierda
12as (cast)izquierda
13- ! ~ (unarios)prefijo
14llamada f(…), campo/método x.f, índice x[i], ?postfijo, izquierda
15primarios

&& y || cortocircuitan. Los operandos se evalúan de izquierda a derecha.

6.2 Primarios

Literales (§1), identificadores, (expr) (agrupación), tuplas (a, b, …), arreglos [a, b, c[,]], literales de struct Nombre { campo: expr, … } (también calificado M.Nombre { … }), funciones anónimas fn(params) [-> R] bloque, if, match, bloques.

6.3 Llamadas, UFCS y métodos

recv.f(args) resuelve, en orden: (1) construcción de variante de enum, (2) campo del struct de tipo función, (3) método de trait del tipo del receptor (incluye el trait object y el parámetro acotado), (4) UFCS: f(recv, args) con f función libre o builtin (el receptor participa en la inferencia de genéricos). Todo se resuelve estáticamente en el checker (salvo el despacho de dyn, que es una llamada a través del objeto).

6.4 Pipelines

x |> f(a, b)f(x, a, b); x |> ff(x). Precedencia mínima, asociativo a la izquierda; el operando derecho es un objetivo de llamada (nivel 14). Azúcar puro del parser.

6.5 Casts

e as T con T ∈ {int, float, char, u8, u32, u64}: float as int trunca hacia cero (saturando en los extremos de int); int as float es la conversión IEEE más cercana; char as int/u* es el code point; int as char valida el code point (error de ejecución si no lo es); intu* y u*u* truncan al ancho destino (bits bajos).

6.6 Interpolación

"a${x}b""a" + to_string(x) + "b". Cada ${expr} debe ser de tipo imprimible (§10). El $ solo interpola seguido de {; en cualquier otra posición es un carácter literal.

6.7 El operador ?

e? con e: Result<T, E> en función que devuelve Result<U, E2>: si Ok(v) produce v; si Err(err), retorna Err(err) si E == E2, o Err(E2.desde(err)) si existe impl From<E> for E2 (si no, error de tipos). Análogo para Option<T> en función que devuelve Option<U>. ? no cruza OptionResult.

7. Sistema de tipos (reglas)

8. Semántica de evaluación

9. Concurrencia (VM y binario nativo; no en el intérprete)

Modelo CSP → actores con aislamiento de heap: cada fibra tiene su propio heap y la única comunicación entre fibras son los canales, que transfieren el valor. No hay estado mutable compartido → data-race freedom por construcción, sin ownership en el sistema de tipos. En el intérprete estas primitivas dan un error limpio ("requires the VM").

Ejecución. El scheduler es M:N: M fibras sobre N hilos worker.

Garantías, y qué depende de N. La semántica de cada primitiva (abajo) es la misma con cualquier N. Lo que solo se garantiza con N = 1 (--deterministic o RAYLANG_THREADS=1) es el orden observable entre fibras: intercalado de la salida, orden de despertar y el resultado de select entre varios canales listos a la vez. Un programa cuya salida deba ser reproducible debe fijar N = 1 o sincronizar por canales. La cancelación es cooperativa (actúa en los puntos de cesión), nunca preemptiva.

10. Builtins y prelude (superficie estable)

La superficie estable tiene tres capas, y solo las dos primeras las fija esta SPEC:

  1. Global (sin importar nada): los builtins visibles y las funciones/traits del prelude (escritas en raylang e inyectadas salvo redefinición del usuario, que puede hacer override).
  2. std/… (opt-in con import std/…;): la biblioteca estándar, embebida en el binario y versionada con el lenguaje. Importar un módulo es además una pista de capacidad legible ("este archivo toca disco / red / procesos").
  3. Paquetes (net, db, web, rpc, …): fuera de esta SPEC; versionan por separado y se instalan con el gestor de paquetes.

Los primitivos __nombre son internos e inestables (no los uses): son el borde con el host sobre el que se escriben las capas 1 y 2.

10.1 Global

10.2 La biblioteca estándar std/

Va embebida en el binario: import std/math; funciona sin que std/ exista en disco. Se usa calificada por el último segmento de la ruta (math.sqrt(2.0), set.add(s, x)).

MóduloQué cubre
std/fsdisco: leer/escribir (texto y bytes), append, exists, list_dir, metadatos, copiar/renombrar/borrar, directorios, handles con read_line/write/write_bytes/read_bytes/seek/sync (durabilidad: fsync) candados consultivos try_lock/unlock (flock), stat (lstat: detecta symlinks), chmod, y watch/next_event (cambios por eventos de kernel; la fibra aparca)
std/nettransporte: TCP (tcp_connect/tcp_listen/tcp_accept/local_port), I/O de sockets en texto y bytes, TLS (tls_connect/tls_accept/tls_upgrade)
std/processejecución de procesos del SO sin shell (argv tipado): run, el builder cmd y el modo streaming stream —con stdin escribible sobre un hijo vivo (stdin_pipe/write/close_stdin) para sesiones persistentes— (§ REFERENCE.md §10)
std/mathPI/E, sqrt pow sin cos tan asin acos atan atan2 ln log2 log10 exp floor ceil round trunc, y abs/min/max genéricos
std/timereloj (now, monotonic), sleep, fechas UTC y formateo (ISO 8601/RFC 1123), constructores de duración a ms (millis/seconds/minutes/hours/days)
std/randomPRNG del proceso: next, below, between, choice, shuffle y seed (semilla explícita → secuencia reproducible)
std/cryptocripto de producción respaldada por ring (tiempo constante): sha256 sha512 sha1, hmac_sha256, ed25519_public_key/ed25519_sign/ed25519_verify, chacha20poly1305_seal/_open, random_bytes (CSPRNG) y —acuerdo de claves— x25519_public_key/x25519_shared_secret (respaldados por x25519-dalek), hkdf_sha256 y constant_time_eq. Las versiones escritas en raylang puro (examples/web/) son demostración del lenguaje, no producción
std/collections/{set,deque,stringbuilder,dict}Set<T> y Dict<K,V> (claves de usuario vía Hash+Eq), Deque<T> y un constructor de strings que evita el O(n²) de concatenar en bucle
std/text, std/sort, std/regex, std/csv, std/toml, std/json, std/template, std/markdownprocesamiento de texto y datos (markdown: parse -> [Block] —AST tipado— y to_html; subconjunto CommonMark con tablas GFM, HTML embebido escapado y URLs javascript: neutralizadas por diseño)
std/hex, std/base64, std/url, std/uuid, std/protobufcodificaciones e identificadores
std/inflate, std/deflate, std/huffmancompresión (gzip/zlib/DEFLATE)
std/kv, std/resiliencealmacén clave-valor persistente (compartible entre fibras) y utilidades de resiliencia: reintentos con política, circuit breaker y plazos (deadline/expired)
std/ffihelpers de la frontera C: errno() (el errno del hilo tras una extern estilo POSIX; leerlo inmediatamente tras la llamada)
std/ioconsola por bytes: write/ewrite/write_bytes (→ Result<int,string>, sin salto de línea) y flush(); read(max) -> Option<bytes> (None = EOF) y read_timeout(max, ms) -> ReadResult (Data/Eof/TimedOut). stdout va con buffer; stderr no. En la VM, una lectura sin datos aparca la fibra (las demás siguen); un solo lector de stdin a la vez, y no se mezcla con input()/lecturas por línea (buffers distintos). El orden entre print e io.write es el de programa en los tres motores
std/unitsconstructores de tamaño a bytes, convención binaria 1024ⁿ (kb/mb/gb)
std/termel terminal: is_tty(fd), size() -> Option<(int, int)>, raw(f) (modo crudo con restauración garantizada — también al salir el proceso; no ante señal fatal/kill -9), read_key() -> Option<Key> y el decodificador puro decode(bytes) -> Option<(Key, int)> (None = secuencia incompleta). enum Key: Char/Enter/Tab/Backspace/Esc/Up/Down/Left/Right/Home/End/PageUp/PageDown/Insert/Delete/Ctrl(char)/F(int). Unix; fuera: is_tty false, size None, raw Err. Ancho en celdas (portable, sin tty): width(s) -> int, char_width(c) -> int (wcwidth: control/combinantes 0, CJK/fullwidth/emoji 2, resto 1), fit(s, cells)/fit_right(s, cells) (trunca sin partir un carácter ancho y rellena a cells)

Un módulo std/… puede depender de que el binario incluya un subsistema (TLS/cripto necesitan net-tls; un binario slim o --without responde con un error de ejecución explícito). El catálogo con firmas está en REFERENCE.md §10.

Decisiones de nombres, congeladas (raylang no tiene sobrecarga; cada firma un nombre): index_of (string) vs position (arreglos); fetch (el cliente HTTP del paquete net) porque get es de Map y colisionaría bajo UFCS; bytes_of([int]) vs to_bytes(string) (entradas distintas); join(arr, sep) vs join(task) es la única dualidad ad-hoc (aridad), junto a close (handle/canal) y len/contains/+ (polimórficos por tipo).

11. Diagnósticos y códigos de salida

12. Versionado y estabilidad

13. Notas de implementación (informativas)

Tres motores compartiendo el mismo front-end (§Conformidad); los genéricos/traits/bounds/dyn se borran en compilación (el runtime no conoce tipos); las posiciones (línea, col) acompañan a todo token, nodo y error. El programa corre en un hilo de pila grande (256 MiB), para que la recursión profunda sea robusta, y la recursión de cola se elimina (TCO) tanto en el intérprete como en la VM; en el binario nativo cada fibra reserva su propia pila (128 MiB de reserva virtual por defecto, ajustable). El compilador auto-alojado (selfhost/) implementa este mismo lenguaje y sirve de validador cruzado de esta gramática: el parser de Rust y el auto-alojado producen el mismo AST nodo a nodo sobre el corpus del repo.