Rendered from SPEC.md, the repository's normative document (in Spanish): in case of doubt, the source file of the tagged version rules. The English catalog of the language and its stdlib is REFERENCE.en.md.

Especificación del lenguaje raylang

Versión del lenguaje: 1.27.46 (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. Una función que el transpilador no sabe traducir no rompe la paridad en silencio: en un build de desarrollo se emite como stub que falla al llamarse (con aviso al compilar) y en --release —y en ray bundle— es un error de compilación (M346; --no-stubs fuerza el error en dev, --allow-stubs admite los stubs en release).

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). Un BOM inicial (U+FEFF) se ignora y no ocupa columna; en cualquier otra posición es un carácter inesperado. 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 | alias_tipo ;
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' nombre_trait { '+' nombre_trait }
     | IDENT [ '<' tipo { ',' tipo } '>' ]        (* struct/enum/Map/Channel/Task/param de tipo *)
     | IDENT '.' IDENT [ '<' … '>' ] ;            (* tipo calificado por módulo: M.Punto *)
nombre_trait = IDENT [ '.' IDENT ] ;                (* trait local o calificado por módulo: M.Trait (M310) *)

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 ] nombre_trait [ '<' tipo … '>' ] 'for' tipo '{' { metodo } '}' ;
const    = 'const' IDENT ':' tipo '=' const_valor ';' ;
alias_tipo = 'type' IDENT [ '<' IDENT { ',' IDENT } '>' ] '=' tipo ';' ;   (* 'type' es contextual: solo abre ítem (M311) *)
const_valor = expresion_constante ;   (* literal, aritmética/bits/concatenación sobre literales y otras const, arreglo/tupla de ellas; M345 *)
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 ] ';'
          | 'break' [ IDENT ] ';'
          | 'continue' [ IDENT ] ';'
          | [ IDENT ':' ] 'while' '(' expresion ')' bloque
          | [ IDENT ':' ] 'for' patron_for 'in' iterable bloque
          | expresion ';'
          | expresion_con_bloque ;                   (* if/match/bloque como sentencia, sin ';' *)
expresion_con_bloque = expresion_if | expresion_while | expresion_match | bloque ;
destino   = IDENT | expresion_postfija '.' IDENT | expresion_postfija '[' expresion ']' ;
patron_for= IDENT | '(' ( IDENT | '_' ) { ',' ( IDENT | '_' ) } ')' ;   (* la tupla, sobre Map, [(…)] o un Iterator de tuplas; M345 *)
iterable  = expresion [ '..' expresion ] ;

6. Expresiones

6.1 Precedencia (de menor a mayor)

Nivel Operadores Asociatividad
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
12 as (cast) izquierda
13 - ! ~ (unarios) prefijo
14 llamada f(…), campo/método x.f, índice x[i], ? postfijo, izquierda
15 primarios —

&& 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), (5) UFCS dirigido por el tipo (M206): si recv es un struct o enum declarado en un módulo M y existe M.f pública cuyo primer parámetro admite el receptor, la llamada es M.f(recv, args) — sin importar f por nombre (import M; basta; el tipo dice dónde mirar, como un método inherente). No aplica a tipos del prelude (Option, Result, Map…) ni a primitivos, ni alcanza funciones privadas de M; una función libre en el ámbito con ese nombre (paso 4) sigue ganando. Todo se resuelve estáticamente en el checker (salvo el despacho de dyn, que es una llamada a través del objeto).

Dos precisiones sobre el paso 4 (M344): un local solo tapa al método si es una función — fn fail(r: Res, status: int) { r.status(status) } llama a la función libre status(Res, int), porque un int no puede ser el callee de r.status(…) (en una llamada directa status(…) el local sí tapa, como siempre); y el ámbito de las funciones del módulo de entrada es léxico: desde otro módulo, recv.f(args) nunca resuelve a una f definida en el archivo de entrada por su nombre pelado (solo el prelude y los builtins son visibles pelados desde un módulo; lo demás va por from M import, por el módulo propio o por el tipo del receptor, paso 5).

6.4 Pipelines

x |> f(a, b) ≡ f(x, a, b); x |> f ≡ f(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); int↔u* 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 Option↔Result.

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.

Plazos y fibras ocupadas. No hay preempción: una fibra que computa sin ceder ocupa su worker hasta que cede (E/S, canal, sleep, yield). Los plazos de las demás fibras (time.sleep, select_timeout, io.read_timeout, net.set_read_timeout, ui.next_event_timeout) y sus despertares por E/S no dependen de que esa fibra ceda mientras haya otro worker que pueda reanudarlas: en la VM, cualquier worker ocioso atiende los plazos vencidos y la E/S lista (con precisión de ~1 ms); en el binario nativo, el reactor los atiende en su propio hilo y la fibra reanuda en su worker de origen, que se elige al nacer entre los de menos fibras vivas. Con N = 1, o cuando hay más fibras ocupadas en CPU que workers, una fibra que no cede sí retrasa a las que comparten su worker: un trabajo largo de CPU que conviva con un bucle de eventos debe ceder periódicamente (time.sleep(1) o yield). Las operaciones de canal que completan sin aparcar (recibir con dato, enviar con hueco) son además puntos de cesión cooperativos en el binario nativo: cada 32 de ellas, si hay otras fibras listas en el mismo worker, la fibra cede el turno. Así un actor cuyo buzón nunca se vacía —y que bloquea el hilo en cada operación (un fsync por mensaje)— no deja sin correr a las fibras que nacieron en su worker: esperan a lo sumo 32 mensajes, no a que el buzón se vacíe. Una llamada bloqueante del sistema (fs.sync_data, un fsync) sí ocupa el worker mientras dura; no hay migración de fibras entre workers.

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ódulo Qué cubre
std/fs disco: 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/net transporte: 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/process ejecució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/math PI/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/time reloj (now, monotonic), sleep, fechas UTC y formateo (ISO 8601/RFC 1123), constructores de duración a ms (millis/seconds/minutes/hours/days)
std/random PRNG del proceso: next, below, between, choice, shuffle y seed (semilla explícita → secuencia reproducible)
std/crypto cripto 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/markdown procesamiento 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/protobuf codificaciones e identificadores
std/inflate, std/deflate, std/huffman compresión (gzip/zlib/DEFLATE)
std/kv, std/resilience almacén clave-valor persistente (compartible entre fibras) y utilidades de resiliencia: reintentos con política, circuit breaker y plazos (deadline/expired)
std/ffi helpers de la frontera C: errno() (el errno del hilo tras una extern estilo POSIX; leerlo inmediatamente tras la llamada)
std/io consola 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/units constructores de tamaño a bytes, convención binaria 1024ⁿ (kb/mb/gb)
std/term el 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.