patx/relay-lang

Add practical HTML tutorial for Relay

Commit 3b3c341 · patx · 2026-09-09T23:01:45-04:00

Changeset
3b3c341ac29f6ab99fa5c360b9a42e9b6c91e67e
Parents
19b858e8aca8ff80a217c3d0ccc6424cb82d2d3b

View source at this commit

Comments

No comments yet.

Log in to comment

Diff

diff --git a/README.md b/README.md
index bfa5ed4..7ffbc83 100644
--- a/README.md
+++ b/README.md
@@ -6,6 +6,8 @@ Relay is an implicitly asynchronous, indentation-based language for trusted web
 
 ## Build and run
 
+New to Relay? Start with the [HTML tutorial](docs/tutorial.html) for a walkthrough from your first script to a tested JSON API. Open the file in a browser to read it locally.
+
 Rust 1.93 or newer is required.
 
 ```sh
diff --git a/docs/tutorial.html b/docs/tutorial.html
new file mode 100644
index 0000000..2891448
--- /dev/null
+++ b/docs/tutorial.html
@@ -0,0 +1,197 @@
+<!doctype html>
+<html lang="en">
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1">
+  <meta name="description" content="Learn Relay: build and run scripts, work with functions and async results, and create your first JSON web service.">
+  <title>Learn Relay — a practical tutorial</title>
+  <style>
+    :root { color-scheme: light dark; --bg: #faf9f6; --ink: #202d32; --muted: #53636a; --line: #d4ddd9; --accent: #126653; --code: #edf1ee; }
+    * { box-sizing: border-box; }
+    body { margin: 0; background: var(--bg); color: var(--ink); font: 18px/1.7 system-ui, sans-serif; }
+    a { color: var(--accent); text-underline-offset: .2em; }
+    a:focus-visible { outline: 3px solid var(--accent); outline-offset: 4px; }
+    .skip { position: absolute; top: -100px; padding: 12px; background: var(--bg); }
+    .skip:focus { top: 0; }
+    header, .layout, footer { max-width: 1120px; margin: auto; padding: 32px; }
+    header { padding-top: 64px; border-bottom: 1px solid var(--line); }
+    .eyebrow { color: var(--accent); font-size: .8rem; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; }
+    h1 { font-size: clamp(2.5rem, 6vw, 4.5rem); letter-spacing: -.05em; line-height: 1.08; margin: 18px 0; }
+    header p { max-width: 720px; color: var(--muted); }
+    .layout { display: grid; grid-template-columns: 200px minmax(0, 1fr); gap: 48px; align-items: start; }
+    nav { position: sticky; top: 24px; font-size: .9rem; }
+    nav ol { padding-left: 22px; }
+    nav li { padding: 5px 0; }
+    main { min-width: 0; }
+    section { margin-bottom: 48px; scroll-margin-top: 24px; }
+    h2 { line-height: 1.25; letter-spacing: -.025em; margin: 0 0 20px; }
+    h3 { font-size: 1.1rem; margin-top: 28px; }
+    pre { background: var(--code); border: 1px solid var(--line); border-radius: 6px; padding: 20px; overflow-x: auto; font-size: .88rem; line-height: 1.65; }
+    code { font-family: ui-monospace, SFMono-Regular, Consolas, monospace; }
+    p code, li code { font-size: .9em; background: var(--code); padding: 2px 4px; border-radius: 3px; }
+    .note { border-left: 3px solid var(--accent); padding-left: 18px; color: var(--muted); }
+    footer { border-top: 1px solid var(--line); font-size: .9rem; color: var(--muted); }
+    @media (max-width: 760px) { .layout { display: block; } nav { position: static; margin-bottom: 40px; } header, .layout, footer { padding: 24px; } header { padding-top: 40px; } }
+    @media (prefers-color-scheme: dark) { :root { --bg: #172024; --ink: #e2eae7; --muted: #afbeb9; --line: #394b45; --accent: #89d4b6; --code: #202d30; } }
+    @media print { nav, .skip { display: none; } .layout { display: block; } body { font-size: 11pt; } pre { white-space: pre-wrap; } }
+  </style>
+</head>
+<body>
+  <a class="skip" href="#main">Skip to tutorial</a>
+  <header>
+    <div class="eyebrow">Relay / Getting started / 0.2.0-rc.1</div>
+    <h1>From first script<br>to first service.</h1>
+    <p>Learn Relay’s indentation-based syntax, work with asynchronous results, and build a small JSON API. You’ll need a terminal, a text editor, and Rust 1.93 or newer. No database is needed for this tutorial.</p>
+  </header>
+  <div class="layout">
+    <nav aria-label="Tutorial chapters">
+      <strong>In this tutorial</strong>
+      <ol>
+        <li><a href="#setup">Build Relay</a></li>
+        <li><a href="#script">Your first script</a></li>
+        <li><a href="#values">Values and loops</a></li>
+        <li><a href="#errors">Handle errors</a></li>
+        <li><a href="#async">Work concurrently</a></li>
+        <li><a href="#web">Build a JSON API</a></li>
+        <li><a href="#modules">Extract a module</a></li>
+        <li><a href="#tests">Test your code</a></li>
+        <li><a href="#next">Keep building</a></li>
+      </ol>
+    </nav>
+    <main id="main">
+      <section id="setup">
+        <h2>01 / Build Relay</h2>
+        <p>From a local checkout of the Relay repository, build the interpreter and confirm its version:</p>
+        <pre><code>cargo build --release --locked
+./target/release/relay --version
+mkdir -p tutorial-work</code></pre>
+        <p>Keep your terminal at the repository root for all commands below. Put the files you create in <code>tutorial-work/</code>. The examples invoke the built binary directly, so you don’t need to install it on your PATH.</p>
+        <p class="note">This tutorial targets the 0.2 release candidate. If you have older Relay code, read the <a href="migration.md">migration notes</a> for changes to scopes, async behavior, and HTTP responses.</p>
+      </section>
+      <section id="script">
+        <h2>02 / Your first script</h2>
+        <p>Create <code>tutorial-work/hello.ry</code>:</p>
+        <pre><code data-file="hello.ry">// A function with a checked argument and a default value.
+fn greet(name: str, prefix = "Hello")
+    return prefix + ", " + name
+
+print(greet("Ada"))
+print(greet("Grace", prefix="Welcome"))</code></pre>
+        <p>Check it, then run it:</p>
+        <pre><code>./target/release/relay check tutorial-work/hello.ry
+./target/release/relay run tutorial-work/hello.ry</code></pre>
+        <p>You should see <code>Hello, Ada</code> and <code>Welcome, Grace</code>. Use four spaces for indentation. Functions begin with <code>fn</code>; block headers have no trailing colon. Comments use <code>//</code> or <code>/* ... */</code>.</p>
+        <p>The <code>str</code> hint checks the argument at runtime. Calling <code>greet(42)</code> raises a type error; use <code>greet(str(42))</code> when you want conversion. The <code>check</code> command validates syntax, signatures, control flow, and imports without running the program; it is not a static type checker.</p>
+      </section>
+      <section id="values">
+        <h2>03 / Values and loops</h2>
+        <p>Create <code>tutorial-work/values.ry</code>. Lists hold ordered values; dictionaries associate string keys with values.</p>
+        <pre><code data-file="values.ry">scores = [12, 7, 19]
+scores.append(15)
+passing = [score for score in scores if score >= 12]
+
+for (score in passing)
+    print("Score: " + str(score))
+
+student = {"name": "Ada", "active": True, "scores": passing}
+print(format("Hello {{ name }}", {"name": student["name"]}))
+assert_equal(len(passing), 3)
+assert(19 in passing)</code></pre>
+        <pre><code>./target/release/relay run tutorial-work/values.ry</code></pre>
+        <p>The output includes scores 12, 19, and 15 and <code>Hello Ada</code>. Printing is asynchronous, so output order may vary. Successful assertions produce no output. Boolean values are <code>True</code> and <code>False</code>; an absent value is <code>None</code>.</p>
+        <p>Convert numbers with <code>str</code> before concatenating them with text. String literals don’t interpolate automatically: call <code>format</code> explicitly.</p>
+        <p class="note">Assignments inside functions are local. Captured values are read-only, including captured collections. For state shared across web requests, use explicit sessions or database operations.</p>
+      </section>
+      <section id="errors">
+        <h2>04 / Handle errors</h2>
+        <p>Create <code>tutorial-work/errors.ry</code>. Use <code>raise</code> for a failure, <code>except(error)</code> to handle it, and <code>finally</code> for cleanup that must run either way.</p>
+        <pre><code data-file="errors.ry">name = "   "
+try
+    if (name.strip() == "")
+        raise "Name cannot be empty"
+    print(name.strip())
+except(error)
+    print(error.code, error.message)
+finally
+    print("Validation finished")</code></pre>
+        <pre><code>./target/release/relay run tutorial-work/errors.ry</code></pre>
+        <p>You’ll see the error’s code and message and <code>Validation finished</code>; asynchronous print output may arrive in either order. Try changing <code>name</code> to <code>" Ada "</code> to follow the successful path.</p>
+      </section>
+      <section id="async">
+        <h2>05 / Work concurrently</h2>
+        <p>Relay has no <code>await</code> keyword. I/O and timers start when called; consuming their results waits for completion. Create <code>tutorial-work/concurrent.ry</code>:</p>
+        <pre><code data-file="concurrent.ry">first = sleep(100, "first")
+second = sleep(20, "second")
+results = all([first, second])
+assert_equal(results, ["first", "second"])
+print(results[0], results[1])</code></pre>
+        <pre><code>./target/release/relay run tutorial-work/concurrent.ry</code></pre>
+        <p>Both timers run concurrently. Although the second finishes earlier, <code>all</code> preserves input order, so the final print produces <code>first second</code>. In this release, <code>sleep</code> also prints its supplied value when the timer completes, so you’ll see separate <code>second</code> and <code>first</code> lines. Timer durations are in milliseconds.</p>
+        <p><code>race</code> returns the first completion, including failures. <code>timeout(expression, milliseconds)</code> limits expression execution, and <code>cancel(task)</code> requests cancellation. Async results cache both success and failure for repeated consumption.</p>
+        <p class="note">Scripts and requests join their child work before finishing. Unobserved task failures propagate. Starting a task and ignoring its result does not make it independent background work.</p>
+      </section>
+      <section id="web">
+        <h2>06 / Build a JSON API</h2>
+        <p>Create <code>tutorial-work/app.ry</code>. This service greets a visitor and adds two incoming integers:</p>
+        <pre><code data-file="app.ry">app = WebApp()
+
[email protected]("/hello/&lt;name&gt;")
+fn hello(name: str)
+    return {"hello": name, "ok": True}
+
[email protected]("/sum")
+fn sum_values(a: int, b: int)
+    return {"total": a + b}
+
+server = WebServer()
+server.run(app)</code></pre>
+        <pre><code>./target/release/relay check tutorial-work/app.ry
+./target/release/relay run tutorial-work/app.ry</code></pre>
+        <p>Leave the server running. In another terminal:</p>
+        <pre><code>curl http://127.0.0.1:8080/hello/Ada
+curl -X POST http://127.0.0.1:8080/sum \
+  -H 'Content-Type: application/json' \
+  -d '{"a": 12, "b": 7}'</code></pre>
+        <p>The responses contain <code>{"hello":"Ada","ok":true}</code> and <code>{"total":19}</code> (JSON key order may differ). Dictionaries and lists become JSON automatically. HTTP handler arguments bind from path, body, then query, in that order of precedence. Unlike ordinary function calls, HTTP input is coerced to the hinted types.</p>
+        <p>Strings produce plain text. To return HTML, use <code>Response(html, content_type="text/html; charset=utf-8")</code> or <code>app.render_template(path, data)</code>, which escapes interpolated HTML values.</p>
+        <p>Stop the server with Ctrl+C. If port 8080 is busy, restart on another port and update your curl URLs:</p>
+        <pre><code>RELAY_BIND=127.0.0.1:8081 ./target/release/relay run tutorial-work/app.ry</code></pre>
+      </section>
+      <section id="modules">
+        <h2>07 / Extract a module</h2>
+        <p>Create <code>tutorial-work/helpers.ry</code> so the addition logic can be reused and tested:</p>
+        <pre><code data-file="helpers.ry">fn add(a: int, b: int)
+    return a + b</code></pre>
+        <p>At the top of <code>app.ry</code>, add <code>import helpers as helpers</code>. Replace the sum handler’s return statement with:</p>
+        <pre><code>    return {"total": helpers.add(a, b)}</code></pre>
+        <p>Imports resolve relative to the source file, so this finds <code>helpers.ry</code> beside <code>app.ry</code>. Each module has its own namespace. Names beginning with <code>_</code> are private, and import cycles are rejected. Restart the server to load your changes and repeat the POST request.</p>
+      </section>
+      <section id="tests">
+        <h2>08 / Test your code</h2>
+        <p>Create <code>tutorial-work/test_helpers.ry</code>:</p>
+        <pre><code data-file="test_helpers.ry">import helpers as helpers
+
+fn test_add_positive()
+    assert_equal(helpers.add(12, 7), 19)
+
+fn test_add_negative()
+    assert_equal(helpers.add(-2, 2), 0)</code></pre>
+        <pre><code>./target/release/relay test tutorial-work
+./target/release/relay fmt --check tutorial-work</code></pre>
+        <p>The test command discovers <code>test_*.ry</code> files and runs their <code>test_*</code> functions in isolated runtimes. Expect two passing tests. If the formatting check reports changes, run <code>./target/release/relay fmt tutorial-work</code> to apply conservative whitespace formatting.</p>
+      </section>
+      <section id="next">
+        <h2>09 / Keep building</h2>
+        <p>You now have working scripts, concurrent tasks, a JSON API, a reusable module, and tests. A useful next exercise: add a <code>/double/&lt;value&gt;</code> route that calls <code>helpers.add(value, value)</code>, then test it with curl.</p>
+        <ul>
+          <li><a href="../README.md">Language overview</a> — collections, networking, and standard library capabilities.</li>
+          <li><a href="../examples/twitter_clone/README.md">Multi-file application example</a> — route registration, authentication, and MongoDB.</li>
+          <li><a href="deployment.md">Deployment guidance</a> — configuration and verification before running a service in production.</li>
+          <li><a href="migration.md">Migration notes</a> — current behavior compared with Relay 0.1.</li>
+        </ul>
+      </section>
+    </main>
+  </div>
+  <footer>Relay · A practical tutorial for 0.2.0-rc.1 · This page works offline; companion documentation links open local Markdown files.</footer>
+</body>
+</html>