Layout — the offside rule
Zelkova is indentation-sensitive: where a line starts decides which construct it belongs to, and there are no braces to fall back on.
Two kinds of thing can go wrong with a line. The leading whitespace may be malformed in
its own right — an odd number of spaces, or a tab — in which case the error is about the
whitespace and says so. Or the whitespace may be well-formed and the column wrong for
the construct the line belongs to, in which case the error names the block that was
broken: "the branches of a case … of", "the expression of a case … of".
An error message may also name open block or close block. Those are not tokens you can
write. They are Zelkova's block structure made explicit so that a diagnostic has something
to point at, and seeing one in a message means a layout rule was violated rather than a
token missed.
Indentation is measured in two-space levels
Leading whitespace on a line must be an even number of spaces. One level is two spaces.
module Example exposing ()
type Flag
= On
| Off
describe f =
case f of
On ->
1
Off ->
0An odd number of leading spaces is rejected before layout ever sees the line:
module Example exposing (f)
f x =
1Tabs are legal only inside a comment
A tab is an error anywhere in a source line except inside a comment. It does not matter whether the tab is indenting the line, separating two tokens, or sitting alone on a line that would otherwise be blank. Zelkova therefore takes no position on how wide a tab is, because no position in a program can depend on one.
A tab in a line's leading whitespace:
module Example exposing (f)
f x =
1A tab between two tokens, well after the line's first non-whitespace character, is the same error:
module Example exposing (f)
f x =
1 + 2So is a tab on a line that carries nothing else:
module Example exposing (f)
f x =
1Inside a comment — from -- to the end of the line, or between {- and -} — a tab is
ordinary text and is accepted:
module Example exposing ()
-- a tab in a line comment
{- and a tab in a block comment -}
f x =
1A blank line is blank whatever spaces it holds
A line holding only spaces is blank whatever its width: it neither breaks the even-width rule nor closes any block.
The blank line in the example below is not empty: it holds three spaces, which would be an odd-width indentation error on any line that had a token on it. Stripping that whitespace, as an editor "cleaning" trailing whitespace will, turns the block into an example that proves nothing.
module Example exposing ()
f x =
1A file starts at column 1
The first token of a source file is the module keyword, and it sits in column 1. A
comment may precede it — comments are consumed as part of the indentation scan, so they
are invisible to this rule:
-- Comments before the header are fine.
module Example exposing ()
f x =
1Whitespace before module is not. A leading space is invalid under this rule; a leading
tab is invalid too, but as a tab (above).
The compiler does not enforce this rule as stated. An indented file holding a single declaration is accepted:
module Example exposing (..)Known gap: that block is tagged expect=ok because that is what happens today, and it
is the gap docs/tickets/err-12.md tracks made visible. The
language's answer is that this file is invalid.
An indented file with a second declaration is rejected, but for a reason one step removed from the rule: the second declaration is what fails, not the indentation on line 1.
module Example exposing (f)
f x =
1Known gap: that block pins "an indented file with two declarations is rejected", not "a
file must start at column 1". The diagnostic is bad in the way ERR-12 describes — the caret
lands on the later declaration and the message asks for close block, which the reader
cannot write. It is tagged expect=parse-error:UnexpectedToken, naming that
wrong-but-current error on purpose: when ERR-12 lands and the message becomes a real one,
the block goes red.
Top-level declarations
Every top-level declaration — module, import, a type declaration, a type annotation, a
function definition — begins in column 1. A token in column 1 on a line after the current
declaration started is what ends that declaration and begins the next; a declaration's own
continuation lines must therefore be indented.
module Example exposing ()
first x =
1
second x =
2A line written in column 1 where a continuation was meant closes the declaration, and the parser is then handed the end of a declaration whose body never arrived.
module Example exposing (f)
f x =
1A line indented past column 1 continues the declaration above it, however it was meant. Written after a complete declaration, a line that begins a new one is rejected with an error naming its indentation, whether it follows a module header:
module Example exposing (f)
f x =
1or a declaration whose last expression could take one more argument:
module Example exposing (f, g)
f = 1
g = 2There is no separator between declarations. Blank lines between them are conventional and carry no meaning.
A declaration written as several clauses is no exception. Each clause begins in column 1 and so ends the one before it; what makes them one declaration is that they share a name and stand together, which is Declarations' rule rather than a layout one. See also Patterns.
case … of
The scrutinee
The expression between case and of may span several lines. It opens a block of its
own, closed by the of:
module Example exposing ()
type Flag
= On
| Off
describe f =
case
f
of
On -> 1
Off -> 0The first branch fixes the column for all of them
When the block of branches opens, the first token after it sets the column that every
branch in that block must start on. That column must be strictly deeper than the column of
the case keyword itself.
Branches may be written one per line:
module Example exposing ()
type Flag
= On
| Off
describe f =
case f of
On -> 1
Off -> 0A branch that starts left of the column the first branch established is an error:
module Example exposing (describe)
type Flag
= On
| Off
describe f =
case f of
On -> 1
Off -> 0A branch that starts right of it is equally an error: a deeper line that begins a new branch is a mistake, not a continuation of the branch above it:
module Example exposing (describe)
type Flag
= On
| Off
describe f =
case f of
On -> 1
Off -> 0Known gap: the compiler rejects that today, but for the wrong reason and with the wrong
caret: layout has no rule for the deeper line, so it is absorbed into the previous branch's
body and the grammar then trips on the second ->.
docs/tickets/err-11.md tracks the diagnostic. The language's
answer is unchanged by that ticket, and the block pins today's UnexpectedToken so this
paragraph goes red along with it.
Even where a branch satisfies the enclosing block's own indentation, sitting level with
case — or left of it — is an error:
module Example exposing (describe)
type Flag
= On
| Off
describe f =
case f of
On -> 1
Off -> 0A case written on the same line as the = that introduces the body puts the column its
branches must clear deep inside the line rather than at its start, so they have to follow it
there:
module Example exposing ()
type Flag
= On
| Off
describe f = case f of
On -> 1
Off -> 0A branch body is deeper than its pattern
A branch body may sit on the same line as its ->, or on following lines indented past
the branch's own column. It may not start at the branch's column — that column belongs to
the next branch, so a body written there closes the branch and leaves it empty:
module Example exposing (describe)
type Flag
= On
| Off
describe f =
case f of
On ->
1
Off ->
0Written correctly, with the body one level deeper than the pattern:
module Example exposing ()
type Flag
= On
| Off
describe f =
case f of
On ->
1
Off ->
0Nesting
A case … of in a branch body follows the same rules relative to its own position:
module Example exposing ()
type Flag
= On
| Off
both a b =
case a of
On ->
case b of
On ->
1
Off ->
2
Off ->
0let … in
let … in is part of the language as designed and is not implemented. A let in a source
file is rejected today:
module Example exposing (f)
f x =
let
y = 1
in
yNot implemented: that example is tagged expect=unimplemented; it will go red the day
let is implemented.
Not implemented: the layout rules below are design intent, not something the compiler
has ever checked. The bindings sit deeper than the let, and in closes the block. Two
questions are open — whether the bindings of a let form a block with the same column
discipline as case … of branches, so that the first binding fixes the column for all of
them; and whether in must align with its let. LANG-33
answers both, as the first step of implementing let … in.