Skip to content

Follow-up on #63: documented quantum-device config schema does not deserialize on the target branch #149

Description

@spital

Hi maintainers and thanks for the configuration handler work that landed in #129.

This report comes from my local work on a PR attempt for issue #63 during UnitaryHack 2026. I was not able to open a PR for it because of the UnitaryHack 2026 PR limit, so I continued testing the current issue target branch locally and found a few behavior mismatches that seem worth tracking.

I tested against the current target branch, not main:

  • branch: dev/rust_impl/pre0.3beta/arch_scaffold
  • commit: 998624d7647afd998c8243dd6e662b3f426ad60a (merge of feat: add configuration handler #129)
  • date re-checked: 2026-06-27
  • baseline: cargo test config::base passes all 8 existing config tests

The main issue is that a quantum_config.toml written to the schema described in #63 does not currently deserialize. The other items are smaller follow-ups found while checking the same code path.

1. A config written to the #63 documented schema does not load

Issue #63 describes a TOML layout intended to be easy to write manually. A config following that layout currently fails at deserialization because the implemented Rust structs expect different field names or values:

#63 schema Current implementation expects Observed result
device_kind = "simulator" / "device" device_kind = "Simulator" / "Device" unknown variant 'simulator', expected 'Simulator' or 'Device'
[devices.specs] [devices.device_specs] missing field 'device_specs'
no priority key in the documented schema mandatory priority: isize missing field 'priority'

Expected result:

A hand-written config using the schema from #63 loads through Config::from_file.

Actual result:

It fails to deserialize. For example:

title = "H-hat Compiler Quantum Device Configuration"
version = "0.1.0"

[[devices]]
name = "AerSimulator"
is_active = true
vendor = "IBM"
device_kind = "simulator"
device_platform = "superconducting"
computation_paradigm = ["gate"]

[devices.specs]
max_n_qubits = 32
qubit_topology = []

[devices.qll]
name = "openqasm"
package_name = "qiskit"
package_version = "1.0.0"
file_extension = "qasm"

prints:

ERROR: unknown variant `simulator`, expected `Simulator` or `Device`

After changing only the enum casing, the next error is:

ERROR: missing field `device_specs`

After changing only [devices.specs] to [devices.device_specs], the next error is:

ERROR: missing field `priority`

The implementation is internally consistent when it reads back what it writes. The mismatch is specifically between the documented/issue schema and the currently accepted TOML shape.

2. add_device reverses equal-priority insertion order

Device::priority is documented as keeping equal-priority devices in insertion order, oldest first.

Config::new behaves that way because it uses a stable sort. add_device does the opposite for equal priorities.

Expected result:

Adding devices A, B, and C with equal priority should produce the same order as constructing the config with those devices at once:

["A", "B", "C"]

Actual result:

Config::new order = ["A", "B", "C"]
add_device  order = ["C", "B", "A"]

The direct cause appears to be:

partition_point(|device| device.priority > new_device.priority)

That inserts the new device before existing devices with the same priority. The inline comment in add_device also says it is finding the first inactive device, but the code is actually ordering by priority.

3. extra serializes with non-deterministic key order

Device.extra is currently:

Option<HashMap<String, toml::Value>>

Because it is a std::collections::HashMap, the emitted key order changes across process runs. That makes to_file output noisy for configs with two or more extra keys.

Expected result:

Serializing the same config repeatedly should produce stable bytes, especially for a config file that may be committed and reviewed in diffs.

Actual result:

With keys inserted as k0..k7, four fresh test processes emitted:

["k6", "k7", "k1", "k5", "k0", "k4", "k2", "k3"]
["k6", "k0", "k7", "k4", "k1", "k2", "k3", "k5"]
["k5", "k2", "k4", "k3", "k7", "k0", "k1", "k6"]
["k2", "k3", "k4", "k5", "k7", "k0", "k6", "k1"]

The toml crate's preserve_order feature does not make a standard HashMap deterministic.

4. PartialEq for Config ignores extra

The custom PartialEq implementation for Config intentionally skips Device.extra with this comment:

// Since toml::Value doesn't implement PartialEq, we define the equality up to the `extra` field

toml::Value does implement PartialEq; this compiles:

fn assert_partial_eq<T: PartialEq>() {}
assert_partial_eq::<toml::Value>();

Actual effect:

Two configs that differ only in extra compare equal:

(config with extra) == (config without extra) ? true

This also means the current round-trip tests would not catch a regression that dropped or corrupted extra.

5. At-least-one-active-device validation is not enforced

Issue #63 says:

There must be at least one `is_active = true` device.

On the current target branch:

Config::new("t".into(), "v".into(), vec![])

returns Ok, and the active-device count is 0.

This may be intentional, since commit f4ae76d is titled fix: remove check for at least one active device and add priority. I am including it only because the issue text still lists it as a requirement, while Config::new still returns a Result that is now always Ok.

Reproduction commands

From a clean checkout:

git clone https://github.com/hhat-lang/hhat_lang.git
cd hhat_lang
git fetch origin
git switch --track origin/dev/rust_impl/pre0.3beta/arch_scaffold
git pull --ff-only origin dev/rust_impl/pre0.3beta/arch_scaffold
git rev-parse HEAD
cd rust/hhat_lang
cargo test config::base

Then append this temporary test module to the end of src/config/base.rs in the Rust crate (or rust/hhat_lang/src/config/base.rs from the repository root) and run the commands below it.

#[cfg(test)]
mod issue63_repro {
    use std::collections::HashMap;

    use super::{Config, Device, DeviceKind, DeviceQuantumLowLevel, DeviceSpecs};

    fn mk(name: &str, priority: isize, extra: Option<HashMap<String, toml::Value>>) -> Device {
        Device::new(
            name.to_string(),
            true,
            "IBM".to_string(),
            DeviceKind::Simulator,
            "superconducting".to_string(),
            vec!["gate".to_string()],
            DeviceSpecs {
                max_n_qubits: 5,
                qubit_topology: vec![(0, 1)],
            },
            DeviceQuantumLowLevel {
                name: "openqasm".to_string(),
                package_name: "qiskit".to_string(),
                package_version: "0.1.0".to_string(),
                file_extension: "qasm".to_string(),
            },
            extra,
            priority,
        )
    }

    fn names(config: &Config) -> Vec<String> {
        config
            .get_devices()
            .iter()
            .map(|device| device.name.clone())
            .collect()
    }

    fn load(label: &str, src: &str) {
        match toml::from_str::<Config>(src) {
            Ok(_) => println!("  [{label}] LOADS OK"),
            Err(err) => println!("  [{label}] ERROR: {}", err.message()),
        }
    }

    #[test]
    fn finding_1_documented_schema_does_not_load() {
        println!("FINDING 1 - loading configs written to the documented schema:");
        load(
            "documented schema (all three divergences)",
            r#"
title = "H-hat Compiler Quantum Device Configuration"
version = "0.1.0"
[[devices]]
name = "AerSimulator"
is_active = true
vendor = "IBM"
device_kind = "simulator"
device_platform = "superconducting"
computation_paradigm = ["gate"]
[devices.specs]
max_n_qubits = 32
qubit_topology = []
[devices.qll]
name = "openqasm"
package_name = "qiskit"
package_version = "1.0.0"
file_extension = "qasm"
"#,
        );
        load("only device_kind = \"simulator\"", "title=\"t\"\nversion=\"0.1.0\"\n[[devices]]\nname=\"X\"\nis_active=true\nvendor=\"IBM\"\ndevice_kind=\"simulator\"\ndevice_platform=\"sc\"\ncomputation_paradigm=[\"gate\"]\npriority=0\n[devices.device_specs]\nmax_n_qubits=5\nqubit_topology=[]\n[devices.qll]\nname=\"openqasm\"\npackage_name=\"q\"\npackage_version=\"1\"\nfile_extension=\"qasm\"\n");
        load("only [devices.specs] section", "title=\"t\"\nversion=\"0.1.0\"\n[[devices]]\nname=\"X\"\nis_active=true\nvendor=\"IBM\"\ndevice_kind=\"Simulator\"\ndevice_platform=\"sc\"\ncomputation_paradigm=[\"gate\"]\npriority=0\n[devices.specs]\nmax_n_qubits=5\nqubit_topology=[]\n[devices.qll]\nname=\"openqasm\"\npackage_name=\"q\"\npackage_version=\"1\"\nfile_extension=\"qasm\"\n");
        load("only priority omitted", "title=\"t\"\nversion=\"0.1.0\"\n[[devices]]\nname=\"X\"\nis_active=true\nvendor=\"IBM\"\ndevice_kind=\"Simulator\"\ndevice_platform=\"sc\"\ncomputation_paradigm=[\"gate\"]\n[devices.device_specs]\nmax_n_qubits=5\nqubit_topology=[]\n[devices.qll]\nname=\"openqasm\"\npackage_name=\"q\"\npackage_version=\"1\"\nfile_extension=\"qasm\"\n");
    }

    #[test]
    fn finding_2_add_device_reverses_equal_priority() {
        let via_new = Config::new(
            "t".into(),
            "v".into(),
            vec![mk("A", 1, None), mk("B", 1, None), mk("C", 1, None)],
        )
        .unwrap();
        let mut via_add = Config::new("t".into(), "v".into(), vec![mk("A", 1, None)]).unwrap();
        via_add.add_device(mk("B", 1, None));
        via_add.add_device(mk("C", 1, None));
        println!("FINDING 2 - equal priorities; devices added in order A, B, C:");
        println!(
            "  Config::new order = {:?}  (matches doc: oldest first)",
            names(&via_new)
        );
        println!(
            "  add_device  order = {:?}  (doc says this should match the above)",
            names(&via_add)
        );
        assert_eq!(
            names(&via_new),
            vec!["A".to_string(), "B".to_string(), "C".to_string()]
        );
        assert_eq!(names(&via_add), names(&via_new));
    }

    #[test]
    fn finding_3_extra_serialization_is_nondeterministic() {
        let mut extra = HashMap::new();
        for i in 0..8 {
            extra.insert(format!("k{i}"), toml::Value::Integer(i));
        }
        let config = Config::new("t".into(), "v".into(), vec![mk("D", 0, Some(extra))]).unwrap();
        let order: Vec<String> = toml::to_string(&config)
            .unwrap()
            .lines()
            .filter(|line| line.trim_start().starts_with('k'))
            .map(|line| line.split('=').next().unwrap().trim().to_string())
            .collect();
        println!(
            "FINDING 3 - extra keys inserted k0..k7, emitted as {:?}",
            order
        );
    }

    #[test]
    fn finding_4_partialeq_ignores_extra() {
        fn assert_partial_eq<T: PartialEq>() {}
        assert_partial_eq::<toml::Value>();
        let with_extra = Config::new(
            "t".into(),
            "v".into(),
            vec![mk(
                "D",
                0,
                Some(HashMap::from([("retired".to_string(), true.into())])),
            )],
        )
        .unwrap();
        let without_extra =
            Config::new("t".into(), "v".into(), vec![mk("D", 0, None)]).unwrap();
        println!(
            "FINDING 4 - (config with extra) == (config without extra) ? {}",
            with_extra == without_extra
        );
        assert!(with_extra == without_extra);
    }

    #[test]
    fn finding_5_empty_config_accepted() {
        let empty = Config::new("t".into(), "v".into(), vec![]).unwrap();
        println!(
            "FINDING 5 - empty config accepted; #active devices = {}",
            empty.get_active_devices().count()
        );
        assert_eq!(empty.get_active_devices().count(), 0);
    }
}

Run:

cargo test config::base::issue63_repro -- --nocapture
for i in 1 2 3 4; do
  cargo test config::base::issue63_repro::finding_3_extra_serialization_is_nondeterministic -- --nocapture
done

Observed output on 998624d7647afd998c8243dd6e662b3f426ad60a, abridged:

FINDING 1 - loading configs written to the documented schema:
  [documented schema (all three divergences)] ERROR: unknown variant `simulator`, expected `Simulator` or `Device`
  [only device_kind = "simulator"] ERROR: unknown variant `simulator`, expected `Simulator` or `Device`
  [only [devices.specs] section] ERROR: missing field `device_specs`
  [only priority omitted] ERROR: missing field `priority`

FINDING 2 - equal priorities; devices added in order A, B, C:
  Config::new order = ["A", "B", "C"]  (matches doc: oldest first)
  add_device  order = ["C", "B", "A"]  (doc says this should match the above)
assertion failed: left ["C", "B", "A"], right ["A", "B", "C"]

FINDING 3 - extra keys inserted k0..k7, emitted as ["k1", "k2", "k6", "k7", "k3", "k0", "k5", "k4"]
FINDING 4 - (config with extra) == (config without extra) ? true
FINDING 5 - empty config accepted; #active devices = 0

Finding 2 fails intentionally in the repro so the ordering mismatch is visible. The other findings print their evidence and pass.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions