• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Qiskit / qiskit / 26659970905

29 May 2026 08:15PM UTC coverage: 87.545% (-0.02%) from 87.564%
26659970905

push

github

web-flow
Use a single type alias for indexmap and indexset to enforce hasher (#16130)

Building off of #15920 which moved our hasher for IndexMap usage to be
foldhash this moves to having a common type alias for IndexMap and
IndexSet that has the hasher set. All the internal usage of IndexMap and
IndexSet are updated to use this new alias instead which removes the
boiler plate around dealing with initializing the hasher for the most
part, the only exception is when using `with_capacity_and_hasher()`
there is no method to create an empty object with a capacity
preallocated and the hasher is initialized from the generic type. To
enforce that we're using the correct hasher for all indexmap usage the
clippy rules are updated to disallow the indexmap::IndexMap and
indexmap::IndexSet types directly. This means that after this commit all
indexmap usage with be forced by clippy to leverage our standard pattern
using foldhash. The advantage is if we ever need to change the default
hasher again in the future we only need to update one central place
(and the places using with_capacity_and_hasher).

152 of 161 new or added lines in 29 files covered. (94.41%)

26 existing lines in 5 files now uncovered.

108486 of 123920 relevant lines covered (87.55%)

963878.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/crates/bindgen-cli/src/abi.rs
1
// This code is part of Qiskit.
2
//
3
// (C) Copyright IBM 2026
4
//
5
// This code is licensed under the Apache License, Version 2.0. You may
6
// obtain a copy of this license in the LICENSE.txt file in the root directory
7
// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0.
8
//
9
// Any modifications or derivative works of this code must retain this
10
// copyright notice, and modified files need to carry a notice indicating
11
// that they have been altered from the originals.
12

13
use std::fmt::{self, Write};
14

15
use anyhow::{anyhow, bail};
16
use itertools::EitherOrBoth;
17
use qiskit_cext_vtable::ExportedFunction;
18
use qiskit_util::IndexMap;
19

20
/// What type of change is permitted in the ABI between the two versions?
21
#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)]
22
pub enum SemVer {
23
    Bugfix,
24
    Feature,
25
    Breaking,
26
}
27
impl SemVer {
28
    fn from_versions(old: &Version, new: &Version) -> anyhow::Result<Self> {
×
29
        if (old.major, old.minor, old.patch) > (new.major, new.minor, new.patch) {
×
30
            bail!("'old' version {} is actually newer than {}", old, new);
×
31
        }
×
32
        if old.major < new.major {
×
33
            Ok(Self::Breaking)
×
34
        } else if old.minor < new.minor {
×
35
            Ok(Self::Feature)
×
36
        } else {
37
            // This also includes two equal versions, which seems possible for developing, and
38
            // comparing two `-dev` versions that aren't actually equal.
39
            Ok(Self::Bugfix)
×
40
        }
41
    }
×
42
}
43

44
/// The minimal semver components extracted from a version that might also have suffixes.
45
#[derive(Clone, Debug)]
46
pub struct Version {
47
    major: usize,
48
    minor: usize,
49
    patch: usize,
50
    suffix: Option<String>,
51
}
52
impl Version {
53
    /// Parse a version out of a `<major>.<minor>.<patch>(-<suffix>)?` form.
54
    fn try_parse(val: &str) -> anyhow::Result<Version> {
×
55
        let (val, suffix) = match val.split_once("-") {
×
56
            Some((val, suffix)) => (val, Some(suffix.to_owned())),
×
57
            None => (val, None),
×
58
        };
59
        let mut parts = val.split(".");
×
60
        let mut part = || -> anyhow::Result<usize> {
×
61
            Ok(parts
×
62
                .next()
×
63
                .ok_or_else(|| anyhow!("not enough version parts"))?
×
64
                .parse()?)
×
65
        };
×
66
        Ok(Version {
67
            major: part()?,
×
68
            minor: part()?,
×
69
            patch: part()?,
×
70
            suffix,
×
71
        })
72
    }
×
73
}
74
impl fmt::Display for Version {
75
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
76
        write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
×
77
        if let Some(suffix) = self.suffix.as_ref() {
×
78
            write!(f, "-{suffix}")?;
×
79
        }
×
80
        Ok(())
×
81
    }
×
82
}
83

84
/// Complete set of exported slots and version for comparison.
85
pub struct SlotsLists {
86
    pub api_version: Version,
87
    /// Association of names to concrete vtables.
88
    pub slots: IndexMap<String, SlotsList>,
89
}
90
impl SlotsLists {
91
    /// The slots lists that are defined in the depended-on `qiskit_cext_vtable`.
92
    pub fn ours() -> Self {
×
93
        let names = |funcs: Vec<Option<ExportedFunction>>| {
×
94
            SlotsList(
95
                funcs
×
96
                    .into_iter()
×
97
                    .map(|f| f.map(|f| f.name.to_owned()))
×
98
                    .collect(),
×
99
            )
100
        };
×
101
        Self {
102
            api_version: Version::try_parse(env!("CARGO_PKG_VERSION"))
×
103
                .expect("our version should be valid"),
×
104
            slots: [
×
105
                ("circuit", &qiskit_cext_vtable::FUNCTIONS_CIRCUIT),
×
106
                ("transpile", &qiskit_cext_vtable::FUNCTIONS_TRANSPILE),
×
107
                ("qi", &qiskit_cext_vtable::FUNCTIONS_QI),
×
108
            ]
×
109
            .iter()
×
110
            .map(|(name, funcs)| (String::from(*name), names(funcs.slots())))
×
111
            .collect(),
×
112
        }
113
    }
×
114

115
    /// Parse a slots list out of a string.
116
    ///
117
    /// The expected format of the string is a literal output of our own `Display` implementation,
118
    /// which in turn is the same as a call to `qiskit-bindgen-cli show-slots`.
119
    pub fn try_parse(val: &str) -> anyhow::Result<Self> {
×
120
        // This isn't fancy or trying to cleanly recover - we're mostly just expected the input to
121
        // be correct already.
122
        let mut lines = val.trim().lines();
×
123
        let Some(line) = lines.next() else {
×
124
            bail!("missing __version__");
×
125
        };
126
        let Some(version) = line.strip_prefix("__version__ =").map(str::trim) else {
×
127
            bail!("missing __version__");
×
128
        };
129
        let Some(version) = version.strip_prefix('"').and_then(|v| v.strip_suffix('"')) else {
×
130
            bail!("malformed __version__: not a string");
×
131
        };
NEW
132
        let mut slots_lists = IndexMap::default();
×
133
        while let Some(intro) = lines.next() {
×
134
            let Some((name, rest)) = intro.split_once(" = [") else {
×
135
                bail!("didn't find expected '<slots_name> = [' opener");
×
136
            };
137
            match rest.trim() {
×
138
                "" => (),
×
139
                "]" => {
×
140
                    slots_lists.insert(name.to_owned(), SlotsList(vec![]));
×
141
                    continue;
×
142
                }
143
                _ => bail!("unexpected line after '{name}': '{rest}'"),
×
144
            }
145
            let mut slots = Vec::new();
×
146
            for line in lines.by_ref().map(str::trim) {
×
147
                if line == "]" {
×
148
                    break;
×
149
                }
×
150
                let Some(func_name) = line
×
151
                    .strip_prefix('"')
×
152
                    .and_then(|line| line.strip_suffix("\","))
×
153
                else {
154
                    bail!("failed to parse slot line '{line}'");
×
155
                };
156
                slots.push((!func_name.is_empty()).then(|| String::from(func_name)));
×
157
            }
158
            slots_lists.insert(name.to_owned(), SlotsList(slots));
×
159
        }
160
        Ok(Self {
161
            api_version: Version::try_parse(version)?,
×
162
            slots: slots_lists,
×
163
        })
164
    }
×
165
}
166
impl fmt::Display for SlotsLists {
167
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
168
        writeln!(f, "__version__ = \"{}\"", &self.api_version)?;
×
169
        for (name, slots) in self.slots.iter() {
×
170
            writeln!(f, "{name} = {slots}")?;
×
171
        }
172
        Ok(())
×
173
    }
×
174
}
175

176
/// An individual concrete vtable, but written in terms of the function name instead of a pointer.
177
pub struct SlotsList(Vec<Option<String>>);
178
impl SlotsList {
179
    /// Iterate over the slot indices and the name stored there in order.
180
    pub fn iter_names(&self) -> impl Iterator<Item = (usize, &str)> {
×
181
        self.0
×
182
            .iter()
×
183
            .enumerate()
×
184
            .filter_map(|(i, fname)| fname.as_deref().map(|fname| (i, fname)))
×
185
    }
×
186
}
187
impl fmt::Display for SlotsList {
188
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
×
189
        // This is meant to be easy both to parse back in, and to be read by humans.  As written,
190
        // this `impl` also produces a valid Python list of strings that can be read by
191
        // `ast.literal_eval`.
192
        if self.0.is_empty() {
×
193
            write!(f, "[]")
×
194
        } else {
195
            writeln!(f, "[")?;
×
196
            for slot in self.0.iter() {
×
197
                writeln!(f, "    \"{}\",", slot.as_deref().unwrap_or_default())?;
×
198
            }
199
            write!(f, "]")
×
200
        }
201
    }
×
202
}
203

204
#[derive(Clone, Debug)]
205
pub enum SlotBreakage {
206
    Deleted(String),
207
    Changed { from: String, to: String },
208
}
209

210
#[derive(Clone, Debug, Default)]
211
pub struct Breakages {
212
    pub deleted_tables: Vec<String>,
213
    pub changed_slots: IndexMap<String, Vec<(usize, SlotBreakage)>>,
214
}
215
impl Breakages {
216
    pub fn is_empty(&self) -> bool {
×
217
        self.deleted_tables.is_empty() && self.changed_slots.is_empty()
×
218
    }
×
219
}
220

221
#[derive(Clone, Debug, Default)]
222
pub struct Features {
223
    pub new_tables: Vec<String>,
224
    pub new_slots: IndexMap<String, Vec<(usize, String)>>,
225
}
226
impl Features {
227
    pub fn is_empty(&self) -> bool {
×
228
        self.new_tables.is_empty() && self.new_slots.is_empty()
×
229
    }
×
230
}
231

232
/// Tracked failures from comparison of two slots lists.
233
#[derive(Clone, Debug)]
234
pub struct Changes {
235
    pub version_prev: Version,
236
    pub version_cur: Version,
237
    pub semver_allowed: SemVer,
238
    pub breakages: Breakages,
239
    pub features: Features,
240
}
241
impl Changes {
242
    pub fn semver_actual(&self) -> SemVer {
×
243
        if !self.breakages.is_empty() {
×
244
            SemVer::Breaking
×
245
        } else if !self.features.is_empty() {
×
246
            SemVer::Feature
×
247
        } else {
248
            SemVer::Bugfix
×
249
        }
250
    }
×
251

252
    pub fn is_allowed(&self) -> bool {
×
253
        self.semver_actual() <= self.semver_allowed
×
254
    }
×
255

256
    pub fn explain(&self) -> String {
×
257
        if self.is_allowed() {
×
258
            return format!(
×
259
                "Current slots list for version {} is compatible with previous version {}.",
260
                &self.version_cur, &self.version_prev,
×
261
            );
262
        }
×
263
        let mut explanation = format!(
×
264
            "Current slots list for version {} is incompatible with previous version {}.",
265
            &self.version_cur, &self.version_prev,
×
266
        );
267
        write!(
×
268
            explanation,
×
269
            " Allowed {:?}, but actual change is {:?}.",
270
            self.semver_allowed,
271
            self.semver_actual()
×
272
        )
273
        .unwrap();
×
274
        if !self.breakages.deleted_tables.is_empty() {
×
275
            write!(explanation, "\n\n[API break] Deleted tables:").unwrap();
×
276
            for table in &self.breakages.deleted_tables {
×
277
                write!(explanation, "\n* {table}").unwrap();
×
278
            }
×
279
        }
×
280
        if !self.breakages.changed_slots.is_empty() {
×
281
            write!(explanation, "\n\n[API break] Changed tables:").unwrap();
×
282
            for (table, changes) in &self.breakages.changed_slots {
×
283
                write!(explanation, "\n* {table}").unwrap();
×
284
                for (slot, change) in changes {
×
285
                    write!(explanation, "\n - {slot}: ").unwrap();
×
286
                    match change {
×
287
                        SlotBreakage::Deleted(name) => {
×
288
                            write!(explanation, "deleted '{name}'").unwrap()
×
289
                        }
290
                        SlotBreakage::Changed { from, to } => {
×
291
                            write!(explanation, "changed '{from}' to '{to}'").unwrap()
×
292
                        }
293
                    }
294
                }
295
            }
296
        }
×
297
        if !self.features.new_tables.is_empty() {
×
298
            write!(explanation, "\n\n[New feature] Added tables:").unwrap();
×
299
            for table in &self.features.new_tables {
×
300
                write!(explanation, "\n* {table}").unwrap();
×
301
            }
×
302
        }
×
303
        if !self.features.new_slots.is_empty() {
×
304
            write!(explanation, "\n\n[New feature] Added slots:").unwrap();
×
305
            for (table, slots) in &self.features.new_slots {
×
306
                write!(explanation, "\n* {table}").unwrap();
×
307
                for (slot, added) in slots {
×
308
                    write!(explanation, "\n - {slot}: {added}").unwrap();
×
309
                }
×
310
            }
311
        }
×
312
        explanation
×
313
    }
×
314
}
315

316
pub fn check(old: &SlotsLists, new: &SlotsLists) -> anyhow::Result<Changes> {
×
317
    let semver_allowed = SemVer::from_versions(&old.api_version, &new.api_version)?;
×
318
    // TODO: this is a very basic checker that's just to get a pass/fail check running in CI.  The
319
    // pass/fail should be entirely accurate, but its descriptions of _what_ changed are wildly
320
    // overcomplex in most cases.  What we actually want is a modification of a line-diff algorithm
321
    // (a Levenshtein-distance minimisation), because the actual most likely failure is a slot
322
    // _insertion_, which is the exact situation that this algorithm will produce the worst
323
    // explanations for (there's no concept of "moved" here).
324
    let mut breakages = Breakages::default();
×
325
    let mut features = Features::default();
×
326
    for (table, slots_old) in old.slots.iter() {
×
327
        let Some(slots_new) = new.slots.get(table) else {
×
328
            breakages.deleted_tables.push(table.clone());
×
329
            continue;
×
330
        };
331
        let mut changed_slots = Vec::new();
×
332
        let mut new_slots = Vec::new();
×
333
        for slot in itertools::merge_join_by(
×
334
            slots_old.iter_names(),
×
335
            slots_new.iter_names(),
×
336
            |(i, _), (j, _)| (*i).cmp(j),
×
337
        ) {
338
            match slot {
×
339
                EitherOrBoth::Left((slot, name)) => {
×
340
                    changed_slots.push((slot, SlotBreakage::Deleted(name.to_owned())));
×
341
                }
×
342
                EitherOrBoth::Right((slot, name)) => {
×
343
                    new_slots.push((slot, name.to_owned()));
×
344
                }
×
345
                EitherOrBoth::Both((slot, from), (_, to)) => {
×
346
                    if from != to {
×
347
                        changed_slots.push((
×
348
                            slot,
×
349
                            SlotBreakage::Changed {
×
350
                                from: from.to_owned(),
×
351
                                to: to.to_owned(),
×
352
                            },
×
353
                        ));
×
354
                    }
×
355
                }
356
            }
357
        }
358
        if !changed_slots.is_empty() {
×
359
            breakages
×
360
                .changed_slots
×
361
                .insert(table.to_owned(), changed_slots);
×
362
        }
×
363
        if !new_slots.is_empty() {
×
364
            features.new_slots.insert(table.to_owned(), new_slots);
×
365
        }
×
366
    }
367
    features.new_tables.extend(
×
368
        new.slots
×
369
            .keys()
×
370
            .filter(|table| !old.slots.contains_key(*table))
×
371
            .cloned(),
×
372
    );
373
    Ok(Changes {
×
374
        version_prev: old.api_version.clone(),
×
375
        version_cur: new.api_version.clone(),
×
376
        semver_allowed,
×
377
        breakages,
×
378
        features,
×
379
    })
×
380
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc