Wire Style

Canonical formatting and layout conventions for Wire source files, examples, and documentation snippets.


On this page
  1. Semicolons
  2. Graph Expressions
  3. Indentation
  4. Nodes
  5. Comments And Strings
  6. Formatter Direction
  7. Validation

Wire Style

This page defines the canonical style for authored Wire source. The grammar reference defines what parses; this page defines what Cortex examples, docs snippets, fixtures, and future formatters should produce.

Semicolons

Semicolons are terminators or separators. They are not operator tokens.

Use no whitespace before a semicolon:

prompt = "Planning mode (high/safe): ";

When another token follows on the same line, use one space after the semicolon:

{ prompt = "Planning mode (high/safe): "; newline = true; }

Prefer multiline records once a record has enough fields that the single-line form stops scanning cleanly:

{
  prompt = "Planning mode (high/safe): ";
  newline = true;
}

Use Nix-style inherit when a record field takes its value from a same-named variable:

{ inherit summary; }

Group related inherited fields when the compact form still reads cleanly:

{ inherit compileSpec testSpec docsSpec lintSpec; }

Prefer this over:

{ summary = summary; }

The file-return graph expression has no trailing semicolon:

read_target
  => plan_build
  => print_report

Graph Expressions

Format graph expressions by causal stage. => advances causal time, so it starts a new continuation line:

read_target
  => plan_build
  => summarize_build
  => print_report

<> expands the current frontier. Keep small frontiers grouped horizontally:

plan_build
  => (compile_check <> test_check <> docs_check <> lint_check)
  => summarize_build

Break large frontiers vertically with leading <> so each line reads as another member of the same frontier:

plan_build
  => (
    compile_check
    <> test_check
    <> docs_check
    <> lint_check
    <> audit_check
    <> package_check
  )
  => summarize_build

Prefer named frontiers once the topology stops scanning cleanly:

let build_checks =
  compile_check
    <> test_check
    <> docs_check
    <> lint_check;

read_target
  => plan_build
  => build_checks
  => summarize_build
  => print_report

The rule is semantic: formatting follows topology. => moves forward in causal time; <> expands the current frontier.

Indentation

Use two spaces inside node declarations, where let blocks, records, and lists:

node summarize
  <- input: BuildReport;
  -> summary: Text = summary;
  where let
    ok = input.ok;
    summary = if ok then "ok" else "failed";
  in
  { inherit summary; };

For lists of records, put each record on its own line:

tasks = [
  { name = "write ADRs"; score = 0.95; hours = 1; },
  { name = "implement stdin/stdout executors"; score = 0.88; hours = 3; },
];

Nodes

Put each port declaration or output equation on its own line. The arrow should be the first non-whitespace token on that line:

node classify
  <- evidence: EvidenceSet;
  -> accepted: AcceptedSet = accepted;

Do not place -> or <- on the node line, and do not combine multiple ports on one line.

Keep executor authority visible at the implementation boundary:

use std.io.{@stdin};

node read_mode
  -> answer: UserInput = @stdin { prompt = "Planning mode (high/safe): "; } (null);

Write CorePure output expressions directly:

node classify
  <- evidence: EvidenceSet;
  -> accepted: AcceptedSet = evidence.items |> filter (item: item.score >= 0.7);

Do not write pure (...) or @pure; CorePure output expressions are direct and are not executor authority.

Comments And Strings

# starts a line comment outside strings:

# Comment

Inside a quoted string, # is ordinary string content and does not need escaping:

argv = ["nix", "run", ".#cortex-tests", "--", "-m", "Cortex.Wire.Parser"];

This matches the production parser. Tree-sitter highlighting should preserve the whole quoted value as a string.

Formatter Direction

Wire formatting is exposed through the wire command:

wire fmt FILE
wire fmt --check FILE
wire fmt --stdout FILE

The formatter is topology-first for graph expressions. It parses Wire source, chooses whether the file can safely flow through the lowered production AST, renders graph composition by causal structure when it can, reparses the output, and rejects rewrites that would not round-trip through the production AST.

When a declaration-bearing Wire file compiles far enough to expose a cyclic topology, wire fmt refuses to format it. Fix the graph first; formatting is only defined for source that can denote an acyclic Wire topology.

The formatter starts with stable layout rules:

  • no whitespace before semicolon terminators;
  • one space after same-line semicolons;
  • inherit name; for same-name record fields instead of name = name;;
  • compact grouped inherit a b c; when related inherited fields fit cleanly on one line;
  • one port arrow per line, with <- or -> as the first non-whitespace token;
  • => starts a causal-stage continuation line;
  • <> groups same-frontier members horizontally when small and vertically with leading <> when large;
  • two-space indentation for node bodies, records, lists, and where let;
  • stable record/list layout;
  • explicit wrapping for mixed graph topology expressions;
  • conservative comment preservation.

It is not a regex-only pass. Regex cleanup can help with one-time migrations, but the canonical formatter operates over parsed Wire syntax and must be idempotent before it is added to treefmt.

The parser desugars several surface forms during parsing: kind and form declarations expand into nodes and lets, source include calls expand before parsing, CorePure ${...} string interpolation lowers to concat/toString calls, and comment trivia is dropped. Round-tripping through the AST cannot recover any of those, so wire fmt preserves files that contain them unchanged rather than silently rewriting source into the lowered shape. Those files should continue to rely on just wire-style-check and manual edits for now. A future concrete-syntax formatter can attach comments/trivia and format these forms with full surface fidelity.

Because wire fmt validates include-bearing files through source elaboration, missing include targets still make formatting fail even when the authored include spelling is preserved unchanged.

Validation

For source files, validate both the production parser and the editor grammar when changing style:

nix run .#wire -- build examples/wire/mini-build-system.wire
just wire-style-check
tree-sitter parse examples/wire/mini-build-system.wire
tree-sitter highlight --scope source.wire examples/wire/mini-build-system.wire