bindgen: determinism

This commit is contained in:
nym21
2026-01-27 23:48:19 +01:00
parent 730e83472a
commit fecaf0f400
21 changed files with 787 additions and 739 deletions
+34 -29
View File
@@ -3,7 +3,7 @@
//! This module detects repeating tree structures and analyzes them
//! using the bottom-up name deconstruction algorithm.
use std::collections::{BTreeSet, HashMap};
use std::collections::{BTreeMap, BTreeSet};
use brk_types::{TreeNode, extract_json_type};
@@ -13,25 +13,25 @@ use crate::{PatternBaseResult, PatternField, StructuralPattern, to_pascal_case};
/// Context for pattern detection, holding all intermediate state.
struct PatternContext {
/// Maps field signatures to pattern names
signature_to_pattern: HashMap<Vec<PatternField>, String>,
signature_to_pattern: BTreeMap<Vec<PatternField>, String>,
/// Counts how many times each signature appears
signature_counts: HashMap<Vec<PatternField>, usize>,
signature_counts: BTreeMap<Vec<PatternField>, usize>,
/// Maps normalized signatures to pattern names (for naming consistency)
normalized_to_name: HashMap<Vec<PatternField>, String>,
normalized_to_name: BTreeMap<Vec<PatternField>, String>,
/// Counts pattern name usage (for unique naming)
name_counts: HashMap<String, usize>,
name_counts: BTreeMap<String, usize>,
/// Maps signatures to their child field lists
signature_to_child_fields: HashMap<Vec<PatternField>, Vec<Vec<PatternField>>>,
signature_to_child_fields: BTreeMap<Vec<PatternField>, Vec<Vec<PatternField>>>,
}
impl PatternContext {
fn new() -> Self {
Self {
signature_to_pattern: HashMap::new(),
signature_counts: HashMap::new(),
normalized_to_name: HashMap::new(),
name_counts: HashMap::new(),
signature_to_child_fields: HashMap::new(),
signature_to_pattern: BTreeMap::new(),
signature_counts: BTreeMap::new(),
normalized_to_name: BTreeMap::new(),
name_counts: BTreeMap::new(),
signature_to_child_fields: BTreeMap::new(),
}
}
}
@@ -45,9 +45,9 @@ pub fn detect_structural_patterns(
tree: &TreeNode,
) -> (
Vec<StructuralPattern>,
HashMap<Vec<PatternField>, String>,
HashMap<Vec<PatternField>, String>,
HashMap<String, PatternBaseResult>,
BTreeMap<Vec<PatternField>, String>,
BTreeMap<Vec<PatternField>, String>,
BTreeMap<String, PatternBaseResult>,
) {
let mut ctx = PatternContext::new();
resolve_branch_patterns(tree, "root", &mut ctx);
@@ -90,7 +90,7 @@ pub fn detect_structural_patterns(
patterns.extend(generic_patterns);
// Build pattern lookup for mode analysis (patterns appearing 2+ times)
let mut pattern_lookup: HashMap<Vec<PatternField>, String> = HashMap::new();
let mut pattern_lookup: BTreeMap<Vec<PatternField>, String> = BTreeMap::new();
for (sig, name) in &ctx.signature_to_pattern {
if ctx.signature_counts.get(sig).copied().unwrap_or(0) >= 2 {
pattern_lookup.insert(sig.clone(), name.clone());
@@ -110,29 +110,30 @@ pub fn detect_structural_patterns(
/// Detect generic patterns by grouping signatures by their normalized form.
fn detect_generic_patterns(
signature_to_pattern: &HashMap<Vec<PatternField>, String>,
signature_to_pattern: &BTreeMap<Vec<PatternField>, String>,
) -> (
Vec<StructuralPattern>,
HashMap<Vec<PatternField>, String>,
HashMap<Vec<PatternField>, String>,
BTreeMap<Vec<PatternField>, String>,
BTreeMap<Vec<PatternField>, String>,
) {
let mut normalized_groups: HashMap<
let mut normalized_groups: BTreeMap<
Vec<PatternField>,
Vec<(Vec<PatternField>, String, String)>,
> = HashMap::new();
> = BTreeMap::new();
for (fields, name) in signature_to_pattern {
if let Some((normalized, extracted_type)) = normalize_fields_for_generic(fields) {
normalized_groups
.entry(normalized)
.or_default()
.push((fields.clone(), name.clone(), extracted_type));
normalized_groups.entry(normalized).or_default().push((
fields.clone(),
name.clone(),
extracted_type,
));
}
}
let mut patterns = Vec::new();
let mut pattern_mappings: HashMap<Vec<PatternField>, String> = HashMap::new();
let mut type_mappings: HashMap<Vec<PatternField>, String> = HashMap::new();
let mut pattern_mappings: BTreeMap<Vec<PatternField>, String> = BTreeMap::new();
let mut type_mappings: BTreeMap<Vec<PatternField>, String> = BTreeMap::new();
for (normalized_fields, group) in normalized_groups {
if group.len() >= 2 {
@@ -205,7 +206,10 @@ fn normalize_fields_for_generic(fields: &[PatternField]) -> Option<(Vec<PatternF
// Only proceed if inner types differ from originals (meaning they had wrappers)
// and all inner types are the same
if inner_types.iter().all(|t| t == first_inner)
&& inner_types.iter().zip(leaf_types.iter()).any(|(inner, orig)| inner != *orig)
&& inner_types
.iter()
.zip(leaf_types.iter())
.any(|(inner, orig)| inner != *orig)
{
let normalized = fields
.iter()
@@ -301,7 +305,8 @@ fn resolve_branch_patterns(
.entry(normalized)
.or_insert_with(|| generate_pattern_name(field_name, &mut ctx.name_counts))
.clone();
ctx.signature_to_pattern.insert(fields.clone(), name.clone());
ctx.signature_to_pattern
.insert(fields.clone(), name.clone());
name
};
@@ -329,7 +334,7 @@ fn normalize_fields_for_naming(fields: &[PatternField]) -> Vec<PatternField> {
}
/// Generate a unique pattern name.
fn generate_pattern_name(field_name: &str, name_counts: &mut HashMap<String, usize>) -> String {
fn generate_pattern_name(field_name: &str, name_counts: &mut BTreeMap<String, usize>) -> String {
let pascal = to_pascal_case(field_name);
let sanitized = if pascal.chars().next().is_some_and(|c| c.is_ascii_digit()) {
format!("_{}", pascal)
+19 -19
View File
@@ -4,7 +4,7 @@
//! suffix mode (fields append to acc) or prefix mode (fields prepend to acc),
//! and extracts the field parts (relatives or prefixes) for code generation.
use std::collections::HashMap;
use std::collections::BTreeMap;
use brk_types::TreeNode;
@@ -18,7 +18,7 @@ struct InstanceAnalysis {
base: String,
/// For suffix mode: field -> relative name
/// For prefix mode: field -> prefix
field_parts: HashMap<String, String>,
field_parts: BTreeMap<String, String>,
/// Whether this instance appears to be suffix mode
is_suffix_mode: bool,
}
@@ -34,12 +34,12 @@ struct InstanceAnalysis {
pub fn analyze_pattern_modes(
tree: &TreeNode,
patterns: &mut [StructuralPattern],
pattern_lookup: &HashMap<Vec<PatternField>, String>,
) -> HashMap<String, PatternBaseResult> {
pattern_lookup: &BTreeMap<Vec<PatternField>, String>,
) -> BTreeMap<String, PatternBaseResult> {
// Collect analyses from all instances, keyed by pattern name
let mut all_analyses: HashMap<String, Vec<InstanceAnalysis>> = HashMap::new();
let mut all_analyses: BTreeMap<String, Vec<InstanceAnalysis>> = BTreeMap::new();
// Also collect base results for each node, keyed by tree path
let mut node_bases: HashMap<String, PatternBaseResult> = HashMap::new();
let mut node_bases: BTreeMap<String, PatternBaseResult> = BTreeMap::new();
// Bottom-up traversal
collect_instance_analyses(tree, "", pattern_lookup, &mut all_analyses, &mut node_bases);
@@ -61,9 +61,9 @@ pub fn analyze_pattern_modes(
fn collect_instance_analyses(
node: &TreeNode,
path: &str,
pattern_lookup: &HashMap<Vec<PatternField>, String>,
all_analyses: &mut HashMap<String, Vec<InstanceAnalysis>>,
node_bases: &mut HashMap<String, PatternBaseResult>,
pattern_lookup: &BTreeMap<Vec<PatternField>, String>,
all_analyses: &mut BTreeMap<String, Vec<InstanceAnalysis>>,
node_bases: &mut BTreeMap<String, PatternBaseResult>,
) -> Option<String> {
match node {
TreeNode::Leaf(leaf) => {
@@ -72,7 +72,7 @@ fn collect_instance_analyses(
}
TreeNode::Branch(children) => {
// First, process all children recursively (bottom-up)
let mut child_bases: HashMap<String, String> = HashMap::new();
let mut child_bases: BTreeMap<String, String> = BTreeMap::new();
for (field_name, child_node) in children {
let child_path = build_child_path(path, field_name);
if let Some(base) = collect_instance_analyses(
@@ -122,13 +122,13 @@ fn collect_instance_analyses(
}
/// Analyze a single pattern instance from its child bases.
fn analyze_instance(child_bases: &HashMap<String, String>) -> InstanceAnalysis {
fn analyze_instance(child_bases: &BTreeMap<String, String>) -> InstanceAnalysis {
let bases: Vec<&str> = child_bases.values().map(|s| s.as_str()).collect();
// Try suffix mode first: look for common prefix among children
if let Some(common_prefix) = find_common_prefix(&bases) {
let base = common_prefix.trim_end_matches('_').to_string();
let mut field_parts = HashMap::new();
let mut field_parts = BTreeMap::new();
for (field_name, child_base) in child_bases {
// Relative = child_base with common prefix stripped
@@ -154,7 +154,7 @@ fn analyze_instance(child_bases: &HashMap<String, String>) -> InstanceAnalysis {
// Try prefix mode: look for common suffix among children
if let Some(common_suffix) = find_common_suffix(&bases) {
let base = common_suffix.trim_start_matches('_').to_string();
let mut field_parts = HashMap::new();
let mut field_parts = BTreeMap::new();
for (field_name, child_base) in child_bases {
// Prefix = child_base with common suffix stripped, normalized to end with _
@@ -214,8 +214,8 @@ fn determine_pattern_mode(
}
// Find the most common field_parts within the majority group
// Convert to sorted Vec for comparison since HashMap isn't hashable
let mut parts_counts: HashMap<Vec<(String, String)>, usize> = HashMap::new();
// Convert to sorted Vec for comparison since BTreeMap isn't hashable
let mut parts_counts: BTreeMap<Vec<(String, String)>, usize> = BTreeMap::new();
for analysis in &majority_instances {
let mut sorted: Vec<_> = analysis
.field_parts
@@ -227,7 +227,7 @@ fn determine_pattern_mode(
}
let (best_parts_vec, _count) = parts_counts.into_iter().max_by_key(|(_, count)| *count)?;
let best_parts: HashMap<String, String> = best_parts_vec.into_iter().collect();
let best_parts: BTreeMap<String, String> = best_parts_vec.into_iter().collect();
// Verify all required fields have parts
for field in fields {
@@ -255,7 +255,7 @@ mod tests {
#[test]
fn test_analyze_instance_suffix_mode() {
let mut child_bases = HashMap::new();
let mut child_bases = BTreeMap::new();
child_bases.insert("max".to_string(), "lth_cost_basis_max".to_string());
child_bases.insert("min".to_string(), "lth_cost_basis_min".to_string());
child_bases.insert("percentiles".to_string(), "lth_cost_basis".to_string());
@@ -276,7 +276,7 @@ mod tests {
fn test_analyze_instance_prefix_mode() {
// Period-prefixed metrics like "1y_lump_sum_stack", "1m_lump_sum_stack"
// share a common suffix "_lump_sum_stack" with different period prefixes
let mut child_bases = HashMap::new();
let mut child_bases = BTreeMap::new();
child_bases.insert("_1y".to_string(), "1y_lump_sum_stack".to_string());
child_bases.insert("_1m".to_string(), "1m_lump_sum_stack".to_string());
child_bases.insert("_1w".to_string(), "1w_lump_sum_stack".to_string());
@@ -293,7 +293,7 @@ mod tests {
#[test]
fn test_analyze_instance_root_suffix() {
// At root level with suffix naming convention
let mut child_bases = HashMap::new();
let mut child_bases = BTreeMap::new();
child_bases.insert("max".to_string(), "cost_basis_max".to_string());
child_bases.insert("min".to_string(), "cost_basis_min".to_string());
child_bases.insert("percentiles".to_string(), "cost_basis".to_string());
+9 -9
View File
@@ -3,7 +3,7 @@
//! This module provides utilities for working with the TreeNode structure,
//! including leaf name extraction and index pattern detection.
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::collections::{BTreeMap, BTreeSet};
use brk_types::{Index, TreeNode, extract_json_type};
@@ -28,7 +28,7 @@ fn get_shortest_leaf_name(node: &TreeNode) -> Option<String> {
/// Get the field signature for a branch node's children.
pub fn get_node_fields(
children: &BTreeMap<String, TreeNode>,
pattern_lookup: &HashMap<Vec<PatternField>, String>,
pattern_lookup: &BTreeMap<Vec<PatternField>, String>,
) -> Vec<PatternField> {
let mut fields: Vec<PatternField> = children
.iter()
@@ -117,7 +117,7 @@ pub struct PatternBaseResult {
pub is_suffix_mode: bool,
/// The field parts (suffix in suffix mode, prefix in prefix mode) for each field.
/// Used to check if instance field parts match the pattern's field parts.
pub field_parts: HashMap<String, String>,
pub field_parts: BTreeMap<String, String>,
}
impl PatternBaseResult {
@@ -128,7 +128,7 @@ impl PatternBaseResult {
base: String::new(),
has_outlier: true,
is_suffix_mode: true,
field_parts: HashMap::new(),
field_parts: BTreeMap::new(),
}
}
@@ -139,7 +139,7 @@ impl PatternBaseResult {
base: String::new(),
has_outlier: false,
is_suffix_mode: true,
field_parts: HashMap::new(),
field_parts: BTreeMap::new(),
}
}
}
@@ -200,7 +200,7 @@ struct FindBaseResult {
base: String,
has_outlier: bool,
is_suffix_mode: bool,
field_parts: HashMap<String, String>,
field_parts: BTreeMap<String, String>,
}
/// Try to find a common base from child names using prefix/suffix detection.
@@ -214,7 +214,7 @@ fn try_find_base(
// Try common prefix first (suffix mode)
if let Some(prefix) = find_common_prefix(&leaf_names) {
let base = prefix.trim_end_matches('_').to_string();
let mut field_parts = HashMap::new();
let mut field_parts = BTreeMap::new();
for (field_name, leaf_name) in child_names {
// Compute the suffix part for this field
let suffix = if leaf_name == &base {
@@ -238,7 +238,7 @@ fn try_find_base(
// Try common suffix (prefix mode)
if let Some(suffix) = find_common_suffix(&leaf_names) {
let base = suffix.trim_start_matches('_').to_string();
let mut field_parts = HashMap::new();
let mut field_parts = BTreeMap::new();
for (field_name, leaf_name) in child_names {
// Compute the prefix part for this field, normalized to end with _
let prefix_part = leaf_name
@@ -300,7 +300,7 @@ pub fn infer_accumulated_name(parent_acc: &str, field_name: &str, descendant_lea
pub fn get_fields_with_child_info(
children: &BTreeMap<String, TreeNode>,
parent_name: &str,
pattern_lookup: &HashMap<Vec<PatternField>, String>,
pattern_lookup: &BTreeMap<Vec<PatternField>, String>,
) -> Vec<(PatternField, Option<Vec<PatternField>>)> {
children
.iter()