Stylistic suggestions for writing Elly by hand or generating it
(examples, tests, benchmarks). A living document — add to it as
conventions settle. These are preferences, not spec rules; the
grammar is in docs/elly-spec.md and the Muon layer it sits
on is in docs/muon-spec.md.
Write f(a, b, c) rather than the bare application spine
f a b c. Inside a (…) group commas and
newlines are interchangeable, and each comma/newline-separated segment
is a full sub-expression spread as one argument to whatever
precedes the group — so f(a, b) is exactly
f a b. The spread form lets a call wrap across lines while
staying one chain:
__Int_for(0, n,
0,
&(i, acc) __Int_add acc (f i))
Mixing the two forms in one program is fine and idiomatic — reach for
the space form when a call is short (__List_get al i) and
the spread form when arguments are long or want their own lines
(__eq(k, target, &_ hit, &_ acc)).
Spread composes: __Int_add(__Int_mul(i, n), k) reads as
__Int_add (__Int_mul i n) k, because each segment is
reduced independently before being spread.
A […] list literal is already a
multi-line-friendly scope, so pass one directly —
__match [c0, c1, …], not __match([c0, c1, …]).
The list's own brackets carry the newlines; wrapping it in a
(…) spread just to reach one argument is redundant.
Write &(i, acc) rather than
&i &acc. They desugar identically (a binder group
curries left to right), but the group reads as a parameter list.
Patterns work in either position: &(i, [seed, acc])
binds i and destructures the second argument.
Keep &x &y (two separate &)
only when the two-stage currying is the point — e.g. a fixpoint
Z (&rec &n …), where rec is supplied
first and the result is applied to n.
Write val |> __match [clauses] rather than
__match [clauses] val. The pipe L |> R
lowers to App(R, L), threading the left operand in as
R's final argument — so it is the same call, read
subject-first:
n |> __match [
&(< 2) n,
&_ __Int_add (rec (__Int_sub n 1)) (rec (__Int_sub n 2))
]
|> binds looser than application but tighter than
&/let, so it resolves within a split-free
segment and needs no extra parentheses in the common case.
A program (and each let body) is a single chain, so
newlines are load-bearing:
MultipleExpressions).let binder group and its body
orphans the body (LetMissingBody) — start the body on the
group's closing-paren line: ) __Int_add a b.(…) /
[…] / {…} / let(…) scope. Even
there, a newline that splits a single abstraction body makes the
&-header its own chain (AbsWithoutBody):
in (&i &acc <body>) the whole
&i &acc <body> must be one segment.So lay multi-line code out as spread arguments or
let bindings (both tolerate newlines
between their segments), not by breaking an abstraction body across
lines. When a callback body is genuinely large, give it a name in a
let:
let (
step = &(i, [s, acc]) let (
s2 = lcg s,
v = (__Int_divrem s2 M).1
) [s2, (__Int_add acc v)]
) (__Int_for(0, n, [7, 0], step)).1
Stopgap for top-level newlines. A whole program that
wants blank lines or top-level comments can be wrapped in one
(…) grouping — a one-element group tolerates stray newlines
and comment-only chains at its top level:
(
// a comment, and blank lines, are fine at the top of a `(…)`
let (x = 1, y = 2) __Int_add x y
)
This is only a stopgap: a proper file will be tuple-like with free
top-level newlines once modules are designed. The (…)
wrapper also happens to sit nicely inside a Rust multiline string
literal (leading/trailing newlines are harmless).
list .N: pair.0,
(__Int_divrem a b).1. The .N binds to the
value on its left; wrap a call whose result you project,
(__Int_for(…)).0.x mod m is
(__Int_divrem x m).1 (the remainder is element 1 of the
[quotient, remainder] pair).{ k: 1 } is { .k: 1 }. Write .5
(symbol) or (5) (integer key) to disambiguate a
digit-leading key.