# Spec 3 — The indented surface Status: **draft**. Sections 1 and 3 are decided by the author. Section 2 is proposed and needs the author's confirmation before the matching piece is built. Nothing here changes what a program means: the indented syntax is a second reader that produces the same `Form.t` tree as `lib/reader.ml`. Evidence for every file:line below was read on 2026-09-25. Re-read before building on it. ## 1. Decided **One tree, two readers.** A new reader turns indented text into `Form.t`, exactly the nine node kinds in `lib/form.ml:11-27`, with locations filled the way `lib/reader.ml:47` fills them. Everything after the reader (`Expand`, `Parse`, `Check`, both backends) is untouched. This is the plan the old WAIT entry in `TODO.org` described, and it rules out Parinfer, wisp and sweet-expressions the same way. **Both syntaxes live side by side.** File extension picks the reader. A program may mix files of both kinds, and imports cross freely. The paren syntax is not deprecated; this is a test drive. **Blocks are indentation.** A header line (`fn …`, `if …`, `while …`, `else`, …) followed by deeper-indented lines opens a block. Headers need no `:`, `then`, `do` or `end`. (An ordinary call takes a block only with a trailing `:`, section 3.) **Calls and indexing are adjacency.** `f(a, b)` with no space before `(` is a call. `x[i]` with no space before `[` is indexing; `x[i, j]` is `(at x i j)`. Arguments are separated by commas. **Lisp names stay.** `key-pressed?`, `dyn->f64`, `bytes=?`, `swap!`, `*earmuffs*` are one token each. The rule that makes this work: **binary operators need spaces around them.** `a-b` is a name; `a - b` is subtraction. No snake_case mapping. **Package qualifier stays `/`.** `rl/draw-fps(20, 20)`. Division is always spaced, `a / b`. **Keywords stay.** `:key-r`, `:else`. A colon glued to the front of a word is a keyword; `x: T` (colon glued to the end, space after) is an annotation. **Collection literals.** `[a b]` or `[a, b]` is a vector, `{k v}` a map or struct literal, `'(a b c)` a list. `'name` stays the quoted symbol that `invoke-restart` takes (60 uses, all restart names). **Types are marked, and omitted means dyn.** `x: i32` gives a type. A parameter, field or global with no `: T` is `dyn`. The reader **writes `dyn` explicitly** into the parameter vector: it emits `[x dyn y dyn]`, never `[x y]`, because `Check.pair_params` (check.ml:1589) reads `[x y]` as one parameter of type `y` when some type is named `y`. That silent misparse is what `parse.ml:1413-1418` warns about; the new syntax must not inherit it. **The return type is inferred when omitted.** Body-local only, as `docs/SPIKE-INFERENCE.md` ("The cheap first step" and "Verdict") scopes it: - the return type is the body's type; a `dyn` body gives `dyn`; the exits (the last form and each `return`) meet exactly as an `if`'s or `match`'s arms do, in any order: each is asked the others' type first, so a literal, `nil` or arithmetic takes it; only arms that are refused that way meet at the join (lossless widening, the read-only side of a const difference, `dyn` beside a genuinely dyn value); what `if` refuses is refused; no value gives `()`. - it reads only the function's own body, never a call site. - a self-recursive or mutually recursive function must write its return type. Refuse by name, naming the whole cycle. The corpus has 17 self-recursive functions and 5 mutual groups (all in `calc-me.flan`, `vendor/edn`, `vendor/json`). - nothing is ever defaulted in signature position. Parameters are never inferred; an omitted parameter type is `dyn`, which is a rule, not inference. The paren syntax spells "infer the return" as `_` in the return slot (section 3), which is what the indented reader emits when `->` is omitted. **A changed signature installs, and the warning names the cause.** The dev loop already accepts a signature change and stops stale callers with `StaleCall` (`lib/session.ml:353-363`, `TODO.org` "Signature generations and stale-caller warnings"). When the change came from inference, the stale-caller warning says so and points at the line: *"speed now returns f64, not i32, because of line 12"*. **Macros stay.** They run on `Form.t`, so they work in both syntaxes. A call can take an indented block as its last arguments, so `rl/with-drawing` and `comment` take a body (with a trailing `:`, section 3): ``` rl/with-drawing(): rl/clear-background(rl/black) game-draw() ``` reads as `(rl/with-drawing (rl/clear-background rl/black) (game-draw))`. Replacing macros with built-in constructs is **not** part of this work. **Diagnostics may print paren syntax** during the test drive. `Form.to_string`, the usage strings in `parse.ml` and `check.ml`, and `Render` print parens today (inventory in section 5). Types in `check.ml`'s messages do not: they are spelled by `Types.spell`, in the syntax of the code the message is about (section 3, item 9). ## 2. Proposed (confirm before building the piece it governs) Each item: the proposal, then the reason in one line. ### Lexical - **Extension `.fln`.** Short; `.flan` keeps meaning parens, so `generated.flan` and every existing path stay valid. **Built.** - **Comments stay `;`.** Nothing else wants the character. **Built.** - **Spaces only.** A tab in indentation is an error. The corpus has no tabs. **Built.** - **Indentation is measured in columns, any width.** A dedent must land on a column already on the stack (GDScript `gdscript_tokenizer.cpp:1291-1296`). **Built.** - **Blank and comment-only lines never open or close a block** (GDScript 1170-1239). **Built.** - **Inside `(` `[` `{`, newlines and indentation are ignored** except where a trailing block is allowed. Make it parser-driven, the way GDScript's `push_multiline` is (`gdscript_parser.cpp` 658-672, 3695-3770), not a paren counter in the lexer, or a block inside a call can't work. *Built as a depth counter instead: inside brackets a line break is whitespace, with one exception. A `=>` that ends a line inside brackets opens a lambda's block there, laid out as at the top level against the column its line starts at, and the block ends where the enclosing bracket closes.* - **Continuation outside brackets:** a line that starts with a spaced infix operator (`+`, `and`, `==`, …) continues the previous line; so does a line after one that ends in a spaced infix operator. (F# `LexFilter.fs` 360-380, 1850-1870, 2345-2360.) No `\` continuation. **Built** (`=` does not continue: `let x =` plus a block is a block value). A continuation line must sit deeper than the line it continues; one that does not is refused. - **Minus.** `-` glued to a digit is a negative literal (`-1`; 269 in the corpus). `-` glued to a name is negation (`-x` becomes `(- x)`; no name starts with `-` except two prelude sentinels, `lib/prelude.ml:2280,2285`, which rename). `a - b` is subtraction. `a -1` is an error: "separate with a comma or space the minus". **Built.** - **`->` needs spaces as the return arrow.** `dyn->f64` stays a name. **Built.** - **Character literals stay `\c`**, lexed before brackets and operators: `\(`, `\,`, `\space`. 277 uses, many of them delimiters of the new syntax. **Built.** ### Collections and separators - **Commas separate elements. With no commas, whitespace does, but only between single terms.** `[1 2 3]`, `[i n]`, `{.x 1 .y 2}` and `[4 f32]` read as today. `[a - 1 b]` is refused: "separate elements with commas". This keeps the Lisp look for data and is refusable by shape. **Built** (in braces a value may have an operator in it, `{.x a + 1, .y 2}`; the comma after it is what is required). - **Indices separate the same way.** `grid[r c]` and `grid[(r + 1) (c - 1)]` are two indices each; `grid[r + 1, c]` needs its comma, and `grid[r + 1 c]` is refused with the commas put in as the fix. `grid[i -1]` is refused as a glued minus. **Built**; the printer writes indices with commas. - **Struct literal:** `Vector2{.x 1, .y 2}` (brace glued to the name) reads `(Vector2 {.x 1 .y 2})`. A bare `{.x 1}` is today's bare literal. `{:a 1}` is a dyn map. **Built.** - **No set literal.** Flan has none today: `#{1 2}` reads as the symbol `#` and a map. Adding sets is a language change, not a syntax one. *In a `.fln` file `#{1 2}` reads `(# {1 2})`, a brace glued to a name.* ### Expressions - **Precedence**, low to high: `or` < `and` < `not` < comparisons (`== != < <= > >=`) < `<< >>` < `+ -` < `* / %` < unary `-` < postfix (call, index, field). **Built.** Mixing comparison operators in one chain, `a < b <= c`, is refused. An operator glued to `(` is always a call. - **`==` is `=`; `=` is assignment.** `x = v` reads `(set x v)`, `a[i] = v` reads `(set (at a i) v)`, `p.x = v` reads `(set (.x p) v)`. `x += v` reads `(set x (+ x v))` where every part of the place is a name or a literal, and `(update x + v)` otherwise, so the place is evaluated once either way, as with `++`. **Built** (also `-=`, `*=`, `/=`). - **A run of the same operator flattens** (variadics, section 3): `a + b + c` reads `(+ a b c)`, `a < b < c` reads `(< a b c)` (Flan's chain semantics, `test/programs/chain.flan`). This keeps the converter round trip exact (section 4). **Built.** - **Field access is postfix:** `camera.target.x` reads `(.x (.target camera))`. A capitalised left side is a qualified case, not a field: `Shape.Rect` stays one symbol. `test/programs/dev-rerun.flan:65` names a global `.init-once.counter`; rename it. **Built**, without the rename: it prints and reads back through the fallback, `defonce(.init-once.counter, i64, 7)`. - **`and`, `or`, `not` are words**, since they are Flan's own names. **Built.** `not` is a prefix word: `not a == b` is `not (a == b)`, `not a and b` is `(not a) and b`, and `not(x)`, glued, is the call. - **Casts and type-taking builtins are calls:** `i32(x)`, `vec-new(u8)`, `max-value(u8)`, `the([3 f32], [1 2 3.5])`. A pointer cast is the type called: `Ptr(Color)(p)` reads `((Ptr Color) p)`. **Built.** ### Statements and blocks - **`let x = v`** scopes to the end of its block and reads as `(let [x v] rest…)`. Consecutive `let`s merge into one binding vector. A `let` takes more bindings on the lines indented under it, lined up with its first name, each seeing the ones above: ``` let row = r + 1 col: i32 = c - 1 {x .x} = p ``` is `(let [row (+ r 1) col (the i32 (- c 1)) {x .x} p] rest…)`. Any other line indented there is refused. At the top level each such line is one more global, `(def col i32 …)`; a pattern there is refused, since a global binds one name. A `let` is otherwise flat: its scope is the rest of its block. To end it early, put it in a `do:` block. The printer writes every `let` flat, and a run of bindings whose values are short (one line, 40 characters or fewer) as one `let` with the rest under the first name; top-level globals stay one `let` each. A `let` with statements after it takes them into its body; when one of them means an outer name the `let` rebinds, the `let`'s is renamed (`x` to `x-2`, a name the top-level form does not use; a struct pattern is written as `{x-2 .x}` pairs). A macro's body counts as statements run in order when its definition splices its rest parameter only into a `do`, a `let`/`fn`/`when`/`while`/`loop` body or another such macro's body; `comment` counts too. Where a rename cannot be trusted (the name quoted, qualified as `x/y`, or called as `x(...)`), and at the top level, among a call's other arguments and in a quasiquote, the `let` goes in a `do:` block instead, and so does one whose longer scope would reach a call of a macro whose template names the `let`'s name. A macro's body counts only if nothing but its templates depends on how the body splits into arguments (a count against the body's start, a predicate on its first form). One case this cannot see: a macro defined nowhere the printer reads (not the prelude, the file or an imported package) whose expansion names a variable its call does not spell. Destructuring: `let {.x .y} = p`, `let [head & tail] = xs`. (`defer` is function-scoped, not let-scoped, `TODO.org` "defer may be written in a let", so merging never moves a cleanup.) **Built**; `let x =` with the value as an indented block also reads, and so does `def`/`once`/`const`. - **`if`/`elif`/`else`.** `else` and `elif` sit at the `if`'s column. No `elif` reads as `if` (with else) or `when` (without); with `elif` it reads as `cond`. One-line form: `if c then a else b`, for use in a `let`. **Built** (a block of one line is that line; of more, `(do …)`). An `else` or `elif` on the line after a one-line `if c then a`, at its column, continues it (section 3, item 6); each such clause is one-line (`elif c then x`, `else y`) or takes a block. - **`while c`, `until c`**, optional label first: `while :outer c`. **Built.** - **`for i in range(n)`**, `range(a, b)`, `range(a, b, step)` read as `dotimes`. `range` here is syntax, not a function. `..` is avoided because `a..b` would lex as one name. **Built** (a label goes first here too: `for :outer i in range(n)`; in a macro template the variable may be an unquote, `for ~i in range(~n)`). - **`return v`, `break`, `break :outer`, `continue`, `defer expr`** (or `defer` plus a block). **Built**; `defer` plus a block reads `(defer a b …)`. `break`, `continue`, `return v` and `x = v`/`x += v` also fit the one-line slots: a match arm's value, `then`/`else`, and after `defer`. - **`match`:** ``` match shape Circle(r) -> 3.14 * r * r Rect(w, h) -> w * h :north -> 0 _ -> 0 match code 404 -> "missing" -1 -> "none" "ok" -> "fine" \a -> "a" _ -> "other" match ready true -> go() false -> wait() ``` An arm's body can be an indented block, which reads as `(do …)`. **Built** (a one-line block reads as that line). A number, char or string pattern is the literal as written, compared as `(= t lit)`; over a dyn a keyword or `true`/`false` is too. A bool's arms are `true` and `false`, and naming both needs no `_`. - **Conditions**, clauses at the header's column: ``` handler-case edn/read-file("game-data.edn") on FileError(c) nil restart-case agent/poll() restart continue() () restart use-value(v: i32) v * 2 ``` `handler-bind` takes the same `on` clauses; the reader moves them in front of the body, where the form wants them. **Built.** A restart's report text goes on its header, `restart retry() "Try the load again"`, and reads `(retry [] :report "Try the load again" …)`. - **Unit:** `()` as a statement reads `(do)`; in a type it is `()`. **Built**; inside an expression `()` stays `()`, and the printer writes a lone `()` statement as `(())`. A bare `()` in a one-line body slot (`fn f() -> () = ()`, `_ -> ()`, `fn() => ()`, `then ()`) is a statement too, and reads `(do)`. - **Lambda:** `fn(i, j) => i * 10 + j`, or `fn(i, j) =>` plus a block. **Built**; its parameters are bare names, as `(fn [i j] …)` wants, with no `dyn`. `fn(…)` followed by anything else is the fallback call. A lambda may state its types, `fn(a: C, b) -> bool => …` or plus a block (section 3, item 7). `=>` is a lambda's only spelling: `fn(a) = x` and a lambda header with a block under it and no `=>` are refused, with the `=>` form as the fix. Named functions keep `=`. A block lambda may sit inside brackets: ``` sort-by(slice(xs), fn(a, b) => let d = a.n - b.n d < 0) ``` The block ends where the brackets close, with the `)` at the end of its last line or on a line of its own at the call's column. It is the last thing in them: a comma after the block is refused (so a call takes one block lambda, as its last argument; name any other with `let`), as is a line inside the brackets at or left of the column the `=>` line starts at. Block lambdas nest, each block ending at its own brackets. `flan convert` writes a call whose last argument is a lambda with a block this way. ### Definitions - `fn name(a: i32, b) -> R` plus a block; `fn name(a) = expr` for one expression. Reads `(defn name [a i32 b dyn] R …)`. A `{:where …}` constraint becomes `where ordered?($t)` after the return type. **Built**; with no `-> R` the return is `_`, read off the body. Several predicates are `where p, q`. - A `let` at the top level is a global: `let x = v`, `let x: T = v`, `let scratch: [4 u8] = uninit` read `(def x dyn v)`, `(def x T v)`; `once x: T`, `once x = v`, `const n = 3`. **Built.** `let x = v` and `once x = v` read with `dyn`; `const n = 3` reads `(defconst n 3)`, its type inferred as today. `def` is refused with the `let` to write. A `let` in a block, `comment:`'s included, is local, and so is one in code the editor evaluates as an expression. - `struct Cell` with a `name: Type` line per field. `data Shape` with a line per case: `Circle(r: f32)`, `Empty`. `enum K` with `lo = -1`, `mid`. `union U` like `struct`. **Built** (an untyped field is `dyn`; `Empty()` is `(Empty [])`). A member is `:mid` or `K.mid`, in a value and in a match arm, in both syntaxes (section 3, item 8). A condition names its parent after the name, `struct DiskFull :parent IoError` with its field lines, reading `(defstruct DiskFull :parent IoError [free i64])`; with no field lines it reads `(defstruct IoError :parent Error)`. A struct or union fits on one line with its fields in parentheses, `struct Pt(x: i32, y: i32)` or `struct DiskFull(free: i64) :parent IoError`; `flan convert` writes that when it fits the line and no comment sits among the fields. **Built.** - `type Row = Vec(i32)` reads `(defalias Row (Vec i32))`. **Built.** - `macro repeat(i, n, & body)` plus a block reads `(defmacro repeat [i n & body] …)`. A parameter is a bare name, a destructuring vector `[a b]`, or `& rest`, last. **Built.** - `loop x = a, y = b` plus a block reads `(loop [x a y b] …)`, as a statement or as a value, `let r = loop i = 0`. `recur(y, x % y)` is a call. A loop with no variables is the fallback, `loop([]):`. **Built.** - `class lambda(param, body, env)`, or `class lambda` with a slot per line, reads `(defclass lambda [param body env])`; a typed slot is `pause: bool` and its type follows its name in the vector. **Built.** - `generic describe(v) -> dyn` reads `(defgeneric describe [v] dyn)`; `multi kind(v) -> dyn = type-of(v)`, or plus a block, reads `(defmulti kind [v] dyn (type-of v))`. Their parameters are bare names. **Built.** - `method describe(f: lambda)` plus a block reads `(defmethod describe lambda [f] …)`: a class is the first parameter's type. Any other dispatch value follows `when`: `method kind(v) when :int`, `when :else` for the default. `= value` for a one-line body. **Built.** - `import rl "vendor:raylib"`. **Built.** - **Every other form uses the fallback** (next item) until someone asks for sugar: `declare`, `declare-c`, `array-fill`. **Built.** ### The fallback **Any form can be written as a call:** `head(arg, …)`, or `head(arg, …):` plus an indented block, reads as `(head arg … block…)`. Commas vanish into the form. `defmethod(describe, :square, [s]):` plus a block is `(defmethod describe :square [s] …)`. So every form is reachable on day one, the printer has something to fall back on, and the sugar above can land one piece at a time. **Built**; a header word glued to `(` is always this call, `if(c, a)`, `let([x 1], x)`. A bare name with a trailing colon takes a block too, `comment:` (author's decision 85). ### Types After `:` and `->`, a small type grammar that reads to today's type forms: `i32`, `$t`, `()`, `[T]`, `[const T]`, `[n T]`, `Vec(T)`, `Map(K, V)`, `Option(T)`, `Ptr(T)`, `Ptr(const T)`, `Fn(A, B) -> R`, `CFn(A) -> R`, `rl/Vector2`. **Built** (the arrow is read only in a type position; inside a value, `vec-new(Fn([i32], i32))` is the call spelling). ### Macro templates ``` macro with-mode-2d(camera, & body) quote begin-mode-2d(~camera) ~@body end-mode-2d() ``` `quote` plus a block is a quasiquote; `~x` and `~@xs` are unquote and splice, the Clojure spellings the reader already has. (An earlier sketch used `$x`; that collides with type variables such as `$t`.) **Built**: one line reads `(quasiquote line)`, more read `(quasiquote (do …))`; `~` takes the atom right after it, so `~name(x)` is `((unquote name) x)`, and `~(f(x))` unquotes a call. ## 3. Settled after review (2026-09-25) 1. **A call takes a block only with a trailing `:`.** `rl/with-drawing():` then the indented body. A deeper-indented line after an ordinary call is an error, never an extra argument, so a stray indent can't be silently absorbed into the call above it. This is F#'s rule too: a block opens only after a specific token (`=`, `->`, `then`, `do`, …; `LexFilter.fs` 2236-2526), and F#'s own `with-drawing` would need `fun () ->`. Headers (`fn`, `if`, `while`, …) are the openers here and need no `:`. The fallback form takes its block the same way: `defmethod(describe, :square, [s]):`. 2. **Variadics stay.** Three spellings, all reading to the same variadic form: - a run of one operator flattens, `a + b + c` → `(+ a b c)`, `a < b < c` → `(< a b c)`, `x and y and z` → `(and x y z)`; - an operator glued to `(` is a call: `+(a, b, c)`, `!=(a, b, c)`, `and(p, q, r)`; - ordinary variadic functions are just calls: `println(a, b, c)`. The one exception: **`a != b != c` is refused**, with a message pointing at `!=(a, b, c)`, because Flan's `!=` means "all distinct", not what the chain suggests. 3. **`'(a b)` is quoted**: a list of symbols, as in Lisp, the same `'` as `'name`. `list(a, b)` builds a list of values. Revisit if lists get common. 4. **Private functions are `fn- name(…)`.** 5. **"Infer the return type" is `_` in the return slot** in the paren syntax: `(defn f [x i32] _ (+ x 1))`. The indented reader emits `_` when `->` is omitted. `_` in type position means "fill this in" in Rust and OCaml too, and no type can be named `_`. Settled 2026-09-26, after writing programs by hand (`test/syntax/handwritten/`): 6. **A one-line if continues on the next line.** `if c then a` followed by `else b` (or `elif c2 then d`, or either with a block) at the if's column is one if. An `else` left of that column, or indented deeper, is refused. After an if with a block, `else x` on one line is accepted too. **Binding:** an `else` or `elif` on a line of its own belongs to the if that starts at its column. An if inside a one-line slot (after `then`, after `else`, in an arm) ends with its line and takes no later clause, so ``` if a then x else if b then y else z ``` is refused at its last line: the first `else` took `if b then y` as its value, and the chain is `if a then x else if b then y else z` on one line, or `elif b then y` on the second. 7. **Typed lambdas.** `fn(a: C, b) -> R => body`, or plus a block, reads `(the (Fn [C dyn] R) (fn [a b] body))`: the paren `fn` has no typed parameters, and `the` is how a value states its type, as in `let x: T = v`. An untyped parameter is `dyn`; the return type is required. Where a `CFn` of the same signature is wanted, the literal is that `CFn`; at a generic's `CFn($t) -> $t` parameter the literal is a `CFn` at its own types, which bind `$t` as any argument's would. The printer writes that form back as the typed lambda. A typed block lambda sits inside brackets as an untyped one does. 8. **`Dir.north` is the enum member `:north`**, in a value and in a match pattern, in both syntaxes. `:north` stays. A local named `Dir` shadows the enum as a local shadows any global: `Dir.north` is then its field. 9. **Types in messages follow the code's syntax.** `Types.spell ~indented` is the one printer, `Fn(A) -> R`, `Option(i32)`, `Small(4, i32)` for a .fln location and `(Fn [A] R)` for a .flan one; `Types.to_string` stays the spelling for keys, symbols and runtime strings. Hard-coded code in a hint is written per message. 10. **`flan convert` keeps adjacent one-line globals adjacent**, in both directions. ## 4. Build order Each step lands on its own, with `dune test --root .` green. 1. **The reader** (`lib/indent_reader.ml` or similar): tokenizer with the indent stack, then a parser to `Form.t`. Start with what `sand.flan` and `algorithms.flan` need, then the fallback, then the sugar in section 2 in order of corpus frequency (`set`, `let`, `+`, `at`, `=`, `if`, `dotimes`, …). **Test:** hand-convert `algorithms.flan` and `sand.flan` to `.fln`; the forms read from each pair must be equal, ignoring locations. 2. **Switch readers by extension** at every program-source entry point: `Front.load` (`lib/front.ml:16`), `Load.import` (`lib/load.ml:1211`, plus `entries` at 137-142 and `is_package_file` at 103-105), `Session.create` (`lib/session.ml:260`), and `bin/main.ml` 327, 334, 393-399, 557-560. The prelude (`lib/prelude.ml:2441`), the wire protocol (`lib/wire.ml:103`) and the registry spelling (`lib/dev.ml:2725`) stay paren syntax. 3. **The printer**, `Form.t` → indented text, and a `flan convert` command. **Test:** for every corpus file, read with parens, print indented, read indented; the forms must be equal to the first read, after one normalisation: every name a `let` binds is renamed through its scope to one numbered by binding order; then, in a body run in order, a `let` counts as equal to itself taking in the later statements of the body (a macro's body by the same rule as the printer's); `(do x)` with `x` a `let` counts as `x`; a `let` whose whole body is another `let` counts as equal to the merged `let`; `(and x)` and `(or x)` count as `x`. Taking in and the one-argument `and` stop at a quote or quasiquote. That covers 394 files and runs on readers alone, so it's fast. 4. **The dev loop.** Code-carrying wire ops (`eval`, `eval-expr`, `macroexpand`, `set`) get an explicit `:syntax` field instead of guessing from `:file`. The `:file` guess breaks for ``/`` origins and for `flan-macroexpand-again` (`emacs/flan.el:3279-3309`), which sends paren-syntax expansion text under the original file's name. Replace the space-padding in `flan--text-at` (`emacs/flan.el:2602-2622`), which breaks significant indentation, with `:line`/`:col` fields; the reader seeds its indent stack with that column. **Built** (also `load-file` and restart arguments; no `:syntax` means paren, except a `load-file` of a `.fln` file; several indented statements sent as one expression read as `(do …)`). 5. **Emacs mode** for `.fln`: - A top-level form runs from a column-0 line that isn't `else`, `elif`, `on` or `restart` to just before the next one, minus trailing blank and comment lines (python.el `python-nav-end-of-defun`, `~/Repositories/emacs/lisp/progmodes/python.el:2175-2195`). - An inner block is its header line plus every deeper-indented line after it. Send the text unchanged, with its start line and column. - Replace the `flan-mode`-only checks: `flan-watch--ghost-buffers` uses `(eq major-mode 'flan-mode)` (`emacs/flan-watch.el:247-255`), and there are `derived-mode-p 'flan-mode` checks at `flan.el:1140, 2095, 2408` and in `flan-dape.el:96-129`. - The breakpoint position Emacs sends (`:pause (LINE COL)`, `flan--pause-bounds` at `flan.el:2637-2660`) must equal the start location the reader gave that form. `Ast.mark_pause` matches exactly (`ast.ml:491-492`). **Built** (`emacs/flan-fln-mode.el`; keys and objects in `emacs/MANUAL.md`, "Indented files"). A line ending in `=` or `=>` also opens a block for TAB, and a body is its statement's own block, up to its first clause. 6. **Return-type inference** in `Check`, with the recursion refusal and the stale-caller cause. This is independent of steps 1-5 once the marker exists. **Built** (`Check.infer_returns`). `_` is refused outside a `defn`'s return slot, in a generic's, and in `defgeneric`/`defmulti`'s (`defmethod` has no return slot). An exit with no value beside one with a value is refused. A self- or mutually recursive group whose every exit gives `()` is `()`. A stale body with `_` keeps the signature it was compiled with. Out of scope: dropping macros, built-in replacements for `with-*`/`defedn`, printing diagnostics in the new syntax, converting the prelude or vendor packages, website and docs. ## 5. Reference **Where the grammar gets hard**, from a survey of the whole corpus (394 files): - 513 `(Type {…})` constructions, all with `.field` keys. - 1304 `(.f x)` accesses; chains of up to 5 `(.a (.b x))` deep. - 65 `cond` and 185 `match`, written as flat pairs. - 1229 `set`s, all with 2 arguments. The target is a symbol 809 times, `at` 217, a field 180, `get` 10 and `deref` 9. - 22 chained comparisons and 16 variadic `!=`. - `and`/`or` with up to 15 arguments (`vendor/edn/edn.flan:251`). - 332 `()`. - 1048 lines of code-generating macros in `vendor/edn/provide.flan` and `vendor/json/provide.flan`, with 139 quasiquotes and 188 unquotes. **What prints paren syntax to a user:** - `Form.to_string` in `parse.ml` and `expand.ml` diagnostics. - About 70 hard-coded usage strings and "write (…)" hints in `parse.ml` and `check.ml`. - `Types.to_string`, which is in every type error and in the eldoc, `defs`, `layout` and locals replies. - The macroexpand `:text`, `:flat` and `:source` fields (`dev.ml:1416-1430`). - Macro signatures in `defs` (`dev.ml:1616-1629`). - `Render`'s value notation (`println`, the REPL, the inspector), which `emacs/flan-inspect.el:122-272` parses and `flan-inspect--literal` (991) writes back as code. **Offside-rule references:** - F#: `~/Repositories/Fable/src/fcs-fable/src/Compiler/SyntaxTree/LexFilter.fs`. Copy the adjacency rule (2663-2670), continuation operators (360-380, 1850-1870), and `else`/`elif` alignment (2028-2032, 202-218). Skip the context stack; it exists for keyword-opened blocks, which this syntax doesn't have. - GDScript: `~/Repositories/godot/modules/gdscript/gdscript_tokenizer.cpp` (`check_indent`, 1143-1304) and `gdscript_parser.cpp` (multiline flags and `push_expression_indented_block`). - tree-sitter-python: `~/Repositories/tree-sitter/languages/tree-sitter-python/src/scanner.c`.