1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
//! Substring-search op id compatibility contract.
//!
//! `substring_search` moved from `vyre_libs::matching` to `vyre_libs::scan`.
//! The move kept a deprecated alias at the old path, and that alias keeps
//! emitting the OLD op id so already-recorded conformance rows, cache keys,
//! and operator dashboards that key on `vyre-libs::matching::substring_search`
//! keep resolving. Two op ids for one builder is a deliberate, bounded
//! compatibility decision, not drift.
//!
//! This suite exists because the split was easy to get backwards: an
//! integration test asserted the canonical builder emitted the LEGACY id and
//! nothing caught it, because the workspace test run could not complete. Each
//! test below pins one half of the contract so neither side can silently take
//! the other's identity.
//!
//! What breaks if this regresses: if the canonical builder emits the legacy
//! id, every new conformance row is filed under a deprecated op id that the
//! removal condition says will disappear. If the alias emits the canonical id,
//! existing consumers pinned to the old id stop matching and their programs
//! fall out of the registry silently.
#![cfg(feature = "matching-substring")]
#![allow(deprecated)]
use vyre::ir::{Node, Program};
use vyre_libs::compat_aliases::{CompatibilityAlias, MATCHING_SUBSTRING_ALIAS};
/// The op id the canonical `vyre_libs::scan` path must emit.
const CANONICAL_OP_ID: &str = "vyre-libs::scan::substring_search";
/// The op id the deprecated `vyre_libs::matching` path must keep emitting.
const LEGACY_OP_ID: &str = "vyre-libs::matching::substring_search";
/// Extract the single region generator name from a substring program.
///
/// Every vyre-libs builder emits exactly one top-level `Node::Region`, and the
/// generator is the op id. Anything else is a shape change that these tests
/// should fail on rather than silently look past.
fn region_generator(program: &Program) -> String {
let entry = program.entry();
assert_eq!(
entry.len(),
1,
"a substring program is exactly one top-level region node, got {} nodes",
entry.len()
);
match &entry[0] {
Node::Region { generator, .. } => generator.as_str().to_string(),
other => panic!("expected Node::Region at the program entry, got {other:?}"),
}
}
fn canonical(haystack_len: u32, needle_len: u32) -> Program {
vyre_libs::scan::substring_search("haystack", "needle", "matches", haystack_len, needle_len)
}
fn legacy(haystack_len: u32, needle_len: u32) -> Program {
vyre_libs::matching::substring::substring_search(
"haystack",
"needle",
"matches",
haystack_len,
needle_len,
)
}
/// The canonical path emits the canonical op id.
///
/// This is the assertion that was inverted in `integration.rs`. Locking it
/// here means the canonical builder cannot drift back onto the deprecated
/// identity that `MATCHING_SUBSTRING_ALIAS.removal_condition` schedules for
/// deletion.
#[test]
fn the_scan_path_emits_the_canonical_op_id() {
assert_eq!(region_generator(&canonical(8, 3)), CANONICAL_OP_ID);
}
/// The deprecated path keeps the legacy op id.
///
/// The whole point of the alias is identity preservation. If it started
/// emitting the canonical id, consumers keyed on the old id would see their
/// programs vanish from the registry with no error, which is exactly the kind
/// of silent behavior change Law 10 forbids.
#[test]
fn the_matching_alias_emits_the_legacy_op_id() {
assert_eq!(region_generator(&legacy(8, 3)), LEGACY_OP_ID);
}
/// The two ids are distinct.
///
/// A rename that collapsed both onto one string would make every other test in
/// this file pass trivially while destroying the compatibility guarantee.
#[test]
fn the_canonical_and_legacy_op_ids_are_not_the_same_string() {
assert_ne!(CANONICAL_OP_ID, LEGACY_OP_ID);
assert_ne!(
region_generator(&canonical(8, 3)),
region_generator(&legacy(8, 3))
);
}
/// Re-exporting the canonical function from `matching` does not relabel it.
///
/// `vyre_libs::matching::substring_search` (the flat re-export, not the
/// `substring` leaf module) is a plain `pub use` of the canonical function, so
/// it must produce the canonical id. Only the `substring` leaf module carries
/// the legacy identity. These two neighbours under the same `matching` parent
/// are the easiest pair in the crate to confuse.
#[test]
fn the_flat_matching_reexport_is_the_canonical_function_not_the_alias() {
let via_reexport = vyre_libs::matching::substring_search("haystack", "needle", "matches", 8, 3);
assert_eq!(region_generator(&via_reexport), CANONICAL_OP_ID);
}
/// The op id is the ONLY difference between the two builders.
///
/// The alias must be a relabel, never a fork. If someone edits the canonical
/// builder and forgets the alias (or vice versa), the two programs diverge in
/// buffers or body and consumers on the old path silently get different work.
#[test]
fn the_alias_differs_from_the_canonical_builder_only_in_its_op_id() {
let canonical = canonical(8, 3);
let legacy = legacy(8, 3);
assert_eq!(canonical.buffers().len(), legacy.buffers().len());
for (c, l) in canonical.buffers().iter().zip(legacy.buffers()) {
assert_eq!(c.name(), l.name(), "buffer names must match");
assert_eq!(c.binding(), l.binding(), "buffer bindings must match");
assert_eq!(c.access(), l.access(), "buffer access must match");
assert_eq!(c.kind(), l.kind(), "buffer memory kinds must match");
assert_eq!(c.element(), l.element(), "buffer element types must match");
assert_eq!(c.count(), l.count(), "buffer element counts must match");
}
assert_eq!(canonical.workgroup_size(), legacy.workgroup_size());
// Substituting the legacy id for the canonical one must make the two
// regions byte-identical: same body, same everything but the label.
let relabelled = format!("{:?}", legacy.entry()[0]).replace(LEGACY_OP_ID, CANONICAL_OP_ID);
assert_eq!(
format!("{:?}", canonical.entry()[0]),
relabelled,
"the alias must relabel the region, never fork its body"
);
}
/// Both builders are deterministic across sizes.
///
/// The id must not be derived from, or perturbed by, the buffer sizes. A
/// size-dependent generator name would fragment the conformance matrix.
#[test]
fn both_op_ids_are_stable_across_haystack_and_needle_sizes() {
for (haystack_len, needle_len) in [(1_u32, 1_u32), (8, 3), (64, 7), (4096, 32)] {
assert_eq!(
region_generator(&canonical(haystack_len, needle_len)),
CANONICAL_OP_ID,
"canonical id changed at haystack_len={haystack_len} needle_len={needle_len}"
);
assert_eq!(
region_generator(&legacy(haystack_len, needle_len)),
LEGACY_OP_ID,
"legacy id changed at haystack_len={haystack_len} needle_len={needle_len}"
);
}
}
/// Both programs pass IR validation.
///
/// A compatibility path is still a shipped path. The alias does not get to be
/// a builder that produces an invalid program because "nobody new uses it".
#[test]
fn both_paths_produce_programs_that_validate() {
for program in [canonical(8, 3), legacy(8, 3)] {
let errors = vyre::ir::validate(&program);
assert!(
errors.is_empty(),
"substring program failed validation: {:?}",
errors
.iter()
.map(|e| e.message().to_string())
.collect::<Vec<_>>()
);
}
}
/// The alias registry row names the real owner.
///
/// `compat_aliases` is the ONE place that records where a shim points. Facade
/// docs, release gates, and import audits all read it, so a row that drifts
/// from the module tree misdirects every one of them at once.
#[test]
fn the_alias_registry_row_points_at_the_canonical_substring_module() {
let alias: CompatibilityAlias = MATCHING_SUBSTRING_ALIAS;
assert_eq!(alias.deprecated_path, "vyre_libs::matching::substring");
assert_eq!(alias.canonical_path, "vyre_libs::scan::substring");
assert_eq!(alias.canonical_owner, "vyre-libs/src/scan/substring");
assert!(
!alias.removal_condition.is_empty() && alias.removal_condition.contains("substring"),
"the removal condition must name a concrete substring-specific gate, got {:?}",
alias.removal_condition
);
}
/// The registry row and the emitted op ids describe the same rename.
///
/// The row is written in Rust path syntax (`vyre_libs::scan::substring`) and
/// the op ids in crate-name syntax (`vyre-libs::scan::substring_search`).
/// Nothing enforced that they agreed, so a future rename could move the module
/// and leave the op id behind. This ties the two spellings together.
#[test]
fn the_registry_row_and_the_op_ids_agree_on_which_side_is_canonical() {
let canonical_module = MATCHING_SUBSTRING_ALIAS.canonical_path.replace('_', "-");
let deprecated_module = MATCHING_SUBSTRING_ALIAS.deprecated_path.replace('_', "-");
assert_eq!(canonical_module, "vyre-libs::scan::substring");
assert_eq!(deprecated_module, "vyre-libs::matching::substring");
// `vyre-libs::scan::substring` -> `vyre-libs::scan::` is the op id prefix.
let canonical_prefix = canonical_module
.rsplit_once("::")
.expect("the canonical path has a module segment")
.0;
let deprecated_prefix = deprecated_module
.rsplit_once("::")
.expect("the deprecated path has a module segment")
.0;
assert_eq!(
CANONICAL_OP_ID,
format!("{canonical_prefix}::substring_search")
);
assert_eq!(
LEGACY_OP_ID,
format!("{deprecated_prefix}::substring_search")
);
}