Elly style

Elly style

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.

Prefer the spread-call form

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.

Prefer binder groups for multiple parameters

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.

Prefer the pipe for a subject-last builtin

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.

Mind the one-chain layout rule

A program (and each let body) is a single chain, so newlines are load-bearing:

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).

Projection and modulo idioms

Symbols and keys