Skip to content

Commit ab7c091

Browse files
hyperpolymathclaude
andcommitted
docs(guides): add migration-playbook.adoc + frontier-guide v1.0 living-doc header
Adds docs/guides/migration-playbook.adoc as the systematic companion to frontier-guide.adoc — the source-of-truth for re-decomposition rules when porting ReScript / TypeScript codebases into AffineScript. Covers: the cardinal rule (re-decompose, do not transliterate), source-pattern indices for ReScript and TypeScript, decision criteria (`Option` vs `Result`, `mut` vs effect, `ref`/`mut`/`own`, linear vs affine), and a file-buffer anti-pattern paired with its re-decomposed form. Bumps frontier-guide.adoc to v1.0: - adds :revnumber: / :revdate: attributes - adds a "canonical, living document" status note pointing at the migration-playbook - cross-links the playbook from "What to Read Next" - adds a Revision History appendix so subsequent edits have a place to land — addresses the gap where the guide had no surface for incremental updates and downstream consumers (idaptik, etc.) had no way to see what had changed. Both files render cleanly with asciidoctor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 2f43839 commit ab7c091

2 files changed

Lines changed: 299 additions & 1 deletion

File tree

docs/guides/frontier-guide.adoc

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
// SPDX-License-Identifier: PMPL-1.0-or-later
22
= AffineScript Frontier Guide: The Unveiling
33
Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
4-
2026-04-11
4+
v1.0, 2026-04-11
55
:toc:
66
:icons: font
77
:source-highlighter: rouge
8+
:revnumber: 1.0
9+
:revdate: 2026-04-11
10+
11+
[NOTE]
12+
====
13+
Status: **canonical, living document.** This guide is the design reference for AffineScript itself and for any migration into AffineScript. Edits are welcome; record them in the <<revision-history>> appendix so the timeline is visible to future readers and AI agents.
14+
15+
For systematic translation patterns from ReScript, TypeScript, or another `-script` family language, read this guide first, then the link:migration-playbook.adoc[Migration Playbook].
16+
====
817

918
The Frontier Guide takes you from "I write JavaScript / Python" to
1019
"I understand why AffineScript's design choices produce better programs."
@@ -359,6 +368,8 @@ fn fetch_with_retry(
359368
== What to Read Next
360369

361370
- link:warmup/README.adoc[Warmup scripts] — hands-on exercises, 15 minutes each
371+
- link:migration-playbook.adoc[Migration Playbook] — systematic re-decomposition rules for porting ReScript / TypeScript / other `-script` codebases into AffineScript
372+
- link:frontier-programming-practices/Human_Programming_Guide.adoc[Frontier Programming Practices] — the wider design philosophy (v2.0, 2026-04-10)
362373
- link:../specs/SPEC.md[Language Specification] — the authoritative grammar and semantics
363374
- link:../specs/SETTLED-DECISIONS.adoc[Settled Decisions] — architectural choices and why they were made
364375
- link:../DESIGN-VISION.adoc[Design Vision] — the long view
@@ -383,3 +394,18 @@ AffineScript. Error messages are translated back to the face you chose, so
383394

384395
Additional faces (JS-face, Pseudocode-face, and others) are on the roadmap.
385396
See link:../specs/faces.md[faces.md] for the architecture.
397+
398+
[appendix#revision-history]
399+
== Revision History
400+
401+
This document is intended to evolve. When you change it — adding a chapter, sharpening an example, retiring an obsolete claim — record the change here so that downstream readers (especially AI agents loading the guide at session start) can see what has moved.
402+
403+
[cols="1,1,3"]
404+
|===
405+
| Revision | Date | Notes
406+
407+
| 1.0
408+
| 2026-04-11
409+
| Initial unveiling. The six X-Script problems and AffineScript's answers; chapters 1–6; complete-program example; faces.
410+
411+
|===
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
// SPDX-License-Identifier: PMPL-1.0-or-later
2+
// SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
3+
= AffineScript Migration Playbook: Re-decomposition Patterns
4+
Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
5+
v1.0, 2026-05-02
6+
:sectnums:
7+
:toc: left
8+
:icons: font
9+
:source-highlighter: rouge
10+
:revnumber: 1.0
11+
:revdate: 2026-05-02
12+
13+
[NOTE]
14+
====
15+
This guide is the **systematic companion** to link:frontier-guide.adoc[`frontier-guide.adoc`].
16+
17+
* The Frontier Guide unveils what AffineScript *is* — read it if you are starting a new program.
18+
* This playbook describes how to **carry an existing codebase across the boundary** — read it if you are translating ReScript, TypeScript, or another `-script` family language into AffineScript.
19+
20+
The machine-readable companion for AI agents is link:frontier-programming-practices/AI.a2ml[`AI.a2ml`].
21+
====
22+
23+
== The Cardinal Rule
24+
25+
**Re-decompose. Do not transliterate.**
26+
27+
A 1:1 syntactic port — `let mutable` becomes `mut`, `option<T>` becomes `Option[T]`, `try/catch` becomes `try/catch`, classes become records-with-methods — produces code that _type-checks in AffineScript while still carrying the design problems AffineScript was built to solve_.
28+
29+
The Frontier Guide's thesis: every X-Script's six pathologies share one root — *the runtime does not know the program's intent*. Translation must therefore re-express *intent*, not just syntax. If a faithful port leaves the original design untouched, the destination language is being used as a syntax veneer and none of its guarantees do any work.
30+
31+
The rest of this document is a catalogue of common source-language patterns and the re-decompositions they invite.
32+
33+
== How to Use This Playbook
34+
35+
. Start from the source file. Identify the patterns it contains using <<source-pattern-index>>.
36+
. For each pattern, read the **Decomposition** column — that is the AffineScript *shape*, not just the AffineScript *syntax*.
37+
. If the source mixes several patterns into one structure (a class with mutable fields, an exception path, and a callback), expect the AffineScript output to have **more, smaller** declarations — not the same number of larger ones.
38+
. Where two decompositions are valid, the <<decision-criteria>> table tells you which one fits.
39+
. The <<anti-patterns>> section shows what a faithful-but-monolithic translation looks like, paired with its re-decomposed form.
40+
41+
[#source-pattern-index]
42+
== Source-Pattern Index
43+
44+
=== ReScript → AffineScript
45+
46+
[cols="1,2,2"]
47+
|===
48+
| Source | Decomposition | Why
49+
50+
| `let mutable x = ...`
51+
| `mut` field on an owned record, **or** lift to a `State` effect
52+
| Local mutation in a single owner stays local; mutation visible across calls becomes an effect.
53+
54+
| `ref<'a>` (mutable cell)
55+
| Same choice as `let mutable`; default to lifting to an effect when the cell crosses a function boundary
56+
| A bare `ref` is mutation that has already escaped its scope — typically the wrong default.
57+
58+
| `option<'a>` (built-in)
59+
| `Option[T]`
60+
| Direct mapping; no decomposition needed.
61+
62+
| `result<'a, 'e>`
63+
| `Result[T, E]`
64+
| Direct mapping; tighten `'e` from `string` or `exn` to a named error type if you can.
65+
66+
| `exception Foo` + `try/catch`
67+
| `Result[T, E]` where `E` is a named variant covering each failure
68+
| Replaces an untyped throw path with an exhaustive return type. The compiler forces every caller to handle the `Err` shape.
69+
70+
| `Js.Promise.t<'a>` / async ReScript
71+
| `Async` effect on the function row
72+
| Promises silently allow racing, ignoring errors, and detached lifetimes. The `Async` effect makes lifetime and error explicit.
73+
74+
| ReScript object types `Js.t<{..}>`
75+
| Records with row variables `{ field: T, ..r }`
76+
| Row polymorphism replaces structural object types principle-for-principle.
77+
78+
| Polymorphic variants `[#A \| #B]`
79+
| Open variant rows / sum types
80+
| Same idea, integrated with the rest of the type system rather than parallel to it.
81+
82+
| Module functors
83+
| Row-polymorphic functions parameterised over their effects
84+
| Most functor uses are an indirect way to parameterise over a small set of operations — effects do this directly.
85+
86+
| Class via PPX (`%@react.component`, etc.)
87+
| Owned record + standalone functions; lift effectful methods into effect rows
88+
| A class is a tangle of state, methods, and implicit lifetimes. Each strand becomes its own declaration.
89+
90+
| `unit` returns from impure code
91+
| `() / IO` (or whichever effects apply)
92+
| In ReScript, `unit` says nothing about whether the function did IO. In AffineScript, the effect row makes it visible.
93+
|===
94+
95+
=== TypeScript → AffineScript
96+
97+
[cols="1,2,2"]
98+
|===
99+
| Source | Decomposition | Why
100+
101+
| `T \| null \| undefined`
102+
| `Option[T]`
103+
| Frontier Guide chapter 1: one nothing, not three states.
104+
105+
| `throw new Error()` + `try/catch`
106+
| `Result[T, E]` for recoverable errors; `Exn[E]` effect for cross-cutting failure
107+
| A bare `throw` in TypeScript is invisible at the call site. Both replacements make it visible.
108+
109+
| `Promise<T>` / `async function`
110+
| `Async` effect
111+
| Same reasoning as the ReScript case.
112+
113+
| `class Foo { private x; method() {} }`
114+
| Owned record + standalone functions; lift mutation to `mut` or `State`
115+
| Encapsulation comes from ownership and module scope, not from class privacy.
116+
117+
| `any` / `unknown`
118+
| Forbidden in AffineScript output. Pin the type, or describe the unknown shape with a row variable.
119+
| `any` is the absence of a thesis. Every translation must replace it with one.
120+
121+
| Discriminated unions (`{ kind: "a" } \| { kind: "b" }`)
122+
| Sum types with named constructors; `match` on the constructor
123+
| Pattern matching is exhaustive; the compiler enforces what TypeScript can only suggest.
124+
|===
125+
126+
[#decision-criteria]
127+
== Decision Criteria
128+
129+
When a source pattern admits more than one AffineScript shape, choose with these tests.
130+
131+
=== `Option[T]` vs `Result[T, E]`
132+
133+
* **`Option[T]`** when *absence is normal* — a lookup that may not find anything, a config field that may be unset.
134+
* **`Result[T, E]`** when *absence has a reason you can describe* — parse failure, validation error, IO error.
135+
136+
If you find yourself reaching for `Result[T, ()]` or `Result[T, String]`, you probably wanted `Option[T]` (or a richer `E`).
137+
138+
=== `mut` vs effect
139+
140+
* **`mut`** for local mutation contained in a single owner — buffer construction, an accumulator inside a function.
141+
* **`State[S]` effect** for mutation that must be observable across calls — game state, session state, a setting visible to many subsystems.
142+
143+
If two callers need to see the same mutation, it is no longer *local* — lift it.
144+
145+
=== `ref` vs `mut` vs `own`
146+
147+
* **`ref Buffer`** — the caller keeps the value and the function only reads it.
148+
* **`mut Buffer`** — the function modifies in place; the caller keeps using the value afterwards.
149+
* **`own Buffer`** — the function consumes the value; the caller cannot use it after the call.
150+
151+
The default for resources (files, sockets, tokens, allocations) is `own` — anything else hides the lifetime.
152+
153+
=== Linear (`@linear`) vs affine (`own`)
154+
155+
* **`own`** (affine) — *may* be used at most once. Dropping is allowed.
156+
* **`@linear`** — *must* be used exactly once. Dropping is a compile error.
157+
158+
Prefer `own`. Reach for `@linear` only when forgetting to consume the value is a real bug — typically protocol handles, transactions, and capability tokens.
159+
160+
[#anti-patterns]
161+
== Anti-patterns: Faithful-but-monolithic Translation
162+
163+
The following ReScript is a small file buffer that conflates state, lifetime, and IO.
164+
165+
[source,rescript]
166+
----
167+
// ReScript — original
168+
type fileBuffer = {
169+
mutable data: array<string>,
170+
mutable open_: bool,
171+
}
172+
173+
let make = (): fileBuffer => { data: [], open_: true }
174+
175+
let read = (b: fileBuffer): string =>
176+
if b.open_ { Array.joinWith(b.data, "") } else { raise(Failure("closed")) }
177+
178+
let write = (b: fileBuffer, s: string): unit => {
179+
if !b.open_ { raise(Failure("closed")) }
180+
b.data = Array.concat(b.data, [s])
181+
Console.log("wrote " ++ s)
182+
}
183+
184+
let close = (b: fileBuffer): unit => { b.open_ = false }
185+
----
186+
187+
=== Faithful-but-monolithic translation — don't do this
188+
189+
[source,affinescript]
190+
----
191+
// AffineScript — same shape, none of the guarantees doing any work
192+
type FileBuffer = own {
193+
data: mut Array[String],
194+
open_: mut Bool,
195+
}
196+
197+
fn make() -> own FileBuffer { FileBuffer { data: [], open_: true } }
198+
199+
fn read(b: ref FileBuffer) -> String {
200+
if b.open_ { String.join(b.data, "") } else { panic("closed") }
201+
}
202+
203+
fn write(b: mut FileBuffer, s: String) -> () {
204+
if !b.open_ { panic("closed") }
205+
b.data = b.data ++ [s];
206+
// IO is silently invisible — same problem as the original
207+
}
208+
209+
fn close(b: mut FileBuffer) -> () { b.open_ = false }
210+
----
211+
212+
This compiles. It is also no better than the ReScript original: the `closed` invariant is still a runtime check, the IO is still invisible, and `close` does not actually consume the buffer.
213+
214+
=== Re-decomposed translation
215+
216+
[source,affinescript]
217+
----
218+
// Two types — a closed buffer is statically distinguishable from an open one
219+
type OpenBuffer = own { data: Array[String] }
220+
type ClosedBuffer = own { data: Array[String] }
221+
222+
effect IO { fn log(s: String); }
223+
224+
fn make() -> own OpenBuffer { OpenBuffer { data: [] } }
225+
226+
// read takes a borrow — caller keeps the buffer:
227+
fn read(b: ref OpenBuffer) -> String { String.join(b.data, "") }
228+
229+
// write is in-place; IO is in the row:
230+
fn write(b: mut OpenBuffer, s: String) -> () / IO {
231+
b.data = b.data ++ [s];
232+
IO.log("wrote " ++ s)
233+
}
234+
235+
// close consumes Open and returns Closed — "use after close" becomes a type error:
236+
fn close(b: own OpenBuffer) -> own ClosedBuffer { ClosedBuffer { data: b.data } }
237+
----
238+
239+
What changed:
240+
241+
. `closed` moved from a runtime field to a *type* (`OpenBuffer` vs `ClosedBuffer`) — "use after close" is now a compile error.
242+
. `close` consumes its argument (`own`) — there is no `b` left to misuse afterwards.
243+
. `IO` is in the effect row of `write` — every caller of `write` declares it transitively.
244+
. The mutable boolean is gone; `data` is reached only through `mut OpenBuffer`, which makes ownership explicit.
245+
246+
The two versions are roughly the same number of lines. They are different programs.
247+
248+
== Lessons from Real Migrations
249+
250+
Case studies from in-flight migrations land in link:lessons/[`docs/guides/lessons/`].
251+
252+
If you complete a non-trivial `.res → .affine` translation and the re-decomposition reveals something future translators would benefit from, write it up there. The format is loose: original snippet, faithful port, re-decomposed port, and what the second one buys you.
253+
254+
== See Also
255+
256+
* link:frontier-guide.adoc[Frontier Guide: The Unveiling] — the *what* and *why* of AffineScript's design.
257+
* link:frontier-programming-practices/Human_Programming_Guide.adoc[Frontier Programming Practices] — the wider design philosophy.
258+
* link:frontier-programming-practices/AI.a2ml[`AI.a2ml`] — machine-readable companion, read by AI agents at session start.
259+
* link:../specs/SETTLED-DECISIONS.adoc[Settled Decisions] — architectural choices and why they were made.
260+
* link:../specs/SPEC.md[Language Specification] — authoritative grammar and semantics.
261+
262+
[appendix]
263+
== Revision History
264+
265+
[cols="1,1,3"]
266+
|===
267+
| Revision | Date | Notes
268+
269+
| 1.0
270+
| 2026-05-02
271+
| Initial draft. ReScript and TypeScript pattern indices, decision criteria, file-buffer anti-pattern. Companion to `frontier-guide.adoc` v1.0.
272+
|===

0 commit comments

Comments
 (0)