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:
- la máquina virtual de bytecode — el motor de producto (
ray run); - 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); - 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.
- Comentarios:
//hasta el fin de línea. No hay comentarios de bloque. - Identificadores:
[A-Za-z_][A-Za-z0-9_]*, excluidas las palabras clave. Los nombres con#o::no son escribibles por el usuario (los usan las bajadas internas y los módulos). - Palabras clave (reservadas):
let var fn return if else while for in true false struct const enum match trait impl dyn pub import from extern asy las de tipoint float bool string char bytes ptr u8 u32 u64. - Literales:
- Entero: dígitos decimales (
42) o con prefijo de base (M118):0x/0Xhexadecimal (0x1F),0o/0Ooctal (0o755),0b/0Bbinario (0b1010). Al menos un dígito tras el prefijo (0xsolo es error léxico). Sufijo opcionalu8/u32/u64pegado a los dígitos (255u8,0xFFu32): el literal es de ese tipo sin contexto, y debe caber en él (si no, error léxico). Sin sufijo, un literal que cabe enint(i64) esint(y se coerciona alu*del contexto, §5); uno que no cabe enintpero sí enu64(0xFFFFFFFFFFFFFFFF) es amplio: valeu64sin contexto. Más allá deu64, error léxico. Sin separador_(diferido). - Flotante: siempre decimal (los prefijos de base son solo para enteros).
- Flotante:
dígitos '.' dígitos(3.14), con exponente opcionale|E [+|-] dígitos(1e21,1.5e-3,2E+10); un exponente hace el literal flotante aunque no lleve punto. Un.sin dígito decimal no es flotante; unesin dígito (o sin dígito tras el signo) no es exponente (1eabc= entero1+ identificador). - Cadena:
"…"con escapes\n \t \r \0 \\ \" \$, más\xNN(dos dígitos hex → el code pointU+00NN, 0–255) y\u{H…H}(1–6 dígitos hex → un code point Unicode; error si excedeU+10FFFF` o es un surrogate). No admite saltos de línea literales. - Cadena plantilla (M95):
`…`— mismo valor y mismo token que"…", con dos diferencias: la comilla doble"es literal (no se escapa) y los saltos de línea están permitidos (multilínea, literales). El backtick literal se escapa\`. Interpola igual que cualquier cadena. - Cadena interpolada: cualquier cadena (
"…"o`…`) con…${expr}….${expr}contiene una expresión; el$solo es especial seguido de{("$5","{n}"son literales;\${es un${literal). Azúcar: se desazucara a concatenación conto_string(expr)(§6.6). - Carácter:
'a'con escapes\n \t \r \0 \\ \', más\xNNy\u{H…H}(como en cadena). Un code point Unicode. - Bytes:
b"…"con los escapes de cadena más\xNN(octeto arbitrario, dos dígitos hex). - Booleano:
true/false.
- Entero: dígitos decimales (
- Operadores y puntuación:
+ - * / % == != < <= > >= && || ! = & | ^ ~ << >> ( ) { } [ ] , ; : . .. -> => ? |> @. El lexer siempre emite>>como un token; el parser lo parte en dos>al cerrar argumentos de tipo (Caja<Caja<int>>es válido).
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 } ')' ] } ;
- Módulo = archivo; su identidad es su ruta desde la raíz del proyecto (el directorio del
archivo de entrada).
import a/b/c;liga el leaf (c;asrenombra). El acceso es calificado:c.f(...),c.Tipo,c.Enum.Variante. El separador/solo existe en elimport. - La lista de un
from … importpuede repartirse en varias líneas (el léxico ignora los saltos de línea) y admite coma final.ray fmtla deja en una línea si cabe en 100 columnas y la envuelve a un nombre por línea si no —sin coma final, que dejaría el;colgando. Con el mismo umbral reparte una cadena de métodos de dos o más eslabones (el receptor se queda en su sitio y cada.metodo(…)baja una línea) y las listas delimitadas: argumentos de llamada, parámetros defny literales de arreglo, tupla, struct y Map. Una lista delimitada cierra su delimitador en línea propia; una forma sin delimitador propio (el;del import, el)que no es de la cadena) lo pega al último elemento. pubexporta funciones, structs, enums, traits y consts. Referenciar un ítem no-pubde otro módulo es error.pub from M import x;reexporta (construye la cara pública).- Nombres de builtin en módulos (M196): un módulo puede definir una función
pubcon el nombre de un builtin (pub fn close(c: Conn)), porque se consume calificada (proto.close(c)). Dentro de ese módulo el nombre pelado es la función propia; el builtin queda alcanzable por el pseudo-módulobuiltin(builtin.close(c.sock);builtin.xcon unxque no existe es el error normal de nombre no declarado). El archivo de entrada no puede redefinir un builtin (su nombre pelado se resuelve antes que cualquier función de usuario), yfrom M import close;sin alias es error por la misma razón:import M;+M.close(...), ofrom M import close as cerrar;. El UFCSx.close()sigue resolviendo al builtin fuera del módulo (el builtin va antes que el paso 5 de §6.3): la forma calificada es la canónica. - Cápsulas: la presencia de
P/mod.rayvuelveP/direccionable (import P;cargaP/mod.ray) y encapsula su subárbol: importarP/internodesde fuera deP/es error.P.rayyP/mod.raya la vez es error (forma canónica única). - Los tipos se namespacan por módulo (dos módulos pueden definir
Node); las funciones también.mainvive en el módulo de entrada. - Anotaciones (conjunto cerrado):
@testsobre funciones() -> boolo() -> unit;@derive(Eq, Show, Hash, ToJson, Clone)sobre structs/enums no genéricos —Clonegeneraclone(self) -> Self, una copia superficial (M221: los campos por valor se copian; los de referencia —arreglos, mapas, structs, canales— se comparten, igual que al escribir el literal a mano);Hashgenerahash(self) -> intcombinando el.hash()de los campos (un campofloat/array no es hashable) yToJsongenerato_json(self) -> string(su trait vive enstd/jsony debe estar en ámbito para derivarlo).Ordno es derivable: se implementa a mano. Cualquier otra anotación es error. maines obligatoria en el programa de entrada: sin parámetros, retornointounit. El código de salida del proceso es eseint(& 0xFF) o0. Excepción: siprint/eprint—o la salida del propio CLI (ray fmt,ray doc…)— encuentran su destino cerrado (un pipe roto,programa | head), el proceso termina en silencio con código 141 (128+SIGPIPE, la convención Unix);io.write/io.flushen cambio devuelvenErry el programa decide.
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) *)
- Primitivos:
int(entero con signo de 64 bits),float(IEEE-754 doble),bool,string(secuencia inmutable de caracteres Unicode; se indexa y mide por carácter),char(code point),bytes(secuencia inmutable de octetos),unit(el tipo del bloque vacío y del retorno omitido; escribible en posición de tipo —fn f() -> unit, útil sobre todo en firmasextern(§FFI) y tiposfn; no es palabra clave: se resuelve como nombre de tipo, igual queMap/Channel/Task, y sombrea cualquier struct/enum del usuario que se llame así). - Enteros sin signo
u8/u32/u64: aritmética, comparación y bits con wrapping al ancho (por diseño). ImplementanEq/Show/Ord/Hashcomoint(M356). Solo operan con su mismo ancho, salvo la cuenta de un desplazamiento (<</>>), que puede serinto el mismo ancho (es un conteo, no un valor del dominio; el resultado es el tipo del operando izquierdo, y una cuenta fuera de rango envuelve). La conversión es explícita conas. Un literal entero sin sufijo adopta el ancho del contexto si cabe (fuera de rango = error de tipos); con sufijo ya tiene ancho (§3) y un contexto de otro ancho es error. - Arreglos
[T],Map<K,V>(claves hashables:int,string,char,bool,bytes—floatno), structs y enums: semántica de referencia (§8). Tuplas(A, B, …): acceso posicionalt.0y desestructuración; las posiciones son de solo lectura (t.0 = ves error de tipos: la tupla es un agregado inmutable — para mutar, desestructura o usa un arreglo), así que la tupla se comporta como valor. - Funciones de primera clase
fn(T…) -> R; los closures capturan por referencia. - Genéricos con erasure total:
Typede runtime no existe; la inferencia es del checker (§7). BoundsT: A + Ben funciones, structs, enums e impls. - Trait objects
dyn A + B: conjunto canónico (ordenado, sin duplicados); upcasting a un subconjunto; un método que usaSelffuera del receptor no es invocable sobre el objeto. Vale como tipo de campo de struct/enum (M310; los nombres de trait se conocen antes de validar los campos, así que el trait puede declararse después) y el nombre puede ir calificado por módulo (dyn M.Trait,impl M.Trait for T). Un valordynse muestra opaco (<dyn A + B>). Channel<T>yTask<T>: tipos de la concurrencia (§9);Selfsolo dentro de traits/impls.
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 ] ';' ;
- Las firmas son explícitas (parámetros y retorno); la inferencia es solo local (§5).
- Un método de trait puede traer cuerpo por defecto; un impl lo redefine u omite. La
cobertura del impl debe ser exacta (ni métodos de más ni de menos, firmas idénticas con
Selfsustituido). Un impl puede ser genérico (impl<T: B> Trait for Caja<T>), aplicado exactamente a los parámetros propios; a lo sumo un impl por(constructor, trait). - Un método (de trait o de impl) puede tener parámetros de tipo propios (M40.2c):
fn map<U>(self, f: fn(T) -> U) -> Iter<U>. Se suman a los del impl al resolver la llamada; la inferencia los fija por los argumentos. Habilita p. ej. los adaptadores deIterator(§10). - Los traits con parámetros de tipo (
trait From<S>,trait Iterator<T>) existen con semántica limitada:From<S> { fn convert(origen: S) -> Self; }alimenta la conversión de?(§6.7), eIterator<T> { fn next(self) -> Option<T>; }habilitafor x in it(§5) por despacho por punto ordinario. Usar un trait parametrizado del usuario en bounds odynes error. - Alias de tipo (M311):
[pub] type Nombre[<T, …>] = tipo;da un nombre a un tipo en posición de tipo. No es un tipo nuevo:type Id = intesinten todas partes (Ideintson intercambiables; unPair<T> = (T, T)casa con la tupla). Los parámetros se sustituyen por los argumentos de cada uso (aridad exacta; sin bounds — van en la función o el struct que lo usa). Se expande en el checker (resolve_type) y se borra del AST tras el chequeo: ningún motor lo ve; los diagnósticos muestran el tipo expandido. Un alias puede nombrar otro alias; un ciclo (type A = [B]; type B = A;) es error, como un nombre que ya es struct/enum/trait.pub typese exporta y califica como un tipo (geo.Pt,from geo import Pt); un alias privado no se ve desde fuera.typees palabra clave contextual: solo abre un ítem seguido de un nombre; en cualquier otra posición es un identificador (campotype, variabletype). La construcción va por el nombre real (Enum.Variante,Struct { … }), no por el alias. constde nivel superior: el valor es una expresión constante (M345): un literal;+ - * / % & | ^ << >>y la negación sobreint,+ - * / %sobrefloat,+sobrestring,!sobrebool, con operandos literales u otras constantes (de cualquier módulo, también declaradas después; un ciclo es error); o un arreglo o una tupla de valores constantes (anidable; M274/M307:const IDS: [int] = [ID_A, ID_B];,const TABLE: [(int, string)] = [(ID_A, "a")]). El checker pliega la expresión a su literal antes de verificar (8 * 3600 * 1000es el literal28800000para los tres motores); una división por cero o un desbordamiento entero en el plegado es error de compilación con posición. Lo que no se pliega (una llamada, una variable) es error: «must be a constant expression». El tipo declarado es el contexto del valor, como en unlettipado (M356):const A: u64 = 5;coerciona el literal au64(yconst T: [u8] = [1, 2];cada elemento), con el rango comprobado. Unconstarreglo tiene semántica de literal inyectado: cada uso del nombre evalúa el arreglo de nuevo (un arreglo fresco por evaluación), así que mutarlo a través de un alias no afecta a otros usos; en un bucle caliente conviene izarlo a un local.- FFI (
extern "lib" { … }, M41): declara funciones de una librería C. Cada firma va sin cuerpo; su nombre es a la vez el identificador en raylang y el símbolo a resolver. La librería se carga condlopeny los símbolos condlsymen tiempo de ejecución (el nombre corto"m"se resuelve al archivo de plataforma o al proceso). Los tipos deben ser marshalables: los primitivosint↔Cint(32 bits, con signo),u64↔Clong/size_t(64 bits),float↔double,bool↔int (aridad 0..=6 — límite del checker, idéntico en todos los motores), y como argumentostring↔char*(NUL-terminado) ybytes↔puntero al buffer (M41.2). Un puntero opaco (FILE*, handle) se pasa comou64o, con seguridad de tipos, comoptr(M41.4b: un puntero opaco — se recibe/pasa/compara por identidad, pero no se desreferencia ni opera). El retorno admiteint/u64/float/bool/unit,ptr/Option<ptr>(NULL → None, p. ej.fopen), y para unchar*,Option<bytes>(NULL → None; la frontera copia los bytes hasta el NUL y no libera el puntero) oOption<string>(azúcar que valida UTF-8; bytes inválidos → error de ejecución) (M41.3). Unstring/bytespelado de retorno es error (unchar*puede ser NULL y no haynull). Una firma fuera del catálogo, o un tipo no marshalable, es error. Llamar a unaextern fnse ve como cualquier llamada. Declarar unaextern fnes la única operación insegura del lenguaje: cruzar a C anula las garantías (memoria, firmas); todo lo demás es seguro por construcción. extern "lib" blocking { … }: marca todas las firmas del bloque como llamadas bloqueantes de verdad (E/S, librerías C lentas).blockinges una palabra contextual (no reservada: sigue siendo un identificador válido). La marca no cambia los valores — tipos, marshalling y resultado son idénticos a un bloque sin marcar —; es una directiva de planificación: en el binario nativo con fibras (el default), la llamada se descarga a un hilo de un pool bloqueante y la fibra queda aparcada, de modo que el worker M:N no se bloquea ni vara a las fibras hermanas fijadas a él. Donde no hay scheduler que proteger (la VM, el intérprete, un binario--without fibers, o una llamada fuera de fibra) la marca es inerte y la llamada es directa.
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 ] ;
letes inmutable,varmutable; los parámetros son inmutables. Reasignar unletes error de tipos; shadowing permitido en ámbitos internos. La anotación de tipo es opcional si el inicializador determina el tipo;[],None,Map.new(),Channel.new()yCaja.Vaciason indeterminados y exigen anotación o contexto (§7).- La mutación interior no exige
var:obj.campo = vyarr[i] = vmutan el objeto referenciado (§8);vargobierna la ligadura, no el objeto.s[i] = csobre string es error (inmutable). foritera: arreglo (elemento), rangoa..b(enteros,ainclusivo,bexclusivo; el rango solo existe en la cabecera delfor), string (char),bytes(cada octeto comoint, igual queb[i]; M356),Map(tupla(clave, valor)en orden de clave — determinista) y cualquier tipo que implementeIterator<T>(§7): el bucle llama anext(self) -> Option<T>hastaNone, ligando cada elemento. El patrón de tuplafor (a, b) in xsdestructura el elemento cuandoxses unMap, un arreglo de tuplas[(A, B)](M345; equivale afor t in xs { let (a, b) = t; … }) o unIteratorde tuplas (enumerate());_descarta una posición y la aridad debe coincidir.returnsale de la función envolvente;return;devuelve unit. El valor de una función también puede caer del bloque (retorno implícito: la expresión final sin;). Además de sentencia,return [e]es expresión (M220): en un brazo dematch, en el valor de unleto donde vaya una expresión,Option.None => return code,equivale aOption.None => { return code; }— diverge (§7), así que cede el tipo al resto (el brazoSome(v) => vfija el tipo delmatch). Es azúcar: el parser lo desazucara a ese bloque.break;sale delwhile/formás interno ycontinue;salta a su siguiente iteración (en unfor, avanza el iterable). Son sentencias (no producen valor; el bucle sigue valiendo unit) y solo son válidas dentro del cuerpo de un bucle de la misma función (una función anónima corta el ámbito:breakdentro de unfn() { … }dentro de un bucle es error). Dentro del cuerpo pueden anidarse enif/else, brazos dematch, bloques y valores delet/asignación/return—la espina de sentencias—, pero no dentro de una expresión que no sea forma-con-bloque (argumento de llamada, operando, elemento de literal, índice): ahí es error de tipos (la expresión envolvente quedaría a medio evaluar). Ambas divergen (§7: una rama que termina enbreak/continuecede su tipo al resto). Unwhile (true)sin unbreakque salga de él diverge (M301): solo termina porreturn, así que puede ser la cola de una función con retorno declarado (fn f() -> Result<…> { while (true) { … return Result.Ok(x); … } }); unbreakde un bucle anidado no cuenta. Comoreturn, también son expresión (M300):Result.Err(e) => break,en un brazo dematchoif (c) { continue } else { v }equivalen a{ break; }/{ continue; }— el mismo azúcar del parser, con la misma restricción a la espina de sentencias; como cola de un bloque no necesitan;. Bucles etiquetados (M308):outer: while (c) { … }/outer: for x in xs { … }en posición de sentencia;break outer;ycontinue outer;(también como expresión) salen de / reanudan ese bucle desde cualquier bucle interior de la misma función (una función anónima corta el ámbito, como sin etiqueta). Una etiqueta desconocida es error de tipos; unbreak outerdesde un bucle interior cuenta como salida deouterpara la divergencia delwhile (true). La etiqueta solo precede a un bucle:x: 1;sigue siendo error de sintaxis.- Expresión-con-bloque en posición de sentencia (M153): dentro de un bloque, una expresión
que COMIENZA con
if/while/match/{se parsea exactamente como esa forma-con-bloque — ningún operador postfijo ((,[,.,?) ni binario la extiende; el token siguiente inicia una sentencia nueva o la cola del bloque. Asíif (c) { … }seguido de(a, b)en la línea siguiente es elifcomo sentencia y la tupla como cola — no una llamada del valor del bloque. Para aplicar postfijos o binarios al VALOR de una forma-con-bloque, ponla en posición de expresión: paréntesis o unlet(let x = if (c) { f } else { g }(1);sigue siendo una llamada). La resolución es la misma familia que la del struct-literal (§6.2): ante la ambigüedad, en posición de sentencia gana la lectura de sentencia.
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.
- Coma final: toda lista delimitada la admite — argumentos de llamada
f(a, b,), parámetros defn, literales de arreglo/tupla/struct/Map y la lista de unfrom … import(§2).ray fmtla elimina (la forma canónica no la lleva). Cerrada la inconsistencia del dogfood raydesk: llamadas y parámetros la rechazaban mientras arrays/structs la aceptaban.
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.
if (cond) bloque [else (bloque | if …)]es expresión: conelse, ambas ramas deben converger en tipo (una rama que diverge —return,panic— cede el tipo a la otra); sinelse, unit. Sin tipo esperado, una rama cuyo valor no determina sus parámetros de tipo (Option.None,[]) toma el tipo que fija la otra rama (M303, la regla M204 de los brazos dematch); si ninguna lo fija, es error de inferencia.if let patrón = expr bloque [else (bloque | if …)](M40.1b) es azúcar dematch (expr) { patrón => bloque, _ => else }(sinelse, el brazo_es unit). El patrón usa la misma gramática que el match (variantes calificadas). El escrutinio va sin paréntesis, hasta el{.match (expr) { patrón [if guarda] => (expr | bloque), … }es expresión; brazos convergentes (misma regla de divergencia; todos divergentes → unit). Un brazo cuyo valor no determina sus parámetros de tipo (Result.Err(e)sin tipo esperado) toma el tipo que fijan los demás brazos, vaya antes o después (M204); si ninguno lo fija, es error de inferencia. Exhaustivo. El escrutinio es un enum, una tupla, un struct o unint/string/char/bool/uN(M310; una función, unit, canal o tarea no se matchean). PatronesEnum.Variante(sub-patrón…)(tambiénM.Enum.Variante), binding suelto,_, tupla(p1, p2, …)(M310: dos o más sub-patrones, uno por posición, anidables) y literal (M310:intcon signo opcional,string,char,bool; el literal debe tener el tipo del valor que casa). No hay patrones alternativos ("a" | "b" =>): el parser lo rechaza con un error que remite a un brazo por patrón o a una guarda. Patrones anidados (M40.1c): cada posición del payload es un sub-patrón completo, recursivo (Result.Ok(Option.Some(v))). Guardas (M40.1a):patrón if <cond>casa solo si el patrón liga Y lacond(bool, con los bindings del patrón en ámbito) estrue; si no, se sigue al siguiente brazo. Patrón de struct (M40.1d):Nombre { campo [: sub-patrón], … }destructura un struct (forma corta{ x, y }={ x: x, y: y }), anidado o como patrón de primer nivel sobre un escrutinio struct (M310). Brazo de asignación (M344):patrón => lugar = valor,es azúcar depatrón => { lugar = valor; },(valorunit;lugardebe ser asignable).ray fmtlo conserva en su forma corta. Exhaustividad por matriz (M310): los brazos sin guarda deben agotar el tipo del escrutinio, recursivamente por variantes y posiciones —Ok(Some(v)) / Ok(None) / Err(e)es exhaustivo sin_;Some(Some(_)) / Noneno lo es (faltaSome(None)). Unint/string/charsolo lo agota un_/binding; unbooltambiéntrueyfalse. Un brazo con guarda nunca cuenta. Un brazo cuya variante ya cubría entera un brazo anterior es «inalcanzable» (error); la inalcanzabilidad de patrones de tupla/literal no se diagnostica.- Ambigüedad struct-literal/bloque:
Nombre { … }se reconoce como literal solo si el receptor es un identificador (oM.Nombre) en posición de expresión; en la cabecera de unfor/if/whilesin paréntesis el{abre el cuerpo. El escrutinio dematchy las condiciones van entre paréntesis por esta razón.
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)
- Inferencia local:
let x = e;toma el tipo dee. Bidireccional: un tipo esperado (anotación, tipo de retorno, parámetro) fija los indeterminados ([],None,Map.new(),Channel.new(), construcción de enum genérico) y la coerción de literal entero au*(también en asignación y elementos de arreglo). - Genéricos: la instanciación se infiere de los argumentos por unificación (las
variables de la firma llamada son incógnitas; las del llamador, rígidas). Dos usos
incompatibles de
Ten una llamada son error; un parámetro no determinado exige anotación. - Bounds:
x.m()conx: TyT: Traitse verifica contra la firma del trait; en cada llamada a una función acotada, el tipo que instanciaTdebe implementar el trait (o ser un parámetro rígido del llamador con el mismo bound). La construcción de unstruct/enumacotado exige lo mismo. (Implementación por diccionarios; semánticamente es despacho estático.) - Operadores sobrecargables:
+ - * /binarios y-unario sobre un tipo de usuario que implementeAdd/Sub/Mul/Div/Neg(fn add(self, otro: Self) -> Self, etc.), ambos operandos del mismo tipo.==/<no son sobrecargables (usarigual/menordeEq/Ord). - Igualdad
==/!=: primitivos,string,char,bytes,u*(mismo ancho) y estructural para arreglos, tuplas, structs (mismo tipo, campo a campo) y enums (misma variante, payload a payload). UnMapsuelto no se compara con==; dentro de un struct/enum (Json.JObject) se compara estructuralmente, sin orden. Un tipo con funciones dentro no es comparable.@derive(Eq)/impl Eqsirven para los bounds (T: Eq), víaigual. Una tupla satisface los boundsEqyShowcuando todos sus elementos los satisfacen (M302:assert_eq(f(), ("h", 81));showda(h, 81)); no tiene impl propio ni satisface otros traits. Orden< <= > >=:int,float,string(lexicográfico),char(code point),u*. - Divergencia:
return,break,continue,panic(…),exit(…)y las ramas que terminan en ellos tipan como "cede el tipo al resto".
8. Semántica de evaluación
- Orden: estricta, izquierda a derecha (argumentos incluidos).
- Valores de referencia: arreglos, structs, enums con payload,
Map,Channel,Task— los alias comparten el objeto; la mutación se observa a través de cualquier alias.string,bytes, los primitivos y las tuplas (§3) se comportan como valores (inmutables). Los closures capturan las celdas de las variables (la mutación posterior se ve). - Aritmética:
int: el desbordamiento es error de ejecución ("desbordamiento aritmético en int"):+ - * /(soloMIN / -1)%(soloMIN % -1) y-unario (-MIN). División y módulo por cero son errores de ejecución.u8/u32/u64: wrapping al ancho, por diseño (también los bits& | ^ ~ << >>).- Los operadores bit a bit sobre
intoperan sobre los 64 bits con wrapping (los desplazamientos enmascaran el contador, semántica de Rust). float: IEEE-754 (división por cero dainf/NaN;NaN != NaN).
- Índices:
arr[i]/s[i]/b[i]conifuera de rango es error de ejecución con posición. - Recursión: profundidad máxima de llamadas
MAX_CALL_DEPTH = 1024(error limpio de "desbordamiento de pila"), con TCO garantizado: una llamada en posición de cola (cuerpo de función, ramas deif/matchen cola, expresión final de bloque, valor dereturn) reutiliza el marco — la recursión de cola corre en O(1) de pila. Los builtins no son posiciones de cola. - Límites del parser: anidamiento máximo
MAX_PARSE_DEPTH = 1000(error de sintaxis). panic(msg)aborta la ejecución con "pánico: msg" y la posición de la llamada; el proceso sale con 70.exit(code)termina el proceso con ese código, desde cualquier fibra, flusheando stdout/stderr. No es un error: sin mensaje ni traza (ytry_callNO lo captura — el proceso muere).
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.
- En la VM, N se decide así, en orden:
--deterministic→ 1;RAYLANG_THREADS=Nexplícito → N; el programa no usaspawn→ 1; en otro caso →available_parallelism()(multicore por defecto). N se acota a1..=256. - En el binario nativo, las fibras son corrutinas de pila propia sobre un reactor del SO
(por defecto);
ray build --native --without fibersrecupera el modelo hilo-por-tarea. - En
wasm32(playground) N es siempre 1: no hay hilos del sistema.
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.
spawn(f: fn() -> T) -> Task<T>lanza una fibra (no cede).join(t: Task<T>) -> Tbloquea hasta que termina y re-lanza su fallo;try_join(t) -> Result<T, string>lo devuelve como valor en vez de re-lanzarlo.- Dominios de handles (M296). Todo handle del runtime (archivo, socket, proceso, ventana…) nace
en el dominio de la tarea que lo crea.
spawnhereda el dominio del padre;spawn_isolated(f: fn() -> T) -> Task<T>esspawncon un dominio nuevo: la tarea aislada, y las que ella lance, no pueden usar los handles de otros dominios —se comportan exactamente como un handle cerrado (invalid handle)— y sus handles son invisibles fuera. Los valores (canales, mensajes) cruzan dominios con normalidad; solo los handles están confinados. El intérprete, sin fibras, vive en un único dominio. - Afinidad de worker (M365).
spawn_local(f: fn() -> T) -> Task<T>esspawncon una pista de colocación: en el binario nativo la fibra nueva queda fijada al worker que la lanza (corre cuando la madre cede, sin despertar a otro hilo); en la VM, cuya cola de listas es compartida, es exactamentespawn.worker_id() -> intes el índice del worker que ejecuta la tarea actual, en0..worker_count(), estable durante toda la vida de la tarea en el binario nativo (las fibras no migran) y el del hilo que la ejecuta en ese momento en la VM;worker_count() -> intes N. En el intérprete valen0y1. Semántica y aislamiento son los despawn(dominio heredado, heap propio):spawn_localsolo decide dónde vive la fibra. Un programa correcto conspawnlo es conspawn_local; la afinidad es rendimiento, nunca corrección. scope(body: fn() -> R) -> Rposee las tareas lanzadas dentro: al salir las une; si una falla, cancela a las hermanas pendientes (transitivo) y propaga el fallo original.Channel.new() -> Channel<T>(no acotado),Channel.bounded(n)(acotado;n = 0rendezvous).send(ch, v)bloquea con la cola llena;recv(ch) -> Option<T>bloquea vacío-y-abierto,Nonecerrado-y-vacío;close(ch)despierta a los receptores (recibenNone) y a los emisores bloqueados (susendfalla consend on a closed channel); es idempotente. Unsendsobre un canal cerrado es error de ejecución;try_send(ch, v) -> boolenvía sin bloquear ni fallar:truesi entregó o encoló,falsesi el canal está cerrado o lleno.select(chs: [Channel<T>]) -> intbloquea hasta que alguno esté listo para recibir y devuelve el menor índice listo en el momento de la comprobación (un canal cerrado está listo para siempre).try_recv(ch: Channel<T>) -> Received<T>recibe sin bloquear:Received.Got(v)si había un valor listo (lo consume, comorecv),Received.Emptysi el canal está abierto y vacío,Received.Closedsi está cerrado y drenado — elenum Received<T> { Got(T), Empty, Closed }del prelude.select_timeout(chs: [Channel<T>], ms: int) -> Option<int>esselectcon plazo:Some(i)con el menor índice listo,Nonesi vencen losmsmilisegundos antes;ms <= 0= poll no bloqueante (Noneinmediato si ninguno listo).signals() -> Channel<int>devuelve el canal singleton de señales del proceso (SIGTERM= 15,SIGINT= 2 ySIGWINCH= 28 —cambio de tamaño del terminal— llegan como enteros), para apagado ordenado y re-maquetado de TUIs; compone conrecv/select. Solo unix (VM y binario nativo).SIGHUP,SIGQUITySIGUSR1/SIGUSR2conservan la acción por defecto del SO salvo que el programa las pida conprocess.listen_signal(sig)(std/process, M357): desde entonces llegan por el mismo canal.- Un
scopecuyo cuerpo devuelveResult.Erres un scope fallido para sus procesos hijos (std/process,stream()): se matan y cosechan antes de unir las fibras (M357). Un scope que termina bien espera, como siempre, a que los flujos del hijo se cierren. - El programa termina cuando
mainretorna (las fibras pendientes se abandonan). Si todas las fibras quedan bloqueadas y ninguna puede progresar: error "deadlock" (las que esperan E/S del exterior o el reloj no cuentan como bloqueadas).
10. Builtins y prelude (superficie estable)
La superficie estable tiene tres capas, y solo las dos primeras las fija esta SPEC:
- 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).
std/…(opt-in conimport 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").- 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
- Núcleo:
print eprint to_string len panic exit assert assert_eq· tipos imprimibles:int float bool string char bytes u*y (víashow) tipos conShow. - Recuperación de fallos (M97):
try_call(f: fn() -> T) -> Result<T, string>ejecutafy convierte unpanico error de ejecución enErr(mensaje)— el fallo como valor, sin excepciones. La recuperación ocurre en la misma fibra: lo quefmutó antes de fallar sigue mutado (mismo compromiso quecatch_unwindde Rust); para aislamiento real,spawn+try_join(t) -> Result<T, string>(§9, VM y binario nativo).try_callfunciona en los tres motores. - Caracteres:
char_code(c) -> int(code point Unicode) ychar_from_code(n) -> Option<char>(su inversa;Nonesinno es un code point válido). — String:trim split chars contains replace starts_with ends_with to_upper to_lower substring repeat index_of join to_bytes parse_int parse_float· Arreglos:push pop reverse contains position sort map filter fold any all iter(+a + bconcatena) · Bytes:bytes_of sub_bytes index_of starts_with from_utf8(+b1 + b2,to_string→ hex). - Entrada/entorno:
args() -> [string](argumentos del programa),env(name) -> Option<string>,input() -> Option<string>(una línea de stdin),read_int() -> Option<int>. El disco vive enstd/fs, no aquí. - Iteradores (M40.2b–f):
xs.iter()yrange(a, b)(semi-abierto) son iteradores de primera clase (Iter<T>, respaldados por un closure) recorribles confor x in …. Adaptadores perezosos (métodos deIterator):.map(f),.filter(pred),.take(n),.skip(n),.enumerate()(pares(int, T)) y.zip(otra)(empareja dos iteradores en(T, U), se agota con el más corto) devuelven otro iterador que solo calcula al recorrerse, encadenables (range(0,n).map(f).filter(p).take(k)). Terminales:.fold(init, f)reduce a un valor,.collect()materializa a[T], ysum(it)(función libre sobreIter<int>, vía UFCSit.sum()) suma enteros.enumerate/zipse consumen con patrón de tupla en elfor:for (i, x) in it.enumerate() { … }. No colisionan con elmap/filter/foldeager de arreglos (xs.map(f) -> [U]): se desambigua por el tipo del receptor. - Map (
Map<K, V>, claves primitivas:int string char bool bytes):Map.new() insert get get_or remove contains_key keys values len—keys()/values()recorren en orden de clave (el almacén interno no expone su orden). Para claves de usuario,std/collections/dict. - Handles:
close(h)cierra un archivo, socket o canal (el mismo nombre para los tres). - Concurrencia: §9 (
spawn join try_join scope channel send recv close select signals). - Traits del prelude (los métodos son los que se implementan y se llaman por UFCS):
Eq(eq)·Show(show)·Ord(less)·Hash(hash)·Len(len)·Push(push)·Reverse(reverse)·Contains(contains)·From<S>(convert)·Iterator<T>(next)·Add(add)/Sub(sub)/Mul(mul)/Div(div)/Neg(neg)(sobrecarga de operadores) ·StrOps/BytesOps/MapOps/OptionOps/ResultOps(los métodos de string, bytes,Map,Option<T>yResult<T,E>). Derivables con@derive(…):Eq,Show,Hash,ToJsonyClone— y solo esos cinco (Ordse implementa a mano).
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
- Formatos de cabecera (estables, en inglés — regla del 21 jul 2026: todo lo que el
lenguaje entrega al usuario va en inglés):
lex error at L:C: msg·syntax error at L:C: msg·type error at L:C: msg·runtime error at L:C: msg. El render añade la línea de fuente (acotada a una ventana si es larguísima) y el subrayado^…del span; en multi-módulo se antepone[módulo]y la línea es la local del archivo. - El compilador reporta múltiples errores (hasta 20) con recuperación; el primer error es siempre el mismo que en modo fail-fast.
- Un ICE (bug del compilador) imprime "error interno del compilador (ICE): …" y pide reporte; ningún programa de usuario debe provocarlo.
- Códigos de salida: el
intdemain(& 0xFF, 0 si unit) · 64 uso incorrecto del CLI · 65 error de compilación (léxico/sintaxis/tipos/carga de módulos) · 66 archivo ilegible · 69 el binario no incluye el subsistema que el comando necesita (build slim) · 70 error de ejecución · 73 no se pudo crear un archivo · 101 ICE.ray testsale con 0 (todo pasó), 1 (alguna prueba falló) o 65 (alguna suite no compila).
12. Versionado y estabilidad
- El lenguaje versiona con SemVer:
MAYOR.MENOR.PARCHE[-pre]. La versión vive en el binario (raylang --version) y en la cabecera de esta SPEC; cambia con esta SPEC.- MAYOR: cambios incompatibles en algo declarado estable por esta SPEC.
- MENOR: superficie nueva compatible (sintaxis, builtins, stdlib).
- PARCHE: correcciones sin cambio de superficie.
- Estable = todo lo definido en §§1–11, salvo lo marcado como interno/inestable:
los primitivos
__nombre, el formato del bytecode y del texto del LSP interno, los detalles del GC y del scheduler más allá de lo garantizado en §9, y los mensajes de error más allá de la cabecera (la línea/ventana/subrayado pueden mejorar en MENOR). - Deprecación: un elemento estable se marca deprecado en esta SPEC (con reemplazo) al menos una versión MENOR antes de retirarse en la siguiente MAYOR.
- Qué versiona con el lenguaje y qué no. El núcleo (§§1–11) y la biblioteca estándar
std/(§10.2) versionan juntos: van embebidos en el mismo binario y una SPEC dada describe ambos. Los paquetes (net,db,web,rpc, …) versionan por separado con semver propio y se resuelven por el gestor de paquetes; las librerías deexamples/son material de demostración y no tienen garantía de estabilidad, aunque algunas sean la fuente de un módulostd/(en cuyo caso manda lo que dice §10.2, no el ejemplo).
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.