# Markup Syntax Specification v0.1

> **⚠ Experimental (v0.x).** The language and its tooling are still evolving;
> expect breaking changes to syntax and APIs as the project matures toward 1.0.

---

## Why Markup Exists

Markup started from one question: **what if HTML looked like functions instead
of angle brackets?**

```html
<form class="login" method="post" action="/sign-in">
  <fieldset>
    <legend>Account</legend>
    <label for="user">Username</label>
    <input id="user" type="text" required>
    <button type="submit" disabled>Sign in</button>
  </fieldset>
</form>
```

```mu
form(class="login", method="post", action="/sign-in"
  fieldset(
    legend("Account")
    label(for="user", "Username")
    input(id="user", type="text", required)
    button(type="submit", disabled, "Sign in")
  )
)
```

Following that idea led to a small general notation rather than a document
format: a node is a parenthesized body `( … )` with an optional tag in front
of it - `()` and `button()` are both nodes - and a body holds values,
`key=value` parameters, and more nodes. In HTML-like trees, put keyed
parameters (attributes) before text content and child nodes - as in the
button and label lines above. Nothing in the language knows about documents,
styles, or servers - which is why the same few rules can describe DOM-like
trees, scene graphs, stylesheets, animations, and plain configuration, while
staying simple enough to read at a glance.

Nodes read as function calls, so there is no new shape to learn; `{…}`
objects and `[…]` lists are first-class values that nest freely with nodes; a
node is itself a value; and tagged multiline strings give heredoc power with
zero escape processing.

---

## Overview

Markup (`.mu`) is a UTF-8 general-purpose markup language - an alternative to
HTML and XML for structured content such as documents, layouts, and scene
graphs. Because the root node accepts `key=value` params, objects, and lists
directly (`app="demo"`, `settings={host="localhost"}`), the same notation
doubles as a configuration language with no extra ceremony. It is built from
a few pieces:

- **Values** - literals such as strings, numbers, booleans, null, lists, and objects.
- **Keyed parameters** - a name, `=`, and a value (`name=value`), including a short form for `name=true`.
- **Nodes** - optional tag plus `( … )` containing more values, parameters, and child nodes.
- **Comments** - `//` line comments and `/* … */` block comments.

A file is one untagged **root node** (no tag). Top-level lines are that node's **body**: the same kinds of items allowed inside `( … )`, without wrapping the file in parentheses. The sections below introduce each piece in order, starting with comments and values.

---

## Source Text

A few rules apply to the raw file, before any syntax:

- A `.mu` file is UTF-8. An invalid UTF-8 byte sequence is an error.
- A leading byte-order mark is accepted and skipped (linters warn).
- Line endings are normalized CRLF → LF before any other processing -
  including inside raw multiline strings. A lone carriage return that is not
  part of a CRLF pair is an error.
- After normalization, a raw control character other than newline or tab is
  an error anywhere in the file, even inside multiline strings. Strings can
  represent control characters with escapes (introduced below).
- Outside strings and comments, the only whitespace characters are space and
  tab. Anything else (no-break space, form feed, …) is an error.

---

## File Shape And Comments

Top-level lines are the root node body.

```mu
// A line comment starts with two slashes.

/*
A block comment can span multiple lines.
Block comments do not nest.
*/

/* Short block comments work too. */
```

### The `@mu` directive

All tool configuration lives in one directive namespace: a comment whose body
begins with `@mu` followed by `key=value` settings configures tools for the
whole file. Parsers treat the comment as an ordinary comment, and unknown
keys are ignored, so older tools skip newer settings.

```mu
// @mu version=0.1
// @mu suppress=comma

/* @mu
width=100
commas=off
inline-nodes=off
*/
```

When an `@mu` directive **comment begins on the first line** of a file and
carries a `version` key (either `// @mu version=…` on line 1, or a
`/* @mu` block that opens on line 1 with `version=` on any line of that
block), tools surface that value as the file's declared syntax version.
(0.1 is currently the only version; the key is a placeholder until a future
revision needs it.) A `version` in a directive that starts after line 1 is
**W020**.

Recognized keys include linter keys such as `suppress=` (comma-separated
codes or aliases such as `comma` for **W007** and **W026**), formatter keys
(`width`, `indent`, `pack`, `blanks`, `kv`, `commas`), and language keys
(`version`, `imports`, `inline`, and the scoped `inline-*` keys). Explicit
tool flags override file directives. See
[`linter.md` section 2.4](linter.md#24-file-directives-mu) and
[`formatter.md` section 5.1](formatter.md#51-file-directives-mu).

The block-comment form configures the same keys as the line form; settings
separate by commas or newlines. Later directives override earlier ones per
key.

---

## String Values

A string value can appear by itself at the top level.

```mu
"Markup" // double-quoted string value
'Markup' // single-quoted string value

"It's fine" // apostrophe inside double quotes; no backslash needed
'Say "hello"' // double quote inside single quotes; no backslash needed

"Say \"hello\"" // same text with double-quote delimiters; escape inner "
'It\'s fine' // apostrophe inside single quotes; escape with \'

"C:\\Users\\alice\\doc.txt" // backslash is escaped as \\
"hello\nworld" // \n represents a newline character inside the string
"a\tb" // \t represents a tab character
"a\rb" // \r represents a carriage return character
"snowman \u{2603}" // \u{…} is a Unicode scalar value, 1–6 hex digits
"\u{1F600}" // any scalar value works, including outside the BMP
"" // empty double-quoted string
'' // empty single-quoted string

/*
String content may contain UTF-8 text.
*/
"Sao Paulo"
```

The escape set is closed: `\"` `\'` `\\` `\n` `\t` `\r` `\u{…}` and nothing
else. `\u{…}` takes 1–6 hex digits and must name a Unicode scalar value -
surrogates and values above `0x10FFFF` are errors.

```mu-err
"club \q diamond" // error: E011 - unknown escape sequence \q
```

```mu-err
"\u{D800}" // error: E011 - \u{…} must be a Unicode scalar value, not a surrogate
```

```mu-err
"line one
line two" // error: E002 - raw newline inside string — use \n escape or multiline form
```

---

## Number Values

Numbers can appear by themselves at the top level.

```mu
+99 // plus-signed integer
42 // decimal integer
0 // zero
-17 // negative integer
1_000 // underscores between digits
5_349_221 // grouped decimal integer

0xDEADBEEF // hexadecimal integer; prefix is lowercase, digits are any case
0xdead_beef // hexadecimal with underscores
0x00FF // radix digits may be zero-padded - the leading-zero rule is decimal-only
0o755 // octal integer
0b11010110 // binary integer

+1.0 // plus-signed float
3.1415 // decimal float
-0.01 // negative float
5e+22 // exponent with plus sign
1e06 // exponent digits may be zero-padded
-2E-2 // uppercase E exponent
6.626e-34 // negative exponent
224_617.445_991_228 // underscores in integer and fraction parts

inf // positive infinity
+inf // explicit positive infinity
-inf // negative infinity
nan // not-a-number
+nan // explicit positive not-a-number
-nan // negative not-a-number
```

Numbers follow TOML: a leading `+` or `-` applies to decimal integers, floats,
`inf`, and `nan` only. Hex, octal, and binary literals are unsigned.

Integers and floats are distinct types. Implementations must support at least
64-bit signed integers and IEEE 754 double-precision floats. Integer literals
beyond ±2⁵³ are legal; linters warn where doubles would lose precision. A
literal beyond the implementation's integer range must produce a clean
error - never a silent wrap, truncation, or conversion.

```mu-err
1. // error: E008 - decimal point requires digits on both sides
.5 // error: E008 - digits are required on both sides of the decimal point
042 // error: E008 - invalid leading zero in decimal integer
0XFF // error: E008 - radix prefixes are lowercase only (0x, 0o, 0b)
1_.5 // error: E008 - invalid underscore placement in number
1e_5 // error: E008 - invalid exponent in number
-0xFF // error: E008 - hex, octal, and binary literals take no sign
+0b101 // error: E008 - hex, octal, and binary literals take no sign
```

---

## Boolean And Null Values

`true`, `false`, and `null` are exact-lowercase literal values - so are `inf`
and `nan`. Case variants (`True`, `NULL`, `Inf`) are ordinary identifiers,
never values.

```mu
true // boolean true value
false // boolean false value
null // null value
```

---

## Keys And Keyed Parameters

A keyed parameter is `key=value`. The key is either an **identifier** or a
**single-line string** (`"…"` or `'…'`) immediately followed by `=`. The
value sits on the right. Boolean shorthand (`debug` ≡ `debug=true`) uses an
identifier only - a quoted string alone is never shorthand.

```mu
// @mu suppress=I004
name="Markup" // identifier key: letters at the start
pool_size=10 // identifier key: underscore in the name
pool-size=10 // identifier key: hyphen in the name
size2=8 // identifier key: digits after the first character
a--b=2 // identifier key: internal double hyphen is legal
_internal=0 // identifier key: leading underscore
True=1 // case-sensitive: True is a name; true is a value

"this is a key"="this is a value" // string key: any single-line string
'retry-count'=10 // string key: single-quoted form
"Content-Type"="text/plain" // string key: reserved-looking text is fine
"true"=1 // string key: reserved words as keys are legal when quoted

port=8080 // number value on the right
tls=true // boolean value on the right
missing=null // null value on the right
limit=inf // special number value on the right

debug // boolean shorthand, equivalent to debug=true
```

When a single-line string is **not** immediately followed by `=`, it is a
positional value - not a key and not boolean shorthand:

```mu
"hello" // positional string at the top level
card("title") // positional string inside a node body
```

### Identifier keys

Identifiers are ASCII. The first character is a letter or `_`; the rest are
letters, digits, `_`, and `-`; the last character may not be `-`:

```text
identifier = [A-Za-z_] ( [A-Za-z0-9_-]* [A-Za-z0-9_] )?
```

The identifier rule applies everywhere a **name** appears: identifier keys,
node tags, boolean shorthand, and multiline string delimiter tags (all
introduced below). Object field keys and node param keys may also use string
keys (above); tags and shorthand remain identifier-only.

```mu-err
2fast=1 // error: E017 - missing separator between adjacent same-line items
-speed=1 // error: E008 - special values must be exactly `inf`, `+inf`, `-inf`, `nan`, `+nan`, or `-nan`
speed-=1 // error: E012 - identifier cannot end with a hyphen
café=1 // error: E012 - non-ASCII character in identifier — use ASCII names
```

The words `true`, `false`, `null`, `inf`, and `nan` are reserved as values.
They are not valid **identifiers** - the same list as above: identifier keys,
tags, boolean shorthand, delimiter tags. A string key may spell any text,
including reserved words (`"true"=1` is legal; bare `true=1` is not).

```mu-err
true=1 // error: E018 - reserved word cannot be used as an identifier
""=1 // error: E007 - empty string is not a valid key
''=1 // error: E007 - empty string is not a valid key
```

An unquoted word is never a string, and wrong-case literals are not values:

```mu-err
color=red // error: E010 - unknown word — add double quotes if this is meant to be a string
tls=True // error: E009 - `True` is not valid — use lowercase `true`
```

Within one scope, a key may appear only once. Quoting does not create a
distinct key: a string key and an identifier key collide when their decoded
text matches, so every key addresses exactly one slot in the scope's
key/value map:

```mu-err
port=80
port=90 // error: E004 - duplicate key in the same scope

"a"=1
a=2 // error: E004 - duplicate key; "a" and a spell the same key
```

---

## Multiline String Values

Use `"""` for indent-aware multiline strings and `'''` for verbatim multiline
strings. **Neither form processes escapes** - a backslash is an ordinary
character. The value is the interior lines joined with newlines; the newline
before the closing delimiter is not part of the value.

```mu
// @mu suppress=W001
greeting="""
  hello
  world
  """ // value is two lines: hello, world - no trailing newline

path_help="""
  Backslashes are literal here: C:\Users\alice
  """ // no escape processing inside multiline strings

script='''
#!/bin/bash
echo "hello"
''' // verbatim ''' preserves content exactly: scripts, logs, SQL

""" // only a // or same-line /* … */ comment may follow the opening delimiter
HELP!!
I NEED HELP!!!!
"""
```

### Opening delimiter lines

After the opening delimiter - quotes plus an optional tag - the rest of that
line may contain only optional whitespace and optional comments. A
`//` line comment, a `/* … */` block comment that closes on the same line, or
a block comment followed by a line comment is not string content; anything
else on the line is **E020**. Whitespace after the delimiter when the line
has no comment is accepted but non-canonical (**W029**).

```mu
// @mu suppress=W001,W028
note=""" // line comment on the opener - not content
inner content
"""

body=''' // line comment on the opener - not content
inner content
'''

asd1="""mu // has comment - no W029
inner content
"""mu

asd2="""mu        // whitespace before the comment is fine - no W029
inner content
"""mu

doc="""mu // allowed
inner content
"""mu
```

```mu-err
bad=""" hello // error: E020 - non-comment content after opening `"""`
content
"""
```

```mu
// @mu suppress=W001,W028
// W029: whitespace after multiline opening delimiter with no comment on the line
loose="""mu 
inner content
"""mu
```

(The opener line ends with a real trailing space; linters underline it.)

Both delimiters live on their own lines - a multiline string cannot open and
close on one line, and content cannot share a line with a delimiter:

```mu-err
banner="""all on one line""" // error: E021 - multiline delimiter not alone on its line — open and close on one line
```

```mu-err
banner="""
all but the close
on one line""" // error: E021 - content shares a line with the closing multiline delimiter
```

### Indent-aware dedent

`"""` lets you indent the string in source without keeping that margin in the
value. The **anchor** is the smaller of the two delimiters' offsets - the
count of characters left of the first quote, measured from column 0, so a
`key=` prefix counts. Every non-blank interior line drops exactly that many
leading whitespace characters (a tab counts as 1 character; linters warn on
mixed indentation).

```mu
  """
  hello
  world
  """

item="""
  This is cool
  """ // 2 chars left of this delimiter vs 5 for the opener - the anchor is 2

poem="""
  one

  two
  """ // blank lines need no indent; each contributes one empty line
```

A non-blank interior line that starts left of the anchor is a hard error -
the diagnostic must report the triggering line, the anchor column, and how
many characters of indentation are missing:

```mu-err
message="""
  Normal content.
Short line.
  Back.
  """
```

The value is the interior lines joined with a newline **between** each pair -
newlines are separators, not line terminators. A single line of text has no
`\n` in it, so one interior line contributes just its own text and N interior
lines join with N−1 newlines. That makes the edge cases fall out naturally:
`a` has zero interior lines, so there is nothing to join - empty string. `b`
has one interior line whose text is empty; one line means no separator, so
the value is that empty text - still the empty string. Only `c`, with two
empty interior lines, has a pair to join - the value is the single `\n`
between them.

```mu
// @mu suppress=W001
a="""
""" // zero interior lines → empty string

b="""

  """ // one blank interior line → empty string as well

c="""


  """ // two blank interior lines → a single newline
```

### Tagged delimiters

A delimiter is exactly three quotes plus an optional **tag** (identifier
rules, no space between the quotes and the tag). The tag abuts the quote run
the same way a node tag abuts `(`. The closing delimiter must match the
opener character for character - same quotes, same tag - and may be followed
only by a comma and/or a `//` comment. An interior line that begins with the
matching closing delimiter, followed by nothing except that optional tail,
closes the string; every other interior line is plain content, so a tagged
delimiter can wrap text that itself contains `"""`.

Opening-line tails follow [Opening delimiter lines](#opening-delimiter-lines):
`"""mu //note`, `"""mu /* note */`, and `'''sh /* sh */` are valid; bare
whitespace after the tag with no comment warns (**W029**).

```mu
// @mu suppress=W001,W028
doc="""mu
a="""
  hello
  """
"""mu // closes the outer string; the inner """ lines are plain content

shell='''sh
echo '''
'''sh // tags work on both forms
```

The untagged form cannot contain a line consisting of exactly its own
delimiter - add any tag. If the content also contains `"""mu`, pick another
tag (`"""mu2`). This is the heredoc rule: there is always an out, and never
an escape sequence.

```mu-err
x=""""
oops
"""" // error: E016 - ambiguous quote count (4 consecutive `"`)
```

```mu-err
y="""true
nope
"""true // error: E018 - reserved word cannot be used as a multiline delimiter tag
```

---

## Nodes

A node is a parenthesized scope. It is **untagged** when there is no
identifier before `(`, or **tagged** when an identifier is immediately
followed by `(` - nothing between them, not even a comment.

```mu
() // empty untagged node

debug() // empty tagged node; the tag touches "("

card(title="Docs") // tagged node; tag card

server(
  host="localhost" // keyed parameter inside a node body
  port=8080
  tls
  health_check() // child node inside a node body
  health_check() // duplicate child TAGS are fine - children form an ordered list
)

verbose, () // a separator makes two items: shorthand verbose=true, then an untagged node

/*
Node bodies can contain keyed parameters, positional values, and child nodes.
Their order is preserved. Duplicate child tags are legal; duplicate KEYS in
one scope are not (see Identifiers).
*/
```

A **tagged** node requires the tag to abut `(` with no whitespace. Whitespace
before `(` separates items - boolean shorthand plus an untagged node (**W027**):

```mu
debug () // W027: boolean shorthand followed by an untagged node — write "debug()" if a tagged node was intended
verbose, () // comma also separates: verbose=true, then untagged ()
```

```mu-err
debug/* gap */() // error: E023 - comment between node tag and `(`
```

---

## Positional Values

A positional value is a value without a key in a scope.

```mu
"localhost" // positional string at the top level
8080 // positional number at the top level
true // positional boolean at the top level
null // positional null at the top level

connect(
  "localhost" // positional string inside a node body
  8080 // positional number inside a node body
  true // positional boolean inside a node body
)

job(
  "ingest" // positional value
  retries=3 // keyed parameter
  dry_run
  notify(email="ops@example.com") // child node
  "second" // positionals, params, and nodes interleave freely; order is preserved
)
```

---

## List Values

A list is a value wrapped in `[` and `]`. List elements can be any value.

```mu
// @mu suppress=W026
[1, 2, 3] // list of number values
[true, false, null] // list of boolean and null values
["web", 8080, true, null] // mixed primitive values

numbers=[1, 2, 3] // keyed parameter whose value is a list
nothing=[] // an empty list is legal

cards=[
  card(title="Slots") // list element can be a node value
  card(title="Live")
  ()
]

matrix=[
  [1, 2] // list element can be another list
  [3, 4]
]

quotes=[
  """
  first
  """, // a comma may follow a closing multiline delimiter
  """
  second
  """
]
```

Lists hold **values only** - no keyed parameters, no boolean shorthand - and
a tag binds only to `(`:

```mu-err
[debug] // error: E010 - unknown word — add double quotes if this is meant to be a string
[a=1] // error: E010 - unknown word — lists hold values, not key=value pairs
card[1] // error: E017 - missing separator between adjacent same-line items — a tag binds only to `(`
```

---

## Object Values

An object is a value wrapped in `{` and `}`. Object entries are explicit
`key=value` fields. Keys may be identifiers or single-line strings (same rules
as keyed parameters).

```mu
{host="localhost", port=8080, tls=true} // object value at the top level

config={host="localhost", port=8080, tls=true} // keyed parameter with object value

blank={} // an empty object is legal

headers={"Content-Type"="application/json", "X-Custom"="yes"} // string keys

service={
  name="api" // object field
  enabled=true // object field
  tags=["web", "v2"] // object field with list value
  endpoint=server(host="localhost", port=8080) // object field with node value
  metadata={owner="ops", tier=1} // object field with nested object value
}

/*
Object bodies use explicit key=value fields only.
*/
```

```mu-err
{debug} // error: E007 - object entries must be `key=value` fields
{"only-a-value"} // error: E007 - positional values are not valid inside objects
{a=1, a=2} // error: E004 - duplicate key `a`
card{a=1} // error: E017 - missing separator between adjacent same-line items — a tag binds only to `(`
```

---

## Whitespace And Separators

Same-line items may be separated by a **comma** or by **whitespace** (one or
more spaces or tabs). Line-separated items omit both - the newline is the
separator.

```mu
server(host="localhost", port=8080, tls=true) // commas are always fine

server(host="localhost" port=8080 tls) // whitespace also separates same-line items

server2(
  host="localhost" // line-separated items need no comma or extra whitespace
  port=8080
  tls=true
)

tags_inline=["web", "api", "v2"] // commas between same-line list values

tags_spaced=["web" "api" "v2"] // whitespace between same-line list values

tags_block=[
  "web" // line-separated list items need no commas
  "api"
  "v2"
]

config_inline={host="localhost", port=8080} // commas between same-line fields

config_spaced={host="localhost" port=8080} // whitespace between same-line fields

config_block={
  host="localhost" // line-separated object fields need no commas
  port=8080
  tls=true
}

server3(
  host="localhost", // trailing commas are accepted (linters flag them)
)

server4(
  host="localhost", // a comma before a newline is legal but non-canonical
  port=8080
)
```

```mu
server(8080 true) // ok - whitespace separates positional values
```

```mu-err
server(8080true) // error: E017 - missing separator between adjacent same-line items
```

```mu-err
server(host="a",, port=1) // error: E005 - consecutive commas create an empty slot
```

```mu-err
server(, host="a") // error: E005 - consecutive commas (a leading comma reads as an empty slot)
```

The key, the `=`, and the **start** of the value share a line. A value that
opens a bracket - `(`, `[`, `{`, `"""`, `'''` - may then span lines.
Whitespace around `=` is allowed (linters note it; the formatter removes it):

```mu
spaced = 1 // legal; canonical form is spaced=1

potato=( // the value STARTS on this line, then the bracket spans lines
  1
)
```

```mu-err
port=
5 // error: E022 - value must start on the same line as `=`
```

Comments may sit between items and around commas, but a block comment may not
separate a key from its `=`, or the `=` from its value:

```mu
// @mu suppress=W026
server(
  host="localhost", // comments around commas and between items are fine
  /* a block comment between items is fine too */
  port=8080
)
```

```mu-err
port=/* gap */8080 // error: E023 - block comment between `=` and its value
```

---

## Imports And Spreads

Markup can pull structured content from other `.mu` files into a scope.
Each file stays a separate document on disk; tools read dependencies when
needed and **insert** the other file's root items - params, values, and child
nodes - at spread sites. Linking merges **parsed structure**, not source text.

There are four surface forms. **Import directives** name a file; **spreads**
reference a prior binding and trigger insertion.

| Form | Role |
|---|---|
| `@@"<path>"` | Import spread - load and insert in one step (**no alias**) |
| `@"<path>"` | Import declaration - register a file for later insertion |
| `@"<path>"<alias>` | Import declaration with an **explicit** alias (alias abuts `"`) |
| `@<alias>` | Spread - insert via a prior `@"` binding |

**Alias attachment** (normative):

| Source text | Parsed as |
|---|---|
| `@"path"` | `import_decl` with implicit alias from path stem (when stem is a valid identifier) |
| `@"path"alias` | `import_decl` with **explicit** alias (`alias` abuts `"`) |
| `@"path" id` | `import_decl` (implicit alias) **then** boolean shorthand `id=true` - whitespace ends the import token |
| `@@"path"` | `import_spread` only |
| `@@"path"id` | `import_spread` + **E034** (abutting alias after `@@` is forbidden) |
| `@@"path" id` | `import_spread` **then** boolean shorthand `id=true` |

Only `@"…"` may bind an explicit alias, and only in the abutting form.
`@@"…"` never registers a name. Retired **E028** - whitespace after `@"path"`
does not error; it begins the next body item ([`linter.md` section 8](linter.md#8-retired-and-redefined-codes)).

Paths are ordinary single- or double-quoted strings (same escape rules as
value strings). Paths are **relative only** and resolve relative to the file
that contains the import directive; `..` segments are allowed. An absolute
path is an error (**E036**).

```mu-err
@@"/etc/base.mu" // error: E036 - absolute import path — import paths are relative only
```

A file may declare that it uses no imports with the `@mu` directive. Any
import directive or spread in such a file is then an error (**E037**).
Parsers running without a file system treat every file as `imports=off`
implicitly.

```mu-err
// @mu imports=off
@@"settings.mu" // error: E037 - imports are disabled for this file (`@mu imports=off`)
```

### Insert in one step

`@@"<path>"` loads the file and inserts its root content at this position.
It always uses that file.

```mu
// settings.mu
env="prod"
port=8080

// main.mu
server(
  @@"settings.mu"
  retries=534
)
```

After linking, `server(…)` contains `env`, `port`, and `retries` in source
order.

Multiple `@@` lines may load **different** files in one body:

```mu
// lib/breakpoints.mu
breakpoints(sm=640, md=900)

// lib/theme.mu
theme(
  dark(palette={ ink="#eee" })
)

// app.mu
@@"lib/breakpoints.mu"
@@"lib/theme.mu"
style(theme="dark")
```

After linking, the root contains items from each dependency in source order.

`@@` does not register a reusable name - there is no `@alias` for that path
afterward.

### Name a file

`@"<path>"` registers an import binding in the current scope. The dependency
file is read when a spread uses that binding, not when the declaration alone
appears.

When the path's basename (directory stripped, `.mu` suffix removed) is a
valid identifier, it becomes the implicit alias:

```mu
@"settings.mu" // implicit alias settings
```

When the stem is not a valid identifier, supply an explicit alias that **abuts**
the closing `"` (no whitespace). Note that a hyphenated stem such as
`my-config` is already a valid identifier, so it needs no alias - only stems
that break the identifier rules do:

```mu
@"2024-report.mu"report
```

```mu-err
@"2024-report.mu" // error: E027 - path stem is not a valid identifier — use an explicit alias, or `@@`
```

When whitespace follows the closing `"`, the next identifier is **not** part of
the import - it is a separate body item (typically boolean shorthand):

```mu
@"settings.mu" settings // import (implicit alias settings) + settings=true
```

The import uses the path stem (`settings`); the spaced `settings` is boolean
shorthand for `settings=true`, not an explicit alias.

A declared import that is never spread anywhere in its scope subtree has no
effect; linters warn (**W031**).

```mu
@"settings.mu" // W031: import is never spread in its scope
```

### Insert by name

After `@"<path>"`, `@<alias>` inserts that file's root content here.

```mu
server(
  @"settings.mu"
  @settings
  retries=534
)
```

Imports and spreads follow the same separator rules as other body items - comma
**or** whitespace on one line, newline alone across lines:

```mu
server(@"settings.mu", retries=534, @settings, debug)
server(@"settings.mu" retries=534 @settings debug)
server(@"settings.mu" settings @settings) // import + shorthand + spread

server(
  @"settings.mu"
  retries=534
  @settings
  debug
)
```

### Where names apply

An import binding is visible in the **declaring body** and **nested** bodies.
**Sibling** bodies do not see it. Aliases occupy their own namespace: an
alias `settings`, a param key `settings`, and a child node tagged `settings`
coexist in one scope without clashing.

```mu-err
server(
  @"settings.mu"
  @settings
  again(
    @settings // ok - same binding, once per again() body
  )
)

debug(
  @settings // error: E026 - undeclared spread — sibling, out of scope
)
```

`@@` leaves no binding for descendants:

```mu-err
wrapper(
  @@"settings.mu"
  inner(
    @settings // error: E026 - undeclared spread — `@@` leaves no binding
  )
)
```

### Awkward filenames

Use an explicit alias whenever the stem is not a valid identifier, or when
you want a different name:

```mu
@"lib/ui-chrome.mu"chrome
@"sd@!!.mu"settings
```

```mu-err
@@"settings.mu"settings // error: E034 - explicit alias after `@@"path"` is not allowed
```

```mu
@@"settings.mu" settings // ok - spread + boolean shorthand settings=true
```

### Multiple spreads

A body may contain any number of `@@` and `@alias` spreads when each targets
a **distinct** dependency:

- `@@`: resolved paths (POSIX-normalized, relative to the declaring file) must
  differ.
- `@alias`: alias identifiers must differ.

```mu
@@"lib/breakpoints.mu"
@@"lib/chrome-shaders.mu"
@@"lib/site-theme.mu"
```

Different child nodes each get their own spread set.

Repeating the **same** spread target in one body is **E033** - regardless of
which form inserts it. An `@alias` spread and an `@@` spread collide when
they resolve to the same file:

```mu-err
server(
  @"settings.mu"
  @settings
  @settings // error: E033 - duplicate spread of the same resolved target — same alias
)
```

```mu-err
server(
  @@"settings.mu"
  @@"settings.mu" // error: E033 - duplicate spread of the same resolved target — same resolved path
)
```

```mu-err
server(
  @"settings.mu"
  @settings
  @@"settings.mu" // error: E033 - duplicate spread of the same resolved target — via a different form
)
```

Equivalent paths resolve to the same target (`./settings.mu` and `settings.mu`
from the same directory → **E033** on the second `@@`).

### Link merge at spread sites

When a spread inserts a dependency file's root items into the linked view,
items accumulate in **source order** across spreads and local body items:

| Item kind | Merge rule |
|---|---|
| **param** (`key=value`) | **Shadow** - later item replaces an earlier value for the same key |
| **raw value** (positional) | **Append** - all values kept; duplicates allowed |
| **child node** | **Append** - all nodes kept; duplicate tags allowed |

Local items authored after spreads follow the same rules (`retries=534` after
`@@` blocks can shadow an imported `retries` param). See
[Data Model](#data-model) for `params` vs `values[]` / `children[]`.

### Allowed places

Import directives and spreads are **body items only**: they may appear in the
file root or in a node body, never in value position - not after `=`, not as
a list element, not as an object field value (**E038**). To use a file's
content as a value, wrap the spread in an untagged node, where it is a body
item again:

```mu
server_config=(@settings) // ok - untagged node value whose body is one spread
handlers=[(@settings)] // ok - node value inside a list
```

```mu-err
retry=@settings // error: E038 - an import or spread cannot be a value — wrap it in an untagged node: `(@alias)`
tags=[@settings] // error: E038 - an import or spread cannot be a list element — wrap it in an untagged node: `[(@alias)]`
config={chrome=@settings} // error: E038 - an import or spread cannot be a value — wrap it in an untagged node: `(@alias)`
```

The diagnostic for **E038** must suggest the `( … )` wrapping.

### Circular imports

When imports form a cycle, linters warn with **W032** on each import directive
in the cycle. Every message includes the full resolved chain:

```text
W032: circular import: main.mu → lib/a.mu → lib/b.mu → main.mu
```

Implementations detect the **back-edge** (importing an ancestor already on the
parse stack), emit **W032** on every participating import, and **do not**
re-traverse that edge - so there is no endless import loop. Parsing and linking
continue; the linked view skips re-inserting content across the blocked
back-edge. See [`linter.md` section 9.7](linter.md#97-circular-imports-w032).

---

## Data Model

Every scope - the file root or a node body - exposes four views. The
terminology is fixed, and a **node is itself a value**: it can sit in a list,
an object field, or on the right of `=`.

| Term | Meaning |
|---|---|
| **raw value** | keyless body item (`"localhost"`, `8080`, `true`) |
| **param** | keyed value (`key=value`), including boolean shorthand |
| **node** | child node item (`db()`, `debug()`) - not boolean shorthand |
| **import declaration** | `@"<path>"` (implicit alias) or `@"<path>"<alias>` (explicit, abutting) - registers a file; `@"<path>" id` is import + boolean shorthand |
| **import-and-insert** | `@@"<path>"` - loads and inserts in one step; never takes an alias |
| **spread** | `@<alias>` - inserts via a prior import declaration |

Canonical storage (lossless):

| Field | Meaning |
|---|---|
| `content[]` | ordered list of all body items; general index = `content[i]` |
| `values[]` | ordered list of raw values only (no params, no nodes) |
| `params` | keyed values as a key-addressable map (`key → value`) |
| `children[]` | ordered list of child nodes (duplicate tags allowed) |

Import directives and spreads live in `content[]` in source order. After
linking, inserted items from dependency files appear at spread sites in the
linked view; per-file source ASTs keep the original import and spread tokens.

```mu
server(
  db()
  "localhost"
  true
  cors(debug=true)
  easterEgg=true
  1
)
```

```text
content[0] = node, tag db
content[1] = raw "localhost"      → values[0]
content[2] = raw true             → values[1]
content[3] = node, tag cors
content[4] = param easterEgg=true → params["easterEgg"]
content[5] = raw 1                → values[2]
```

The four kinds are distinct, so names never clash across them: a param is a
key/value pair, a node is not - a key `debug` and a child node tagged `debug`
coexist without ambiguity.

```mu
status(debug=true, debug()) // no clash: params["debug"] and one child tagged debug
```

**Choosing between an object and a node:** use `{}` when the thing is pure
data - named fields only. Use a node when it has identity (a tag), order,
positional values, or children.

There is no mandated nesting-depth limit. Implementations may impose one, but
exceeding it must produce a clean error, never a crash or silent truncation.

---

## Complete Example

This example combines the syntax from the previous sections.

```mu
// @mu version=0.1

/*
Kitchen-sink file: comments, every value type, scopes, separators,
and layout styles from the sections above.
*/

app="demo"
environment="dev"
debug

tagline='Say "hello"'
note="It's fine"
escaped="Say \"hello\""
path="C:\\Users\\alice\\doc.txt"
empty=""
''
cols="a\tb"
utf8="Sao Paulo"
snowman="\u{2603}"
apostrophe='It\'s fine'

+99
0xDEADBEEF
0o755
0b11010110
3.1415
-2E-2
6.626e-34
inf
+inf
-inf
nan
+nan
-nan

verbose=false
missing=null
true
false
null

name="Markup"
pool_size=10
pool-size=8080 // I004: identifier differs from the dominant snake casing
_internal=0
True=1

"Content-Type"="text/plain"
'X-Trace'="abc123"
"true"=1

tags=["web", "api", "v2"]
matrix=[[1, 2], [3, 4]]
settings={host="localhost", port=8080, tls=true}
headers={"Content-Type"="application/json", "X-Custom"="yes"}

()
(42)
"wow"

'''
#!/bin/bash
echo "hello"
'''

prologue="""
  Alpha
  Beta
  """

// W001: `"""` anchor at column 0 — use raw `'''` mode instead
spec_sample="""mu
motd="""
  Hello
  """
"""mu

server(
  "localhost" // positional host
  8080 // positional port
  tls

  routes=[
    route(path="/", handler=page(title="Home"))
    route(path="/docs", handler=page(title="Docs"))
  ]

  config={
    retries=3
    timeout=+1.5
    limit=inf
  }

  /*
  Empty tagged child node.
  Formatter preserves this as health_check().
  */
  health_check()

  banner="""
    Welcome to the demo.
    Everything in this file is acceptable syntax.
    """

  host="127.0.0.1", port=9090, mode="admin"

  tags_block=[
    "web"
    "api"
    "v2"
  ]

  cards=[
    card(title="Slots")
    card(title="Live")
    ()
  ]

  service={
    name="api"
    enabled=true
    tags=["web", "v2"]
    "Accept-Language"="en-US"
    endpoint=server(host="localhost", port=8080)
    metadata={owner="ops", tier=1}
  }

  debug()
  debug() // duplicate child tags are legal - children form an ordered list

  connect("localhost", 8080, true)

  job(
    "ingest"
    retries=3
    dry_run
    notify(email="ops@example.com")
  )

  deploy_script='''
set -e
./run.sh
'''

  snippet(
    lang="sql"
    code="""
      SELECT *
      FROM users
      WHERE id = 42
      """
  )

  pipeline(
    "ingest"
    debug(verbose=true)
    3
  )

  potato(what={potato="green", salad="blue"}, {fork="blue"})

  limits(rate=100, burst=200, window="1m",) // W007: trailing comma
)

limits_inline={rate=100, burst=200}

/*
Lint showcase: everything below is valid syntax, kept intentionally
non-canonical so a linter (and the studio's Lint tab) has warnings and
info to display. TODO the marker in this comment is itself I002.
*/

spaced_out = 1 // I003: whitespace around "="
regrouped=1_23_456 // W008: discouraged numeric grouping
huge=9_007_199_254_740_993 // W009: exceeds safe integer range (±2^53) — doubles lose precision
dense(a=1, b=2, c=3, d=4) // W023: same-line group with 4 or more key=value pairs — consider multiline layout
packed=[1, 2, 3, 4, 5] // W024: same-line list with 4 or more separators — consider multiline layout
first=1, // W026: comma before a newline-separated item — omit commas in multiline layout
second=2
flagged () // W027: boolean shorthand followed by an untagged node — write "flagged()" if a tagged node was intended

tagged="""x
plain text
"""x // W028: multiline delimiter tag is unnecessary for this content

lazy(
) // W011: empty parentheses spread across lines — prefer the same-line form

// I005: a single-param node spread across lines could be inline
lonely(
  retries=3
)
```

---

## Appendix A - Grammar

Compact EBNF for the whole language. On conflict with prose or examples, the
grammar is normative. Lexical rules that EBNF cannot express cleanly - anchor
math, dedent, same-line separator rules, and tag-adjacency - carry comments.

```ebnf
file        = lead , [ item , { sep , item } , [ "," ] , lead ] , EOF ;
body        = lead , [ item , { sep , item } , [ "," ] , lead ] ;
lead        = { NEWLINE } ;
sep         = "," , lead             (* comma follows its item on the same line *)
            | whitespace , { whitespace } , lead
                                     (* one or more spaces/tabs on the same line;
                                        zero separation between items is E017 *)
            | NEWLINE , lead ;       (* a newline alone separates *)

item        = param | node | value | import_decl | import_spread | spread_ref ;
              (* source order is preserved *)
import_spread = "@@" , ( '"' , { sl-char | escape } , '"'
                       | "'" , { sl-char | escape } , "'" ) ;
              (* no abutting identifier - E034 if present *)
import_decl = "@" , ( '"' , { sl-char | escape } , '"'
                    | "'" , { sl-char | escape } , "'" )
            , [ identifier ] ;   (* abutting identifier only = explicit alias;
                                    omitted → implicit alias from path stem when
                                    valid; whitespace after closing quote → next item *)
spread_ref  = "@" , identifier ;   (* must not begin "@@" or '@"' / "@'" *)
param       = key , "=" , value      (* key, "=", and the value's FIRST token share a line;
                                        no block comment may touch the "=" on either side *)
            | identifier ;           (* boolean shorthand: identifier ≡ identifier=true *)
node        = [ tag ] , "(" , body , ")" ;   (* a tag abuts "(" - zero whitespace *)
value       = string | number | boolean | "null" | list | object | node ;
list        = "[" , lead , [ value , { sep , value } , [ "," ] , lead ] , "]" ;
object      = "{" , lead , [ field , { sep , field } , [ "," ] , lead ] , "}" ;
field       = key , "=" , value ;    (* duplicate keys in one scope or object: error *)
boolean     = "true" | "false" ;

key         = identifier | sl-string ;   (* sl-string key: non-empty after decoding;
                                             decoded text must be unique per scope
                                             regardless of quoting *)
tag         = identifier ;
identifier  = ident-start , [ { ident-mid } , ident-end ] ;
              (* ASCII only; must not equal a reserved word *)
ident-start = letter | "_" ;
ident-mid   = letter | digit | "_" | "-" ;
ident-end   = letter | digit | "_" ;
reserved    = "true" | "false" | "null" | "inf" | "nan" ;

string      = sl-string | ml-string ;
sl-string   = '"' , { sl-char | escape } , '"'
            | "'" , { sl-char | escape } , "'" ;
              (* sl-char: any char except the delimiter, backslash, newline *)
escape      = "\" , ( '"' | "'" | "\" | "n" | "t" | "r" )
            | "\u{" , hex , [hex] , [hex] , [hex] , [hex] , [hex] , "}" ;
              (* must be a Unicode scalar value *)
ml-string   = ml-open , NEWLINE , { content-line , NEWLINE } , indent , ml-close ;
ml-open     = delim , [ opening-tail ] ;          (* nothing else on the line *)
opening-tail = { whitespace } , [ same-line-block-comment , { whitespace } ] , [ line-comment ] ;
same-line-block-comment = "/*" , { any-char-except-NEWLINE } , "*/" ;
ml-close    = delim , [ "," ] , [ line-comment ] ; (* nothing else on the line *)
delim       = ( '"""' | "'''" ) , [ identifier ] ;
              (* exactly three quotes; closer matches the opener char-for-char;
                 a quote run of four or more is an error.
                 For """ the anchor = min(opener offset, closer offset), where
                 offset = characters left of the first quote on that line;
                 each non-blank content line drops anchor leading whitespace
                 chars and starting left of the anchor is an error; blank
                 lines are exempt and contribute one empty line. ''' is
                 verbatim. Neither form processes escapes. *)

number      = [ "+" | "-" ] , ( "inf" | "nan" | dec-int | float )
            | radix-int ;            (* radix literals are unsigned - TOML rule *)
radix-int   = "0x" , hex-digit , { [ "_" ] , hex-digit }
            | "0o" , oct-digit , { [ "_" ] , oct-digit }
            | "0b" , bin-digit , { [ "_" ] , bin-digit } ;
dec-int     = "0" | nonzero , { [ "_" ] , digit } ;  (* no leading zeros *)
float       = dec-int , frac , [ exp ]
            | dec-int , exp ;
frac        = "." , digit , { [ "_" ] , digit } ;
exp         = ( "e" | "E" ) , [ "+" | "-" ] , digit , { [ "_" ] , digit } ;

comment     = line-comment | block-comment ;
line-comment  = "//" , { any-char-except-NEWLINE } ;
block-comment = "/*" , { any-char } , "*/" ;       (* does not nest *)
```
