patx/relay-lang
An experimental async web first programming language.
$ git clone https://gitman.io/git/patx/relay-lang
Relay
Relay is an implicitly asynchronous, indentation-based language for trusted web services. The interpreter is written in Rust and uses Tokio, Axum, MongoDB, Reqwest and MiniJinja.
Current development release: 0.2.0-rc.1. This is a breaking release candidate. See implementation status, migration notes, and deployment guidance. Passing unit tests alone is not a production certification.
Build and run
Rust 1.93 or newer is required.
cargo build --release --locked
./target/release/relay --help
./target/release/relay check examples/param_types.ry
./target/release/relay run examples/param_types.ry
app = WebApp()
@app.get("/hello/<name>")
fn hello(name: str)
return {"hello": name, "ok": True}
server = WebServer()
server.run(app)
The default bind address is 127.0.0.1:8080. Use RELAY_BIND to change it. Use four spaces for indentation, fn for functions, and // or /* ... */ for comments. Blocks do not use colons.
Language
Values include integers, floats, booleans, strings, lists, dictionaries, JSON, bytes, functions, modules and asynchronous results. Strings are Unicode; dictionary keys are strings. Type hints are optional runtime checks, not static types.
fn greet(name: str, prefix = "Hello")
return prefix + ", " + name
items = [1, 2, 3]
items.append(4)
assert_equal(items[::-1], [4, 3, 2, 1])
assert(2 in items)
names = [str(item) for item in items if item > 1]
for (item in names)
print(item)
try
raise "example failure"
except(error)
print(error.code, error.message)
finally
print("finished")
Functions have lexical scopes and may return closures. Captured bindings are read-only. Assignments inside a function are local; explicit sessions and database operations provide shared state across requests. Optional parameters use =; missing required arguments and unsupported keyword arguments are errors. Use return, or a final expression for an implicit function result.
Integer arithmetic is checked. Division by zero and unsupported mixed arithmetic raise errors. Equality is structural, including nested collections. String interpolation is explicit:
message = format("Hello {{ name }}", {"name": "Ada"})
Collection helpers include len, range, membership, slicing, += and -=. Legacy =+ and =- are accepted for migration. Strings support split, join, strip, replace, lower, upper, startswith, and endswith; lists support append, extend, and pop; dictionaries support get, keys, values, items, and pop. Explicit dictionary fields take precedence over method names.
Modules
import lib.helpers as helpers
from "../shared/validation.ry" import validate_name
Imports resolve relative to the source file. Each module has its own namespace and is cached by canonical path. Names beginning with _ are private; cycles are rejected. A route module exports a register(app) function and defines its decorated handlers within that function. See the multi-file Twitter example.
Async
There is no await keyword. I/O and timer calls start immediately; consuming their values waits for completion. Results and failures can be consumed repeatedly.
http = Http()
first = http.get("https://example.com")
second = http.get("https://example.org")
responses = all([first, second])
print(responses[0].status)
spawn(expression)schedules an expression in an isolated execution environment.all(items)waits concurrently and preserves input order.race(items)returns the first completion, including errors; the input must be nonempty.timeout(expression, milliseconds)bounds execution of the expression.cancel(task)requests cancellation; repeated cancellation is harmless.sleep(milliseconds, expression)evaluates the optional expression after the delay.app.background(function, ...args)runs supervised application work without request context.
Scripts and HTTP requests join their child work before exiting or sending their response. Unobserved failures propagate. Background tasks are process-local, not durable jobs. There are bounds on syntax nesting, function recursion and active tasks.
Web services
Routes support GET, POST, PUT, PATCH, DELETE and WebSockets; GET routes support HEAD through Axum. Middleware uses fn middleware(ctx, next); next() may be called only once. Route groups, static mounts, request validation and OpenAPI generation remain available.
Arguments bind with path > body > query precedence. Handler type hints coerce incoming values. request exposes method, actual path, route pattern, query, headers, cookies, form, JSON and request ID. get_query(), get_body(), and get_json() are convenience helpers.
Dictionaries and lists produce JSON preserving nested types. Strings produce plain text. Use Response(body, status=..., content_type=...), app.json(value), app.redirect(url), or app.render_template(path, data) for explicit responses. HTML templates automatically escape interpolated values.
mongo = Mongo(env.require("RELAY_MONGO_URI"))
db = mongo.db("example")
app.sessions(db.collection("relay_sessions"))
@app.get("/me")
fn me()
return {"user": session["user"], "csrf_token": session.csrf_token()}
Rotate sessions after login with session.regenerate(), destroy them at logout with session.destroy(), and send the CSRF token for unsafe cookie-authenticated requests. MongoDB sessions support expiry and versioned writes across processes. Production mode requires them.
MongoDB provides CRUD, find(filter, limit=100, skip=0, sort={...}), count(filter), find_one_and_update(filter, update, upsert=True), and create_index(keys, unique=True). Find results are capped at 1,000. Use atomic updates for counters and indexes for uniqueness.
The standard library also includes file/JSON I/O, json_parse/json_stringify, environment access, SMTP with attachments, password hashing/verification, multipart uploads and HTTP clients. The archived reference documents existing APIs; consult the migration guide for changed behavior.
Tooling and validation
relay check app.ry # syntax, signatures, control flow and import checks; no app execution
relay fmt app.ry # conservative whitespace formatting; preserves comments and strings
relay fmt --check examples
relay test tests # test_*.ry files, test_* functions, isolated runtimes
cargo test --locked # Rust unit/router tests
Use assert(value, message) and assert_equal(actual, expected) in Relay tests. No external package registry or static type checker is included.
The source is grouped into frontend, runtime, standard library, web, session, networking and CLI files under src/. The binary entry point delegates to the library. Unit tests, live integration scripts and the hour-long soak are separate checks; see deployment verification.
Licensed under MIT.