Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
conformance/invalid/byte-order-mark/source.stack binary
conformance/invalid/invalid-utf8/source.stack binary
conformance/formatter/comments-and-layout/input.stack binary
11 changes: 10 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,24 @@ jobs:
test -f "$case/source.stack"
test -f "$case/expected.diagnostics.json"
done
for case in conformance/formatter/*; do
test -d "$case"
test -f "$case/input.stack"
test -f "$case/expected.stack"
test -f "$case/expected.ir.json"
done

- name: Validate JSON Schemas
run: check-jsonschema --check-metaschema schemas/*.json

- name: Validate normalized IR fixtures
run: check-jsonschema --schemafile schemas/normalized-ir.schema.json conformance/valid/*/expected.ir.json
run: check-jsonschema --schemafile schemas/normalized-ir.schema.json conformance/valid/*/expected.ir.json conformance/formatter/*/expected.ir.json

- name: Validate diagnostic fixtures
run: find conformance -name expected.diagnostics.json -print0 | xargs -0 check-jsonschema --schemafile schemas/diagnostic-expectations.schema.json

- name: Validate compiler diagnostic coverage
run: python scripts/validate-compiler-diagnostics.py

- name: Validate formatter fixtures
run: python scripts/validate-formatter-fixtures.py
138 changes: 138 additions & 0 deletions FORMATTER.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Stack Canonical Formatter Specification

## 1. Status and Scope

This document is a normative part of the draft Stack 1.0 specification. It defines the one canonical byte representation of a compiler-valid `.stack` document.

A canonical formatter accepts UTF-8 Stack source without a byte order mark. It MUST reject a document that produces a compiler-stage error and MUST NOT present partial rewritten source as a successful result. Warnings do not prevent formatting.

Formatting MUST preserve the document's normalized meaning and every line comment. Formatting never changes the declared language version.

## 2. Canonical Output

Canonical output MUST:

- be UTF-8 without a byte order mark;
- use LF (`U+000A`) for every line ending, regardless of the input line endings;
- use ASCII spaces, never tabs, for formatting whitespace;
- contain no formatting whitespace at the end of a line;
- end with exactly one LF.

Whitespace inside a line comment is comment text rather than formatting whitespace and is preserved as described in [Section 6](#6-comments).

## 3. Indentation and Lines

Each block increases indentation by two ASCII spaces. The version directive and diagram declaration have zero indentation. A closing brace has the same indentation as the declaration that opened its block.

Absent a comment between two tokens that would otherwise share a line, the formatter MUST put each of the following on one line:

- the version directive;
- a diagram, group, node, or edge declaration header, including its opening brace when present;
- a theme statement;
- a node or edge property;
- a layout statement.

Opening braces are preceded by one ASCII space and remain on the declaration or `layout` line. Closing braces are on their own line. There is no blank line immediately after an opening brace or immediately before its closing brace.

The canonical token spacing is:

```stack
stack 1.0

diagram "Title" {
theme dark

node client "Client"

group services "Services" {
node api "API" {
kind service
icon "service"
detail "Public API"
}
}

edge client -> api "HTTPS" {
kind request
}

layout {
direction right
rank same [client, services]
order [client, services]
}
}
```

The formatter MUST use one ASCII space at the positions shown above, no space around the version dot, and no space just inside brackets.

## 4. Blank Lines

The version directive and diagram declaration are separated by exactly one empty line.

Adjacent members of a diagram or group body are separated by exactly one empty line. A member is a node, group, edge, theme, or layout construct. Properties in node and edge blocks and statements in layout blocks have no empty lines between them.

Leading comments belong to the member or statement that follows them. A separator is placed before the first leading comment, not between that comment and its member. A trailing comment belongs to the preceding line, so any separator follows the comment.

No other empty lines are emitted.

## 5. Order, Lists, and Strings

### 5.1 Order

The formatter MUST preserve the authored order of:

- diagram and group members, including the position of theme and layout constructs;
- node and edge properties;
- layout statements;
- identifiers in `rank same` and `order` lists.

It MUST NOT group or sort declarations or properties. This preserves declaration-order data in normalized IR and keeps comments attached to the same token boundaries.

### 5.2 Identifier Lists

An identifier list is emitted on one line as an opening bracket, the identifiers in authored order separated by a comma and one ASCII space, and a closing bracket. No trailing comma is emitted.

```stack
rank same [frontend, backend]
order [frontend, backend]
```

### 5.3 Strings

The formatter decodes each valid source string and emits its Unicode scalar values without Unicode normalization. A double quote is emitted as `\"`, a backslash is emitted as `\\`, and every other permitted scalar value is emitted directly as UTF-8. Canonical output therefore does not use `\uXXXX` escapes.

For example, `"API \u56F3 \uD83D\uDE80"` becomes `"API 図 🚀"`, while decoded quote and backslash characters remain escaped.

## 6. Comments

The bytes from `//` through the byte before its line ending form the comment lexeme. The formatter MUST preserve that lexeme exactly and MUST preserve comment order. It also MUST preserve the comment's gap between the same preceding and following non-comment tokens; string canonicalization does not change token identity for this rule.

A comment is **trailing** when a non-comment token precedes it on the same input line. A trailing comment is emitted immediately after its preceding token, preceded by one ASCII space. The line ends immediately after the comment lexeme. If another token in the same construct follows the comment, formatting resumes on the next line using the continuation indentation defined below.

Every other comment is an **own-line** comment. Consecutive own-line comments at one token gap remain consecutive, use the indentation of the following member, statement, or property, and are emitted immediately before it. If the next token closes a block, the comments use the indentation of that block's members. If the next token is end-of-file, they use zero indentation.

A comment may occur at a token gap inside a construct that canonical formatting would otherwise place on one line. The comment remains at that token gap and forces a line break. The comment and the remaining tokens use one additional indentation level relative to the construct's first line when they do not already have a greater block indentation. This comment-forced continuation is the only exception to the one-line rules in [Section 3](#3-indentation-and-lines).

Comments before the version directive form its leading comment block. Comments after the diagram's closing brace form a final own-line comment block separated from the diagram by one empty line.

## 7. Conformance Fixtures

Canonical formatter cases live in `conformance/formatter/`. Each case directory contains exactly:

```text
conformance/formatter/<case-id>/input.stack
conformance/formatter/<case-id>/expected.stack
conformance/formatter/<case-id>/expected.ir.json
```

For every case, a conforming formatter runner MUST:

1. format `input.stack` and compare the output bytes exactly with `expected.stack`;
2. format `expected.stack` and require byte-identical `expected.stack` output, proving idempotence;
3. compile both `input.stack` and `expected.stack` with compiler stages enabled and catalog, layout, and renderer stages disabled;
4. require both compilations to produce no compiler-stage error;
5. compare both normalized IR documents semantically with `expected.ir.json`, proving semantic preservation;
6. record the specification release or commit revision used for the run.

JSON object-member order and JSON whitespace are not significant. Array order is significant. A warning does not invalidate a formatter case, but portable diagnostic code, severity, and range are not compared across formatting because canonical whitespace changes source ranges.
4 changes: 2 additions & 2 deletions INTERCHANGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,9 @@ A range contains an inclusive `start` position and an exclusive `end` position.

Implementations may emit non-`STK` diagnostics. Canonical fixtures only require portable `STK` diagnostics unless a case explicitly documents an implementation extension.

## 5. Canonical Conformance Suite
## 5. Canonical Compiler Conformance Suite

The canonical suite lives in [`conformance/`](./conformance). Each case is one directory named with a lowercase ASCII identifier.
The canonical compiler suite lives in `conformance/valid/` and `conformance/invalid/`. Each compiler case is one directory named with a lowercase ASCII identifier. Formatter fixtures use a separate contract and layout defined in the [Stack Canonical Formatter Specification](./FORMATTER.md).

### 5.1 Valid Cases

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The language is currently a proposal for Stack 1.0. No compatibility guarantee a
## Documents

- [Language specification](./SPECIFICATION.md)
- [Canonical formatter specification](./FORMATTER.md)
- [Compiler interchange specification](./INTERCHANGE.md)
- [ADR-0001: Adopt a constrained declarative topology language](./docs/decisions/0001-constrained-declarative-language.md)
- [ADR-0002: Make the canonical theme catalog own icons](./docs/decisions/0002-theme-owned-icons.md)
Expand Down Expand Up @@ -59,9 +60,10 @@ Install the development requirements and validate the portable schemas and confo
```sh
python -m pip install --requirement requirements-dev.txt
check-jsonschema --check-metaschema schemas/*.json
check-jsonschema --schemafile schemas/normalized-ir.schema.json conformance/valid/*/expected.ir.json
check-jsonschema --schemafile schemas/normalized-ir.schema.json conformance/valid/*/expected.ir.json conformance/formatter/*/expected.ir.json
find conformance -name expected.diagnostics.json -print0 | xargs -0 check-jsonschema --schemafile schemas/diagnostic-expectations.schema.json
python scripts/validate-compiler-diagnostics.py
python scripts/validate-formatter-fixtures.py
```

## Design Principles
Expand Down
12 changes: 3 additions & 9 deletions SPECIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,15 +234,9 @@ They remain valid identifiers where the grammar expects an identifier. This cont

### 5.2 Canonical Formatting

Formatting does not affect meaning. A canonical formatter SHOULD:

- use two spaces per nesting level;
- put one declaration or property on each line;
- place opening braces on the declaration line;
- place the theme statement before layout and element declarations;
- include a blank line between top-level declarations;
- preserve comments where practical;
- preserve declaration order.
Formatting does not affect meaning. The normative canonical source representation, including comment placement, ordering, whitespace, string escaping, line endings, and conformance requirements, is defined in the [Stack Canonical Formatter Specification](./FORMATTER.md).

A canonical formatter MUST preserve normalized meaning and comments, and formatting canonical source again MUST produce byte-identical output.

## 6. Document and Diagram Semantics

Expand Down
8 changes: 7 additions & 1 deletion conformance/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Stack Conformance Suite

This directory contains implementation-independent compiler conformance cases for the Stack language.
This directory contains implementation-independent compiler and formatter conformance cases for the Stack language.

## Layout

Expand All @@ -13,6 +13,10 @@ valid/<case-id>/expected.diagnostics.json # optional

invalid/<case-id>/source.stack
invalid/<case-id>/expected.diagnostics.json

formatter/<case-id>/input.stack
formatter/<case-id>/expected.stack
formatter/<case-id>/expected.ir.json
```

`source.stack` must be read as bytes. This permits future encoding-error fixtures even though valid Stack documents are UTF-8.
Expand All @@ -23,6 +27,8 @@ The canonical suite covers every Stack 1.0 diagnostic assigned to compiler stage

The encoding cases intentionally include raw invalid UTF-8, a UTF-8 byte order mark, CRLF line endings, and a Unicode scalar before an error position. Tools must preserve `source.stack` bytes rather than decoding and rewriting fixtures during discovery.

Formatter inputs are compiler-valid Stack documents. A formatter runner compares canonical source bytes, formats the expected source again to verify idempotence, and compiles both input and expected source to verify that each is semantically equal to `expected.ir.json`. The complete formatter behavior is defined in the [Stack Canonical Formatter Specification](../FORMATTER.md).

## Comparison

- JSON values are compared semantically; formatting and object-member order do not matter.
Expand Down
85 changes: 85 additions & 0 deletions conformance/formatter/comments-and-layout/expected.ir.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{
"schemaVersion": "1.0",
"languageVersion": {
"major": 1,
"minor": 0
},
"title": "Formatter 図",
"themeId": "dark",
"children": [
{
"type": "group",
"id": "services"
},
{
"type": "node",
"id": "client"
}
],
"nodes": [
{
"id": "frontend",
"label": "Web app",
"kind": "client",
"iconId": "browser",
"detail": "Uses \"quotes\" and \\ paths",
"parentGroupId": "services"
},
{
"id": "backend",
"label": "API 🚀",
"kind": "service",
"iconId": "service",
"detail": null,
"parentGroupId": "services"
},
{
"id": "client",
"label": "Client",
"kind": "service",
"iconId": null,
"detail": null,
"parentGroupId": null
}
],
"groups": [
{
"id": "services",
"label": "Services",
"parentGroupId": null,
"children": [
{
"type": "node",
"id": "frontend"
},
{
"type": "node",
"id": "backend"
}
],
"layout": null
}
],
"edges": [
{
"from": "client",
"to": "frontend",
"direction": "forward",
"kind": "request",
"label": "HTTPS"
}
],
"layout": {
"direction": "right",
"sameRanks": [
[
"services",
"client"
]
],
"order": [
"services",
"client"
]
}
}
33 changes: 33 additions & 0 deletions conformance/formatter/comments-and-layout/expected.stack
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Leading file comment
stack 1.0 // Version comment

diagram "Formatter 図" {
group services "Services" {
// Nested declaration comment
node frontend "Web app" {
detail "Uses \"quotes\" and \\ paths"
icon "browser"
kind client
} // Frontend node

node backend "API 🚀" {
icon "service" // Backend icon
kind service
}
}

// Between declarations
node client "Client"

theme dark

layout {
order [services, client]
direction right
rank same [services, client]
}

edge client -> frontend "HTTPS" {
kind request
}
}
19 changes: 19 additions & 0 deletions conformance/formatter/comments-and-layout/input.stack
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Leading file comment
stack 1 . 0// Version comment


diagram "Formatter \u56F3"{
group services "Services"{
// Nested declaration comment
node frontend "\u0057eb app" {detail "Uses \u0022quotes\u0022 and \\ paths" icon "browser" kind client}// Frontend node


node backend "API \uD83D\uDE80"{icon "service"// Backend icon
kind service}
}
// Between declarations
node client "Client"
theme dark
layout {order[services,client] direction right rank same[services,client]}
edge client->frontend "HTTPS"{kind request}
}
Loading