patx/relay-lang

# Migrating from Relay 0.1

This release changes language semantics. Test an application against the candidate before deployment. The archived [0.1 reference](v0.1-reference.md) describes the old behavior.

## Imports and functions

Imports no longer merge globals. Use `import helpers as helpers` and `helpers.function()`, or `from helpers import function`. Quoted relative paths such as `from "../lib/common.ry" import helper` are supported. Names beginning with `_` are private; cycles are errors.

Route modules define `fn register(app)` and register their decorators within it; the entry script imports the module and calls `module.register(app)`. The Twitter example demonstrates this pattern.

Functions resolve names lexically. Rebinding a name creates or updates a local binding, not a caller's variable. Captured/module collections cannot be mutated from a function; copy them into a local variable first. Sessions and database handles provide explicit shared persistence. Blocks use their surrounding function scope. Closures capture read-only lexical values.

Missing, extra, duplicate, and unknown arguments are errors. Function type hints validate without coercion; HTTP inputs are coerced before invocation. Integer overflow, division by zero, and unsupported mixed arithmetic raise language errors. Use `str(value)` to concatenate a number with text. Equality compares values structurally instead of comparing their printed representation.

## Async and errors

I/O and timer operations start immediately, including assignments. Reading an async result waits for it; repeated readers receive the same value or error. `spawn(expression)` schedules the expression itself. `timeout(expression, milliseconds)` covers expression execution. `all` waits concurrently in input order; `race` returns the first completion and rejects an empty input. Cancel explicitly when other consumers no longer need a task.

Scripts and requests join their child operations before exit/response. Unobserved failures now fail the scope instead of being discarded. `app.background(function, ...arguments)` creates supervised, process-local background work and returns `None`; it must not capture request or session context. It is not a durable queue.

Caught errors expose `code` and `message`; `raise` and `finally` are supported. Diagnostics include source locations and function frames. `RELAY_TRACE=1` emits statement-line trace events for local debugging.

## Strings and HTTP

String literals no longer evaluate templates. Use `format("Hello {{ name }}", {"name": name})` for text formatting. `app.render_template(path, data)` renders HTML with automatic escaping and returns an HTML response. `Response(existing_response, status=404)` preserves its body and content type. MiniJinja's explicit `safe` filter should be used only for trusted HTML.

Plain strings are returned as plain text, even if they contain HTML. Use `Response(html, content_type="text/html; charset=utf-8")` for explicit HTML. Dictionaries and lists now preserve JSON types recursively. Schema failures return 422. Unexpected handler failures return a generic 500 response with a request ID; details are not returned to clients.

`request.path` is the actual path; `request.route_pattern` is the registered pattern. Request IDs are present on responses. Untrusted forwarded headers are removed; configure trusted proxy IP addresses explicitly.

## Sessions and databases

Configure shared sessions with `app.sessions(db.collection("relay_sessions"))` before starting the server. This creates the expiry index. Production mode requires MongoDB-backed sessions and an HTTPS public origin. Callback session backends remain for development compatibility and do not provide versioned distributed guarantees.

Sessions expire after 30 minutes idle or seven days absolute. Use `session.regenerate()` after authentication, `session.destroy()` at logout, and `session.csrf_token()` to obtain a token. Existing-session POST/PUT/PATCH/DELETE requests require that token in `x-csrf-token` or the `csrf_token` form field. Refresh the token after login because rotation changes it. Concurrent conflicting changes return 409; clients may retry after refreshing their session. Revoked sessions cannot be recreated by stale writes.

MongoDB `find(filter, limit=100, skip=0, sort={...})` is bounded; the maximum limit is 1,000. Use `count(filter)` for totals and paginate larger result sets. Use `find_one_and_update(filter, update, upsert=True)` for atomic counters. `create_index(keys, unique=True)` establishes constraints. The Twitter example uses these APIs.

## Tools

The CLI provides `run`, `check`, `fmt`, and `test`. `relay file.ry` remains an alias for `run`. `fmt` conservatively normalizes trailing whitespace while preserving comments and literal content. `check` validates syntax, function signatures, control flow and imports without executing the app; it is not a static type checker.