Networking-first, async-by-default
Everything is async, but nothing waits unless it has to.

Why Relay?

No await keyword needed Relay v0.1
fn main()
    x = sleep(1000, 10)
    y = sleep(1000, 20)
    print(x + y)  # Automatically waits when values are needed

Implicit Async

No await keyword. Async values resolve automatically when you need them. Write synchronous-looking code that runs asynchronously.

🌐

Built for Networking

HTTP client and web server built into the standard library. Create APIs and services with minimal boilerplate.

🎯

Simple Syntax

Python-like syntax with indentation-based blocks. Easy to read, easy to write, easy to maintain.

🚀

Non-blocking I/O

All I/O operations are non-blocking by default. Node.js-style runtime behavior with better ergonomics.

🔧

Concurrency Primitives

Built-in spawn, all, race, timeout, and cancel. Powerful concurrency without the complexity.

📦

Batteries Included

File I/O, JSON processing, HTTP client/server, and async primitives all in the standard library.

🎨

Template Rendering

Built-in template strings with {{ variable }} syntax. Return HTML from handlers with automatic variable interpolation.

🔍

Smart Type Coercion

Optional type hints (str, int, float, Json) with automatic runtime validation and coercion in function parameters.

⚙️

Decorator Syntax

Python-style decorators for web routing. Use @app.get("/path") or @route.post("/api") to define endpoints declaratively.

How Relay Compares

JavaScript/TypeScript
const x = await sleep(1000, 10);
const y = await sleep(1000, 20);
console.log(x + y);

Sequential execution, must remember await

Python (asyncio)
x = await sleep(1, 10)
y = await sleep(1, 20)
print(x + y)

Requires async/await keywords everywhere

Relay
x = sleep(1000, 10)
y = sleep(1000, 20)
print(x + y)

Concurrent execution, no await needed

Quick Examples

Web server with routing server.ry
app = WebApp()
server = WebServer()

@app.get("/")
fn index()
    return "Hello world"

@app.get("/hello/<name>")
fn hello(name)
    return "Hello {{ name }}"

server.run(app)
Concurrent tasks concurrent.ry
fn work(n)
    sleep(500, n * 2)

jobs = [spawn(work(2)), spawn(work(5)), spawn(work(7))]
results = all(jobs)
print(results)  # [4, 10, 14]
HTTP client & JSON http-client.ry
http = Http()
resp = http.get("https://api.example.com/data")
data = resp.json()
print(data["results"])
JSON API with type hints api.ry
app = WebApp()
server = WebServer()

@app.post("/users")
fn create_user(data: Json)
    return {"id": 123, "name": data["name"]}

server.run(app)
Augmented assignment counters.ry
count = 0
count =+ 5  # count is now 5
count =- 2  # count is now 3
print(count)

Under the Hood

🦀 Built with Rust

Single-file interpreter with AST-based evaluation. Uses Tokio for async runtime, ensuring robust concurrency.

🌐 Axum-powered Web Server

Web server built on Axum framework with automatic JSON responses, path parameters, query strings, and request body handling.

⚡ Deferred Values

Implicit async through Value::Deferred. Operations return immediately; resolution happens automatically when values are needed.

📝 Indentation-based Parser

4 spaces per level, no tabs. Clean syntax without semicolons or braces, similar to Python but with async superpowers.

Request handling with smart parameter injection Path > Body > Query precedence
@app.post("/api/users/<id>")
fn update_user(id: int, data: Json, role: str = "user")
    # id comes from path parameter
    # data is the JSON request body
    # role comes from query string (defaults to "user")
    return {"updated": id, "role": role}

Get Started

1 Clone or download Relay
git clone https://github.com/patx/relay-lang && cd relay-lang
2 Build with Cargo

Relay is a single-file Rust interpreter. Dependencies: tokio, serde, serde_json, reqwest, axum, tower, tower-http, html-escape, indexmap, async-recursion, thiserror, anyhow

cargo build --release
3 Install globally (optional)
cargo install --path .
4 Run your first program
relay test.ry # or use cargo directly: cargo run -- test.ry
5 Configure web server (optional)

Default bind: 127.0.0.1:3000. Override with environment variable:

RELAY_BIND=0.0.0.0:8080 relay server.ry

Standard Library Reference

Core & Type Conversion
print(value)           # Output to console
str(x)                 # Convert to string
int(x)                 # Convert to integer
float(x)               # Convert to float
Async & Concurrency
sleep(ms, value)      # Non-blocking delay, returns value after ms
spawn(expr)            # Spawn task, returns Task handle
all([tasks])          # Wait for all tasks, return list of results
race([tasks])         # Return first completed task result
timeout(expr, ms)     # Timeout an expression after ms
cancel(task)          # Cancel a running task
task.join()             # Wait for task completion
deferred.resolve()    # Force resolution of deferred value
File System & JSON
read_file(path)       # Read file as string
save_file(content, path) # Write string to file
read_json(path)       # Read and parse JSON file
save_json(data, path) # Serialize and save JSON
HTTP Client
http = Http()
resp = http.get(url)
resp = http.post(url, data)

# Response object members:
resp.status             # HTTP status code (int)
resp.text               # Response body as string
resp.json()             # Parse response as JSON
Web Server & Routing
app = WebApp()
route = app.route()         # Optional: get route handle
server = WebServer()

# Decorator routing (use @app or @route):
@app.get(path)
@app.post(path)
@route.get(path)
@route.post(path)

# Manual response control:
Response(body, status=200, content_type="text/plain")

# Start server (bind address via RELAY_BIND env var):
server.run(app)