From e28a8b7956fe3fbd96d2fd1e5dd59c71c59c3e80 Mon Sep 17 00:00:00 2001 From: LGUG2Z Date: Sat, 31 Oct 2026 08:38:06 -0700 Subject: [PATCH] fix(nixgen): make the generated output order the same on every run The generator kept its definitions, reference counts and type bindings in HashMap and HashSet, whose iteration order changes from run to run. The topological sort walked the keys in that order, so which shared types were bound before their dependents, and so how some types were written out, changed between runs. Running the same schema eight times gave eight different files. Use BTreeMap and BTreeSet in generator.rs and prepare.rs so every walk is in sorted key order. The final let block was already sorted by identifier and is unchanged. Verified: cargo test passes (48 tests). Running the release binary on the KDE schema.json eight times gave one identical output; the same eight runs before the change gave eight different outputs. --- src/generator.rs | 36 ++++++++++++++++++------------------ src/prepare.rs | 6 +++--- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/generator.rs b/src/generator.rs index a3b18caa..00c7b3a8 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -22,8 +22,8 @@ use crate::prepare::resolve_ref_name; use crate::types::NixOption; use crate::types::NixType; use serde_json::Value; -use std::collections::HashMap; -use std::collections::HashSet; +use std::collections::BTreeMap; +use std::collections::BTreeSet; /// Configuration options for the generator #[derive(Debug, Clone)] @@ -56,13 +56,13 @@ pub enum GeneratorError { /// The main Nix code generator pub struct Generator { - definitions: HashMap, + definitions: BTreeMap, /// Cache for resolved type definitions to avoid infinite recursion resolving: Vec, /// Count of how many times each $def is referenced - ref_counts: HashMap, + ref_counts: BTreeMap, /// Pre-generated types for frequently used definitions - type_bindings: HashMap, + type_bindings: BTreeMap, /// Whether we're in the second pass (using type refs) use_type_refs: bool, } @@ -73,8 +73,8 @@ impl Generator { Self { definitions: collect_definitions(schema), resolving: Vec::new(), - ref_counts: HashMap::new(), - type_bindings: HashMap::new(), + ref_counts: BTreeMap::new(), + type_bindings: BTreeMap::new(), use_type_refs: false, } } @@ -149,9 +149,9 @@ impl Generator { } /// Build a dependency graph of $defs - /// Returns HashMap> - fn build_dependency_graph(&self) -> HashMap> { - let mut graph: HashMap> = HashMap::new(); + /// Returns BTreeMap> + fn build_dependency_graph(&self) -> BTreeMap> { + let mut graph: BTreeMap> = BTreeMap::new(); for (name, def) in &self.definitions { let deps = Self::collect_refs_in_schema(def); @@ -233,10 +233,10 @@ impl Generator { } /// Topologically sort definitions so dependencies come before dependents - fn topological_sort(graph: &HashMap>) -> Vec { + fn topological_sort(graph: &BTreeMap>) -> Vec { let mut result = Vec::new(); - let mut visited = HashSet::new(); - let mut temp_visited = HashSet::new(); // For cycle detection + let mut visited = BTreeSet::new(); + let mut temp_visited = BTreeSet::new(); // For cycle detection for name in graph.keys() { Self::topo_visit(name, graph, &mut visited, &mut temp_visited, &mut result); @@ -248,9 +248,9 @@ impl Generator { /// DFS visit for topological sort fn topo_visit( name: &str, - graph: &HashMap>, - visited: &mut HashSet, - temp_visited: &mut HashSet, + graph: &BTreeMap>, + visited: &mut BTreeSet, + temp_visited: &mut BTreeSet, result: &mut Vec, ) { if visited.contains(name) { @@ -286,7 +286,7 @@ impl Generator { let sorted_defs = Self::topological_sort(&graph); // Only generate bindings for refs used more than once - let frequent_refs: HashSet = self + let frequent_refs: BTreeSet = self .ref_counts .iter() .filter(|&(_, &count)| count > 1) @@ -480,7 +480,7 @@ impl Generator { // Collect all other properties from all variants (excluding the discriminator) // Track property types to merge them if they differ across variants - let mut prop_types: HashMap> = HashMap::new(); + let mut prop_types: BTreeMap> = BTreeMap::new(); for (_, variant_schema) in &variants { if let Some(props) = variant_schema.get("properties").and_then(|p| p.as_object()) { diff --git a/src/prepare.rs b/src/prepare.rs index f83173e7..d9ae4dc5 100644 --- a/src/prepare.rs +++ b/src/prepare.rs @@ -1,15 +1,15 @@ //! Schema preparation utilities for normalizing JSON Schema before conversion use serde_json::Value; -use std::collections::HashMap; +use std::collections::BTreeMap; /// Result type for mixed enum + object union detection /// Contains (enum_variants, object_variants) where object_variants are (tag_name, inner_schema) tuples pub type MixedUnionResult = (Vec, Vec<(String, Value)>); /// Extract all definitions from the schema's $defs section -pub fn collect_definitions(schema: &Value) -> HashMap { - let mut defs = HashMap::new(); +pub fn collect_definitions(schema: &Value) -> BTreeMap { + let mut defs = BTreeMap::new(); if let Some(definitions) = schema.get("$defs").and_then(|d| d.as_object()) { for (name, def) in definitions {