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.