diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..00657cd --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +conformance/invalid/byte-order-mark/source.stack binary +conformance/invalid/invalid-utf8/source.stack binary diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af7a038..96721a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,3 +49,6 @@ jobs: - 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 diff --git a/README.md b/README.md index f5db645..5c067cf 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ 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 find conformance -name expected.diagnostics.json -print0 | xargs -0 check-jsonschema --schemafile schemas/diagnostic-expectations.schema.json +python scripts/validate-compiler-diagnostics.py ``` ## Design Principles diff --git a/conformance/README.md b/conformance/README.md index 0c275e8..0fca87b 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -19,6 +19,10 @@ invalid//expected.diagnostics.json Expected IR documents conform to [`normalized-ir.schema.json`](../schemas/normalized-ir.schema.json). Diagnostic expectation documents conform to [`diagnostic-expectations.schema.json`](../schemas/diagnostic-expectations.schema.json). +The canonical suite covers every Stack 1.0 diagnostic assigned to compiler stages 1 through 4: `STK1001` through `STK3014`, excluding unassigned numbers, plus `STK4002` and `STK4003`. [`validate-compiler-diagnostics.py`](../scripts/validate-compiler-diagnostics.py) verifies that every required code occurs in at least one expectation document. Renderer-stage `STK4001`, `STK5001`, and `STK6001` remain outside compiler conformance. + +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. + ## Comparison - JSON values are compared semantically; formatting and object-member order do not matter. diff --git a/conformance/invalid/byte-order-mark/expected.diagnostics.json b/conformance/invalid/byte-order-mark/expected.diagnostics.json new file mode 100644 index 0000000..9c4b3b5 --- /dev/null +++ b/conformance/invalid/byte-order-mark/expected.diagnostics.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": "1.0", + "diagnostics": [ + { + "code": "STK1002", + "severity": "error", + "range": { + "start": { "byteOffset": 0, "line": 1, "column": 1 }, + "end": { "byteOffset": 3, "line": 1, "column": 2 } + } + } + ] +} diff --git a/conformance/invalid/byte-order-mark/source.stack b/conformance/invalid/byte-order-mark/source.stack new file mode 100644 index 0000000..d020a50 --- /dev/null +++ b/conformance/invalid/byte-order-mark/source.stack @@ -0,0 +1,4 @@ +stack 1.0 +diagram "BOM" { + node api "API" +} diff --git a/conformance/invalid/duplicate-theme-dense-node/expected.diagnostics.json b/conformance/invalid/duplicate-theme-dense-node/expected.diagnostics.json new file mode 100644 index 0000000..1a3381f --- /dev/null +++ b/conformance/invalid/duplicate-theme-dense-node/expected.diagnostics.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": "1.0", + "diagnostics": [ + { + "code": "STK3014", + "severity": "error", + "range": { + "start": { "byteOffset": 44, "line": 4, "column": 3 }, + "end": { "byteOffset": 54, "line": 4, "column": 13 } + } + }, + { + "code": "STK4002", + "severity": "warning", + "range": { + "start": { "byteOffset": 62, "line": 5, "column": 8 }, + "end": { "byteOffset": 65, "line": 5, "column": 11 } + } + } + ] +} diff --git a/conformance/invalid/duplicate-theme-dense-node/source.stack b/conformance/invalid/duplicate-theme-dense-node/source.stack new file mode 100644 index 0000000..f1e153c --- /dev/null +++ b/conformance/invalid/duplicate-theme-dense-node/source.stack @@ -0,0 +1,32 @@ +stack 1.0 +diagram "Dense" { + theme light + theme dark + node hub "Hub" + node n0 "N 0" + node n1 "N 1" + node n2 "N 2" + node n3 "N 3" + node n4 "N 4" + node n5 "N 5" + node n6 "N 6" + node n7 "N 7" + node n8 "N 8" + node n9 "N 9" + node n10 "N 10" + node n11 "N 11" + node n12 "N 12" + edge hub -> n0 + edge hub -> n1 + edge hub -> n2 + edge hub -> n3 + edge hub -> n4 + edge hub -> n5 + edge hub -> n6 + edge hub -> n7 + edge hub -> n8 + edge hub -> n9 + edge hub -> n10 + edge hub -> n11 + edge hub -> n12 +} diff --git a/conformance/invalid/edge-rules/expected.diagnostics.json b/conformance/invalid/edge-rules/expected.diagnostics.json new file mode 100644 index 0000000..c63e800 --- /dev/null +++ b/conformance/invalid/edge-rules/expected.diagnostics.json @@ -0,0 +1,29 @@ +{ + "schemaVersion": "1.0", + "diagnostics": [ + { + "code": "STK3007", + "severity": "error", + "range": { + "start": { "byteOffset": 61, "line": 3, "column": 34 }, + "end": { "byteOffset": 67, "line": 3, "column": 40 } + } + }, + { + "code": "STK3005", + "severity": "error", + "range": { + "start": { "byteOffset": 85, "line": 5, "column": 3 }, + "end": { "byteOffset": 96, "line": 5, "column": 14 } + } + }, + { + "code": "STK3006", + "severity": "error", + "range": { + "start": { "byteOffset": 113, "line": 7, "column": 3 }, + "end": { "byteOffset": 124, "line": 7, "column": 14 } + } + } + ] +} diff --git a/conformance/invalid/edge-rules/source.stack b/conformance/invalid/edge-rules/source.stack new file mode 100644 index 0000000..ef48811 --- /dev/null +++ b/conformance/invalid/edge-rules/source.stack @@ -0,0 +1,8 @@ +stack 1.0 +diagram "Edges" { + node a "A" { kind service kind worker } + node b "B" + edge a -> a + edge a -- b + edge b -- a +} diff --git a/conformance/invalid/empty-diagram/expected.diagnostics.json b/conformance/invalid/empty-diagram/expected.diagnostics.json new file mode 100644 index 0000000..252472c --- /dev/null +++ b/conformance/invalid/empty-diagram/expected.diagnostics.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": "1.0", + "diagnostics": [ + { + "code": "STK4003", + "severity": "error", + "range": { + "start": { "byteOffset": 10, "line": 2, "column": 1 }, + "end": { "byteOffset": 28, "line": 2, "column": 19 } + } + } + ] +} diff --git a/conformance/invalid/empty-diagram/source.stack b/conformance/invalid/empty-diagram/source.stack new file mode 100644 index 0000000..16f68a4 --- /dev/null +++ b/conformance/invalid/empty-diagram/source.stack @@ -0,0 +1,2 @@ +stack 1.0 +diagram "Empty" {} diff --git a/conformance/invalid/independent-semantic-errors/expected.diagnostics.json b/conformance/invalid/independent-semantic-errors/expected.diagnostics.json new file mode 100644 index 0000000..1acece6 --- /dev/null +++ b/conformance/invalid/independent-semantic-errors/expected.diagnostics.json @@ -0,0 +1,77 @@ +{ + "schemaVersion": "1.0", + "diagnostics": [ + { + "code": "STK2001", + "severity": "error", + "range": { + "start": { "byteOffset": 0, "line": 1, "column": 1 }, + "end": { "byteOffset": 9, "line": 1, "column": 10 } + } + }, + { + "code": "STK3008", + "severity": "error", + "range": { + "start": { "byteOffset": 18, "line": 2, "column": 9 }, + "end": { "byteOffset": 25, "line": 2, "column": 16 } + } + }, + { + "code": "STK3009", + "severity": "error", + "range": { + "start": { "byteOffset": 36, "line": 3, "column": 9 }, + "end": { "byteOffset": 41, "line": 3, "column": 14 } + } + }, + { + "code": "STK3001", + "severity": "error", + "range": { + "start": { "byteOffset": 60, "line": 4, "column": 8 }, + "end": { "byteOffset": 63, "line": 4, "column": 11 } + } + }, + { + "code": "STK3008", + "severity": "error", + "range": { + "start": { "byteOffset": 64, "line": 4, "column": 12 }, + "end": { "byteOffset": 66, "line": 4, "column": 14 } + } + }, + { + "code": "STK3001", + "severity": "error", + "range": { + "start": { "byteOffset": 74, "line": 5, "column": 8 }, + "end": { "byteOffset": 77, "line": 5, "column": 11 } + } + }, + { + "code": "STK3002", + "severity": "error", + "range": { + "start": { "byteOffset": 74, "line": 5, "column": 8 }, + "end": { "byteOffset": 77, "line": 5, "column": 11 } + } + }, + { + "code": "STK3003", + "severity": "error", + "range": { + "start": { "byteOffset": 97, "line": 6, "column": 8 }, + "end": { "byteOffset": 104, "line": 6, "column": 15 } + } + }, + { + "code": "STK3004", + "severity": "error", + "range": { + "start": { "byteOffset": 108, "line": 6, "column": 19 }, + "end": { "byteOffset": 113, "line": 6, "column": 24 } + } + } + ] +} diff --git a/conformance/invalid/independent-semantic-errors/source.stack b/conformance/invalid/independent-semantic-errors/source.stack new file mode 100644 index 0000000..9a8f424 --- /dev/null +++ b/conformance/invalid/independent-semantic-errors/source.stack @@ -0,0 +1,7 @@ +stack 2.0 +diagram " Bad " { + group empty "Empty" {} + node Bad "" + node Bad "Duplicate" + edge missing -> empty +} diff --git a/conformance/invalid/invalid-string-escape/expected.diagnostics.json b/conformance/invalid/invalid-string-escape/expected.diagnostics.json new file mode 100644 index 0000000..1afc9c7 --- /dev/null +++ b/conformance/invalid/invalid-string-escape/expected.diagnostics.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": "1.0", + "diagnostics": [ + { + "code": "STK1003", + "severity": "error", + "range": { + "start": { "byteOffset": 23, "line": 2, "column": 14 }, + "end": { "byteOffset": 23, "line": 2, "column": 14 } + } + } + ] +} diff --git a/conformance/invalid/invalid-string-escape/source.stack b/conformance/invalid/invalid-string-escape/source.stack new file mode 100644 index 0000000..1ffa5a9 --- /dev/null +++ b/conformance/invalid/invalid-string-escape/source.stack @@ -0,0 +1,4 @@ +stack 1.0 +diagram "Bad\q" { + node api "API" +} diff --git a/conformance/invalid/invalid-utf8/expected.diagnostics.json b/conformance/invalid/invalid-utf8/expected.diagnostics.json new file mode 100644 index 0000000..1ff684c --- /dev/null +++ b/conformance/invalid/invalid-utf8/expected.diagnostics.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": "1.0", + "diagnostics": [ + { + "code": "STK1001", + "severity": "error", + "range": { + "start": { "byteOffset": 17, "line": 2, "column": 5 }, + "end": { "byteOffset": 17, "line": 2, "column": 5 } + } + } + ] +} diff --git a/conformance/invalid/invalid-utf8/source.stack b/conformance/invalid/invalid-utf8/source.stack new file mode 100644 index 0000000..9597601 --- /dev/null +++ b/conformance/invalid/invalid-utf8/source.stack @@ -0,0 +1,2 @@ +stack 1.0 +// 図ÿ \ No newline at end of file diff --git a/conformance/invalid/layout-rules/expected.diagnostics.json b/conformance/invalid/layout-rules/expected.diagnostics.json new file mode 100644 index 0000000..6954c16 --- /dev/null +++ b/conformance/invalid/layout-rules/expected.diagnostics.json @@ -0,0 +1,37 @@ +{ + "schemaVersion": "1.0", + "diagnostics": [ + { + "code": "STK3012", + "severity": "error", + "range": { + "start": { "byteOffset": 100, "line": 7, "column": 15 }, + "end": { "byteOffset": 104, "line": 7, "column": 19 } + } + }, + { + "code": "STK3011", + "severity": "error", + "range": { + "start": { "byteOffset": 123, "line": 8, "column": 19 }, + "end": { "byteOffset": 130, "line": 8, "column": 26 } + } + }, + { + "code": "STK3011", + "severity": "error", + "range": { + "start": { "byteOffset": 146, "line": 9, "column": 15 }, + "end": { "byteOffset": 147, "line": 9, "column": 16 } + } + }, + { + "code": "STK3012", + "severity": "error", + "range": { + "start": { "byteOffset": 155, "line": 11, "column": 3 }, + "end": { "byteOffset": 181, "line": 11, "column": 29 } + } + } + ] +} diff --git a/conformance/invalid/layout-rules/source.stack b/conformance/invalid/layout-rules/source.stack new file mode 100644 index 0000000..f986a6d --- /dev/null +++ b/conformance/invalid/layout-rules/source.stack @@ -0,0 +1,12 @@ +stack 1.0 +diagram "Layout" { + node a "A" + node b "B" + layout { + direction right + direction down + rank same [a, missing] + order [a, a] + } + layout { direction right } +} diff --git a/conformance/invalid/nested-group-icon/expected.diagnostics.json b/conformance/invalid/nested-group-icon/expected.diagnostics.json new file mode 100644 index 0000000..274b9a4 --- /dev/null +++ b/conformance/invalid/nested-group-icon/expected.diagnostics.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": "1.0", + "diagnostics": [ + { + "code": "STK3010", + "severity": "error", + "range": { + "start": { "byteOffset": 113, "line": 6, "column": 15 }, + "end": { "byteOffset": 117, "line": 6, "column": 19 } + } + }, + { + "code": "STK3013", + "severity": "error", + "range": { + "start": { "byteOffset": 161, "line": 7, "column": 35 }, + "end": { "byteOffset": 171, "line": 7, "column": 45 } + } + } + ] +} diff --git a/conformance/invalid/nested-group-icon/source.stack b/conformance/invalid/nested-group-icon/source.stack new file mode 100644 index 0000000..f648814 --- /dev/null +++ b/conformance/invalid/nested-group-icon/source.stack @@ -0,0 +1,12 @@ +stack 1.0 +diagram "Nested" { + group one "One" { + group two "Two" { + group three "Three" { + group four "Four" { + node leaf "Leaf" { icon "Bad_icon" } + } + } + } + } +} diff --git a/conformance/invalid/unexpected-end/expected.diagnostics.json b/conformance/invalid/unexpected-end/expected.diagnostics.json new file mode 100644 index 0000000..6f0ca54 --- /dev/null +++ b/conformance/invalid/unexpected-end/expected.diagnostics.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": "1.0", + "diagnostics": [ + { + "code": "STK2003", + "severity": "error", + "range": { + "start": { "byteOffset": 50, "line": 4, "column": 1 }, + "end": { "byteOffset": 50, "line": 4, "column": 1 } + } + } + ] +} diff --git a/conformance/invalid/unexpected-end/source.stack b/conformance/invalid/unexpected-end/source.stack new file mode 100644 index 0000000..b42ec4e --- /dev/null +++ b/conformance/invalid/unexpected-end/source.stack @@ -0,0 +1,3 @@ +stack 1.0 +diagram "Incomplete" { + node api "API" diff --git a/conformance/invalid/unexpected-token/expected.diagnostics.json b/conformance/invalid/unexpected-token/expected.diagnostics.json new file mode 100644 index 0000000..bcd0bec --- /dev/null +++ b/conformance/invalid/unexpected-token/expected.diagnostics.json @@ -0,0 +1,13 @@ +{ + "schemaVersion": "1.0", + "diagnostics": [ + { + "code": "STK2002", + "severity": "error", + "range": { + "start": { "byteOffset": 35, "line": 3, "column": 3 }, + "end": { "byteOffset": 41, "line": 3, "column": 9 } + } + } + ] +} diff --git a/conformance/invalid/unexpected-token/source.stack b/conformance/invalid/unexpected-token/source.stack new file mode 100644 index 0000000..3b98620 --- /dev/null +++ b/conformance/invalid/unexpected-token/source.stack @@ -0,0 +1,4 @@ +stack 1.0 +diagram "Unexpected" { + server api "API" +} diff --git a/scripts/validate-compiler-diagnostics.py b/scripts/validate-compiler-diagnostics.py new file mode 100644 index 0000000..b57dd84 --- /dev/null +++ b/scripts/validate-compiler-diagnostics.py @@ -0,0 +1,58 @@ +import json +from pathlib import Path + + +REQUIRED_COMPILER_DIAGNOSTICS = { + "STK1001", + "STK1002", + "STK1003", + "STK2001", + "STK2002", + "STK2003", + "STK3001", + "STK3002", + "STK3003", + "STK3004", + "STK3005", + "STK3006", + "STK3007", + "STK3008", + "STK3009", + "STK3010", + "STK3011", + "STK3012", + "STK3013", + "STK3014", + "STK4002", + "STK4003", +} + + +def main() -> None: + root = Path(__file__).resolve().parent.parent + observed = set() + for path in sorted((root / "conformance").glob("*/*/expected.diagnostics.json")): + document = json.loads(path.read_bytes()) + observed.update(diagnostic["code"] for diagnostic in document["diagnostics"]) + + missing = sorted(REQUIRED_COMPILER_DIAGNOSTICS - observed) + if missing: + raise SystemExit(f"missing compiler diagnostic expectations: {', '.join(missing)}") + + invalid_utf8 = (root / "conformance/invalid/invalid-utf8/source.stack").read_bytes() + try: + invalid_utf8.decode("utf-8") + except UnicodeDecodeError: + pass + else: + raise SystemExit("invalid-utf8/source.stack must contain invalid UTF-8") + if b"\r\n" not in invalid_utf8: + raise SystemExit("invalid-utf8/source.stack must exercise CRLF positions") + + byte_order_mark = (root / "conformance/invalid/byte-order-mark/source.stack").read_bytes() + if not byte_order_mark.startswith(b"\xef\xbb\xbf"): + raise SystemExit("byte-order-mark/source.stack must begin with a UTF-8 BOM") + + +if __name__ == "__main__": + main()