patx/relay-lang

Add break/continue flow control and refresh README examples

Commit 878e30b · patx · 2026-02-12T23:39:45-05:00

Changeset
878e30b98d36536ad663313bc48917162e1e0e90
Parents
1b2c3e6304c9b38ca0f302d74aa6e43495011fb4

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/README.md b/README.md
index 39b9982..f8d1608 100644
--- a/README.md
+++ b/README.md
@@ -106,13 +106,13 @@ relay examples/url_shortener.ry     # requires local MongoDB
 
 ### Hello World
 
-```relay
+```python
 print("Hello, Relay!")
 ```
 
 ### Async Hello World
 
-```relay
+```python
 sleep(2000, print("world"))
 print("hello")
 ```
@@ -127,7 +127,7 @@ Notice how "hello" prints immediately while "world" waits 2 seconds—all withou
 
 ### Simple Web Server
 
-```relay
+```python
 app = WebApp()
 server = WebServer()
 
@@ -206,7 +206,7 @@ If your goal is to learn Relay end-to-end, use this order:
 
 Relay uses **indentation-based syntax** with 4 spaces per indentation level. Tabs are not allowed.
 
-```relay
+```python
 fn example()
     x = 10
     if (x > 5)
@@ -221,7 +221,7 @@ fn example()
 - Function bodies, control flow blocks, and loops all require indentation
 - Comments start with `//` and continue to the end of the line
 
-```relay
+```python
 // This is a comment
 x = 42  // This is also a comment
 ```
@@ -230,7 +230,7 @@ x = 42  // This is also a comment
 
 Relay supports loading code from multiple `.ry` files with `import`:
 
-```relay
+```python
 import utils
 import web.routes
 import shared/helpers.ry
@@ -242,13 +242,15 @@ import shared/helpers.ry
 - Relative paths are resolved from the importing file's directory
 - A module is loaded only once per run (duplicate imports are ignored)
 
+The module filenames above are illustrative examples of resolution behavior.
+
 Imported modules execute in the same global scope, so functions and variables they define become directly available.
 
 ### Data Types
 
 #### Primitives
 
-```relay
+```python
 // Integers
 age = 25
 count = -10
@@ -272,7 +274,7 @@ result = None
 #### Collections
 
 **Lists:**
-```relay
+```python
 numbers = [1, 2, 3, 4, 5]
 mixed = [1, "two", 3.0, True]
 nested = [[1, 2], [3, 4]]
@@ -290,7 +292,7 @@ numbers[0] = 10
 ```
 
 **Dictionaries:**
-```relay
+```python
 user = {"name": "Ada", "age": 30, "active": True}
 
 // Access values
@@ -307,7 +309,7 @@ age = user["age"]       // 30
 
 Relay supports destructuring assignment for iterables such as lists, strings, and dictionaries (dictionary keys are unpacked):
 
-```relay
+```python
 a, b, c = [10, 20, 30]
 x, y = "hi"           // x = "h", y = "i"
 k1, k2 = {"a": 1, "b": 2}
@@ -319,20 +321,22 @@ The number of variables must match the number of unpacked values.
 
 #### If-Else
 
-```relay
+```python
 if (condition)
     // then block
     print("condition is true")
 ```
 
-```relay
+```python
 if (x > 0)
     print("positive")
+else
+    print("zero or negative")
 ```
 
 #### While Loops
 
-```relay
+```python
 i = 0
 while (i < 5)
     print(i)
@@ -340,7 +344,7 @@ while (i < 5)
 ```
 
 Augmented assignment operators are supported:
-```relay
+```python
 i = 0
 while (i < 5)
     print(i)
@@ -349,7 +353,7 @@ while (i < 5)
 
 #### For Loops
 
-```relay
+```python
 // Iterate over lists
 for (item in [1, 2, 3, 4, 5])
     print(item)
@@ -362,11 +366,24 @@ for (key in user)
 
 **Note:** For loops iterate over collection elements or dictionary keys.
 
+#### `break` and `continue`
+
+```python
+i = 0
+while (i < 6)
+    i =+ 1
+    if (i == 2)
+        continue
+    if (i == 5)
+        break
+    print(i)
+```
+
 #### Error Handling (`try/except`)
 
 Use `try/except` to catch runtime errors and continue execution:
 
-```relay
+```python
 try
     value = int("not-a-number")
     print(value)
@@ -376,7 +393,7 @@ except
 
 You can also bind the error message:
 
-```relay
+```python
 try
     result = missing_name + 1
 except(err)
@@ -387,7 +404,7 @@ except(err)
 
 #### Basic Functions
 
-```relay
+```python
 fn greet(name)
     return "Hello, " + name
 
@@ -397,7 +414,7 @@ print(message)  // Hello, World
 
 #### Default Parameters
 
-```relay
+```python
 fn greet(name: str = "World")
     return "Hello, " + name
 
@@ -409,7 +426,7 @@ print(greet("Relay"))   // Hello, Relay
 
 Relay supports runtime type checking for parameters:
 
-```relay
+```python
 fn add(a: int, b: int)
     return a + b
 
@@ -426,7 +443,7 @@ Type hints on regular functions are strict validation (no implicit coercion). Un
 - `bool`: Boolean
 - `json` or `Json`: JSON object
 
-```relay
+```python
 fn process_data(data: json)
     return data["key"]
 ```
@@ -435,7 +452,7 @@ fn process_data(data: json)
 
 Functions can return any value:
 
-```relay
+```python
 fn get_user()
     return {"name": "Ada", "id": 1}
 
@@ -455,7 +472,7 @@ Relay's async model is unique: **there is no `await` keyword**. Instead, async o
 
 When you call an async function without using its return value, it runs in the background:
 
-```relay
+```python
 sleep(1000, print("delayed"))
 print("immediate")
 ```
@@ -470,7 +487,7 @@ delayed
 
 When you assign the result of an async operation, you get a `Deferred` value. The operation starts immediately but doesn't block:
 
-```relay
+```python
 x = sleep(1000, 10)     // Returns immediately with Deferred<10>
 y = sleep(1000, 20)     // Returns immediately with Deferred<20>
 print(x + y)            // Waits for both, then prints 30
@@ -483,23 +500,23 @@ Both sleeps start at the same time, so this takes ~1 second, not 2.
 A `Deferred` value automatically resolves (waits for the async operation to complete) when:
 
 1. **Used in an expression:**
-```relay
+```python
 result = deferred_value + 10
 ```
 
 2. **Passed to a function:**
-```relay
+```python
 print(deferred_value)
 ```
 
 3. **Returned from a function:**
-```relay
+```python
 fn get_data()
     return http.get("https://api.example.com")
 ```
 
 4. **Used in a comparison:**
-```relay
+```python
 if (deferred_value > 10)
     print("greater than 10")
 ```
@@ -507,7 +524,7 @@ if (deferred_value > 10)
 #### Concurrency Primitives
 
 **`spawn(expr)`** - Run an expression in parallel:
-```relay
+```python
 fn work(n)
     sleep(500, n * 2)
 
@@ -518,24 +535,24 @@ result2 = task2.join()
 ```
 
 **`all(tasks)`** - Wait for all tasks to complete:
-```relay
+```python
 tasks = [spawn(work(1)), spawn(work(2)), spawn(work(3))]
 results = all(tasks)    // [2, 4, 6]
 ```
 
 **`race(tasks)`** - Wait for the first task to complete:
-```relay
+```python
 tasks = [spawn(sleep(1000, "slow")), spawn(sleep(100, "fast"))]
 winner = race(tasks)    // "fast"
 ```
 
 **`timeout(expr, ms)`** - Add a timeout to any operation:
-```relay
+```python
 result = timeout(http.get("https://slow-api.com"), 5000)
 ```
 
 **`cancel(task)`** - Cancel a running task:
-```relay
+```python
 task = spawn(long_operation())
 cancel(task)
 ```
@@ -549,7 +566,7 @@ cancel(task)
 #### `print(value, ...)`
 Print values to stdout. Multiple arguments are printed space-separated.
 
-```relay
+```python
 print("Hello")                  // Hello
 print("x =", 42)                // x = 42
 print("a", "b", "c")            // a b c
@@ -558,7 +575,7 @@ print("a", "b", "c")            // a b c
 #### `str(value)`
 Convert any value to a string.
 
-```relay
+```python
 str(123)        // "123"
 str(3.14)       // "3.14"
 str(True)       // "True"
@@ -568,7 +585,7 @@ str([1, 2, 3])  // "[1, 2, 3]"
 #### `int(value)`
 Convert a value to an integer.
 
-```relay
+```python
 int("42")       // 42
 int(3.9)        // 3
 int(True)       // 1
@@ -578,7 +595,7 @@ int("invalid")  // Runtime error
 #### `float(value)`
 Convert a value to a float.
 
-```relay
+```python
 float("3.14")   // 3.14
 float(42)       // 42.0
 float("2.5e3")  // 2500.0
@@ -591,7 +608,7 @@ float("2.5e3")  // 2500.0
 
 Sleep for the specified duration, then resolve to `value`.
 
-```relay
+```python
 sleep(1000, print("done"))          // Print after 1 second
 result = sleep(2000, 42)            // Wait 2s, result = 42
 ```
@@ -601,7 +618,7 @@ result = sleep(2000, 42)            // Wait 2s, result = 42
 
 Execute an expression in parallel. Returns a `Task` object.
 
-```relay
+```python
 task = spawn(expensive_computation())
 // Do other work...
 result = task.join()
@@ -615,7 +632,7 @@ result = task.join()
 
 Wait for all tasks to complete. Returns results in order.
 
-```relay
+```python
 tasks = [spawn(work(1)), spawn(work(2)), spawn(work(3))]
 results = all(tasks)  // Wait for all, returns [result1, result2, result3]
 ```
@@ -625,7 +642,7 @@ results = all(tasks)  // Wait for all, returns [result1, result2, result3]
 
 Wait for the first task to complete, return its result.
 
-```relay
+```python
 tasks = [
     spawn(http.get("https://api1.com")),
     spawn(http.get("https://api2.com"))
@@ -638,7 +655,7 @@ fastest = race(tasks)  // Returns whichever completes first
 
 Add a timeout to any async operation.
 
-```relay
+```python
 result = timeout(http.get("https://slow.com"), 5000)  // 5 second timeout
 ```
 
@@ -647,7 +664,7 @@ If the timeout is exceeded, a runtime error is raised.
 #### `cancel(task)`
 Cancel a running task.
 
-```relay
+```python
 task = spawn(long_running_operation())
 cancel(task)
 ```
@@ -659,7 +676,7 @@ cancel(task)
 
 Read a file's contents as a UTF-8 string.
 
-```relay
+```python
 content = read_file("data.txt")
 print(content)
 ```
@@ -669,7 +686,7 @@ print(content)
 
 Write a string to a file.
 
-```relay
+```python
 save_file("Hello, World!", "output.txt")
 ```
 
@@ -678,7 +695,7 @@ save_file("Hello, World!", "output.txt")
 
 Read and parse a JSON file.
 
-```relay
+```python
 data = read_json("config.json")
 print(data["api_key"])
 ```
@@ -688,7 +705,7 @@ print(data["api_key"])
 
 Serialize data to JSON and write to a file.
 
-```relay
+```python
 config = {"host": "localhost", "port": 8080}
 save_json(config, "config.json")
 ```
@@ -698,7 +715,7 @@ save_json(config, "config.json")
 #### `Http()`
 Create an HTTP client instance.
 
-```relay
+```python
 http = Http()
 ```
 
@@ -707,7 +724,7 @@ http = Http()
 
 Send a GET request.
 
-```relay
+```python
 http = Http()
 resp = http.get(
     "https://api.example.com/users",
@@ -725,7 +742,7 @@ print(resp.text)    // Response body as string
 
 Send requests with optional payloads and headers.
 
-```relay
+```python
 http = Http()
 payload = {"name": "Ada", "email": "[email protected]"}
 resp = http.post(
@@ -744,7 +761,7 @@ HTTP responses have the following properties:
 - `resp.json()` - Parse response body as JSON
 - `resp.headers` - Response headers (dict)
 
-```relay
+```python
 http = Http()
 resp = http.get("https://api.github.com/users/octocat")
 
@@ -758,7 +775,7 @@ print(data["login"])                  // octocat
 #### `Email(host, port=587, username=None, password=None, from=None, tls="starttls")`
 Create an SMTP email client instance.
 
-```relay
+```python
 email = Email(
     "smtp.example.com",
     587,
@@ -784,7 +801,7 @@ Send an email asynchronously over SMTP.
 - `from` can be passed per-call or configured in `Email(...)`
 - `attachments` accepts a single attachment dict or list of attachment dicts
 
-```relay
+```python
 email = Email(
     host="smtp.example.com",
     username="smtp-user",
@@ -807,7 +824,7 @@ Attachment dict format:
 - `bytes` or `content` or `data` (required) - file payload (`bytes`, `str`, or `list[int]`)
 - `content_type` (optional) - MIME type (default: `application/octet-stream`)
 
-```relay
+```python
 result = email.send(
     to="[email protected]",
     subject="Monthly report",
@@ -835,7 +852,7 @@ Send result fields:
 
 Render an email template string with MiniJinja variables.
 
-```relay
+```python
 body = email.render("Hi {{ name }}, welcome!", {"name": "Ada"})
 ```
 
@@ -844,7 +861,7 @@ body = email.render("Hi {{ name }}, welcome!", {"name": "Ada"})
 
 Load and render a template file asynchronously.
 
-```relay
+```python
 html = email.render_file("templates/welcome.html", {"name": "Ada"})
 email.send(to="[email protected]", subject="Welcome", html=html)
 ```
@@ -854,7 +871,7 @@ email.send(to="[email protected]", subject="Welcome", html=html)
 #### `WebApp()`
 Create a web application instance.
 
-```relay
+```python
 app = WebApp()
 ```
 
@@ -869,7 +886,7 @@ Define HTTP endpoints using decorators:
 - `@app.delete(path)`
 - `@app.ws(path)`
 
-```relay
+```python
 app = WebApp()
 
 @app.get("/")
@@ -887,7 +904,7 @@ Decorator schema options:
 - `body=...` - Validate/coerce form body
 - `json=...` - Validate/coerce JSON body
 
-```relay
+```python
 @app.get("/search", query={"limit": "int", "q?": "str"})
 fn search(limit, q = None)
     return {"limit": limit, "q": q}
@@ -901,7 +918,7 @@ fn create_user(name, age = None)
 
 Use grouped route prefixes to organize larger APIs:
 
-```relay
+```python
 app = WebApp()
 api = app.group("/api")
 v1 = api.group("/v1")
@@ -913,7 +930,7 @@ fn get_user(user_id)
 
 Enable generated OpenAPI docs:
 
-```relay
+```python
 app.openapi(title="Relay API", version="1.0.0")
 // Exposes GET /openapi.json
 ```
@@ -926,7 +943,7 @@ WebSocket handlers receive a `socket` object with:
 - `socket.send(value)` → sends text (or binary when `bytes`)
 - `socket.close()` → closes the connection
 
-```relay
+```python
 @app.ws("/chat")
 fn chat_room()
     while True
@@ -940,7 +957,7 @@ fn chat_room()
 
 Use `<name>` syntax to capture path segments:
 
-```relay
+```python
 @app.get("/users/<user_id>")
 fn get_user(user_id)
     return {"id": user_id, "name": "Ada"}
@@ -957,7 +974,7 @@ Handler parameters are automatically bound from:
 2. **Request body form fields** (`application/x-www-form-urlencoded`)
 3. **Query parameters** (lowest priority)
 
-```relay
+```python
 // GET /search?q=relay&limit=10
 @app.get("/search")
 fn search(q: str, limit: int = 20)
@@ -976,7 +993,7 @@ fn get_user(user_id)
 
 JSON bodies are available as the `data` parameter (default name) or by typing a handler param as `Json`.
 
-```relay
+```python
 // POST /events with JSON body {"type":"signup","user":"ada"}
 @app.post("/events")
 fn create_event(data: Json)
@@ -998,7 +1015,7 @@ If middleware returns a non-`None` value, Relay short-circuits and sends that re
 
 `next()` runs the remainder of the middleware chain and then the handler.
 
-```relay
+```python
 fn audit(ctx, next)
     print("before:", ctx["path"])
     result = next()
@@ -1010,7 +1027,7 @@ fn audit(ctx, next)
 
 Use type hints to enforce parameter types and enable automatic coercion:
 
-```relay
+```python
 @app.post("/calculate")
 fn calculate(a: int, b: int)
     return {"result": a + b}
@@ -1030,7 +1047,7 @@ fn calculate(a: int, b: int)
 
 Every handler has access to a `request` dictionary:
 
-```relay
+```python
 @app.get("/debug")
 fn debug_request()
     print(request["method"])    // GET
@@ -1069,7 +1086,7 @@ Use helpers when you want payload access without binding handler parameters:
 - `get_body()` - Parsed form fields as a dict (empty dict when unavailable)
 - `get_json()` - Parsed JSON body (or `None` when unavailable)
 
-```relay
+```python
 @app.get("/search")
 fn search()
     query = get_query()
@@ -1096,7 +1113,7 @@ Schema format:
 - `"field?": "type"` for optional fields
 - `"field": {"type": "int", "required": True, "default": 10}` for explicit rules
 
-```relay
+```python
 @app.get("/search")
 fn search()
     params = require_query({"limit": "int", "q?": "str"})
@@ -1107,7 +1124,7 @@ fn search()
 
 Access cookies via the `cookies` dict:
 
-```relay
+```python
 @app.get("/")
 fn index()
     user_id = cookies["user_id"]
@@ -1118,7 +1135,7 @@ fn index()
 
 Relay provides built-in session management with HttpOnly cookies:
 
-```relay
+```python
 @app.get("/login")
 fn login(username: str)
     session["user"] = username
@@ -1141,13 +1158,13 @@ fn profile()
 
 Customize cookie policy:
 
-```relay
+```python
 app.session(secure=True, http_only=True, same_site="Lax")
 ```
 
 Use a custom session backend (for any database/service):
 
-```relay
+```python
 session_db = {}
 
 fn load_session(sid)
@@ -1163,7 +1180,7 @@ app.session_backend(load_session, save_session)
 
 Configure upload safety controls per app:
 
-```relay
+```python
 app.uploads(
     max_body_bytes=10 * 1024 * 1024,   // default 10 MiB
     max_file_bytes=5 * 1024 * 1024,    // default 5 MiB per file field
@@ -1183,28 +1200,28 @@ When limits are exceeded or a MIME type is disallowed, Relay returns `400 bad_re
 Handlers can return various types:
 
 **JSON (automatic):**
-```relay
+```python
 @app.get("/api/user")
 fn get_user()
     return {"name": "Ada", "id": 123}  // Auto-serialized to JSON
 ```
 
 **Plain text:**
-```relay
+```python
 @app.get("/")
 fn index()
     return "Hello, World!"  // Content-Type: text/plain
 ```
 
 **HTML:**
-```relay
+```python
 @app.get("/")
 fn index()
     return "<h1>Welcome</h1>"  // Content-Type: text/html
 ```
 
 **Custom Response:**
-```relay
+```python
 @app.get("/custom")
 fn custom()
     return Response(
@@ -1215,7 +1232,7 @@ fn custom()
 ```
 
 **Redirect:**
-```relay
+```python
 @app.post("/old-path")
 fn old_endpoint()
     return app.redirect("/new-path")
@@ -1224,7 +1241,7 @@ fn old_endpoint()
 #### `Response(body, status=200, content_type=None)`
 Create a custom HTTP response.
 
-```relay
+```python
 @app.get("/xml")
 fn get_xml()
     xml = "<root><item>data</item></root>"
@@ -1240,7 +1257,7 @@ fn get_xml()
 Create a structured API error response.
 When called inside a handler, Relay also includes `request_id` in the error payload.
 
-```relay
+```python
 @app.post("/users")
 fn create_user(name)
     if (name == None)
@@ -1256,7 +1273,7 @@ fn create_user(name)
 #### `auth_verify_password(password, hash)`
 **Returns:** `bool`
 
-```relay
+```python
 hash = auth_hash_password("super-secret")
 is_valid = auth_verify_password("super-secret", hash)   // True
 ```
@@ -1273,7 +1290,7 @@ Available methods:
 - `store.get_hash(username)` - returns stored hash or `None`
 - `store.set_hash(username, hash)` - stores precomputed hash
 
-```relay
+```python
 store = AuthStore()
 store.register("ada", "pw")
 print(store.verify("ada", "pw"))  // True
@@ -1295,7 +1312,7 @@ print(custom.verify("bob", "pw2"))  // True
 
 Create a redirect response.
 
-```relay
+```python
 @app.post("/submit")
 fn submit(data)
     // Process data...
@@ -1308,7 +1325,7 @@ Relay uses **MiniJinja** (Rust implementation of Jinja2) for template interpolat
 
 Template strings are evaluated anywhere in the interpreter (not only in web handlers) when a string contains both `{{` and `}}`.
 
-```relay
+```python
 name = "Relay"
 version = "0.1"
 
@@ -1321,7 +1338,7 @@ print("Count: {{ items | length }}")
 
 Web handlers use the same engine:
 
-```relay
+```python
 @app.get("/")
 fn index()
     return "<h1>{{ name }} v{{ version }}</h1>"
@@ -1331,7 +1348,7 @@ Templates can reference values in the current scope and support MiniJinja expres
 
 Use `app.render_template(path, ...kwargs)` when you want explicit template rendering from a file without relying on implicit `{{ ... }}` string evaluation:
 
-```relay
+```python
 app = WebApp()
 html = app.render_template("templates/welcome.html", name="Ada", plan="Pro")
 ```
@@ -1339,14 +1356,14 @@ html = app.render_template("templates/welcome.html", name="Ada", plan="Pro")
 #### `WebServer()`
 Create a web server instance.
 
-```relay
+```python
 server = WebServer()
 ```
 
 #### `server.run(app)`
 Start the web server.
 
-```relay
+```python
 app = WebApp()
 server = WebServer()
 
@@ -1369,7 +1386,7 @@ server.run(app)  // Starts server on 127.0.0.1:8080
 #### `Mongo(connection_string)`
 Create a MongoDB client.
 
-```relay
+```python
 mongo = Mongo("mongodb://localhost:27017")
 ```
 
@@ -1379,7 +1396,7 @@ mongodb://[username:password@]host[:port][/database]
 ```
 
 Examples:
-```relay
+```python
 // Local MongoDB
 mongo = Mongo("mongodb://localhost:27017")
 
@@ -1395,7 +1412,7 @@ mongo = Mongo("mongodb://admin:password@localhost:27017")
 
 Access a database.
 
-```relay
+```python
 mongo = Mongo("mongodb://localhost:27017")
 db = mongo.db("my_app")
 ```
@@ -1405,7 +1422,7 @@ db = mongo.db("my_app")
 
 Access a collection.
 
-```relay
+```python
 users = db.collection("users")
 posts = db.collection("posts")
 ```
@@ -1415,7 +1432,7 @@ posts = db.collection("posts")
 
 Insert a single document.
 
-```relay
+```python
 users = db.collection("users")
 result = users.insert_one({"name": "Ada", "email": "[email protected]"})
 print(result["inserted_id"])  // ObjectId as string
@@ -1426,7 +1443,7 @@ print(result["inserted_id"])  // ObjectId as string
 
 Insert multiple documents.
 
-```relay
+```python
 users = db.collection("users")
 docs = [
     {"name": "Ada", "email": "[email protected]"},
@@ -1441,7 +1458,7 @@ print(result["inserted_ids"])  // Dict of index -> ObjectId string
 
 Find a single document matching the filter.
 
-```relay
+```python
 users = db.collection("users")
 user = users.find_one({"email": "[email protected]"})
 if (user != None)
@@ -1449,7 +1466,7 @@ if (user != None)
 ```
 
 **Filter examples:**
-```relay
+```python
 // Exact match
 user = users.find_one({"name": "Ada"})
 
@@ -1465,7 +1482,7 @@ user = users.find_one({"_id": "507f1f77bcf86cd799439011"})
 
 Find all documents matching the filter.
 
-```relay
+```python
 users = db.collection("users")
 active_users = users.find({"active": True})
 for (user in active_users)
@@ -1473,7 +1490,7 @@ for (user in active_users)
 ```
 
 **Find all documents:**
-```relay
+```python
 all_users = users.find({})
 ```
 
@@ -1482,7 +1499,7 @@ all_users = users.find({})
 
 Update a single document.
 
-```relay
+```python
 users = db.collection("users")
 result = users.update_one(
     {"email": "[email protected]"},
@@ -1492,7 +1509,7 @@ print(result["modified_count"])  // 1
 ```
 
 **Update operators:**
-```relay
+```python
 // Set fields
 users.update_one({"_id": id}, {"$set": {"status": "active"}})
 
@@ -1508,7 +1525,7 @@ users.update_one({"_id": id}, {"$unset": {"temp_field": ""}})
 
 Update multiple documents.
 
-```relay
+```python
 users = db.collection("users")
 result = users.update_many(
     {"active": False},
@@ -1522,7 +1539,7 @@ print(result["modified_count"])
 
 Delete a single document.
 
-```relay
+```python
 users = db.collection("users")
 result = users.delete_one({"email": "[email protected]"})
 print(result["deleted_count"])  // 1 or 0
@@ -1533,7 +1550,7 @@ print(result["deleted_count"])  // 1 or 0
 
 Delete multiple documents.
 
-```relay
+```python
 users = db.collection("users")
 result = users.delete_many({"active": False})
 print(result["deleted_count"])  // Number of deleted documents
@@ -1545,7 +1562,7 @@ print(result["deleted_count"])  // Number of deleted documents
 
 ### 1. Hello World (Async)
 
-```relay
+```python
 sleep(2000, print("world"))
 print("hello")
 ```
@@ -1558,7 +1575,7 @@ world
 
 ### 2. Simple Web API
 
-```relay
+```python
 app = WebApp()
 server = WebServer()
 
@@ -1575,7 +1592,7 @@ server.run(app)
 
 ### 3. Pastebin Service
 
-```relay
+```python
 app = WebApp()
 server = WebServer()
 mongo = Mongo("mongodb://localhost:27017")
@@ -1620,7 +1637,7 @@ Template files used by this example:
 
 ### 4. Concurrent HTTP Requests
 
-```relay
+```python
 http = Http()
 
 fn fetch_user(user_id)
@@ -1641,7 +1658,7 @@ for (user in users)
 
 ### 5. File Processing Pipeline
 
-```relay
+```python
 fn process_file(filename)
     content = read_file(filename)
     lines = len(content.split("\n"))
@@ -1659,7 +1676,7 @@ print("Processing complete!")
 
 ### 6. Session-based Authentication
 
-```relay
+```python
 app = WebApp()
 server = WebServer()
 mongo = Mongo("mongodb://localhost:27017")
@@ -1697,7 +1714,7 @@ server.run(app)
 
 ### 7. REST API with MongoDB
 
-```relay
+```python
 app = WebApp()
 server = WebServer()
 mongo = Mongo("mongodb://localhost:27017")
@@ -1747,7 +1764,7 @@ server.run(app)
 
 ### 8. Timeout and Error Handling
 
-```relay
+```python
 http = Http()
 
 fn fetch_with_timeout(url)
@@ -1760,7 +1777,7 @@ print(result.text)
 
 ### 9. Race Condition Example
 
-```relay
+```python
 http = Http()
 
 // Fetch from multiple mirrors, use whichever responds first
@@ -1780,7 +1797,7 @@ print("Fastest mirror returned:", fastest.text)
 
 ### 10. Background Task Processing
 
-```relay
+```python
 fn process_item(item)
     sleep(1000, print("Processed: " + str(item)))
 
@@ -1796,7 +1813,7 @@ print("All tasks started, continuing...")
 
 ### 11. SMTP Welcome Email with Templates
 
-```relay
+```python
 email = Email(
     host="smtp.example.com",
     username="smtp-user",
@@ -1845,7 +1862,7 @@ Relay is built on:
 
 When you call an async function, Relay immediately starts the operation and returns a `Deferred` value:
 
-```relay
+```python
 // This starts the HTTP request immediately
 response = http.get("https://api.example.com")
 // response is Deferred<Response>
@@ -1862,26 +1879,26 @@ print(response.status)  // <-- Blocks here if not complete
 `Deferred` values automatically resolve when:
 
 1. **Used in operations:**
-```relay
+```python
 x = sleep(1000, 10)
 y = x + 5  // Waits for x to resolve
 ```
 
 2. **Passed to functions:**
-```relay
+```python
 result = sleep(1000, 42)
 print(result)  // Waits before printing
 ```
 
 3. **Used in control flow:**
-```relay
+```python
 data = http.get("https://api.example.com")
 if (data.status == 200)  // Waits before comparison
     print("Success")
 ```
 
 4. **Indexed:**
-```relay
+```python
 resp = http.get("https://api.example.com")
 json_data = resp.json()
 print(json_data["key"])  // Waits for json() before indexing
@@ -1891,7 +1908,7 @@ print(json_data["key"])  // Waits for json() before indexing
 
 Expression statements (expressions not assigned to variables) run without blocking:
 
-```relay
+```python
 // This starts the sleep but doesn't wait
 sleep(1000, print("delayed"))
 
@@ -1907,7 +1924,7 @@ print("immediate")
 
 Relay uses Tokio's work-stealing scheduler to run tasks concurrently:
 
-```relay
+```python
 // Start 3 HTTP requests concurrently
 task1 = spawn(http.get("https://api1.com"))
 task2 = spawn(http.get("https://api2.com"))
@@ -1945,7 +1962,7 @@ Every web request is also assigned a `request_id` and echoed as the `x-request-i
 
 MongoDB operations return `Deferred` values that resolve when the database operation completes:
 
-```relay
+```python
 // This starts the query immediately
 users = collection.find({"active": True})
 
@@ -1964,7 +1981,7 @@ for (user in users)  // <-- Blocks here
 ### 1. Leverage Concurrent Execution
 
 Instead of:
-```relay
+```python
 // Sequential (slow)
 result1 = http.get("https://api1.com")
 result2 = http.get("https://api2.com")
@@ -1972,7 +1989,7 @@ result3 = http.get("https://api3.com")
 ```
 
 Do:
-```relay
+```python
 // Concurrent (fast)
 tasks = [
     spawn(http.get("https://api1.com")),
@@ -1986,7 +2003,7 @@ results = all(tasks)
 
 Type hints provide automatic validation and coercion:
 
-```relay
+```python
 @app.post("/calculate")
 fn calculate(a: int, b: int, operation: str = "add")
     if (operation == "add")
@@ -1999,7 +2016,7 @@ fn calculate(a: int, b: int, operation: str = "add")
 
 Always check for `None` when querying databases or processing optional parameters:
 
-```relay
+```python
 @app.get("/users/<user_id>")
 fn get_user(user_id)
     user = users.find_one({"_id": user_id})
@@ -2012,7 +2029,7 @@ fn get_user(user_id)
 
 Don't try to maintain state in global variables. Use sessions:
 
-```relay
+```python
 // Bad
 current_user = None
 
@@ -2030,7 +2047,7 @@ fn login(username)
 
 Always add timeouts to external HTTP requests:
 
-```relay
+```python
 fn fetch_data(url)
     return timeout(http.get(url), 10000)  // 10 second timeout
 ```
@@ -2039,7 +2056,7 @@ fn fetch_data(url)
 
 Split handlers into logical groups:
 
-```relay
+```python
 app = WebApp()
 server = WebServer()
 
@@ -2073,7 +2090,7 @@ server.run(app)
 
 For cleaner counter increments:
 
-```relay
+```python
 // Instead of
 i = i + 1
 
@@ -2085,7 +2102,7 @@ i =+ 1
 
 Structure handlers with early returns for error cases:
 
-```relay
+```python
 @app.get("/posts/<post_id>")
 fn get_post(post_id)
     post = posts.find_one({"_id": post_id})
@@ -2110,7 +2127,7 @@ fn get_post(post_id)
 
 **Fix:** Ensure all indentation uses 4 spaces (not tabs, not 2 spaces).
 
-```relay
+```python
 // Wrong
 fn example()
   print("hello")  // 2 spaces
@@ -2132,7 +2149,7 @@ fn example()
 
 **Fix:** Use explicit type conversion:
 
-```relay
+```python
 // Wrong
 x = 10 + "5"
 
@@ -2146,7 +2163,7 @@ x = 10 + int("5")
 
 **Fix:** Ensure variables are assigned before use:
 
-```relay
+```python
 // Wrong
 print(x)
 x = 10
@@ -2162,7 +2179,7 @@ print(x)
 
 **Fix:** Check list length before accessing:
 
-```relay
+```python
 items = [1, 2, 3]
 if (len(items) > 5)
     print(items[5])
@@ -2171,7 +2188,7 @@ if (len(items) > 5)
 ### Debugging Tips
 
 1. **Use print statements:** Relay's simplest debugging tool
-```relay
+```python
 fn process_data(data)
     print("Processing:", data)  // Debug output
     result = transform(data)
@@ -2180,7 +2197,7 @@ fn process_data(data)
 ```
 
 2. **Check async resolution:** If something seems to hang, check if you're waiting for a `Deferred` value
-```relay
+```python
 // This might hang if the HTTP request never completes
 result = http.get("https://unreachable.com")
 print(result.status)
@@ -2194,7 +2211,7 @@ result = timeout(http.get("https://unreachable.com"), 5000)
 4. **Check file paths:** File operations use paths relative to where you run the `relay` command
 
 5. **Inspect request objects:** Log the request object to debug handler issues
-```relay
+```python
 @app.post("/debug")
 fn debug()
     print(request)
@@ -2204,7 +2221,7 @@ fn debug()
 ### Performance Tips
 
 1. **Batch database operations:** Use `insert_many` instead of multiple `insert_one` calls
-```relay
+```python
 // Slow
 for (item in items)
     collection.insert_one(item)
@@ -2214,7 +2231,7 @@ collection.insert_many(items)
 ```
 
 2. **Use `spawn` for I/O-heavy tasks:** Parallelize independent operations
-```relay
+```python
 // Serial: 5 seconds total
 sleep(1000, "a")
 sleep(1000, "b")
diff --git a/src/main.rs b/src/main.rs
index 729d7b5..c43be67 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -30,11 +30,11 @@ use axum::{
     Router,
 };
 
-use async_recursion::async_recursion;
 use argon2::{
-    password_hash::{SaltString, rand_core::OsRng},
+    password_hash::{rand_core::OsRng, SaltString},
     Argon2, PasswordHash, PasswordHasher, PasswordVerifier,
 };
+use async_recursion::async_recursion;
 use futures::{stream::TryStreamExt, SinkExt, StreamExt};
 use indexmap::IndexMap;
 use lettre::{
@@ -103,6 +103,8 @@ enum Stmt {
         expr: Expr,
     },
     Return(Option<Expr>),
+    Break,
+    Continue,
 
     If {
         cond: Expr,
@@ -253,7 +255,7 @@ enum Tok {
     Colon,  // used in dict literals only
     Op(String),
 
-    Keyword(String), // fn if for while return try except import True False None str int float
+    Keyword(String), // fn if else for while return break continue try except import True False None str int float
 }
 
 #[derive(Debug, Clone)]
@@ -487,8 +489,8 @@ impl<'a> Lexer<'a> {
                         let id = self.read_ident();
                         let k = match id.as_str() {
                             "fn" | "if" | "else" | "for" | "while" | "return" | "try"
-                            | "except" | "import" | "True" | "False" | "None" | "str" | "int"
-                            | "float" => Tok::Keyword(id),
+                            | "except" | "import" | "break" | "continue" | "True" | "False"
+                            | "None" | "str" | "int" | "float" => Tok::Keyword(id),
                             _ => Tok::Ident(id),
                         };
                         out.push(self.wrap(k));
@@ -728,6 +730,12 @@ impl Parser {
         if self.peek_kw("return") {
             return self.parse_return();
         }
+        if self.peek_kw("break") {
+            return self.parse_break();
+        }
+        if self.peek_kw("continue") {
+            return self.parse_continue();
+        }
         if self.peek_kw("try") {
             return self.parse_try_except();
         }
@@ -746,8 +754,7 @@ impl Parser {
         }
 
         let target = self.parse_expr()?;
-        if self.peek_is(&Tok::Assign) || self.peek_is(&Tok::AugAdd) || self.peek_is(&Tok::AugSub)
-        {
+        if self.peek_is(&Tok::Assign) || self.peek_is(&Tok::AugAdd) || self.peek_is(&Tok::AugSub) {
             if !Self::is_assignment_target(&target) {
                 return Err(self.err_here("Invalid assignment target"));
             }
@@ -935,6 +942,22 @@ impl Parser {
         Ok(Stmt::Return(Some(self.parse_expr()?)))
     }
 
+    fn parse_break(&mut self) -> RResult<Stmt> {
+        self.expect_kw("break")?;
+        if self.peek_is(&Tok::Newline) || self.peek_is(&Tok::Dedent) || self.eof() {
+            return Ok(Stmt::Break);
+        }
+        Err(self.err_here("break does not take a value"))
+    }
+
+    fn parse_continue(&mut self) -> RResult<Stmt> {
+        self.expect_kw("continue")?;
+        if self.peek_is(&Tok::Newline) || self.peek_is(&Tok::Dedent) || self.eof() {
+            return Ok(Stmt::Continue);
+        }
+        Err(self.err_here("continue does not take a value"))
+    }
+
     fn parse_try_except(&mut self) -> RResult<Stmt> {
         self.expect_kw("try")?;
         self.expect_newline()?;
@@ -1604,6 +1627,8 @@ struct ModuleState {
 enum Flow {
     None,
     Return(Value),
+    Break,
+    Continue,
 }
 
 impl Evaluator {
@@ -1628,6 +1653,12 @@ impl Evaluator {
             match self.eval_stmt(s).await? {
                 Flow::None => {}
                 Flow::Return(v) => return Ok(v),
+                Flow::Break => {
+                    return Err(RelayError::Runtime("break used outside loop".into()));
+                }
+                Flow::Continue => {
+                    return Err(RelayError::Runtime("continue used outside loop".into()));
+                }
             }
             last = Value::None;
         }
@@ -1703,7 +1734,7 @@ impl Evaluator {
         for s in b {
             match self.eval_stmt(s).await? {
                 Flow::None => {}
-                r @ Flow::Return(_) => return Ok(r),
+                r => return Ok(r),
             }
         }
         Ok(Flow::None)
@@ -1719,6 +1750,12 @@ impl Evaluator {
                 match self.eval_stmt(s).await? {
                     Flow::None => {}
                     Flow::Return(v) => return Ok(v),
+                    Flow::Break => {
+                        return Err(RelayError::Runtime("break used outside loop".into()));
+                    }
+                    Flow::Continue => {
+                        return Err(RelayError::Runtime("continue used outside loop".into()));
+                    }
                 }
                 continue;
             }
@@ -1728,6 +1765,12 @@ impl Evaluator {
                 _ => match self.eval_stmt(s).await? {
                     Flow::None => return Ok(Value::None),
                     Flow::Return(v) => return Ok(v),
+                    Flow::Break => {
+                        return Err(RelayError::Runtime("break used outside loop".into()));
+                    }
+                    Flow::Continue => {
+                        return Err(RelayError::Runtime("continue used outside loop".into()));
+                    }
                 },
             }
         }
@@ -1809,6 +1852,8 @@ impl Evaluator {
                 };
                 Ok(Flow::Return(v))
             }
+            Stmt::Break => Ok(Flow::Break),
+            Stmt::Continue => Ok(Flow::Continue),
             Stmt::If {
                 cond,
                 then_block,
@@ -1840,8 +1885,11 @@ impl Evaluator {
                     let mut env = self.env.lock().await;
                     env.pop();
                     drop(env);
-                    if let Flow::Return(v) = r {
-                        return Ok(Flow::Return(v));
+                    match r {
+                        Flow::None => {}
+                        Flow::Continue => continue,
+                        Flow::Break => break,
+                        Flow::Return(v) => return Ok(Flow::Return(v)),
                     }
                 }
                 Ok(Flow::None)
@@ -1859,8 +1907,11 @@ impl Evaluator {
                     let mut env = self.env.lock().await;
                     env.pop();
                     drop(env);
-                    if let Flow::Return(v) = r {
-                        return Ok(Flow::Return(v));
+                    match r {
+                        Flow::None => {}
+                        Flow::Continue => continue,
+                        Flow::Break => break,
+                        Flow::Return(v) => return Ok(Flow::Return(v)),
                     }
                 }
                 Ok(Flow::None)
@@ -2669,8 +2720,8 @@ fn assign_member_value(object: Value, name: &str, value: Value) -> RResult<Value
 fn assign_index_value(container: Value, index: Value, value: Value) -> RResult<Value> {
     match (container, index) {
         (Value::List(mut values), Value::Int(i)) => {
-            let idx = usize::try_from(i)
-                .map_err(|_| RelayError::Runtime("Index out of range".into()))?;
+            let idx =
+                usize::try_from(i).map_err(|_| RelayError::Runtime("Index out of range".into()))?;
             let slot = values
                 .get_mut(idx)
                 .ok_or_else(|| RelayError::Runtime("Index out of range".into()))?;
@@ -2682,9 +2733,9 @@ fn assign_index_value(container: Value, index: Value, value: Value) -> RResult<V
             Ok(Value::Dict(map))
         }
         (Value::Json(mut j), Value::Str(key)) => {
-            let obj = j.as_object_mut().ok_or_else(|| {
-                RelayError::Type("JSON indexing expects object value".into())
-            })?;
+            let obj = j
+                .as_object_mut()
+                .ok_or_else(|| RelayError::Type("JSON indexing expects object value".into()))?;
             obj.insert(key, value_to_json(&value));
             Ok(Value::Json(j))
         }
@@ -2841,12 +2892,10 @@ fn install_stdlib(env: &mut Env, evaluator: Arc<Evaluator>) -> RResult<()> {
                 };
 
                 match request {
-                    Some(Value::Dict(req)) => {
-                        Ok(req
-                            .get("form")
-                            .cloned()
-                            .unwrap_or(Value::Dict(IndexMap::new())))
-                    }
+                    Some(Value::Dict(req)) => Ok(req
+                        .get("form")
+                        .cloned()
+                        .unwrap_or(Value::Dict(IndexMap::new()))),
                     _ => Ok(Value::Dict(IndexMap::new())),
                 }
             })
@@ -2865,12 +2914,10 @@ fn install_stdlib(env: &mut Env, evaluator: Arc<Evaluator>) -> RResult<()> {
                 };
 
                 match request {
-                    Some(Value::Dict(req)) => {
-                        Ok(req
-                            .get("query")
-                            .cloned()
-                            .unwrap_or(Value::Dict(IndexMap::new())))
-                    }
+                    Some(Value::Dict(req)) => Ok(req
+                        .get("query")
+                        .cloned()
+                        .unwrap_or(Value::Dict(IndexMap::new()))),
                     _ => Ok(Value::Dict(IndexMap::new())),
                 }
             })
@@ -3426,9 +3473,7 @@ fn install_stdlib(env: &mut Env, evaluator: Arc<Evaluator>) -> RResult<()> {
                     }
                 };
                 Ok(Value::Obj(Object::AuthStore(AuthStoreHandle::callback(
-                    evaluator,
-                    load_fn,
-                    save_fn,
+                    evaluator, load_fn, save_fn,
                 ))))
             })
         })),
@@ -3482,7 +3527,12 @@ fn expect_int(args: &[Value], i: usize, sig: &str) -> RResult<i64> {
     }
 }
 
-fn kwarg_or_arg(kwargs: &[(String, Value)], args: &[Value], name: &str, index: usize) -> Option<Value> {
+fn kwarg_or_arg(
+    kwargs: &[(String, Value)],
+    args: &[Value],
+    name: &str,
+    index: usize,
+) -> Option<Value> {
     kwargs
         .iter()
         .find(|(k, _)| k == name)
@@ -3516,7 +3566,9 @@ fn parse_u16_value(v: Value, sig: &str, field: &str) -> RResult<u16> {
         _ => return Err(RelayError::Type(format!("{sig} {field} must be int"))),
     };
     if !(0..=65535).contains(&n) {
-        return Err(RelayError::Type(format!("{sig} {field} must be in range 0..65535")));
+        return Err(RelayError::Type(format!(
+            "{sig} {field} must be in range 0..65535"
+        )));
     }
     Ok(n as u16)
 }
@@ -3572,9 +3624,11 @@ fn parse_optional_mime_allowlist(
                 Ok(Some(set))
             }
         }
-        Value::Json(J::Array(items)) => {
-            parse_optional_mime_allowlist(Some(Value::List(items.iter().map(json_to_value).collect())), sig, field)
-        }
+        Value::Json(J::Array(items)) => parse_optional_mime_allowlist(
+            Some(Value::List(items.iter().map(json_to_value).collect())),
+            sig,
+            field,
+        ),
         _ => Err(RelayError::Type(format!(
             "{sig} {field} must be str or list[str]"
         ))),
@@ -3594,7 +3648,9 @@ fn parse_string_list_value(v: Value, sig: &str, field: &str) -> RResult<Vec<Stri
             .iter()
             .map(|item| expect_str_value(json_to_value(item), sig, field))
             .collect(),
-        _ => Err(RelayError::Type(format!("{sig} {field} must be str or list[str]"))),
+        _ => Err(RelayError::Type(format!(
+            "{sig} {field} must be str or list[str]"
+        ))),
     }
 }
 
@@ -3657,9 +3713,8 @@ fn validate_data_with_schema(data: Value, schema: Value, sig: &str) -> RResult<V
 
         match (existing, expected_type, default) {
             (Some(v), Some(expected_ty), _) => {
-                let coerced = coerce_param_type(&expected_ty, v).map_err(|e| {
-                    RelayError::Type(format!("validate({field}): {e}"))
-                })?;
+                let coerced = coerce_param_type(&expected_ty, v)
+                    .map_err(|e| RelayError::Type(format!("validate({field}): {e}")))?;
                 input.insert(field, coerced);
             }
             (Some(_), None, Some(default)) => {
@@ -3937,13 +3992,7 @@ impl WebAppHandle {
     }
 
     // New spec: decorator attaches directly to the app: @app.get("/"), @app.post("/")
-    fn register(
-        &self,
-        method: String,
-        path: String,
-        fn_name: String,
-        validation: RouteValidation,
-    ) {
+    fn register(&self, method: String, path: String, fn_name: String, validation: RouteValidation) {
         let mut st = self.inner.lock().unwrap();
         st.routes.push(RouteSpec {
             method,
@@ -4027,13 +4076,7 @@ impl RouteHandle {
         }
     }
 
-    fn register(
-        &self,
-        method: String,
-        path: String,
-        fn_name: String,
-        validation: RouteValidation,
-    ) {
+    fn register(&self, method: String, path: String, fn_name: String, validation: RouteValidation) {
         let full_path = join_route_prefix(&self.prefix, &path);
         let mut st = self.app.inner.lock().unwrap();
         st.routes.push(RouteSpec {
@@ -4214,22 +4257,22 @@ async fn run_app(app: WebAppHandle) -> RResult<()> {
                 let upload_config = app_handle.upload_config();
                 let (json_body, form_body) =
                     match parse_request_body(&headers, body, &upload_config).await {
-                    Ok(parts) => parts,
-                    Err(e) => {
-                        let error = error_response(
-                            400,
-                            "bad_request",
-                            &e.to_string(),
-                            None,
-                            Some(request_id),
-                        );
-                        return Ok::<_, (StatusCode, String)>(
-                            value_to_axum_response(Value::Response(error))
-                                .await
-                                .into_response(),
-                        );
-                    }
-                };
+                        Ok(parts) => parts,
+                        Err(e) => {
+                            let error = error_response(
+                                400,
+                                "bad_request",
+                                &e.to_string(),
+                                None,
+                                Some(request_id),
+                            );
+                            return Ok::<_, (StatusCode, String)>(
+                                value_to_axum_response(Value::Response(error))
+                                    .await
+                                    .into_response(),
+                            );
+                        }
+                    };
 
                 let req = RequestParts {
                     method: method.to_string(),
@@ -4932,7 +4975,9 @@ fn parse_headers_value(v: Value, sig: &str) -> RResult<HashMap<String, String>>
             }
             Ok(out)
         }
-        _ => Err(RelayError::Type(format!("{sig} headers must be dict/json object"))),
+        _ => Err(RelayError::Type(format!(
+            "{sig} headers must be dict/json object"
+        ))),
     }
 }
 
@@ -4966,7 +5011,11 @@ async fn execute_http_request(
         "PUT" => client.put(url),
         "PATCH" => client.patch(url),
         "DELETE" => client.delete(url),
-        _ => return Err(RelayError::Runtime(format!("Unsupported HTTP method: {method}"))),
+        _ => {
+            return Err(RelayError::Runtime(format!(
+                "Unsupported HTTP method: {method}"
+            )))
+        }
     };
 
     for (k, v) in headers {
@@ -5289,9 +5338,9 @@ fn parse_attachment_bytes(v: Value, sig: &str) -> RResult<Vec<u8>> {
             }
             Ok(out)
         }
-        Value::Json(J::Array(values)) => parse_attachment_bytes(Value::List(
-            values.iter().map(json_to_value).collect(),
-        ), sig),
+        Value::Json(J::Array(values)) => {
+            parse_attachment_bytes(Value::List(values.iter().map(json_to_value).collect()), sig)
+        }
         _ => Err(RelayError::Type(format!(
             "{sig} attachment content must be bytes, str, or list[int]"
         ))),
@@ -5305,12 +5354,9 @@ fn parse_email_attachment(v: Value, sig: &str) -> RResult<EmailAttachmentSpec> {
         .ok_or_else(|| RelayError::Type(format!("{sig} attachment missing filename")))?;
     let filename = expect_str_value(filename, sig, "attachment.filename")?;
 
-    let content_type = parse_optional_str_value(
-        map.remove("content_type"),
-        sig,
-        "attachment.content_type",
-    )?
-    .unwrap_or_else(|| "application/octet-stream".to_string());
+    let content_type =
+        parse_optional_str_value(map.remove("content_type"), sig, "attachment.content_type")?
+            .unwrap_or_else(|| "application/octet-stream".to_string());
 
     let body_value = map
         .remove("bytes")
@@ -5368,10 +5414,10 @@ fn smtp_sender_for_config(config: &EmailConfig) -> EmailSender {
                     host.as_str(),
                 )
                 .map_err(|e| RelayError::Runtime(format!("email transport setup failed: {e}")))?,
-                EmailTlsMode::Wrapper => AsyncSmtpTransport::<Tokio1Executor>::relay(
-                    host.as_str(),
-                )
-                .map_err(|e| RelayError::Runtime(format!("email transport setup failed: {e}")))?,
+                EmailTlsMode::Wrapper => AsyncSmtpTransport::<Tokio1Executor>::relay(host.as_str())
+                    .map_err(|e| {
+                        RelayError::Runtime(format!("email transport setup failed: {e}"))
+                    })?,
                 EmailTlsMode::Insecure => {
                     AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(host.as_str())
                 }
@@ -5415,19 +5461,22 @@ impl EmailHandle {
     }
 
     fn from_constructor_args(args: &[Value], kwargs: &[(String, Value)]) -> RResult<Self> {
-        let sig = "Email(host, port=587, username=None, password=None, from=None, tls=\"starttls\")";
+        let sig =
+            "Email(host, port=587, username=None, password=None, from=None, tls=\"starttls\")";
 
-        let host = kwarg_or_arg(kwargs, args, "host", 0).ok_or_else(|| {
-            RelayError::Type(format!("{sig} missing host"))
-        })?;
+        let host = kwarg_or_arg(kwargs, args, "host", 0)
+            .ok_or_else(|| RelayError::Type(format!("{sig} missing host")))?;
         let host = expect_str_value(host, sig, "host")?;
 
         let port = kwarg_or_arg(kwargs, args, "port", 1).unwrap_or(Value::Int(587));
         let port = parse_u16_value(port, sig, "port")?;
 
-        let username = parse_optional_str_value(kwarg_or_arg(kwargs, args, "username", 2), sig, "username")?;
-        let password = parse_optional_str_value(kwarg_or_arg(kwargs, args, "password", 3), sig, "password")?;
-        let default_from = parse_optional_str_value(kwarg_or_arg(kwargs, args, "from", 4), sig, "from")?;
+        let username =
+            parse_optional_str_value(kwarg_or_arg(kwargs, args, "username", 2), sig, "username")?;
+        let password =
+            parse_optional_str_value(kwarg_or_arg(kwargs, args, "password", 3), sig, "password")?;
+        let default_from =
+            parse_optional_str_value(kwarg_or_arg(kwargs, args, "from", 4), sig, "from")?;
 
         let tls_raw = parse_optional_str_value(kwarg_or_arg(kwargs, args, "tls", 5), sig, "tls")?
             .unwrap_or_else(|| "starttls".to_string());
@@ -5454,7 +5503,11 @@ impl EmailHandle {
         Self { config, sender }
     }
 
-    fn prepare_send_request(&self, args: &[Value], kwargs: &[(String, Value)]) -> RResult<PreparedEmail> {
+    fn prepare_send_request(
+        &self,
+        args: &[Value],
+        kwargs: &[(String, Value)],
+    ) -> RResult<PreparedEmail> {
         let sig = "email.send(to, subject, text=None, html=None, cc=None, bcc=None, reply_to=None, from=None, headers=None, attachments=None)";
 
         let to_raw = kwarg_or_arg(kwargs, args, "to", 0)
@@ -5486,15 +5539,15 @@ impl EmailHandle {
             sig,
             "bcc",
         )?;
-        let reply_to = parse_optional_mailbox(kwarg_or_arg(kwargs, args, "reply_to", 6), sig, "reply_to")?;
+        let reply_to =
+            parse_optional_mailbox(kwarg_or_arg(kwargs, args, "reply_to", 6), sig, "reply_to")?;
 
-        let from_override = parse_optional_str_value(kwarg_or_arg(kwargs, args, "from", 7), sig, "from")?;
+        let from_override =
+            parse_optional_str_value(kwarg_or_arg(kwargs, args, "from", 7), sig, "from")?;
         let from = from_override
             .or_else(|| self.config.default_from.clone())
             .ok_or_else(|| {
-                RelayError::Type(format!(
-                    "{sig} requires from in Email(...) or send(...)"
-                ))
+                RelayError::Type(format!("{sig} requires from in Email(...) or send(...)"))
             })?;
         let from = parse_mailbox(from, sig, "from")?;
 
@@ -5502,7 +5555,8 @@ impl EmailHandle {
             .map(|v| parse_headers_value(v, sig))
             .transpose()?
             .unwrap_or_default();
-        let attachments = parse_email_attachments(kwarg_or_arg(kwargs, args, "attachments", 9), sig)?;
+        let attachments =
+            parse_email_attachments(kwarg_or_arg(kwargs, args, "attachments", 9), sig)?;
 
         let mut builder = MailMessage::builder().from(from).subject(subject);
         for mb in &to {
@@ -5519,10 +5573,10 @@ impl EmailHandle {
         }
 
         for (name, value) in headers {
-            let header_name = mail_header::HeaderName::new_from_ascii(name.clone())
-                .map_err(|e| RelayError::Type(format!(
-                    "{sig} invalid header name '{name}': {e}"
-                )))?;
+            let header_name =
+                mail_header::HeaderName::new_from_ascii(name.clone()).map_err(|e| {
+                    RelayError::Type(format!("{sig} invalid header name '{name}': {e}"))
+                })?;
             builder = builder.raw_header(mail_header::HeaderValue::new(header_name, value));
         }
 
@@ -5634,17 +5688,11 @@ impl EmailHandle {
                             );
                             out.insert(
                                 "smtp_code".into(),
-                                metadata
-                                    .smtp_code
-                                    .map(Value::Int)
-                                    .unwrap_or(Value::None),
+                                metadata.smtp_code.map(Value::Int).unwrap_or(Value::None),
                             );
                             out.insert(
                                 "smtp_message".into(),
-                                metadata
-                                    .smtp_message
-                                    .map(Value::Str)
-                                    .unwrap_or(Value::None),
+                                metadata.smtp_message.map(Value::Str).unwrap_or(Value::None),
                             );
                             Ok(Value::Dict(out))
                         }));
@@ -5652,39 +5700,35 @@ impl EmailHandle {
                     })
                 }))
             }
-            "render" => {
-                Value::Builtin(Arc::new(move |args, kwargs| {
-                    Box::pin(async move {
-                        let sig = "email.render(template, data=None)";
-                        let template = kwarg_or_arg(&kwargs, &args, "template", 0)
-                            .ok_or_else(|| RelayError::Type(format!("{sig} missing template")))?;
-                        let template = expect_str_value(template, sig, "template")?;
-                        let data = parse_template_locals(kwarg_or_arg(&kwargs, &args, "data", 1), sig)?;
+            "render" => Value::Builtin(Arc::new(move |args, kwargs| {
+                Box::pin(async move {
+                    let sig = "email.render(template, data=None)";
+                    let template = kwarg_or_arg(&kwargs, &args, "template", 0)
+                        .ok_or_else(|| RelayError::Type(format!("{sig} missing template")))?;
+                    let template = expect_str_value(template, sig, "template")?;
+                    let data = parse_template_locals(kwarg_or_arg(&kwargs, &args, "data", 1), sig)?;
+                    let rendered = render_template(&template, &data)?;
+                    Ok(Value::Str(rendered))
+                })
+            })),
+            "render_file" => Value::Builtin(Arc::new(move |args, kwargs| {
+                Box::pin(async move {
+                    let sig = "email.render_file(path, data=None)";
+                    let path = kwarg_or_arg(&kwargs, &args, "path", 0)
+                        .ok_or_else(|| RelayError::Type(format!("{sig} missing path")))?;
+                    let path = expect_str_value(path, sig, "path")?;
+                    let data = parse_template_locals(kwarg_or_arg(&kwargs, &args, "data", 1), sig)?;
+
+                    let d = Deferred::new(Box::pin(async move {
+                        let template = tokio::fs::read_to_string(path)
+                            .await
+                            .map_err(|e| RelayError::Runtime(e.to_string()))?;
                         let rendered = render_template(&template, &data)?;
                         Ok(Value::Str(rendered))
-                    })
-                }))
-            }
-            "render_file" => {
-                Value::Builtin(Arc::new(move |args, kwargs| {
-                    Box::pin(async move {
-                        let sig = "email.render_file(path, data=None)";
-                        let path = kwarg_or_arg(&kwargs, &args, "path", 0)
-                            .ok_or_else(|| RelayError::Type(format!("{sig} missing path")))?;
-                        let path = expect_str_value(path, sig, "path")?;
-                        let data = parse_template_locals(kwarg_or_arg(&kwargs, &args, "data", 1), sig)?;
-
-                        let d = Deferred::new(Box::pin(async move {
-                            let template = tokio::fs::read_to_string(path)
-                                .await
-                                .map_err(|e| RelayError::Runtime(e.to_string()))?;
-                            let rendered = render_template(&template, &data)?;
-                            Ok(Value::Str(rendered))
-                        }));
-                        Ok(Value::Deferred(Arc::new(d)))
-                    })
-                }))
-            }
+                    }));
+                    Ok(Value::Deferred(Arc::new(d)))
+                })
+            })),
             _ => Value::None,
         }
     }
@@ -5726,9 +5770,7 @@ impl AuthStoreHandle {
         match &self.backend {
             AuthStoreBackend::Memory(store) => Ok(store.lock().await.get(&username).cloned()),
             AuthStoreBackend::Callback {
-                load_fn,
-                evaluator,
-                ..
+                load_fn, evaluator, ..
             } => {
                 let out = evaluator
                     .call_named_function(load_fn, vec![Value::Str(username)])
@@ -5752,9 +5794,7 @@ impl AuthStoreHandle {
                 Ok(())
             }
             AuthStoreBackend::Callback {
-                save_fn,
-                evaluator,
-                ..
+                save_fn, evaluator, ..
             } => {
                 let _ = evaluator
                     .call_named_function(save_fn, vec![Value::Str(username), Value::Str(hash)])
@@ -5815,8 +5855,10 @@ impl AuthStoreHandle {
                 Value::Builtin(Arc::new(move |args, _| {
                     let store = store.clone();
                     Box::pin(async move {
-                        let username = expect_str(&args, 0, "auth_store.verify(username, password)")?;
-                        let password = expect_str(&args, 1, "auth_store.verify(username, password)")?;
+                        let username =
+                            expect_str(&args, 0, "auth_store.verify(username, password)")?;
+                        let password =
+                            expect_str(&args, 1, "auth_store.verify(username, password)")?;
                         let Some(hash) = store.load_hash(username).await? else {
                             return Ok(Value::Bool(false));
                         };
@@ -6318,42 +6360,32 @@ impl Object {
                     Value::Builtin(Arc::new(move |args, _| {
                         let a = a.clone();
                         Box::pin(async move {
-                            let load_fn = args
-                                .get(0)
-                                .cloned()
-                                .ok_or_else(|| {
-                                    RelayError::Type(
-                                        "app.session_backend(load_fn, save_fn) expects load_fn"
-                                            .into(),
-                                    )
-                                })?;
-                            let save_fn = args
-                                .get(1)
-                                .cloned()
-                                .ok_or_else(|| {
-                                    RelayError::Type(
-                                        "app.session_backend(load_fn, save_fn) expects save_fn"
-                                            .into(),
-                                    )
-                                })?;
-                            let load_fn = match load_fn {
-                                Value::Function(f) => f.name.clone(),
-                                _ => {
-                                    return Err(RelayError::Type(
+                            let load_fn = args.get(0).cloned().ok_or_else(|| {
+                                RelayError::Type(
+                                    "app.session_backend(load_fn, save_fn) expects load_fn".into(),
+                                )
+                            })?;
+                            let save_fn = args.get(1).cloned().ok_or_else(|| {
+                                RelayError::Type(
+                                    "app.session_backend(load_fn, save_fn) expects save_fn".into(),
+                                )
+                            })?;
+                            let load_fn =
+                                match load_fn {
+                                    Value::Function(f) => f.name.clone(),
+                                    _ => return Err(RelayError::Type(
                                         "app.session_backend(load_fn, save_fn) expects functions"
                                             .into(),
-                                    ))
-                                }
-                            };
-                            let save_fn = match save_fn {
-                                Value::Function(f) => f.name.clone(),
-                                _ => {
-                                    return Err(RelayError::Type(
+                                    )),
+                                };
+                            let save_fn =
+                                match save_fn {
+                                    Value::Function(f) => f.name.clone(),
+                                    _ => return Err(RelayError::Type(
                                         "app.session_backend(load_fn, save_fn) expects functions"
                                             .into(),
-                                    ))
-                                }
-                            };
+                                    )),
+                                };
 
                             a.set_session_backend(SessionBackendConfig::Callback {
                                 load_fn,
@@ -6575,11 +6607,14 @@ impl Evaluator {
             let Some(j) = json_body.clone() else {
                 return Ok(validation_error("Missing JSON body".to_string()));
             };
-            let validated =
-                match validate_data_with_schema(Value::Json(j), schema, "decorator json validate") {
-                    Ok(v) => v,
-                    Err(e) => return Ok(validation_error(e.to_string())),
-                };
+            let validated = match validate_data_with_schema(
+                Value::Json(j),
+                schema,
+                "decorator json validate",
+            ) {
+                Ok(v) => v,
+                Err(e) => return Ok(validation_error(e.to_string())),
+            };
             let map = expect_object_like(validated, "decorator json validate")?;
             json_body = Some(value_to_json(&Value::Dict(map.clone())));
             json_values = Some(map);
@@ -6587,14 +6622,11 @@ impl Evaluator {
 
         if let Some(schema) = route_validation.validate_schema {
             if let Some(j) = json_body.clone() {
-                let validated = match validate_data_with_schema(
-                    Value::Json(j),
-                    schema,
-                    "decorator validate",
-                ) {
-                    Ok(v) => v,
-                    Err(e) => return Ok(validation_error(e.to_string())),
-                };
+                let validated =
+                    match validate_data_with_schema(Value::Json(j), schema, "decorator validate") {
+                        Ok(v) => v,
+                        Err(e) => return Ok(validation_error(e.to_string())),
+                    };
                 let map = expect_object_like(validated, "decorator validate")?;
                 json_body = Some(value_to_json(&Value::Dict(map.clone())));
                 json_values = Some(map);
@@ -6680,10 +6712,7 @@ impl Evaluator {
         req_dict.insert("method".into(), Value::Str(method));
         req_dict.insert("path".into(), Value::Str(path));
         req_dict.insert("request_id".into(), Value::Str(request_id.clone()));
-        req_dict.insert(
-            "query".into(),
-            Value::Dict(query_values.clone()),
-        );
+        req_dict.insert("query".into(), Value::Dict(query_values.clone()));
         req_dict.insert(
             "headers".into(),
             Value::Dict(
@@ -6705,10 +6734,7 @@ impl Evaluator {
         if let Some(j) = json_body.clone() {
             req_dict.insert("json".into(), Value::Json(j));
         }
-        req_dict.insert(
-            "form".into(),
-            Value::Dict(form_values.clone()),
-        );
+        req_dict.insert("form".into(), Value::Dict(form_values.clone()));
 
         let existing_session = self.load_session_data(&app, &session_id).await?;
         let session_cfg = app.session_config();
@@ -6759,6 +6785,8 @@ impl Evaluator {
                 match evaluator.eval_block(&handler_fn.body).await? {
                     Flow::None => Ok(Value::None),
                     Flow::Return(v) => Ok(v),
+                    Flow::Break => Err(RelayError::Runtime("break used outside loop".into())),
+                    Flow::Continue => Err(RelayError::Runtime("continue used outside loop".into())),
                 }
             })
         });
@@ -6980,6 +7008,153 @@ mod tests {
         ));
     }
 
+    #[test]
+    fn parses_break_and_continue_statements_in_loop_body() {
+        let src = r#"fn loop_control()
+    while (True)
+        continue
+        break
+"#;
+
+        let program = parse_src(src).expect("loop control statements should parse");
+        let Stmt::FuncDef { body, .. } = &program.stmts[0] else {
+            panic!("expected function definition");
+        };
+        let Stmt::While { body, .. } = &body[0] else {
+            panic!("expected while statement");
+        };
+        assert!(matches!(body[0], Stmt::Continue));
+        assert!(matches!(body[1], Stmt::Break));
+    }
+
+    #[tokio::test]
+    async fn while_loop_respects_break_and_continue() {
+        let src = r#"i = 0
+sum = 0
+while (i < 6)
+    i =+ 1
+    if (i == 2)
+        continue
+    if (i == 5)
+        break
+    sum =+ i
+"#;
+
+        let program = parse_src(src).expect("program should parse");
+        let env = Arc::new(tokio::sync::Mutex::new(Env::new_global()));
+        let evaluator = Arc::new(Evaluator::new(env.clone()));
+        {
+            let mut env_lock = env.lock().await;
+            install_stdlib(&mut env_lock, evaluator.clone()).expect("stdlib install should work");
+        }
+        evaluator
+            .eval_program(&program)
+            .await
+            .expect("program should evaluate");
+
+        let env_lock = env.lock().await;
+        let i = env_lock.get("i").expect("i should exist");
+        let sum = env_lock.get("sum").expect("sum should exist");
+        assert!(matches!(i, Value::Int(5)));
+        assert!(matches!(sum, Value::Int(8)));
+    }
+
+    #[tokio::test]
+    async fn for_loop_respects_break_and_continue() {
+        let src = r#"sum = 0
+for (n in [1, 2, 3, 4, 5])
+    if (n == 2)
+        continue
+    if (n == 5)
+        break
+    sum =+ n
+"#;
+
+        let program = parse_src(src).expect("program should parse");
+        let env = Arc::new(tokio::sync::Mutex::new(Env::new_global()));
+        let evaluator = Arc::new(Evaluator::new(env.clone()));
+        {
+            let mut env_lock = env.lock().await;
+            install_stdlib(&mut env_lock, evaluator.clone()).expect("stdlib install should work");
+        }
+        evaluator
+            .eval_program(&program)
+            .await
+            .expect("program should evaluate");
+
+        let env_lock = env.lock().await;
+        let sum = env_lock.get("sum").expect("sum should exist");
+        assert!(matches!(sum, Value::Int(8)));
+    }
+
+    #[tokio::test]
+    async fn break_outside_loop_raises_runtime_error() {
+        let program = parse_src("break\n").expect("program should parse");
+        let env = Arc::new(tokio::sync::Mutex::new(Env::new_global()));
+        let evaluator = Arc::new(Evaluator::new(env.clone()));
+        {
+            let mut env_lock = env.lock().await;
+            install_stdlib(&mut env_lock, evaluator.clone()).expect("stdlib install should work");
+        }
+
+        let err = evaluator.eval_program(&program).await;
+        let err = match err {
+            Ok(v) => panic!("break outside loop should fail, got {}", v.repr()),
+            Err(e) => e,
+        };
+        assert!(matches!(
+            err,
+            RelayError::Runtime(msg) if msg.contains("break used outside loop")
+        ));
+    }
+
+    #[tokio::test]
+    async fn continue_outside_loop_raises_runtime_error() {
+        let program = parse_src("continue\n").expect("program should parse");
+        let env = Arc::new(tokio::sync::Mutex::new(Env::new_global()));
+        let evaluator = Arc::new(Evaluator::new(env.clone()));
+        {
+            let mut env_lock = env.lock().await;
+            install_stdlib(&mut env_lock, evaluator.clone()).expect("stdlib install should work");
+        }
+
+        let err = evaluator.eval_program(&program).await;
+        let err = match err {
+            Ok(v) => panic!("continue outside loop should fail, got {}", v.repr()),
+            Err(e) => e,
+        };
+        assert!(matches!(
+            err,
+            RelayError::Runtime(msg) if msg.contains("continue used outside loop")
+        ));
+    }
+
+    #[tokio::test]
+    async fn break_inside_function_without_loop_raises_runtime_error() {
+        let src = r#"fn bad()
+    break
+
+bad()
+"#;
+        let program = parse_src(src).expect("program should parse");
+        let env = Arc::new(tokio::sync::Mutex::new(Env::new_global()));
+        let evaluator = Arc::new(Evaluator::new(env.clone()));
+        {
+            let mut env_lock = env.lock().await;
+            install_stdlib(&mut env_lock, evaluator.clone()).expect("stdlib install should work");
+        }
+
+        let err = evaluator.eval_program(&program).await;
+        let err = match err {
+            Ok(v) => panic!("break outside loop should fail, got {}", v.repr()),
+            Err(e) => e,
+        };
+        assert!(matches!(
+            err,
+            RelayError::Runtime(msg) if msg.contains("break used outside loop")
+        ));
+    }
+
     #[test]
     fn bson_object_id_is_exposed_as_plain_string() {
         let oid = ObjectId::parse_str("698d35e0ef97187ab267d11e").expect("valid object id");
@@ -7063,10 +7238,8 @@ mod tests {
         let mut headers = HeaderMap::new();
         headers.insert(
             axum::http::header::CONTENT_TYPE,
-            axum::http::HeaderValue::from_str(&format!(
-                "multipart/form-data; boundary={boundary}"
-            ))
-            .expect("header should be valid"),
+            axum::http::HeaderValue::from_str(&format!("multipart/form-data; boundary={boundary}"))
+                .expect("header should be valid"),
         );
 
         let body = format!(
@@ -7118,8 +7291,7 @@ mod tests {
             ..UploadConfig::default()
         };
 
-        let err = parse_request_body(&headers, Bytes::from_static(b"{\"x\":1}"), &upload_cfg)
-            .await;
+        let err = parse_request_body(&headers, Bytes::from_static(b"{\"x\":1}"), &upload_cfg).await;
         let err = match err {
             Ok(_) => panic!("body larger than limit should fail"),
             Err(err) => err,
@@ -7136,10 +7308,8 @@ mod tests {
         let mut headers = HeaderMap::new();
         headers.insert(
             axum::http::header::CONTENT_TYPE,
-            axum::http::HeaderValue::from_str(&format!(
-                "multipart/form-data; boundary={boundary}"
-            ))
-            .expect("header should be valid"),
+            axum::http::HeaderValue::from_str(&format!("multipart/form-data; boundary={boundary}"))
+                .expect("header should be valid"),
         );
         let body = format!(
             "--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"big.txt\"\r\nContent-Type: text/plain\r\n\r\nhello world\r\n--{boundary}--\r\n"
@@ -7149,8 +7319,7 @@ mod tests {
             ..UploadConfig::default()
         };
 
-        let err = parse_request_body(&headers, Bytes::from(body), &upload_cfg)
-            .await;
+        let err = parse_request_body(&headers, Bytes::from(body), &upload_cfg).await;
         let err = match err {
             Ok(_) => panic!("file larger than max_file_bytes should fail"),
             Err(err) => err,
@@ -7167,10 +7336,8 @@ mod tests {
         let mut headers = HeaderMap::new();
         headers.insert(
             axum::http::header::CONTENT_TYPE,
-            axum::http::HeaderValue::from_str(&format!(
-                "multipart/form-data; boundary={boundary}"
-            ))
-            .expect("header should be valid"),
+            axum::http::HeaderValue::from_str(&format!("multipart/form-data; boundary={boundary}"))
+                .expect("header should be valid"),
         );
         let body = format!(
             "--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"hello.txt\"\r\nContent-Type: text/plain\r\n\r\nhello world\r\n--{boundary}--\r\n"
@@ -7180,8 +7347,7 @@ mod tests {
             ..UploadConfig::default()
         };
 
-        let err = parse_request_body(&headers, Bytes::from(body), &upload_cfg)
-            .await;
+        let err = parse_request_body(&headers, Bytes::from(body), &upload_cfg).await;
         let err = match err {
             Ok(_) => panic!("disallowed mime should fail"),
             Err(err) => err,
@@ -7499,7 +7665,10 @@ fn from_query()
         }
 
         let mut form = HashMap::new();
-        form.insert("name".to_string(), Value::Str("from-form-helper".to_string()));
+        form.insert(
+            "name".to_string(),
+            Value::Str("from-form-helper".to_string()),
+        );
         let out_form = evaluator
             .call_web_handler(
                 WebAppHandle::new(),
@@ -7584,10 +7753,8 @@ fn from_query()
         let mut headers = HeaderMap::new();
         headers.insert(
             axum::http::header::CONTENT_TYPE,
-            axum::http::HeaderValue::from_str(&format!(
-                "multipart/form-data; boundary={boundary}"
-            ))
-            .expect("header should be valid"),
+            axum::http::HeaderValue::from_str(&format!("multipart/form-data; boundary={boundary}"))
+                .expect("header should be valid"),
         );
         let body = format!(
             "--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"hello.txt\"\r\nContent-Type: text/plain\r\n\r\nhello world\r\n--{boundary}--\r\n"
@@ -7715,7 +7882,10 @@ fn from_query()
                 );
                 let body = String::from_utf8(resp.body).expect("response body should be utf-8");
                 let parsed: J = serde_json::from_str(&body).expect("body should be json");
-                assert_eq!(parsed["error"]["code"], J::String("bad_request".to_string()));
+                assert_eq!(
+                    parsed["error"]["code"],
+                    J::String("bad_request".to_string())
+                );
                 assert_eq!(
                     parsed["error"]["message"],
                     J::String("Missing field".to_string())
@@ -7724,7 +7894,10 @@ fn from_query()
                     parsed["error"]["request_id"],
                     J::String("rid-test".to_string())
                 );
-                assert_eq!(parsed["error"]["details"]["field"], J::String("name".to_string()));
+                assert_eq!(
+                    parsed["error"]["details"]["field"],
+                    J::String("name".to_string())
+                );
             }
             other => panic!("expected response, got {}", other.repr()),
         }
@@ -8231,7 +8404,10 @@ custom_ok = custom.verify("bob", "pw2")
 
         let env_lock = env.lock().await;
         assert!(matches!(env_lock.get("ok").expect("ok"), Value::Bool(true)));
-        assert!(matches!(env_lock.get("bad").expect("bad"), Value::Bool(false)));
+        assert!(matches!(
+            env_lock.get("bad").expect("bad"),
+            Value::Bool(false)
+        ));
         assert!(matches!(
             env_lock.get("store_ok").expect("store_ok"),
             Value::Bool(true)
@@ -8411,7 +8587,10 @@ fn get_user(user_id)
         };
 
         let state = app.inner.lock().unwrap();
-        let route = state.routes.first().expect("one route should be registered");
+        let route = state
+            .routes
+            .first()
+            .expect("one route should be registered");
         assert_eq!(route.path, "/api/v1/users/<user_id>");
     }
 
@@ -8428,7 +8607,10 @@ fn get_user(user_id)
             "2026.1",
         );
 
-        assert_eq!(doc["info"]["title"], J::String("Relay Test API".to_string()));
+        assert_eq!(
+            doc["info"]["title"],
+            J::String("Relay Test API".to_string())
+        );
         assert_eq!(doc["info"]["version"], J::String("2026.1".to_string()));
         assert!(doc["paths"]["/users/{id}"]["get"].is_object());
     }
@@ -8598,7 +8780,10 @@ fn get_user(user_id)
                 Value::Str("Attachment test".into()),
                 Value::Str("See attachment".into()),
             ],
-            vec![("attachments".into(), Value::List(vec![Value::Dict(attachment)]))],
+            vec![(
+                "attachments".into(),
+                Value::List(vec![Value::Dict(attachment)]),
+            )],
         )
         .await
         .expect("send should succeed");
@@ -8607,7 +8792,10 @@ fn get_user(user_id)
             Value::Deferred(d) => d,
             other => panic!("send should return Deferred, got {}", other.repr()),
         };
-        let _ = deferred.resolve().await.expect("send deferred should resolve");
+        let _ = deferred
+            .resolve()
+            .await
+            .expect("send deferred should resolve");
 
         let formatted = captured
             .lock()
@@ -8721,7 +8909,10 @@ fn get_user(user_id)
         .await
         .expect("render_file call should succeed");
         let rendered_file = match rendered_file {
-            Value::Deferred(d) => d.resolve().await.expect("render_file deferred should resolve"),
+            Value::Deferred(d) => d
+                .resolve()
+                .await
+                .expect("render_file deferred should resolve"),
             other => panic!("render_file should return Deferred, got {}", other.repr()),
         };
         assert!(matches!(