diff --git a/crates/brk_binder/src/javascript.rs b/crates/brk_binder/src/javascript.rs index f23152a79..6a8419f8f 100644 --- a/crates/brk_binder/src/javascript.rs +++ b/crates/brk_binder/src/javascript.rs @@ -336,9 +336,12 @@ fn generate_structural_patterns( // Generate JSDoc typedef writeln!(output, "/**").unwrap(); + if pattern.is_generic { + writeln!(output, " * @template T").unwrap(); + } writeln!(output, " * @typedef {{Object}} {}", pattern.name).unwrap(); for field in &pattern.fields { - let js_type = field_to_js_type(field, metadata); + let js_type = field_to_js_type_generic(field, metadata, pattern.is_generic); writeln!( output, " * @property {{{}}} {}", @@ -488,17 +491,45 @@ fn generate_tree_path_field( } } -/// Convert pattern field to JavaScript/JSDoc type -fn field_to_js_type(field: &PatternField, metadata: &ClientMetadata) -> String { +/// Convert pattern field to JavaScript/JSDoc type, with optional generic support +fn field_to_js_type_generic( + field: &PatternField, + metadata: &ClientMetadata, + is_generic: bool, +) -> String { + field_to_js_type_with_generic_value(field, metadata, is_generic, None) +} + +/// Convert pattern field to JavaScript/JSDoc type. +/// - `is_generic`: If true and field.rust_type is "T", use T in the output +/// - `generic_value_type`: For branch fields that reference a generic pattern, this is the concrete type to substitute +fn field_to_js_type_with_generic_value( + field: &PatternField, + metadata: &ClientMetadata, + is_generic: bool, + generic_value_type: Option<&str>, +) -> String { + // For generic patterns, use T instead of concrete value type + let value_type = if is_generic && field.rust_type == "T" { + "T".to_string() + } else { + field.rust_type.clone() + }; + if metadata.is_pattern_type(&field.rust_type) { - // Pattern type - use pattern name directly + // Check if this pattern is generic and we have a value type + if metadata.is_pattern_generic(&field.rust_type) { + if let Some(vt) = generic_value_type { + return format!("{}<{}>", field.rust_type, vt); + } + } field.rust_type.clone() } else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) { - // Leaf with accessor - use rust_type as the generic (e.g., DateIndexAccessor) - format!("{}<{}>", accessor.name, field.rust_type) + // Leaf with accessor - use value_type as the generic + format!("{}<{}>", accessor.name, value_type) } else { - // Leaf - use rust_type as the generic (e.g., MetricNode) - format!("MetricNode<{}>", field.rust_type) + // Leaf - use value_type as the generic + format!("MetricNode<{}>", value_type) } } @@ -533,8 +564,36 @@ fn generate_tree_typedef( generated: &mut HashSet, ) { if let TreeNode::Branch(children) = node { - // Build signature - let fields = get_node_fields(children, pattern_lookup); + // Build signature with child field info for generic pattern lookup + let fields_with_child_info: Vec<(PatternField, Option>)> = children + .iter() + .map(|(child_name, child_node)| { + let (rust_type, json_type, indexes, child_fields) = match child_node { + TreeNode::Leaf(leaf) => ( + leaf.value_type().to_string(), + leaf.schema.get("type").and_then(|v| v.as_str()).unwrap_or("object").to_string(), + leaf.indexes().clone(), + None, + ), + TreeNode::Branch(grandchildren) => { + let child_fields = get_node_fields(grandchildren, pattern_lookup); + let pattern_name = pattern_lookup + .get(&child_fields) + .cloned() + .unwrap_or_else(|| format!("{}_{}", name, to_pascal_case(child_name))); + (pattern_name.clone(), pattern_name, std::collections::BTreeSet::new(), Some(child_fields)) + } + }; + (PatternField { + name: child_name.clone(), + rust_type, + json_type, + indexes, + }, child_fields) + }) + .collect(); + + let fields: Vec = fields_with_child_info.iter().map(|(f, _)| f.clone()).collect(); // Skip if this matches a pattern (already generated) if pattern_lookup.contains_key(&fields) @@ -551,8 +610,12 @@ fn generate_tree_typedef( writeln!(output, "/**").unwrap(); writeln!(output, " * @typedef {{Object}} {}", name).unwrap(); - for field in &fields { - let js_type = field_to_js_type(field, metadata); + for (field, child_fields) in &fields_with_child_info { + // For generic patterns, extract the value type from child fields + let generic_value_type = child_fields.as_ref().and_then(|cf| { + metadata.get_generic_value_type(&field.rust_type, cf) + }); + let js_type = field_to_js_type_with_generic_value(field, metadata, false, generic_value_type.as_deref()); writeln!( output, " * @property {{{}}} {}", diff --git a/crates/brk_binder/src/python.rs b/crates/brk_binder/src/python.rs index 2b46e4f48..9700d9eda 100644 --- a/crates/brk_binder/src/python.rs +++ b/crates/brk_binder/src/python.rs @@ -255,7 +255,12 @@ fn generate_structural_patterns( for pattern in patterns { let is_parameterizable = pattern.is_parameterizable(); - writeln!(output, "class {}:", pattern.name).unwrap(); + // For generic patterns, inherit from Generic[T] + if pattern.is_generic { + writeln!(output, "class {}(Generic[T]):", pattern.name).unwrap(); + } else { + writeln!(output, "class {}:", pattern.name).unwrap(); + } writeln!( output, " \"\"\"Pattern struct for repeated tree structure.\"\"\"" @@ -298,7 +303,7 @@ fn generate_parameterized_python_field( metadata: &ClientMetadata, ) { let field_name = to_snake_case(&field.name); - let py_type = field_to_python_type(field, metadata); + let py_type = field_to_python_type_generic(field, metadata, pattern.is_generic); // For branch fields, pass the accumulated name to nested pattern if metadata.is_pattern_type(&field.rust_type) { @@ -388,15 +393,48 @@ fn generate_tree_path_python_field( /// Convert pattern field to Python type annotation fn field_to_python_type(field: &PatternField, metadata: &ClientMetadata) -> String { + field_to_python_type_generic(field, metadata, false) +} + +/// Convert pattern field to Python type annotation, with optional generic support +fn field_to_python_type_generic( + field: &PatternField, + metadata: &ClientMetadata, + is_generic: bool, +) -> String { + field_to_python_type_with_generic_value(field, metadata, is_generic, None) +} + +/// Convert pattern field to Python type annotation. +/// - `is_generic`: If true and field.rust_type is "T", use T in the output +/// - `generic_value_type`: For branch fields that reference a generic pattern, this is the concrete type to substitute +fn field_to_python_type_with_generic_value( + field: &PatternField, + metadata: &ClientMetadata, + is_generic: bool, + generic_value_type: Option<&str>, +) -> String { + // For generic patterns, use T instead of concrete value type + let value_type = if is_generic && field.rust_type == "T" { + "T".to_string() + } else { + field.rust_type.clone() + }; + if metadata.is_pattern_type(&field.rust_type) { - // Pattern type - use pattern name directly + // Check if this pattern is generic and we have a value type + if metadata.is_pattern_generic(&field.rust_type) { + if let Some(vt) = generic_value_type { + return format!("{}[{}]", field.rust_type, vt); + } + } field.rust_type.clone() } else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) { - // Leaf with accessor - use rust_type as the generic (e.g., DateIndexAccessor[Height]) - format!("{}[{}]", accessor.name, field.rust_type) + // Leaf with accessor - use value_type as the generic + format!("{}[{}]", accessor.name, value_type) } else { - // Leaf - use rust_type as the generic (e.g., MetricNode[Height]) - format!("MetricNode[{}]", field.rust_type) + // Leaf - use value_type as the generic + format!("MetricNode[{}]", value_type) } } @@ -431,8 +469,36 @@ fn generate_tree_class( generated: &mut HashSet, ) { if let TreeNode::Branch(children) = node { - // Build signature - let fields = get_node_fields(children, pattern_lookup); + // Build signature with child field info for generic pattern lookup + let fields_with_child_info: Vec<(PatternField, Option>)> = children + .iter() + .map(|(child_name, child_node)| { + let (rust_type, json_type, indexes, child_fields) = match child_node { + TreeNode::Leaf(leaf) => ( + leaf.value_type().to_string(), + leaf.schema.get("type").and_then(|v| v.as_str()).unwrap_or("object").to_string(), + leaf.indexes().clone(), + None, + ), + TreeNode::Branch(grandchildren) => { + let child_fields = get_node_fields(grandchildren, pattern_lookup); + let pattern_name = pattern_lookup + .get(&child_fields) + .cloned() + .unwrap_or_else(|| format!("{}_{}", name, to_pascal_case(child_name))); + (pattern_name.clone(), pattern_name, std::collections::BTreeSet::new(), Some(child_fields)) + } + }; + (PatternField { + name: child_name.clone(), + rust_type, + json_type, + indexes, + }, child_fields) + }) + .collect(); + + let fields: Vec = fields_with_child_info.iter().map(|(f, _)| f.clone()).collect(); // Skip if this matches a pattern (already generated) if pattern_lookup.contains_key(&fields) @@ -455,8 +521,12 @@ fn generate_tree_class( ) .unwrap(); - for (field, (child_name, child_node)) in fields.iter().zip(children.iter()) { - let py_type = field_to_python_type(field, metadata); + for ((field, child_fields_opt), (child_name, child_node)) in fields_with_child_info.iter().zip(children.iter()) { + // For generic patterns, extract the value type from child fields + let generic_value_type = child_fields_opt.as_ref().and_then(|cf| { + metadata.get_generic_value_type(&field.rust_type, cf) + }); + let py_type = field_to_python_type_with_generic_value(field, metadata, false, generic_value_type.as_deref()); let field_name_py = to_snake_case(&field.name); if metadata.is_pattern_type(&field.rust_type) { diff --git a/crates/brk_binder/src/rust.rs b/crates/brk_binder/src/rust.rs index 397e183df..01146002c 100644 --- a/crates/brk_binder/src/rust.rs +++ b/crates/brk_binder/src/rust.rs @@ -233,20 +233,21 @@ fn generate_pattern_structs(output: &mut String, patterns: &[StructuralPattern], for pattern in patterns { let is_parameterizable = pattern.is_parameterizable(); + let generic_params = if pattern.is_generic { "<'a, T>" } else { "<'a>" }; writeln!(output, "/// Pattern struct for repeated tree structure.").unwrap(); - writeln!(output, "pub struct {}<'a> {{", pattern.name).unwrap(); + writeln!(output, "pub struct {}{} {{", pattern.name, generic_params).unwrap(); for field in &pattern.fields { let field_name = to_snake_case(&field.name); - let type_annotation = field_to_type_annotation(field, metadata); + let type_annotation = field_to_type_annotation_generic(field, metadata, pattern.is_generic); writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap(); } writeln!(output, "}}\n").unwrap(); // Generate impl block with constructor - writeln!(output, "impl<'a> {}<'a> {{", pattern.name).unwrap(); + writeln!(output, "impl{} {}{} {{", generic_params, pattern.name, generic_params).unwrap(); if is_parameterizable { writeln!(output, " /// Create a new pattern node with accumulated metric name.").unwrap(); @@ -358,16 +359,45 @@ fn generate_tree_path_rust_field( } } -/// Convert a PatternField to the full type annotation -fn field_to_type_annotation(field: &PatternField, metadata: &ClientMetadata) -> String { +/// Convert a PatternField to the full type annotation, with optional generic support +fn field_to_type_annotation_generic( + field: &PatternField, + metadata: &ClientMetadata, + is_generic: bool, +) -> String { + field_to_type_annotation_with_generic(field, metadata, is_generic, None) +} + +/// Convert a PatternField to the full type annotation. +/// - `is_generic`: If true and field.rust_type is "T", use T in the output +/// - `generic_value_type`: For branch fields that reference a generic pattern, this is the concrete type to substitute +fn field_to_type_annotation_with_generic( + field: &PatternField, + metadata: &ClientMetadata, + is_generic: bool, + generic_value_type: Option<&str>, +) -> String { + // For generic patterns, use T instead of concrete value type + let value_type = if is_generic && field.rust_type == "T" { + "T".to_string() + } else { + field.rust_type.clone() + }; + if metadata.is_pattern_type(&field.rust_type) { + // Check if this pattern is generic and we have a value type + if metadata.is_pattern_generic(&field.rust_type) { + if let Some(vt) = generic_value_type { + return format!("{}<'a, {}>", field.rust_type, vt); + } + } format!("{}<'a>", field.rust_type) } else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) { // Leaf with a reusable accessor pattern - format!("{}<'a, {}>", accessor.name, field.rust_type) + format!("{}<'a, {}>", accessor.name, value_type) } else { // Leaf with unique index set - use MetricNode directly - format!("MetricNode<'a, {}>", field.rust_type) + format!("MetricNode<'a, {}>", value_type) } } @@ -399,15 +429,16 @@ fn generate_tree_node( generated: &mut HashSet, ) { if let TreeNode::Branch(children) = node { - // Build the signature for this node - let mut fields: Vec = children + // Build the signature for this node, also tracking child fields for generic pattern lookup + let mut fields_with_child_info: Vec<(PatternField, Option>)> = children .iter() .map(|(child_name, child_node)| { - let (rust_type, json_type, indexes) = match child_node { + let (rust_type, json_type, indexes, child_fields) = match child_node { TreeNode::Leaf(leaf) => ( leaf.value_type().to_string(), leaf.schema.get("type").and_then(|v| v.as_str()).unwrap_or("object").to_string(), leaf.indexes().clone(), + None, ), TreeNode::Branch(grandchildren) => { // Get pattern name for this child @@ -416,18 +447,20 @@ fn generate_tree_node( .get(&child_fields) .cloned() .unwrap_or_else(|| format!("{}_{}", name, to_pascal_case(child_name))); - (pattern_name.clone(), pattern_name, std::collections::BTreeSet::new()) + (pattern_name.clone(), pattern_name, std::collections::BTreeSet::new(), Some(child_fields)) } }; - PatternField { + (PatternField { name: child_name.clone(), rust_type, json_type, indexes, - } + }, child_fields) }) .collect(); - fields.sort_by(|a, b| a.name.cmp(&b.name)); + fields_with_child_info.sort_by(|a, b| a.0.name.cmp(&b.0.name)); + + let fields: Vec = fields_with_child_info.iter().map(|(f, _)| f.clone()).collect(); // Check if this matches a reusable pattern if let Some(pattern_name) = pattern_lookup.get(&fields) { @@ -447,9 +480,13 @@ fn generate_tree_node( writeln!(output, "/// Catalog tree node.").unwrap(); writeln!(output, "pub struct {}<'a> {{", name).unwrap(); - for field in &fields { + for (field, child_fields) in &fields_with_child_info { let field_name = to_snake_case(&field.name); - let type_annotation = field_to_type_annotation(field, metadata); + // For generic patterns, extract the value type from child fields + let generic_value_type = child_fields.as_ref().and_then(|cf| { + metadata.get_generic_value_type(&field.rust_type, cf) + }); + let type_annotation = field_to_type_annotation_with_generic(field, metadata, false, generic_value_type.as_deref()); writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap(); } diff --git a/crates/brk_binder/src/types.rs b/crates/brk_binder/src/types.rs index 722bc2bdd..a672f91aa 100644 --- a/crates/brk_binder/src/types.rs +++ b/crates/brk_binder/src/types.rs @@ -28,6 +28,8 @@ pub struct ClientMetadata { pub used_indexes: BTreeSet, /// Index set patterns - sets of indexes that appear together on metrics pub index_set_patterns: Vec, + /// Maps concrete field signatures to pattern names (includes generic pattern mappings) + pub concrete_to_pattern: HashMap, String>, } /// A pattern of indexes that appear together on multiple metrics @@ -48,6 +50,8 @@ pub struct StructuralPattern { pub fields: Vec, /// How each field modifies the accumulated name (field_name -> position) pub field_positions: HashMap, + /// If true, all leaf fields use a type parameter T instead of concrete types + pub is_generic: bool, } impl StructuralPattern { @@ -112,7 +116,7 @@ impl ClientMetadata { /// Extract metadata from brk_query::Vecs pub fn from_vecs(vecs: &Vecs) -> Self { let catalog = vecs.catalog().clone(); - let structural_patterns = detect_structural_patterns(&catalog); + let (structural_patterns, concrete_to_pattern) = detect_structural_patterns(&catalog); let (used_indexes, index_set_patterns) = detect_index_patterns(&catalog); ClientMetadata { @@ -120,6 +124,7 @@ impl ClientMetadata { structural_patterns, used_indexes, index_set_patterns, + concrete_to_pattern, } } @@ -135,19 +140,47 @@ impl ClientMetadata { self.structural_patterns.iter().any(|p| p.name == type_name) } - /// Build a lookup map from field signatures to pattern names - pub fn pattern_lookup(&self) -> HashMap, String> { - self.structural_patterns + /// Find a pattern by name + pub fn find_pattern(&self, name: &str) -> Option<&StructuralPattern> { + self.structural_patterns.iter().find(|p| p.name == name) + } + + /// Check if a pattern is generic + pub fn is_pattern_generic(&self, name: &str) -> bool { + self.find_pattern(name).map(|p| p.is_generic).unwrap_or(false) + } + + /// Extract the value type from concrete fields for a generic pattern. + /// Returns the first leaf field's rust_type if this pattern is generic. + pub fn get_generic_value_type(&self, pattern_name: &str, fields: &[PatternField]) -> Option { + if !self.is_pattern_generic(pattern_name) { + return None; + } + // Find first leaf field (has indexes) + fields .iter() - .map(|p| (p.fields.clone(), p.name.clone())) - .collect() + .find(|f| !f.indexes.is_empty()) + .map(|f| f.rust_type.clone()) + } + + /// Build a lookup map from field signatures to pattern names. + /// Includes both generic pattern signatures and concrete signatures. + pub fn pattern_lookup(&self) -> HashMap, String> { + // Start with concrete-to-pattern mappings (includes generic pattern concrete signatures) + let mut lookup = self.concrete_to_pattern.clone(); + // Also add the normalized generic signatures + for p in &self.structural_patterns { + lookup.insert(p.fields.clone(), p.name.clone()); + } + lookup } } /// Detect structural patterns in the tree using a bottom-up approach. /// For every branch node, create a signature from its children (sorted field names + types). /// Patterns that appear 2+ times are deduplicated. -fn detect_structural_patterns(tree: &TreeNode) -> Vec { +/// Returns (patterns, concrete_to_pattern_mapping). +fn detect_structural_patterns(tree: &TreeNode) -> (Vec, HashMap, String>) { // Map from sorted fields signature to pattern name let mut signature_to_pattern: HashMap, String> = HashMap::new(); // Count how many times each signature appears @@ -156,19 +189,42 @@ fn detect_structural_patterns(tree: &TreeNode) -> Vec { // Process tree bottom-up to resolve all branch types resolve_branch_patterns(tree, &mut signature_to_pattern, &mut signature_counts); - // Build initial list of patterns (only those appearing 2+ times) + // First, identify generic patterns by grouping ALL signatures by their normalized form. + // Even if each concrete signature appears only once, if 2+ different value types + // normalize to the same pattern, we create a generic pattern. + let (generic_patterns, generic_mappings) = detect_generic_patterns(&signature_to_pattern); + + // Build non-generic patterns: signatures appearing 2+ times that weren't merged into generics let mut patterns: Vec = signature_to_pattern .iter() - .filter(|(sig, _)| signature_counts.get(*sig).copied().unwrap_or(0) >= 2) + .filter(|(sig, _)| { + signature_counts.get(*sig).copied().unwrap_or(0) >= 2 + && !generic_mappings.contains_key(*sig) + }) .map(|(fields, name)| StructuralPattern { name: name.clone(), fields: fields.clone(), field_positions: HashMap::new(), + is_generic: false, }) .collect(); - // Build lookup for second pass - let pattern_lookup: HashMap, String> = signature_to_pattern; + // Add the generic patterns + patterns.extend(generic_patterns); + + // Build lookup for second pass - include all concrete signatures + let mut pattern_lookup: HashMap, String> = HashMap::new(); + // Add non-generic patterns that appear 2+ times + for (sig, name) in &signature_to_pattern { + if signature_counts.get(sig).copied().unwrap_or(0) >= 2 { + pattern_lookup.insert(sig.clone(), name.clone()); + } + } + // Add generic mappings (overwrite if there's overlap) + pattern_lookup.extend(generic_mappings.clone()); + + // Build the concrete_to_pattern map to return + let concrete_to_pattern = pattern_lookup.clone(); // Second pass: analyze field positions by traversing tree instances analyze_pattern_field_positions(tree, &mut patterns, &pattern_lookup); @@ -176,7 +232,93 @@ fn detect_structural_patterns(tree: &TreeNode) -> Vec { // Sort by number of fields descending (larger patterns first) patterns.sort_by(|a, b| b.fields.len().cmp(&a.fields.len())); - patterns + (patterns, concrete_to_pattern) +} + +/// Detect generic patterns by grouping all signatures by their normalized form. +/// Returns (generic_patterns, concrete_signature -> generic_pattern_name mapping). +fn detect_generic_patterns( + signature_to_pattern: &HashMap, String>, +) -> (Vec, HashMap, String>) { + // Group signatures by their normalized (generic) form + let mut normalized_groups: HashMap, Vec<(Vec, String)>> = HashMap::new(); + + for (fields, name) in signature_to_pattern { + if let Some(normalized) = normalize_fields_for_generic(fields) { + normalized_groups + .entry(normalized) + .or_default() + .push((fields.clone(), name.clone())); + } + } + + let mut patterns = Vec::new(); + let mut mappings: HashMap, String> = HashMap::new(); + + // Create generic patterns for groups with 2+ different concrete signatures + for (normalized_fields, group) in normalized_groups { + if group.len() >= 2 { + // Use the first pattern's name as the generic pattern name + let generic_name = group[0].1.clone(); + + // Map all concrete signatures to this generic pattern + for (concrete_fields, _) in &group { + mappings.insert(concrete_fields.clone(), generic_name.clone()); + } + + patterns.push(StructuralPattern { + name: generic_name, + fields: normalized_fields, + field_positions: HashMap::new(), + is_generic: true, + }); + } + } + + (patterns, mappings) +} + +/// Normalize fields by replacing concrete value types with "T" for generic matching. +/// Returns None if the pattern is not suitable for generics (e.g., mixed value types). +fn normalize_fields_for_generic(fields: &[PatternField]) -> Option> { + // Get all leaf field value types + let leaf_types: Vec<&str> = fields + .iter() + .filter(|f| !f.indexes.is_empty()) // Only leaves have indexes + .map(|f| f.rust_type.as_str()) + .collect(); + + // Need at least one leaf to be generic + if leaf_types.is_empty() { + return None; + } + + // All leaves must have the same value type + let first_type = leaf_types[0]; + if !leaf_types.iter().all(|t| *t == first_type) { + return None; + } + + // Create normalized fields with "T" as the value type + let normalized: Vec = fields + .iter() + .map(|f| { + if f.indexes.is_empty() { + // Branch field - keep as is + f.clone() + } else { + // Leaf field - replace value type with T + PatternField { + name: f.name.clone(), + rust_type: "T".to_string(), + json_type: "T".to_string(), + indexes: f.indexes.clone(), + } + } + }) + .collect(); + + Some(normalized) } /// Analyze field positions for all patterns by traversing tree instances.