Fuente: handbook/ en el repositorio. Cada bloque de código compila: el CI corre ray check sobre cada uno.
Empezar
El camino más corto de cero a un programa útil: instalar, preparar el editor, crear un proyecto, el lenguaje en quince minutos, la concurrencia, trabajar con un asistente LLM y las herramientas. Al final sabrás qué guía seguir según lo que quieras construir.
1. Instalar
curl -sSfL https://raylang.dev/install.sh | sh # macOS / Linux → ~/.local/bin/ray
irm https://raylang.dev/install.ps1 | iex # Windows (PowerShell)
Comprueba la instalación y busca versiones nuevas:
ray version
ray upgrade --check # 0 = al día, 1 = hay versión nueva
ray upgrade # instala la última
Sin instalar nada, el playground corre el lenguaje en el navegador, con diagnósticos y autocompletado.
ray build --native necesita una toolchain de Rust. Si no tienes una, ray toolchain install
instala una privada en ~/.ray/toolchain sin tocar tu sistema.
2. El editor
Todas las extensiones hablan con el mismo servidor de lenguaje, ray lsp: diagnósticos al
escribir, autocompletado, hover con la firma, ir a la definición, renombrar y formatear.
| Editor | Cómo |
|---|---|
| VS Code | la extensión raylang del marketplace |
| Sublime Text | Package Control: Install Package → raylang |
| Zed | la extensión raylang |
| Neovim, Helix | apuntan a ray lsp; los snippets de configuración están en editors/README.md |
3. Un proyecto
ray new hola && cd hola
ray run # ejecuta src/main.ray en la VM
ray new deja tres archivos: ray.toml, src/main.ray y un .gitignore.
[package]
name = "hola"
version = "0.1.0"
[dependencies]
fn main() -> int {
print("hello from hola");
0
}
main devuelve un int, que es el código de salida del proceso, o nada. Un archivo suelto también
vale: ray run archivo.ray, y los argumentos que van detrás del archivo llegan por args().
El ciclo de trabajo tiene cuatro comandos:
ray dev # recompila y reinicia al guardar
ray test # corre las funciones @test
ray fmt --write src/ # formato canónico
ray build --native # binario nativo, con la misma salida que la VM
Dos motores, un comportamiento. Mientras desarrollas, el programa corre en la VM: arranca al
instante y ray dev lo reinicia en cada cambio. Para desplegar, ray build --native lo traduce a
Rust y lo compila a código máquina. Los dos producen exactamente la misma salida, byte a byte, y el
CI del lenguaje lo comprueba en cada cambio. El binario nativo es varias veces más rápido; las
cifras están en la página de benchmarks.
VM (ray run, ray dev, ray test) |
Nativo (ray build --native) |
|
|---|---|---|
| Arranca | al instante, sin compilar | tras compilar: segundos, y más con --release |
| Sirve para | desarrollar, probar, scripts | desplegar y distribuir |
| Necesita | solo ray |
además, una toolchain de Rust |
| Entrega | nada: corre el fuente | un ejecutable que no depende de ray |
Dependencias
La biblioteca estándar va dentro del binario ray y se importa con import std/…. Lo demás son
paquetes que se declaran en ray.toml:
ray search http # busca en el índice público
ray add web # añade la dependencia a ray.toml y la descarga
Los paquetes oficiales son net (HTTP/1.1 y 2, WebSocket, DNS, TLS, gRPC), web (el framework de
aplicación al estilo Express), rpc, db (Postgres, MySQL, SQLite, Redis, MongoDB), tz, cron, y
los tres de agentes: llm (hablar con un modelo), mcp (herramientas por el Model Context
Protocol) y agent (el bucle que los une); y oidc (iniciar sesión con un proveedor OpenID
Connect y aceptar sus tokens).
Las versiones quedan fijadas en ray.lock con su hash. Los paquetes se descargan a .ray-deps/,
que no va al control de versiones: tras clonar un proyecto, ray fetch los vuelve a bajar.
Una dependencia es siempre una cadena, con tres formas:
[dependencies]
web = "^0.6" # una versión del índice (lo que escribe `ray add`)
oidc = "path:../oidc" # un directorio local, relativo a la raíz del proyecto
geo = "git+https://host/geo@v1.0" # un repositorio git en una etiqueta o commit
Un paquete propio es un directorio con su ray.toml y sus módulos; se importa por directorio y
archivo (import oidc/client;). Si no tiene programa —ni entry en [package] ni
src/main.ray— es un paquete-librería: ray check y ray test lo recorren módulo a módulo
(los @test pueden ir junto al código o en tests/), y ray run/ray build dicen que no hay
nada que ejecutar.
4. El lenguaje en quince minutos
Valores, variables y funciones
fn square(x: int) -> int { x * x } // el último valor del bloque es el resultado
fn sign(x: int) -> int {
if (x > 0) { return 1; } // `return` solo para salir antes
if (x < 0) { return -1; }
0
}
fn main() -> int {
let x = 10; // inmutable, tipo inferido
var total = 0; // mutable
total = total + square(x);
let ratio: float = 2.5; // anotación explícita cuando quieras
print("total ${total}, ratio ${ratio}, sign ${sign(-4)}"); // interpolación
0
}
Todo es una expresión: if, match y los bloques producen valor. Las firmas de función se
anotan siempre; los locales se infieren. No hay null.
Cuando algo no compila
El compilador comprueba el programa entero antes de ejecutar nada. Un error señala la línea y la columna, con la línea del fuente debajo:
type error at 2:22: 'total' is declared as int but initialized with string
2 | let total: int = "42";
| ^^^^
ray run sale entonces con el código 65. Los mensajes llevan una de tres cabeceras, lex error,
syntax error o type error, y siempre la posición. El editor muestra lo mismo mientras escribes,
y ray check lo comprueba sin ejecutar.
Structs, enums y match
struct Point { x: int, y: int }
enum Shape {
Circle(float),
Rect(float, float),
Dot,
}
fn area(s: Shape) -> float {
match (s) {
Shape.Circle(r) => 3.14159 * r * r,
Shape.Rect(w, h) => w * h,
Shape.Dot => 0.0,
}
}
fn main() -> int {
let p = Point { x: 1, y: 2 };
p.x = 5; // los structs tienen semántica de referencia
print(p.x + p.y);
print(area(Shape.Rect(2.0, 3.0)));
0
}
El match es exhaustivo: el compilador exige cubrir todas las variantes, así que añadir una
variante al enum señala cada match que hay que actualizar. El escrutinio va entre paréntesis.
Los patrones también casan literales, tuplas y variantes anidadas
(Result.Ok(Option.Some(v))). Sobre un int o un string hace falta un brazo _, porque los
valores posibles no se pueden enumerar:
fn label(code: int) -> string {
match (code) {
200 => "ok",
404 => "not found",
_ => "other",
}
}
fn main() -> int {
print(label(404));
0
}
Errores como valores: Option, Result y ?
fn divide(a: int, b: int) -> Result<int, string> {
if (b == 0) { Result.Err("division by zero") } else { Result.Ok(a / b) }
}
fn average(xs: [int]) -> Option<int> {
if (xs.len() == 0) { return Option.None; }
var sum = 0;
for x in xs { sum = sum + x; }
Option.Some(sum / xs.len())
}
fn compute() -> Result<int, string> {
let q = divide(10, 2)?; // desempaqueta o devuelve el Err al llamador
Result.Ok(q + 1)
}
fn main() -> int {
match (compute()) {
Result.Ok(v) => print("ok ${v}"),
Result.Err(e) => print("error: " + e),
}
match (average([3, 4, 5])) {
Option.Some(m) => print(m),
Option.None => print("empty"),
}
0
}
No hay excepciones: una función que puede fallar lo dice en su tipo, y ? propaga el fallo hacia
arriba con una sola tecla.
Arreglos, mapas e iteradores
fn main() -> int {
var xs = [3, 1, 2];
xs.push(4);
print(xs.sort()); // [1, 2, 3, 4] (copia ordenada)
print(xs.contains(2));
var ages: Map<string, int> = Map.new();
ages.insert("ada", 36);
ages.insert("grace", 45);
for (name, age) in ages { // recorrido en orden de clave, determinista
print("${name}: ${age}");
}
let squares = xs.iter()
.filter(fn(x: int) -> bool { x % 2 == 0 })
.map(fn(x: int) -> int { x * x })
.collect(); // los iteradores son perezosos hasta el terminal
print(squares);
print(range(1, 6).sum()); // 15
0
}
x.f(args) es azúcar para f(x, args) (UFCS), así que cualquier función libre se puede encadenar;
x |> f(a) es lo mismo en forma de tubería.
Bucles, break y continue
fn main() -> int {
var i = 0;
var odd_sum = 0;
while (true) {
i = i + 1;
if (i % 2 == 0) { continue; }
if (i > 9) { break; }
odd_sum = odd_sum + i;
}
print(odd_sum); // 1+3+5+7+9 = 25
for k in 0..3 { print(k); } // rango semiabierto
0
}
Traits y genéricos
trait Describe { fn describe(self) -> string; }
struct User { name: string, age: int }
impl Describe for User {
fn describe(self) -> string { "${self.name} (${self.age})" }
}
fn show_all<T: Describe>(items: [T]) {
for it in items { print(it.describe()); }
}
@derive(Eq, Show)
struct Version { major: int, minor: int }
fn main() -> int {
show_all([User { name: "Ada", age: 36 }, User { name: "Grace", age: 45 }]);
let a = Version { major: 1, minor: 7 };
print(a == Version { major: 1, minor: 7 }); // Eq derivado
print(a); // Show derivado
0
}
Los traits despachan estáticamente (con dyn Trait también dinámicamente). Eq, Show, Hash y
ToJson se derivan; Ord y los operadores (Add, Sub, …) se implementan a mano.
Módulos
Un módulo es un archivo. pub expone; se importa por ruta y se usa calificado por el último
segmento:
// src/geo/point.ray
pub struct Point { x: int, y: int }
pub fn origin() -> Point { Point { x: 0, y: 0 } }
// src/main.ray
import geo/point;
from geo/point import origin;
fn main() -> int {
let p = point.origin(); // calificado
let q = origin(); // traído al ámbito
print(p.x + q.y);
0
}
La biblioteca estándar va embebida en el binario y se importa igual: import std/math; →
math.sqrt(2.0). El catálogo completo está en la
referencia.
Tests
fn double(x: int) -> int { x * 2 }
@test
fn double_doubles() -> bool { double(21) == 42 }
@test
fn double_zero() { assert_eq(double(0), 0); }
fn main() -> int { 0 }
ray test corre los @test del proyecto (también los de tests/*.ray); cada uno corre aislado.
Un test que devuelve bool pasa con true; uno sin valor de retorno pasa si ningún assert
falla. ray test --watch los repite al guardar.
Si vienes de otro lenguaje
raylang se parece a Rust y a TypeScript lo bastante como para escribir por costumbre algo que no compila. Estas son las diferencias que más se notan el primer día:
| Si escribes | En raylang es |
|---|---|
if x > 0 { |
if (x > 0) {: la condición de if, while y match va entre paréntesis |
let mut total = 0 |
var total = 0 |
Some(3), Ok(v), None |
Option.Some(3), Result.Ok(v), Option.None: las variantes van calificadas |
null, nil, undefined |
no existe: un valor que puede faltar es un Option<T> |
try / catch, throw |
no existen: una función que puede fallar devuelve Result<T, E>, y ? propaga |
f"hola {x}" |
"hola ${x}", en cualquier cadena |
| una cadena de varias líneas | comillas invertidas: `…`, que admiten saltos de línea y comillas dobles |
| una variable global mutable | no existe: arriba solo hay const; el estado vive en main o en una fibra |
| dos funciones con el mismo nombre | un nombre, una firma: no hay sobrecarga |
5. Concurrencia en dos minutos
Fibras con heap aislado que se comunican por canales tipados, sobre un scheduler multicore. No
hay estado mutable compartido: lo que captura una closure pasada a spawn se copia; entre fibras
solo se comparten canales y handles.
fn main() -> int {
let ch: Channel<int> = Channel.bounded(4); // acotado: contrapresión
let producer = spawn(fn() {
for i in 0..10 { send(ch, i * i); }
close(ch); // cerrar es la señal de "fin"
});
var total = 0;
while (true) {
match (recv(ch)) {
Option.Some(v) => { total = total + v; },
Option.None => { break; }, // canal cerrado y drenado
}
}
join(producer);
print(total); // 285
0
}
scope(fn() { … }) une al salir todas las tareas lanzadas dentro (y si una falla, cancela a las
hermanas); try_join y try_call convierten un fallo en Result; try_send/try_recv no
bloquean; select/select_timeout esperan a varios canales. Con --deterministic la planificación
es reproducible.
6. Trabajar con un asistente LLM
raylang trae dos piezas para que un asistente de código escriba código que compila:
llms.txtes el contexto destilado del lenguaje: en qué se diferencia de Rust, las formas canónicas y los mensajes de error exactos. Pégalo en elCLAUDE.mddel proyecto o en el prompt.ray mcpes un servidor MCP que le da al asistente las herramientasray_check,ray_run,ray_test,ray_fmtyray_doc. El asistente escribe, compila, lee el diagnóstico exacto y corrige, sin que tengas que copiar errores a mano.
Con Claude Code se conecta así:
claude mcp add raylang -- ray mcp
En un proyecto, el asistente debe pasar path (el archivo o el directorio del proyecto) a las
herramientas, no el código suelto: así los imports entre archivos y las dependencias de ray.toml
resuelven igual que con ray run. Las instrucciones del propio servidor se lo indican. El
detalle está en docs/mcp.md.
7. Las herramientas
| Comando | Para qué |
|---|---|
ray run / ray dev |
ejecutar en la VM / modo desarrollo con reinicio y recarga del navegador |
ray check |
comprueba que el programa compila, sin ejecutarlo |
ray test |
las funciones @test; --watch vuelve a correr al guardar |
ray fmt --write src/ |
formato canónico (conserva tus paréntesis y comentarios) |
ray build --native --release |
binario nativo optimizado |
ray bundle |
app de escritorio (.app, .desktop, .exe) o proyecto iOS (--ios) y Android (--android) |
ray dev --device |
recarga en caliente del programa en el teléfono |
ray doc src/main.ray |
documentación a partir de los comentarios /// |
ray profile |
dónde se va el tiempo de un programa |
ray serve _site |
sirve un directorio estático para previsualizar |
ray lsp / ray mcp |
el editor / los asistentes LLM |
ray add, ray search, ray registry publish |
dependencias y publicación |
ray help lista todo, y ray <comando> --help explica cada uno.
8. Qué construir
Desde aquí, cada guía del handbook es un proyecto completo, con su app de ejemplo:
- una app móvil para iOS y Android con frontend React;
- la misma app en escritorio (macOS, Linux, Windows), y las ventanas a fondo;
- un sitio con plantillas renderizado en el servidor;
- una API web con el framework
weby Postgres; - un sitio con frontend React embebido en el binario;
- una herramienta de terminal;
- un agente LLM con herramientas, y un servidor MCP propio.
Cuando funcione, rendimiento enseña a medirlo y a hacerlo rápido.
Y para llevar todo eso a sus usuarios: distribuir, con firma, tiendas móviles y actualizaciones automáticas.
Para todo lo demás: la referencia tiene cada función con su firma, y el manual explica el lenguaje en detalle.