======================================================================== # Library guide (@mu-lang/parser) # Source: https://mu.milthe.dev/library.md ======================================================================== # Library guide — `@mu-lang/parser` Complete guide to the TypeScript toolkit for Markup (`.mu`): install, parse, format, lint, typed schemas, multi-file imports, and the CLI. This page is for **application authors** who consume Markup as data. For the language itself, see the [Specification](/) tab. > **⚠ Experimental (v0.x).** The Markup language and this library are still > evolving. Expect breaking changes to the API, the value model, and the `.mu` > syntax as the project matures toward 1.0. Pin an exact version > (`@mu-lang/parser@0.1.0`) if you depend on it in production. --- ## What you get One npm package covers the whole toolchain: | Capability | Entry points | | --- | --- | | Parse → document | `parse`, `parseSafe`, `parseSource` | | Typed structure | `mu`, `parseAs`, `loadAs`, `Infer` | | Format | `format` | | Lint | `lint`, `lintProjectFile` | | Multi-file imports | `load`, `openProject`, `linked()` | | Serialize | `stringify`, `stringifyNode` | | CLI | `mu fmt`, `mu lint`, `mu parse` | Package: **`@mu-lang/parser`** (Node 18+, ESM + CJS). License: MIT OR Apache-2.0. --- ## Install ```bash npm install @mu-lang/parser ``` Or with pnpm / yarn / bun: ```bash pnpm add @mu-lang/parser yarn add @mu-lang/parser bun add @mu-lang/parser ``` After install, the `mu` binary is available via `npx mu` or your package manager’s bin path. ### TypeScript The package ships its own types. No extra `@types` package is required. ```json // tsconfig.json — "moduleResolution": "bundler" or "node16" works { "compilerOptions": { "module": "ESNext", "moduleResolution": "bundler", "strict": true } } ``` ### Browser vs Node | Environment | Notes | | --- | --- | | Node | Full surface: `load` / `loadAs` with `fs`, CLI | | Browser / bundler | `parse`, `parseAs`, `format`, `lint`, `stringify` — inject your own `readFile` for multi-file | | Edge / Workers | Same as browser; avoid sync FS APIs | --- ## Quick start ```typescript import { parse, format, lint, isMuNode, isMuParamEntry, } from '@mu-lang/parser'; const src = ` title="Ops Console" server(host="localhost", port=8080, tls) `; const doc = parse(src); // Channel views console.log(doc.params.title); // "Ops Console" console.log(doc.nodes[0]?.tag); // "server" console.log(doc.nodes[0]?.params.port); // 8080 // Full body order (same as doc.items) — params + positionals + children console.log(doc.length); // 2 console.log(isMuParamEntry(doc[0])); // true → { kind: 'param', key: 'title', value: 'Ops Console' } console.log(isMuNode(doc[1])); // true → the server(...) node console.log(doc[1]?.params?.port); // 8080 (if you narrow as MuNode) // Walk any node the same way const server = doc.nodes[0]!; console.log(server[0]); // { kind: 'param', key: 'host', value: 'localhost' } console.log(server[1]); // { kind: 'param', key: 'port', value: 8080 } console.log(server[2]); // { kind: 'param', key: 'tls', value: true } console.log(format('a=1,b=2')); // "a=1, b=2\n" console.log(lint(src)); // [] when clean ``` That’s enough for many apps: **parse**, read **params** / **nodes** or walk **`doc[i]`**, optionally **format** or **lint**. See [Walking body order](#walking-body-order-nodei) for the full rules. --- ## Mental model A Markup **scope** (the file root or any node body) is not a single JSON object. It has **three channel views** plus **full-order walk**: | Access | What | | --- | --- | | `node.params` | Dict of named params (`key=value`, bare `tls` → `true`) — plain JS | | `node.values` | List of **keyless positionals only** — **raw** plain JS (`"hi"`, `42`, …) | | `node.nodes` | List of **child nodes only** — `MuNode` objects | | `node[i]` / `node.items` | **Everything in source order** (params + positionals + children) | ```text params → { host: "x", tls: true } // by name values → [true, "world"] // positionals only, raw nodes → [optsNode] // children only node[i] → walk body in the order written ``` The **file root** is an untagged node (`tag === ''`). Top-level lines *are* that body — there is no outer `root(…)`. ```mu title="Ops" // root param 42 // root positional server(port=8080) // root child node ``` ### Value encoding - **`params`** and **`values`** are **plain JS** (already unwrapped). Nested Markup nodes still appear as `MuNode`. - Proxy sugar: non-reserved param names also appear as `node.port` (same as `node.params.port`). Reserved: `tag`, `params`, `nodes`, `values`, `items`, `kind`, `isEmpty`, `toJSON`, `length`. ### Nodes vs objects | Form | Use when | | --- | --- | | `{ host="x", port=1 }` | Pure named data (maps cleanly to JSON) | | `server(host="x", port=1)` | Identity (tag), children, positionals, or dialect “elements” | ### Walking body order (`node[i]`) There is **no** `node[0]` syntax inside `.mu` source. After `parse`, every body item has an index in **source order**: | Body item | `node[i]` returns | | --- | --- | | Param (`host="x"`, bare `a`) | `{ kind: 'param', key, value }` — `value` is plain JS | | Positional (`true`, `"hi"`, `42`) | **Raw** plain JS (same as that slot in `values`) | | Child node (`server(…)`) | **`MuNode`** (same object family as `nodes`) | Helpers: `isMuParamEntry`, `isMuNode`, `atParam(node, i)`, `atNode(node, i)`. ```typescript import { parse, isMuNode, isMuParamEntry, atParam, atNode } from '@mu-lang/parser'; const n = parse(` test( a "test" server(port=1) host="x" ) `).nodes[0]!; n.params; // { a: true, host: "x" } n.values; // ["test"] n.nodes[0]?.tag; // "server" n[0]; // { kind: 'param', key: 'a', value: true } n[1]; // "test" n[2]; // MuNode tag "server" n[3]; // { kind: 'param', key: 'host', value: 'x' } n.length; // 4 isMuParamEntry(n[0]); // true atParam(n, 0)?.key; // "a" isMuNode(n[2]); // true atNode(n, 2)?.tag; // "server" // Function-call style: positionals + child, then named kwargs const call = parse(`greet(true, "world", opts(loud))`).nodes[0]!; call[0]; // true call[1]; // "world" call[2]; // MuNode "opts" call.values; // [true, "world"] ``` --- ## Parsing ### `parse` — throw on errors ```typescript import { parse, MarkupParseError } from '@mu-lang/parser'; try { const doc = parse('server(host="x", port=8080)'); // Document = MuNode (root) } catch (e) { if (e instanceof MarkupParseError) { console.error(e.message, e.diagnostics); } } ``` ### `parseSafe` — never throws ```typescript import { parseSafe } from '@mu-lang/parser'; const { ok, document, diagnostics } = parseSafe(source); if (!ok || !document) { // show diagnostics return; } // use document ``` ### `parseSource` — document + AST When you need spans or import metadata: ```typescript import { parseSource } from '@mu-lang/parser'; const { document, ast, diagnostics } = parseSource(source); // ast is the span-rich Root used by tooling ``` ### Navigating a document ```typescript import { parse, childrenByTag, childByTag, getParam, isMuNode, isMuParamEntry, } from '@mu-lang/parser'; const doc = parse(` title="App" server(host="a", port=1) server(host="b", port=2) database(engine="postgres") `); getParam(doc, 'title'); // "App" childrenByTag(doc, 'server'); // two nodes childByTag(doc, 'database'); // one node (throws if multiple by default) // Full body order (params + positionals + children) — same as doc.items / doc[i] for (const item of doc) { if (isMuParamEntry(item)) console.log('param', item.key, item.value); else if (isMuNode(item)) console.log('node', item.tag); else console.log('value', item); // raw positional } ``` ### JSON projection ```typescript const doc = parse('a=1\nb="x"'); console.log(JSON.stringify(doc, null, 2)); // uses toJSON() // CLI: mu parse file.mu ``` --- ## Format and lint ### Format ```typescript import { format, MarkupError } from '@mu-lang/parser'; // Throws MarkupError if the input has E-level syntax errors const out = format(source, { width: 80, indent: 2, // pack, blanks, … also via // @mu directives in the file }); ``` File directives (comments) configure tools without code: ```mu // @mu version=0.1, width=100, indent=2, suppress=W007 ``` ### Lint ```typescript import { lint } from '@mu-lang/parser'; const diags = lint(source); // { code, severity, message, line, col, endLine?, endCol? }[] for (const d of diags) { console.log(`${d.severity} ${d.code}: ${d.message} @${d.line}:${d.col}`); } ``` Project-aware lint (imports): ```typescript import { lintProjectFile } from '@mu-lang/parser'; import { readFileSync } from 'node:fs'; const diags = await lintProjectFile('app.mu', (p) => readFileSync(p, 'utf8')); ``` --- ## Typed structure with schemas When you accept a **specific shape** (config, UI dialect, scene file), declare it in TypeScript. Runtime validation + `Infer<>` types. ### Minimal config ```typescript import { mu, parseAs, type Infer } from '@mu-lang/parser'; const Config = mu.document({ params: { title: mu.string(), enabled: mu.boolean().default(false), }, children: { server: mu .node('server', { params: { host: mu.string(), port: mu.int(), tls: mu.boolean().default(false), }, }) .one(), }, values: mu.none(), // reject stray root positionals }); type Config = Infer; const config = parseAs( ` title="Ops" server(host="localhost", port=8080, tls) `, Config, ); config.title; // string config.server.port; // number config.enabled; // boolean (default applied) ``` ### Ordered body (`items` / `node[i]`) When a node is used like a **function call**, declare the full source-order body with `items`. Each slot is validated against that index of `node.items`: | Slot schema | Matches | | --- | --- | | `mu.string()` / `mu.int()` / … | Raw positional | | `mu.node('tag', { … })` | Child node with that tag | | `mu.param('key', schema)` | Param entry `{ key, value }` at that index | | `mu.param(schema)` | Any param key; value must match | ```typescript const Opts = mu.node('opts', { params: { loud: mu.boolean().default(false) }, }); const Greet = mu.node('greet', { items: [mu.boolean(), mu.string(), Opts] as const, }); // greet(true, "world", opts(loud)) // → { $items: [true, "world", { loud: true }] } ``` Mixed param + positional + child (same as Document `node[i]`): ```typescript const Test = mu.node('test', { items: [ mu.param('a', mu.boolean()), mu.string(), mu.node('server', { params: { port: mu.int() } }), mu.param('host', mu.string()), ] as const, }); // test(a, "test", server(port=1), host="x") // → $items: [{key:'a', value:true}, "test", {port:1}, {key:'host', value:'x'}] ``` You can still declare `params` / `children` / `values` **alongside** `items` (named lookup + ordered walk). With `items` only, channel strictness defaults off so undeclared params/children do not fail unless you set `strict*`. Projection field: **`$items`** (tuple-typed via `Infer`). Tree mode (`.asTree()`) also exposes `items` on the structured result. ### Scalar and container builders | Builder | Accepts | | --- | --- | | `mu.string()` | string (`.minLength` / `.maxLength`) | | `mu.number()` | number; Markup `inf`/`nan` allowed (`.finite()` to reject) | | `mu.int()` | integer | | `mu.float()` | number | | `mu.boolean()` | boolean | | `mu.null()` | null | | `mu.literal(x)` | exact value | | `mu.enums(['a','b'] as const)` | string union | | `mu.param(key, schema)` | Body-order param slot (for `items`) | | `mu.any()` | anything | | `mu.object({ … })` | plain object / Markup `{}` (strict keys by default; `.passthrough()`) | | `mu.list(item)` | array | | `mu.tuple([a, b])` | fixed-length list | | `mu.union([a, b])` | first match | | `mu.record(value)` | open string keys | | `mu.none()` | empty positionals list | Value schemas chain **`.optional()`** and **`.default(v)`**. ### Node and document ```typescript const Route = mu.node('route', { params: { path: mu.string(), method: mu.enums(['GET', 'POST'] as const).default('GET') }, }); const App = mu.document({ params: { name: mu.string() }, children: { route: Route.many(), // zero or more // also: .one() | .optional() }, values: mu.none(), // strictParams / strictChildren / strictValues default true }); ``` **Child policies** (on a node schema): - `.one()` — exactly one child with that tag - `.many()` — zero or more → array - `.optional()` — zero or one Bare `mu.node('x', …)` as a child entry means `.one()`. **Projection:** params and children flatten into one object (`config.server.host`). Positionals validated with a non-`none` values schema appear as **`$values`**. **Tree mode** (UI-style): ```typescript const Card = mu .node('card', { params: { title: mu.string().optional() }, strictValues: false, }) .asTree(); // → { tag, params, children, values } ``` ### Errors ```typescript import { parseAs, parseAsSafe, MarkupParseError, MarkupSchemaError } from '@mu-lang/parser'; try { parseAs(source, Config); } catch (e) { if (e instanceof MarkupParseError) { // language / syntax (E###) } else if (e instanceof MarkupSchemaError) { for (const issue of e.issues) { console.log(issue.code, issue.message, issue.path); } } } const r = parseAsSafe(source, Config); if (!r.ok) { // r.diagnostics — language // r.issues — schema } ``` ### Imports and `parseAs` `parseAs` is **single-file only**. If the source uses `@` / `@@` imports, it throws (or safe-fails) and tells you to use **`loadAs`**. Nested imports inside nodes are detected too. --- ## Loading files and multi-file projects ### Simple load (linked document) ```typescript import { readFileSync } from 'node:fs'; import { loadSync, loadAsSync } from '@mu-lang/parser'; const readFile = (path: string) => readFileSync(path, 'utf8'); const doc = loadSync('config.mu', { readFile }); // imports expanded; doc is the linked MuNode const config = loadAsSync('config.mu', Config, { readFile }); // linked + schema-projected ``` Async variants: `load`, `loadAs`, `loadAsSafe` (and `loadAsSafeSync`). Always inject **`readFile`** — the library never hard-codes `fs`, so the same API works in tests with a `Map` of paths. ### Lower-level project API ```typescript import { openProjectAsync, waitForReady } from '@mu-lang/parser'; const project = await openProjectAsync({ entryPath: 'app.mu', readFile: async (p) => /* … */, }); await waitForReady(project); project.linked(); // Document | null project.allDiagnostics(); project.files; // per-file parse results ``` Sync: `openProjectSync` when `readFile` is synchronous. ### Import merge rules (reminder) | Channel | On spread | | --- | --- | | params | later shadows earlier | | positionals | append | | children | append (duplicate tags fine) | Schema validation should run on the **linked** document. --- ## Serializing data back to Markup ```typescript import { stringify, stringifyNode, parseAs } from '@mu-lang/parser'; const data = { title: 'Ops', settings: { host: 'local', tls: false }, server: { $tag: 'server', host: 'localhost', port: 8080 }, }; const source = stringify(data); // title="Ops" // settings={host="local", tls=false} // server(host="localhost", port=8080) // Multi-line nodes: stringify(data, { indent: 2 }); // Explicit node helper: stringifyNode('card', { values: ['Hello'], params: { theme: 'dark' }, children: [{ $tag: 'label', text: 'x' }], }); ``` **Not** a lossless round-trip of comments or exact layout — good for codegen, tests, and config emission. Round-trip through `parseAs` for schema-shaped data. --- ## CLI ```bash npx mu fmt file.mu # print formatted source npx mu lint file.mu # diagnostics; exit 1 on errors npx mu parse file.mu # JSON projection of the document ``` `mu fmt` options: `--width `, `--indent `, `--pack`, `--collapse-blanks`. Omit the file (or pass `-`) to read stdin for `fmt` / `parse`. --- ## Recipes ### 1. Config file in a Node app ```typescript import { readFileSync } from 'node:fs'; import { mu, loadAsSync } from '@mu-lang/parser'; const Schema = mu.document({ params: { databaseUrl: mu.string(), port: mu.int().default(3000), }, values: mu.none(), }); export function loadConfig(path = 'app.mu') { return loadAsSync(path, Schema, { readFile: (p) => readFileSync(p, 'utf8'), }); } ``` ### 2. Editor diagnostics (LSP-style) ```typescript import { lint, spanToLspRange, analyze } from '@mu-lang/parser'; // Simple: line/col list lint(source); // Spans for precise ranges: const diags = analyze(source); for (const d of diags) { const range = spanToLspRange(d.span); // map to your editor API } ``` ### 3. Format-on-save pipeline ```typescript import { format, MarkupError } from '@mu-lang/parser'; export function formatOnSave(source: string): string { try { return format(source); } catch (e) { if (e instanceof MarkupError) return source; // refuse broken input throw e; } } ``` ### 4. Node-as-param (handlers, plugins) ```typescript const Plugin = mu.node('plugin', { params: { name: mu.string(), enabled: mu.boolean().default(true) }, }); const App = mu.document({ params: { handler: Plugin, // param value is a node }, values: mu.none(), }); parseAs('handler=plugin(name="auth")', App); // → { handler: { name: 'auth', enabled: true } } ``` ### 5. Browser: parse without FS ```typescript import { parseAs, mu } from '@mu-lang/parser'; const Schema = mu.document({ params: { title: mu.string() }, values: mu.none(), }); export function fromTextarea(value: string) { return parseAs(value, Schema); } ``` ### 6. Unit tests with virtual files ```typescript import { loadAsSync, mu } from '@mu-lang/parser'; const files = new Map([ ['/app.mu', 'title="T"\n@@"./settings.mu"\n'], ['/settings.mu', 'env="test"\n'], ]); const Schema = mu.document({ params: { title: mu.string(), env: mu.string() }, values: mu.none(), }); const cfg = loadAsSync('/app.mu', Schema, { readFile: (p) => { const s = files.get(p); if (s === undefined) throw new Error(p); return s; }, }); ``` --- ## API map (cheat sheet) | Export | Role | | --- | --- | | `parse` / `parseSafe` / `parseSource` / `parseBytes` | Document parse | | `parseAst` | Span-rich AST (tooling) | | `mu` / `parseAs` / `parseAsSafe` / `projectWithSchema` | Schema dialect | | `Infer` / `MarkupSchemaError` | Types and schema errors | | `load` / `loadAs` / `loadAsSafe` (+ Sync) | Files + imports | | `openProject` / `waitForReady` / `linked` | Project graph | | `format` / `lint` / `analyze` | Style tools | | `stringify` / `stringifyNode` | Emit `.mu` | | `childrenByTag` / `getParam` / `atNode` / `atParam` | Navigation | | `isMuNode` / `isMuParamEntry` | Discriminate body items | | `node.params` / `node.values` / `node.nodes` | Channel views (dict / raw list / nodes) | | `node[i]` / `node.items` / `node.length` | Full source-order walk (params + values + nodes) | | `lex` / `DiagCode` / `Severity` | Low-level | --- ## Limits and design notes 1. **Schema is a host dialect**, not Markup syntax — different apps define different catalogs. 2. **No Zod dependency** — optional adapters can wrap projected plain objects later. 3. **Integer vs float** in the AST can collapse to JS `number` after unwrap; use care for `42` vs `42.0` if it matters. 4. **`parseAs` vs `loadAs`** — imports always need linking first. 5. **Strict by default** — unknown params/children fail unless you set `strictParams: false` / `strictChildren: false`. --- ## Next steps - Language rules: [Specification](/) (`spec/syntax.md`) - Try examples: [Playground](/playground) - See live dialects: [Demos](/demos) - Package README: `packages/typescript/README.md` in the monorepo ======================================================================== # Markup syntax specification # Source: https://mu.milthe.dev/syntax.md ======================================================================== # 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 ``` ```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 | |---|---| | `@@""` | Import spread - load and insert in one step (**no alias**) | | `@""` | Import declaration - register a file for later insertion | | `@""` | Import declaration with an **explicit** alias (alias abuts `"`) | | `@` | 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 `@@""` 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 `@""` 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 `@""`, `@` 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** | `@""` (implicit alias) or `@""` (explicit, abutting) - registers a file; `@"" id` is import + boolean shorthand | | **import-and-insert** | `@@""` - loads and inserts in one step; never takes an alias | | **spread** | `@` - 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 *) ```