Formatting

Formatting Guide

This page describes the source-code formatting conventions used across MPL sources. The rules here are about visual layout and idiomatic expression choice, not about language semantics.

Whitespace

Scope closing

Closing a scope with ), ], or } ends one logical unit. Subsequent code belongs to an outer scope and represents a new step. Insert a blank line after any line that closes a scope, unless the very next line also closes a scope.

Correct:

[
  body
] loop

next step

Correct (consecutive closers, no blank required):

    ] if
  ] loop
] &&

Incorrect (missing blank line):

[
  body
] loop
next step

A closing bracket followed by a control operator on the same line (for example ] if, ] loop, ] when) is still a scope-closing line for this rule. It must be followed by a blank line or by another closing bracket, never directly by a non-closing statement.

Column alignment

When consecutive lines form a table of analogous items, align them into columns. Alignment is required whenever it is achievable by inserting spaces; do not leave items visually scattered when a small amount of padding would put them in columns.

The following kinds of tables require column alignment:

Aligned imports

"Array.Array"       use
"Span.Span"         use
"algorithm.between" use
"control.Cond"      use

Every quoted name is padded to the same width so use appears in one column.

Aligned bindings

isDecimal: ["0" "9" between];
isHex:     [(@isDecimal ["A" "F" between]) meetsAny];
toDecimal: [.data 48n8 -];
toHex:     [.data dup 65n8 < [48n8] [55n8] if -];

Each name is padded so : and the opening [ of each body appear in their own columns.

Aligned dispatch descriptors

For predicate-action pairs in a dispatch construct, align three columns:

(
  [char ""   = ] ["Unterminated escape sequence" fail]
  [char "\"" = ] ["\"" @textValue.cat            skip]
  [char "\\" = ] ["\\" @textValue.cat            skip]
  [char "n"  = ] ["\n" @textValue.cat            skip]
  [char "«"  = ] ["«"  @textValue.cat            skip]
  [char isHex ~] ["Invalid escape sequence"      fail]
) cond0

When the action chain itself varies in length between rows (for example when some rows perform two checks and others perform four), pad the shorter chains so a shared downstream column (such as [isDelimiter ~] ||, the error arm, or the terminating if) aligns with the longer rows. Pad inside the descriptor brackets rather than after them.

Within any aligned block, never carry more than one column of spaces between two aligned columns. If several consecutive columns are spaces in every row, compress that run to a single separator column. Each row may then have its own internal run of spaces before the separator to reach the aligned column width.

(
  [char "8" =] [skip ~                               [isDelimiter ~] || ["short" fail] [0x7Fi8     AstNode.INT8  processInteger] if]
  [char "1" =] [skip ~ [char "6" = ~] || [skip ~] || [isDelimiter ~] || ["short" fail] [0x7FFFi16  AstNode.INT16 processInteger] if]
  [char "3" =] [skip ~ [char "2" = ~] || [skip ~] || [isDelimiter ~] || ["short" fail] [0x7FFFFFFF AstNode.INT32 processInteger] if]
) cond0

Between the hex constant and AstNode.INTn, only one column is all-spaces across every row; the varying space count on each side of that separator is the internal padding that each row needs to reach the aligned column's width.

Do not insert trailing spaces to pad a bare item inside brackets when there is no column to align against it. The default arm of a dispatch takes just its action with no padding.

Aligned comments on item lists

(
  "INVALID" # An invalid class, used as a default value
  "BLOCK"   # A single code block
  "COND"    # An 8-bit conditional value
  "INT8"    # A generic 8-bit two's-complement integer
  "INT16"   # A generic 16-bit two's-complement integer
)

Each value is padded so the trailing # comment appears in one column. This applies whenever several consecutive lines each end in a comment about that line's item.

When alignment does not apply

Control-flow idioms

Express common two-branch patterns with short-circuit operators rather than full conditionals when possible.

Intent Prefer Instead of
Run body only if X is not TRUE. X [body] || X [TRUE] [body] if
Run body only if X is TRUE and propagate a FALSE otherwise. X [body] && X [body] [FALSE] if

Prefer the shorter form when the body is small and the intent is a plain short-circuit. Fall back to an explicit if when both arms carry substantial code.

Error-first convention

When a step tests a compound condition and chooses between an error/no-op branch and a work branch, place the shorter error/no-op branch first, attached to the condition. The condition is then phrased so that TRUE means "error".

Shape:

anyFailure [short-path] [work-path] if

When combining several sub-checks, build the failure condition with ~ on each sub-term and combine the terms with ||, so the chain evaluates to TRUE on any failure:

A ~ [B ~] || [C ~] || [short-path] [work-path] if

Avoid ending a &&-chain with a trailing ~. Swap the if arms and invert the chain to ||-of-negations instead.

Incorrect:

A [B] && [C] && ~ [short-path] [work-path] if

Correct:

A ~ [B ~] || [C ~] || [short-path] [work-path] if

Multi-line expansion

When a branch carries substantial body, expand the dispatch onto multiple lines rather than packing everything onto one line. Nest each arm so the opening [ ends its line and the closing ] sits on its own line at the arm's indent level.

Incorrect (too compressed):

X [Y [char ":" =] || [short] [work with multiple statements] if] &&

Correct:

X [
  Y [char ":" =] || [
    short
  ] [
    work
    with multiple
    statements
  ] if
] &&

Tail-token placement

Inside a multi-line block, put a trailing single-token loop/dispatch signal on its own line rather than appending it to a dense statement line. This makes the block's return value visible at a glance.

Incorrect:

[
  statement
  another
  TRUE flag FALSE
] ||

Correct:

[
  statement
  another
  TRUE flag
  FALSE
] ||

Set-membership checks

When a value is compared against several alternatives, write a membership check against a small tuple rather than a chain of equalities combined with logical operators. This mirrors the intent and stays short when the set grows.

Incorrect:

char "a" = [char "b" =] || [char "c" =] ||

Correct:

("a" "b" "c") (char) contains

Error messages

Parser and semantic-compiler error messages follow a fixed shape so they read uniformly regardless of the specific diagnostic.

Shape

<who> <what>[[,] <extra>]

Examples

"Text literal is unterminated, expected hex digit"
"Text literal has non-hex character «G» in hex escape"
"Real number has exponent that starts with zero"
"Real number has non-digit character «a» after «.»"
"Number starts with zero"
"Number has hex digit «A» in decimal integer"
"Number overflows"
"Line ends with space"
"Line contains carriage return"
"«(» is closed by «]»"
"«(» is unterminated"
"Member access has no name after «@»"

Choosing <who>

Choosing <what>

Dynamic character inclusion

Include the offending character in the message when it speeds diagnosis. Use assembleString with « and » as visual quotes around the literal character:

("Name has unexpected character «" char "»") assembleString fail

For the » character itself — where «»» would be visually ambiguous — quote with ASCII double quotes:

""»" has no matching opener"

Unterminated constructs

When a construct ends prematurely (for example EOF before an expected terminator), use the shape X is unterminated[, expected Y] where Y names the specific thing that was expected next:

"Text literal is unterminated, expected hex digit"
"Text literal is unterminated, expected escape character"
"Text literal is unterminated, expected «"»"
"Text literal is unterminated, expected "»""
"«(» is unterminated"

For missing sub-parts inside a node, use has no X after «Y»:

"Member access has no name after «@»"
"Member access has no name after «!»"