diff --git a/.github/workflows/loop-tools-gate.yml b/.github/workflows/loop-tools-gate.yml new file mode 100644 index 0000000000..e61f59861c --- /dev/null +++ b/.github/workflows/loop-tools-gate.yml @@ -0,0 +1,63 @@ +# A loop tool that exists only in one working directory is not in the project. +# +# scripts/tri_loop/cost.py and diffbin.py were written, used to produce numbers +# that were quoted in a pull request, and lost -- never committed, and the working +# copy was later re-cloned. Every figure they had produced became unreproducible +# at once. Six recovery routes came back empty (#2158). +# +# Nothing in CI could tell the difference between a tool that exists and a tool +# that exists nowhere but one untracked directory. This gate is that difference. +# It is deliberately cheap: no build, no compiler, no corpus run, so there is no +# reason for it to be skipped or made optional. +name: loop-tools-gate + +on: + pull_request: + paths: + - "scripts/tri" + - "scripts/tri_loop/**" + - "scripts/ci/loop-tools-tracked.sh" + - ".github/workflows/loop-tools-gate.yml" + push: + branches: [master] + paths: + - "scripts/tri" + - "scripts/tri_loop/**" + - "scripts/ci/loop-tools-tracked.sh" + +permissions: + contents: read + +jobs: + loop-tools-tracked: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + # The check reads git index state, so it needs real history rather than + # a detached blob export. + fetch-depth: 1 + + - name: Every loop tool is present, tracked, and routed + run: bash scripts/ci/loop-tools-tracked.sh + + - name: Each loop helper is syntactically loadable + # A tracked tool that cannot be imported is tracked and useless. This is + # a syntax check only and asserts nothing about behaviour. + run: | + for f in scripts/tri_loop/*.py; do + python3 -c "import py_compile,sys; py_compile.compile('$f', doraise=True)" \ + && echo "ok $f" \ + || { echo "BROKEN $f"; exit 1; } + done + + - name: The dispatcher runs its helpers without a built compiler + # tri triage and tri damage read the tracker and the spec text and have no + # use for t27c. The dispatcher used to look for the binary first and + # refuse to run them on a machine with no build, so this step exists to + # keep that ordering from coming back. There is no compiler in this job, + # which is the whole point. + run: | + ./scripts/tri loop-help + ./scripts/tri damage specs >/dev/null || true + echo "ok helpers dispatch with no t27c present" diff --git a/bootstrap/tests/fixtures/damage/damage_class_01.t27 b/bootstrap/tests/fixtures/damage/damage_class_01.t27 new file mode 100644 index 0000000000..f58109de50 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_01.t27 @@ -0,0 +1,9 @@ +module damage_class_01 + +// shape: [[]X", +// 52 line(s) in the corpus share this shape +// first seen: specs/tri/pipeline/builder.t27:14 +// signals: doubled-bracket,odd-quote +pub struct Damaged { + items : [[]T", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_02.t27 b/bootstrap/tests/fixtures/damage/damage_class_02.t27 new file mode 100644 index 0000000000..67ab30ab19 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_02.t27 @@ -0,0 +1,9 @@ +module damage_class_02 + +// shape: [[]X X", +// 37 line(s) in the corpus share this shape +// first seen: specs/tri/pipeline/spec_writer.t27:14 +// signals: doubled-bracket,odd-quote +pub struct Damaged { + field_type : [[]Const u8", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_03.t27 b/bootstrap/tests/fixtures/damage/damage_class_03.t27 new file mode 100644 index 0000000000..f8f3acf3fb --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_03.t27 @@ -0,0 +1,9 @@ +module damage_class_03 + +// shape: [[]X [, +// 10 line(s) in the corpus share this shape +// first seen: specs/tri/pipeline/spec_writer.t27:23 +// signals: doubled-bracket +pub struct Damaged { + steps : [[]Const [, +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_04.t27 b/bootstrap/tests/fixtures/damage/damage_class_04.t27 new file mode 100644 index 0000000000..5f6e2e55e7 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_04.t27 @@ -0,0 +1,9 @@ +module damage_class_04 + +// shape: [[][, +// 7 line(s) in the corpus share this shape +// first seen: specs/tri/encoding/mime.t27:15 +// signals: doubled-bracket +pub struct Damaged { + to : [[][, +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_05.t27 b/bootstrap/tests/fixtures/damage/damage_class_05.t27 new file mode 100644 index 0000000000..61147193ba --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_05.t27 @@ -0,0 +1,9 @@ +module damage_class_05 + +// shape: [[9]X", +// 5 line(s) in the corpus share this shape +// first seen: specs/tri/crypto/hmac.t27:14 +// signals: odd-quote +pub struct Damaged { + opad : [[64]U8", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_06.t27 b/bootstrap/tests/fixtures/damage/damage_class_06.t27 new file mode 100644 index 0000000000..cccdb197b4 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_06.t27 @@ -0,0 +1,9 @@ +module damage_class_06 + +// shape: [?[]X X", +// 3 line(s) in the corpus share this shape +// first seen: specs/tri/pipeline/spec_writer.t27:36 +// signals: doubled-bracket,odd-quote +pub struct Damaged { + error_msg : [?[]Const u8", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_07.t27 b/bootstrap/tests/fixtures/damage/damage_class_07.t27 new file mode 100644 index 0000000000..da3ec57353 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_07.t27 @@ -0,0 +1,9 @@ +module damage_class_07 + +// shape: [X.X([]X X)", +// 2 line(s) in the corpus share this shape +// first seen: specs/tri/encoding/html.t27:15 +// signals: odd-quote +pub struct Damaged { + attributes : [std.StringHashMap([]Const u8)", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_08.t27 b/bootstrap/tests/fixtures/damage/damage_class_08.t27 new file mode 100644 index 0000000000..d81e4e9be7 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_08.t27 @@ -0,0 +1,9 @@ +module damage_class_08 + +// shape: [[9]?X", +// 2 line(s) in the corpus share this shape +// first seen: specs/tri/trees/octree.t27:24 +// signals: odd-quote +pub struct Damaged { + children : [[8]?OctNode", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_09.t27 b/bootstrap/tests/fixtures/damage/damage_class_09.t27 new file mode 100644 index 0000000000..c070a80bec --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_09.t27 @@ -0,0 +1,9 @@ +module damage_class_09 + +// shape: [[]?X", +// 1 line(s) in the corpus share this shape +// first seen: specs/tri/graph/dijkstra.t27:15 +// signals: doubled-bracket,odd-quote +pub struct Damaged { + parent : [[]?Usize", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_10.t27 b/bootstrap/tests/fixtures/damage/damage_class_10.t27 new file mode 100644 index 0000000000..096cf0e18f --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_10.t27 @@ -0,0 +1,9 @@ +module damage_class_10 + +// shape: [X.X(X, []X)", +// 1 line(s) in the corpus share this shape +// first seen: specs/tri/graph/graph.t27:14 +// signals: odd-quote +pub struct Damaged { + nodes : [std.HashMap(T, []T)", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_11.t27 b/bootstrap/tests/fixtures/damage/damage_class_11.t27 new file mode 100644 index 0000000000..1cd48eca65 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_11.t27 @@ -0,0 +1,9 @@ +module damage_class_11 + +// shape: [[]?*X", +// 1 line(s) in the corpus share this shape +// first seen: specs/tri/trees/b_tree.t27:15 +// signals: doubled-bracket,odd-quote +pub struct Damaged { + children : [[]?*BTreeNode", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_12.t27 b/bootstrap/tests/fixtures/damage/damage_class_12.t27 new file mode 100644 index 0000000000..689a32ea57 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_12.t27 @@ -0,0 +1,9 @@ +module damage_class_12 + +// shape: [[][9, +// 1 line(s) in the corpus share this shape +// first seen: specs/tri/trees/quadtree.t27:23 +// signals: doubled-bracket +pub struct Damaged { + points : [[][2, +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_13.t27 b/bootstrap/tests/fixtures/damage/damage_class_13.t27 new file mode 100644 index 0000000000..5c6756ca4a --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_13.t27 @@ -0,0 +1,9 @@ +module damage_class_13 + +// shape: [[9]?*X", +// 1 line(s) in the corpus share this shape +// first seen: specs/tri/search/aho_corasick.t27:14 +// signals: odd-quote +pub struct Damaged { + children : [[256]?*ACTrieNode", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_14.t27 b/bootstrap/tests/fixtures/damage/damage_class_14.t27 new file mode 100644 index 0000000000..0b4bfaf393 --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_14.t27 @@ -0,0 +1,9 @@ +module damage_class_14 + +// shape: [[]X(X, X)", +// 1 line(s) in the corpus share this shape +// first seen: specs/tri/collections/btree.t27:21 +// signals: doubled-bracket,odd-quote +pub struct Damaged { + children : [[]BTreeNode(K, V)", +} diff --git a/bootstrap/tests/fixtures/damage/damage_class_15.t27 b/bootstrap/tests/fixtures/damage/damage_class_15.t27 new file mode 100644 index 0000000000..85a5082aec --- /dev/null +++ b/bootstrap/tests/fixtures/damage/damage_class_15.t27 @@ -0,0 +1,9 @@ +module damage_class_15 + +// shape: [[]?X(X)", +// 1 line(s) in the corpus share this shape +// first seen: specs/tri/collections/skip_list.t27:15 +// signals: doubled-bracket,odd-quote +pub struct Damaged { + forward : [[]?SkipNode(T)", +} diff --git a/bootstrap/tests/fixtures/damage_classes/DC-06dafedd.t27 b/bootstrap/tests/fixtures/damage_classes/DC-06dafedd.t27 new file mode 100644 index 0000000000..b102d2b1f3 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-06dafedd.t27 @@ -0,0 +1,15 @@ +// damage class DC-06dafedd +// shape: [[]?X", +// lines: 1 in the corpus, across 1 file(s) +// origin: specs/tri/graph/dijkstra.t27:15 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[]?Usize", + + +// expected: parse-restored + +pub const DamageCase = struct { + parent : [[]?Usize", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-546b13cd.t27 b/bootstrap/tests/fixtures/damage_classes/DC-546b13cd.t27 new file mode 100644 index 0000000000..012c31775d --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-546b13cd.t27 @@ -0,0 +1,15 @@ +// damage class DC-546b13cd +// shape: [[]?*X", +// lines: 1 in the corpus, across 1 file(s) +// origin: specs/tri/trees/b_tree.t27:15 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[]?*BTreeNode", + + +// expected: parse-restored + +pub const DamageCase = struct { + children : [[]?*BTreeNode", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-550ec752.t27 b/bootstrap/tests/fixtures/damage_classes/DC-550ec752.t27 new file mode 100644 index 0000000000..599ece469d --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-550ec752.t27 @@ -0,0 +1,15 @@ +// damage class DC-550ec752 +// shape: [X.X([]X X)", +// lines: 2 in the corpus, across 2 file(s) +// origin: specs/tri/encoding/html.t27:15 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "std.StringHashMap([]Const u8)", + + +// expected: parse-restored + +pub const DamageCase = struct { + attributes : [std.StringHashMap([]Const u8)", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-7247f52e.t27 b/bootstrap/tests/fixtures/damage_classes/DC-7247f52e.t27 new file mode 100644 index 0000000000..87c6aacf04 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-7247f52e.t27 @@ -0,0 +1,15 @@ +// damage class DC-7247f52e +// shape: [[9]X", +// lines: 5 in the corpus, across 4 file(s) +// origin: specs/tri/crypto/hmac.t27:14 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[64]U8", + + +// expected: parse-restored + +pub const DamageCase = struct { + opad : [[64]U8", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-72bb7dcf.t27 b/bootstrap/tests/fixtures/damage_classes/DC-72bb7dcf.t27 new file mode 100644 index 0000000000..8dd919ec6e --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-72bb7dcf.t27 @@ -0,0 +1,15 @@ +// damage class DC-72bb7dcf +// shape: [[]X [, +// lines: 10 in the corpus, across 3 file(s) +// origin: specs/tri/pipeline/spec_writer.t27:23 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: NONE -- substituting the delimiter yields '"[]Const [,', +// which is not a closed string. The type text was truncated as well, +// so the element type is gone and a repair would have to invent it. +// expected: needs-human-language-decision + +pub const DamageCase = struct { + steps : [[]Const [, +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-73be92fd.t27 b/bootstrap/tests/fixtures/damage_classes/DC-73be92fd.t27 new file mode 100644 index 0000000000..a9dbe1bab8 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-73be92fd.t27 @@ -0,0 +1,15 @@ +// damage class DC-73be92fd +// shape: [[]X(X, X)", +// lines: 1 in the corpus, across 1 file(s) +// origin: specs/tri/collections/btree.t27:21 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[]BTreeNode(K, V)", + + +// expected: parse-restored + +pub const DamageCase = struct { + children : [[]BTreeNode(K, V)", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-774471d4.t27 b/bootstrap/tests/fixtures/damage_classes/DC-774471d4.t27 new file mode 100644 index 0000000000..eb6f6d7cdf --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-774471d4.t27 @@ -0,0 +1,15 @@ +// damage class DC-774471d4 +// shape: [?[]X X", +// lines: 3 in the corpus, across 3 file(s) +// origin: specs/tri/pipeline/spec_writer.t27:36 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "?[]Const u8", + + +// expected: parse-restored + +pub const DamageCase = struct { + error_msg : [?[]Const u8", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-801c2390.t27 b/bootstrap/tests/fixtures/damage_classes/DC-801c2390.t27 new file mode 100644 index 0000000000..6a46376562 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-801c2390.t27 @@ -0,0 +1,15 @@ +// damage class DC-801c2390 +// shape: [[][9, +// lines: 1 in the corpus, across 1 file(s) +// origin: specs/tri/trees/quadtree.t27:23 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: NONE -- substituting the delimiter yields '"[][2,', +// which is not a closed string. The type text was truncated as well, +// so the element type is gone and a repair would have to invent it. +// expected: needs-human-language-decision + +pub const DamageCase = struct { + points : [[][2, +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-83e0cb30.t27 b/bootstrap/tests/fixtures/damage_classes/DC-83e0cb30.t27 new file mode 100644 index 0000000000..e0d03cd209 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-83e0cb30.t27 @@ -0,0 +1,15 @@ +// damage class DC-83e0cb30 +// shape: [[][, +// lines: 7 in the corpus, across 6 file(s) +// origin: specs/tri/encoding/mime.t27:15 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: NONE -- substituting the delimiter yields '"[][,', +// which is not a closed string. The type text was truncated as well, +// so the element type is gone and a repair would have to invent it. +// expected: needs-human-language-decision + +pub const DamageCase = struct { + to : [[][, +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-ab5c5903.t27 b/bootstrap/tests/fixtures/damage_classes/DC-ab5c5903.t27 new file mode 100644 index 0000000000..dfe33d42c4 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-ab5c5903.t27 @@ -0,0 +1,15 @@ +// damage class DC-ab5c5903 +// shape: [[]?X(X)", +// lines: 1 in the corpus, across 1 file(s) +// origin: specs/tri/collections/skip_list.t27:15 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[]?SkipNode(T)", + + +// expected: parse-restored + +pub const DamageCase = struct { + forward : [[]?SkipNode(T)", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-b8514836.t27 b/bootstrap/tests/fixtures/damage_classes/DC-b8514836.t27 new file mode 100644 index 0000000000..1cdbcdc65e --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-b8514836.t27 @@ -0,0 +1,15 @@ +// damage class DC-b8514836 +// shape: [[]X X", +// lines: 37 in the corpus, across 17 file(s) +// origin: specs/tri/pipeline/spec_writer.t27:14 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[]Const u8", + + +// expected: parse-restored + +pub const DamageCase = struct { + field_type : [[]Const u8", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-c349b5d1.t27 b/bootstrap/tests/fixtures/damage_classes/DC-c349b5d1.t27 new file mode 100644 index 0000000000..10847cfa82 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-c349b5d1.t27 @@ -0,0 +1,15 @@ +// damage class DC-c349b5d1 +// shape: [[9]?X", +// lines: 2 in the corpus, across 2 file(s) +// origin: specs/tri/trees/octree.t27:24 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[8]?OctNode", + + +// expected: parse-restored + +pub const DamageCase = struct { + children : [[8]?OctNode", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-c580e907.t27 b/bootstrap/tests/fixtures/damage_classes/DC-c580e907.t27 new file mode 100644 index 0000000000..6250e87dcd --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-c580e907.t27 @@ -0,0 +1,15 @@ +// damage class DC-c580e907 +// shape: [X.X(X, []X)", +// lines: 1 in the corpus, across 1 file(s) +// origin: specs/tri/graph/graph.t27:14 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "std.HashMap(T, []T)", + + +// expected: parse-restored + +pub const DamageCase = struct { + nodes : [std.HashMap(T, []T)", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-d9efbc31.t27 b/bootstrap/tests/fixtures/damage_classes/DC-d9efbc31.t27 new file mode 100644 index 0000000000..7c471a5ad6 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-d9efbc31.t27 @@ -0,0 +1,15 @@ +// damage class DC-d9efbc31 +// shape: [[]X", +// lines: 52 in the corpus, across 44 file(s) +// origin: specs/tri/pipeline/builder.t27:14 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[]T", + + +// expected: parse-restored + +pub const DamageCase = struct { + items : [[]T", +}; diff --git a/bootstrap/tests/fixtures/damage_classes/DC-f918a784.t27 b/bootstrap/tests/fixtures/damage_classes/DC-f918a784.t27 new file mode 100644 index 0000000000..0446698856 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_classes/DC-f918a784.t27 @@ -0,0 +1,15 @@ +// damage class DC-f918a784 +// shape: [[9]?*X", +// lines: 1 in the corpus, across 1 file(s) +// origin: specs/tri/search/aho_corasick.t27:14 +// snapshot: 1b5a37b7a89efb782db7efd2ca7af728d4ed48b1c13ac2a35088c7926b16afd9 +// +// The damage: the opening quote of the type string was replaced by '['. +// candidate: replace the leading [ with a quote -> "[256]?*ACTrieNode", + + +// expected: parse-restored + +pub const DamageCase = struct { + children : [[256]?*ACTrieNode", +}; diff --git a/bootstrap/tests/fixtures/damage_negative/neg_01_latency_bound.t27 b/bootstrap/tests/fixtures/damage_negative/neg_01_latency_bound.t27 new file mode 100644 index 0000000000..82cc908456 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_negative/neg_01_latency_bound.t27 @@ -0,0 +1,7 @@ +// NEGATIVE fixture: a latency bound, not damage. +// The first regex reported 230 of these as damaged, which made 429 a metric of +// the regex rather than of the corpus. tri damage must report zero here. +pub const Budget = struct { + target : < 5000ns, + ceiling : < 12us, +}; diff --git a/bootstrap/tests/fixtures/damage_negative/neg_02_match_arms.t27 b/bootstrap/tests/fixtures/damage_negative/neg_02_match_arms.t27 new file mode 100644 index 0000000000..854b93ffb4 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_negative/neg_02_match_arms.t27 @@ -0,0 +1,8 @@ +// NEGATIVE fixture: match arms. 17 false positives came from these. +fn classify(x: u8) -> u8 { + match x { + 0 : 1, + 1 : 2, + _ : 0, + } +} diff --git a/bootstrap/tests/fixtures/damage_negative/neg_03_multiline_array.t27 b/bootstrap/tests/fixtures/damage_negative/neg_03_multiline_array.t27 new file mode 100644 index 0000000000..870c3b80b5 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_negative/neg_03_multiline_array.t27 @@ -0,0 +1,9 @@ +// NEGATIVE fixture: an array literal opening a multi-line value. 10 false +// positives came from the opening line being read as a damaged type side. +pub const Table = struct { + rows : "[]u8", +}; +pub const DATA = [ + 1, 2, 3, + 4, 5, 6, +]; diff --git a/bootstrap/tests/fixtures/damage_negative/neg_04_fn_signature.t27 b/bootstrap/tests/fixtures/damage_negative/neg_04_fn_signature.t27 new file mode 100644 index 0000000000..59fa90449c --- /dev/null +++ b/bootstrap/tests/fixtures/damage_negative/neg_04_fn_signature.t27 @@ -0,0 +1,7 @@ +// NEGATIVE fixture: function signatures. 6 false positives came from these. +fn insert(key: []const u8, value: u32) -> bool { + return true; +} +fn find(needle: []const u8) -> ?usize { + return null; +} diff --git a/bootstrap/tests/fixtures/damage_negative/neg_05_raw_string.t27 b/bootstrap/tests/fixtures/damage_negative/neg_05_raw_string.t27 new file mode 100644 index 0000000000..741cc33ece --- /dev/null +++ b/bootstrap/tests/fixtures/damage_negative/neg_05_raw_string.t27 @@ -0,0 +1,6 @@ +// NEGATIVE fixture: a legal raw string whose body contains a bracket and an odd +// number of quotes. The odd-quote signal must not fire inside a raw string. +pub const Patterns = struct { + rx : r#"[a-z]+"[0-9]"#, + tpl : "[]u8", +}; diff --git a/bootstrap/tests/fixtures/damage_negative/neg_06_intact_field.t27 b/bootstrap/tests/fixtures/damage_negative/neg_06_intact_field.t27 new file mode 100644 index 0000000000..5e7108cf39 --- /dev/null +++ b/bootstrap/tests/fixtures/damage_negative/neg_06_intact_field.t27 @@ -0,0 +1,8 @@ +// NEGATIVE fixture: the intact convention itself, including the slice and +// optional-slice forms that the damaged classes are corruptions of. +pub const Intact = struct { + children : "[4]?QuadNode", + parts : "[]Const u8", + name : "[]u8", + level : "usize", +}; diff --git a/bootstrap/tests/fixtures/terminator/eof_generic.t27 b/bootstrap/tests/fixtures/terminator/eof_generic.t27 new file mode 100644 index 0000000000..6bbe35ead1 --- /dev/null +++ b/bootstrap/tests/fixtures/terminator/eof_generic.t27 @@ -0,0 +1,8 @@ +module eof_generic + +// End of file inside an unclosed generic argument list. The lexer returns Eof +// for every subsequent call, so a recovery scanner that refuses to accept Eof as +// a terminator has a stationary state on an infinite input, and the loop is a +// consequence of the construction rather than a rare race. +pub struct Hangs { + a : Map, + tuple : (A, B), + nested : Vec>, + arr : [4]u16, + last : i32 +} + +pub fn use_holder(h : Holder) -> u8 { + return h.plain; +} diff --git a/bootstrap/tests/fixtures/terminator/semicolon_phantom.t27 b/bootstrap/tests/fixtures/terminator/semicolon_phantom.t27 new file mode 100644 index 0000000000..a08c37c7c4 --- /dev/null +++ b/bootstrap/tests/fixtures/terminator/semicolon_phantom.t27 @@ -0,0 +1,6 @@ +module semicolon_phantom + +pub struct Semi { + a : Map, u8), + last : u16 +} diff --git a/bootstrap/tests/fixtures/terminator/unbalanced.t27 b/bootstrap/tests/fixtures/terminator/unbalanced.t27 new file mode 100644 index 0000000000..1977f275de --- /dev/null +++ b/bootstrap/tests/fixtures/terminator/unbalanced.t27 @@ -0,0 +1,8 @@ +module unbalanced + +pub struct Broken { + a : Map u8 { return 0; } diff --git a/bootstrap/tests/struct_body_terminator.rs b/bootstrap/tests/struct_body_terminator.rs new file mode 100644 index 0000000000..0eb0e5fb4e --- /dev/null +++ b/bootstrap/tests/struct_body_terminator.rs @@ -0,0 +1,325 @@ +// ============================================================================ +// #2127 -- nesting decides the separator, never the terminator. +// +// The field collector consults bracket nesting depth when it meets a `Comma`, +// because a comma inside `Map` or `(A, B)` separates type arguments and +// not fields. It must NOT consult depth when it meets a terminator: `RBrace`, +// `Semicolon` and `Eof` end the field list at any depth. Truncated input leaves +// the depth counter positive, so a depth-gated terminator is never accepted and +// the loop runs to the end of the token stream -- or, with a buggy bound, does +// not stop at all. +// +// The test that matters here is therefore a LIVENESS test, not an output test: +// a spec whose last line opens a bracket and then ends must make the parser +// TERMINATE. Asserting on the error message alone would pass even if the parser +// hung, because a hung process never produces a message to compare -- the test +// would sit there until the CI job's own timeout killed it, and report as +// infrastructure flake rather than as this defect. +// +// So every case here runs under a HARD wall-clock timeout enforced in-process. +// Exceeding it fails the test with a message that names the hang, and the child +// is killed so a wedged parser cannot outlive the test binary. +// +// Deliberately NOT asserted: the exact text of the parse error. On malformed +// input there is no single correct reading of the field list, and pinning the +// message would freeze one arbitrary recovery as the specification. What is +// pinned is: the process ends, its exit status is non-zero, it names the token +// it stopped at, and it fails as a diagnostic rather than as a panic. +// +// The wording is deliberately not pinned either. The recorded baseline for this +// fixture read `Error: Parse error: Expected RBrace, got Eof` and the binary now +// prints `Error: Expected RBrace, got Eof` -- the prefix moved at some point +// between the two. An assertion on the phrase "parse" fails on that alone while +// the parser behaves correctly, which is a test measuring diagnostic prose +// instead of parser behaviour. It asserts on the token names instead, because +// those are what the invariant is about. +// ============================================================================ + +use std::io::Read; +use std::path::PathBuf; +use std::process::{Command, Stdio}; +use std::sync::mpsc; +use std::thread; +use std::time::{Duration, Instant}; + +/// Wall-clock ceiling for a single parse of a four-line fixture. Generous by +/// three orders of magnitude: these files are under 100 bytes, and a healthy +/// parse of them is a few milliseconds even on a loaded shared runner. The +/// number exists to separate "slow" from "not returning", and 10 s cannot be +/// reached by any amount of ordinary slowness on this input. +const HARD_TIMEOUT: Duration = Duration::from_secs(10); + +fn t27c() -> &'static str { + env!("CARGO_BIN_EXE_t27c") +} + +/// Pull the `(field name, type text)` pairs out of a `t27c parse` dump. +/// +/// The three liveness cases above use `check`, which prints a verdict and hides +/// the field list. The two cases below are about the field list itself, so they +/// read the parse dump. This is the same extraction the corpus differential used, +/// reimplemented here without a regex crate: find each `name:` line and take the +/// `extra_type:` two lines below it. +fn fields(name: &str) -> Vec<(String, String)> { + let path = fixture(name); + let out = Command::new(t27c()) + .arg("parse") + .arg(&path) + .output() + .expect("failed to spawn t27c parse"); + let text = format!( + "{}{}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + let lines: Vec<&str> = text.lines().collect(); + let unquote = |s: &str, key: &str| -> Option { + let t = s.trim(); + let rest = t.strip_prefix(key)?.trim(); + let rest = rest.strip_prefix('"')?; + let end = rest.rfind('"')?; + Some(rest[..end].to_string()) + }; + let mut out_pairs = Vec::new(); + for i in 0..lines.len() { + if !lines[i].trim_start().starts_with("kind: ExprIdentifier") { + continue; + } + if i + 3 >= lines.len() { + continue; + } + let n = unquote(lines[i + 1], "name:"); + let ty = unquote(lines[i + 3], "extra_type:"); + if let (Some(n), Some(ty)) = (n, ty) { + out_pairs.push((n, ty)); + } + } + out_pairs +} + +fn fixture(name: &str) -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") + .join("terminator") + .join(name) +} + +struct Outcome { + status: i32, + output: String, + elapsed: Duration, +} + +/// Run `t27c check ` under a hard timeout. +/// +/// The child is spawned, then waited on from a helper thread so the main thread +/// keeps a clock the child cannot influence. On timeout the child is killed +/// before the assertion fires, so a wedged parser does not survive the test. +fn parse_within_timeout(name: &str) -> Outcome { + let path = fixture(name); + assert!(path.exists(), "fixture missing: {}", path.display()); + + let mut child = Command::new(t27c()) + .arg("check") + .arg(&path) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .expect("failed to spawn t27c"); + + let started = Instant::now(); + let mut out = child.stdout.take().expect("stdout"); + let mut err = child.stderr.take().expect("stderr"); + + // Drain both pipes from threads. A parser that fills a pipe buffer and then + // blocks on write would otherwise look like a hang caused by this test. + let (otx, orx) = mpsc::channel(); + thread::spawn(move || { + let mut s = String::new(); + let _ = out.read_to_string(&mut s); + let _ = otx.send(s); + }); + let (etx, erx) = mpsc::channel(); + thread::spawn(move || { + let mut s = String::new(); + let _ = err.read_to_string(&mut s); + let _ = etx.send(s); + }); + + // Poll rather than block, so the timeout is owned by this thread. + let status = loop { + match child.try_wait().expect("try_wait") { + Some(st) => break st, + None => { + if started.elapsed() > HARD_TIMEOUT { + let _ = child.kill(); + let _ = child.wait(); + panic!( + "HANG: t27c check {} did not terminate within {:?}. \ + This is #2127: a terminator ({{RBrace, Semicolon, Eof}}) was \ + gated on bracket nesting depth, and truncated input leaves \ + the depth positive, so the field loop never accepted an end.", + name, HARD_TIMEOUT + ); + } + thread::sleep(Duration::from_millis(20)); + } + } + }; + + let elapsed = started.elapsed(); + let combined = format!( + "{}{}", + orx.recv_timeout(Duration::from_secs(5)).unwrap_or_default(), + erx.recv_timeout(Duration::from_secs(5)).unwrap_or_default() + ); + Outcome { + status: status.code().unwrap_or(-1), + output: combined, + elapsed, + } +} + +/// The case the fix is about: the file ends mid-type, at depth 1, with no +/// closing brace anywhere. `Eof` must end the field list at any depth. +#[test] +fn eof_at_positive_depth_terminates() { + let r = parse_within_timeout("eof_hazard.t27"); + assert!( + r.elapsed < HARD_TIMEOUT, + "took {:?}, ceiling {:?}", + r.elapsed, + HARD_TIMEOUT + ); + assert_ne!( + r.status, 0, + "truncated input must be rejected, not accepted; output was:\n{}", + r.output + ); + let lower = r.output.to_lowercase(); + assert!( + lower.contains("eof"), + "the diagnostic must name the end of input it stopped at, got:\n{}", + r.output + ); + assert!( + !lower.contains("panicked") && !lower.contains("unwrap"), + "must fail as a diagnostic, not as a panic:\n{}", + r.output + ); +} + +/// A type argument list left open, then a further field, then a closing brace. +/// `RBrace` must end the field list even though depth is still positive. +#[test] +fn rbrace_at_positive_depth_terminates() { + let r = parse_within_timeout("unbalanced.t27"); + assert!( + r.elapsed < HARD_TIMEOUT, + "took {:?}, ceiling {:?}", + r.elapsed, + HARD_TIMEOUT + ); + let lower = r.output.to_lowercase(); + assert!( + !lower.contains("panicked"), + "must not panic:\n{}", + r.output + ); +} + +/// The control, and the reason the two above are not vacuous: a well-formed +/// struct whose fields contain commas inside `Map`, `(A, B)`, `Vec>` +/// and `[4]u16` must still parse cleanly. A fix that accepted any terminator by +/// ignoring depth altogether would break this one, by treating a comma inside a +/// type argument list as a field separator. +#[test] +fn commas_inside_types_are_not_field_separators() { + let r = parse_within_timeout("nested_types.t27"); + assert!( + r.elapsed < HARD_TIMEOUT, + "took {:?}, ceiling {:?}", + r.elapsed, + HARD_TIMEOUT + ); + assert_eq!( + r.status, 0, + "well-formed nested types must parse; output was:\n{}", + r.output + ); +} + +// --------------------------------------------------------------------------- +// The two cases below discriminate the fixed parser from the unfixed one. The +// three liveness cases above do NOT: run against the pre-fix binary they pass +// unchanged, because `check` prints the same verdict either way. A test that +// cannot fail on the defect it names is a regression guard, not evidence, and +// these two exist so the branch has at least one of each. +// --------------------------------------------------------------------------- + +/// The improvement, stated as a field set. `Map` should be. Before the fix the collector produced a PHANTOM field +/// named `V` with an empty type -- an identifier from inside a type argument +/// list promoted to a field of the struct. After the fix `Semicolon` terminates +/// at any depth, `V` stays inside the type text, and the struct has exactly the +/// two fields it was written with. +/// +/// This is the case that fails on the pre-fix binary, and the reason the change +/// is worth making. +#[test] +fn semicolon_at_depth_does_not_invent_a_field() { + let f = fields("semicolon_phantom.t27"); + let names: Vec<&str> = f.iter().map(|(n, _)| n.as_str()).collect(); + assert!( + !names.contains(&"V"), + "phantom field V promoted out of a type argument list: {:?}", + f + ); + assert_eq!(names, vec!["a", "b"], "expected exactly the declared fields: {:?}", f); +} + +/// The cost, pinned so it cannot grow unnoticed. +/// +/// On a type argument list left open by a comma, the fixed collector absorbs the +/// following `name : type` pairs into the type text of the first field: three +/// declared fields become one, whose type reads `Map