kumi 0.0.5 → 0.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +76 -174
  3. data/README.md +205 -52
  4. data/{documents → docs}/AST.md +29 -29
  5. data/{documents → docs}/SYNTAX.md +95 -8
  6. data/docs/features/README.md +45 -0
  7. data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
  8. data/docs/features/analysis-type-inference.md +42 -0
  9. data/docs/features/analysis-unsat-detection.md +71 -0
  10. data/docs/features/array-broadcasting.md +170 -0
  11. data/docs/features/input-declaration-system.md +42 -0
  12. data/docs/features/performance.md +16 -0
  13. data/docs/schema_metadata/broadcasts.md +53 -0
  14. data/docs/schema_metadata/cascades.md +45 -0
  15. data/docs/schema_metadata/declarations.md +54 -0
  16. data/docs/schema_metadata/dependencies.md +57 -0
  17. data/docs/schema_metadata/evaluation_order.md +29 -0
  18. data/docs/schema_metadata/examples.md +95 -0
  19. data/docs/schema_metadata/inferred_types.md +46 -0
  20. data/docs/schema_metadata/inputs.md +86 -0
  21. data/docs/schema_metadata.md +108 -0
  22. data/examples/federal_tax_calculator_2024.rb +11 -6
  23. data/lib/kumi/analyzer/constant_evaluator.rb +1 -1
  24. data/lib/kumi/analyzer/passes/broadcast_detector.rb +246 -0
  25. data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +4 -4
  26. data/lib/kumi/analyzer/passes/dependency_resolver.rb +78 -38
  27. data/lib/kumi/analyzer/passes/input_collector.rb +91 -30
  28. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  29. data/lib/kumi/analyzer/passes/pass_base.rb +1 -1
  30. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +24 -25
  31. data/lib/kumi/analyzer/passes/toposorter.rb +44 -8
  32. data/lib/kumi/analyzer/passes/type_checker.rb +34 -14
  33. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -2
  34. data/lib/kumi/analyzer/passes/type_inferencer.rb +130 -21
  35. data/lib/kumi/analyzer/passes/unsat_detector.rb +134 -56
  36. data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -2
  37. data/lib/kumi/analyzer.rb +16 -17
  38. data/lib/kumi/compiler.rb +188 -16
  39. data/lib/kumi/constraint_relationship_solver.rb +6 -6
  40. data/lib/kumi/domain/validator.rb +0 -4
  41. data/lib/kumi/error_reporting.rb +1 -1
  42. data/lib/kumi/explain.rb +32 -20
  43. data/lib/kumi/export/node_registry.rb +26 -12
  44. data/lib/kumi/export/node_serializers.rb +1 -1
  45. data/lib/kumi/function_registry/collection_functions.rb +14 -9
  46. data/lib/kumi/function_registry/function_builder.rb +4 -3
  47. data/lib/kumi/function_registry.rb +8 -2
  48. data/lib/kumi/input/type_matcher.rb +3 -0
  49. data/lib/kumi/input/validator.rb +0 -3
  50. data/lib/kumi/json_schema/generator.rb +63 -0
  51. data/lib/kumi/json_schema/validator.rb +25 -0
  52. data/lib/kumi/json_schema.rb +14 -0
  53. data/lib/kumi/{parser → ruby_parser}/build_context.rb +1 -1
  54. data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +36 -0
  55. data/lib/kumi/{parser → ruby_parser}/dsl.rb +1 -1
  56. data/lib/kumi/{parser → ruby_parser}/dsl_cascade_builder.rb +5 -5
  57. data/lib/kumi/{parser → ruby_parser}/expression_converter.rb +20 -20
  58. data/lib/kumi/{parser → ruby_parser}/guard_rails.rb +1 -1
  59. data/lib/kumi/{parser → ruby_parser}/input_builder.rb +41 -10
  60. data/lib/kumi/ruby_parser/input_field_proxy.rb +46 -0
  61. data/lib/kumi/{parser → ruby_parser}/input_proxy.rb +4 -4
  62. data/lib/kumi/ruby_parser/nested_input.rb +15 -0
  63. data/lib/kumi/{parser → ruby_parser}/parser.rb +11 -10
  64. data/lib/kumi/{parser → ruby_parser}/schema_builder.rb +11 -10
  65. data/lib/kumi/{parser → ruby_parser}/sugar.rb +62 -10
  66. data/lib/kumi/ruby_parser.rb +10 -0
  67. data/lib/kumi/schema.rb +10 -4
  68. data/lib/kumi/schema_instance.rb +6 -6
  69. data/lib/kumi/schema_metadata.rb +524 -0
  70. data/lib/kumi/syntax/array_expression.rb +15 -0
  71. data/lib/kumi/syntax/call_expression.rb +11 -0
  72. data/lib/kumi/syntax/cascade_expression.rb +11 -0
  73. data/lib/kumi/syntax/case_expression.rb +11 -0
  74. data/lib/kumi/syntax/declaration_reference.rb +11 -0
  75. data/lib/kumi/syntax/hash_expression.rb +11 -0
  76. data/lib/kumi/syntax/input_declaration.rb +12 -0
  77. data/lib/kumi/syntax/input_element_reference.rb +12 -0
  78. data/lib/kumi/syntax/input_reference.rb +12 -0
  79. data/lib/kumi/syntax/literal.rb +11 -0
  80. data/lib/kumi/syntax/trait_declaration.rb +11 -0
  81. data/lib/kumi/syntax/value_declaration.rb +11 -0
  82. data/lib/kumi/vectorization_metadata.rb +108 -0
  83. data/lib/kumi/version.rb +1 -1
  84. data/lib/kumi.rb +14 -0
  85. metadata +55 -25
  86. data/lib/generators/trait_engine/templates/schema_spec.rb.erb +0 -27
  87. data/lib/kumi/domain.rb +0 -8
  88. data/lib/kumi/input.rb +0 -8
  89. data/lib/kumi/syntax/declarations.rb +0 -26
  90. data/lib/kumi/syntax/expressions.rb +0 -34
  91. data/lib/kumi/syntax/terminal_expressions.rb +0 -30
  92. data/lib/kumi/syntax.rb +0 -9
  93. /data/{documents → docs}/DSL.md +0 -0
  94. /data/{documents → docs}/FUNCTIONS.md +0 -0
@@ -3,18 +3,29 @@
3
3
  module Kumi
4
4
  module Analyzer
5
5
  module Passes
6
- # RESPONSIBILITY: Build dependency graph and leaf map, validate references
7
- # DEPENDENCIES: :definitions, :input_meta
8
- # PRODUCES: :dependency_graph - Hash of name → [DependencyEdge], :leaf_map - Hash of name → Set[nodes]
6
+ # RESPONSIBILITY: Build dependency graph and detect conditional dependencies in cascades
7
+ # DEPENDENCIES: :declarations from NameIndexer, :inputs from InputCollector
8
+ # PRODUCES: :dependencies, :dependents, :leaves - Dependency analysis results
9
9
  # INTERFACE: new(schema, state).run(errors)
10
10
  class DependencyResolver < PassBase
11
- # A Struct to hold rich dependency information
12
- DependencyEdge = Struct.new(:to, :type, :via, keyword_init: true)
11
+ # Enhanced edge with conditional flag and cascade metadata
12
+ class DependencyEdge
13
+ attr_reader :to, :type, :via, :conditional, :cascade_owner
14
+
15
+ def initialize(to:, type:, via:, conditional: false, cascade_owner: nil)
16
+ @to = to
17
+ @type = type
18
+ @via = via
19
+ @conditional = conditional
20
+ @cascade_owner = cascade_owner
21
+ end
22
+ end
23
+
13
24
  include Syntax
14
25
 
15
26
  def run(errors)
16
- definitions = get_state(:definitions)
17
- input_meta = get_state(:input_meta)
27
+ definitions = get_state(:declarations)
28
+ input_meta = get_state(:inputs)
18
29
 
19
30
  dependency_graph = Hash.new { |h, k| h[k] = [] }
20
31
  reverse_dependencies = Hash.new { |h, k| h[k] = [] }
@@ -22,7 +33,7 @@ module Kumi
22
33
 
23
34
  each_decl do |decl|
24
35
  # Traverse the expression for each declaration, passing context down
25
- visit_with_context(decl.expression) do |node, context|
36
+ visit_with_context(decl.expression, { decl_name: decl.name }) do |node, context|
26
37
  process_node(node, decl, dependency_graph, reverse_dependencies, leaf_map, definitions, input_meta, errors, context)
27
38
  end
28
39
  end
@@ -30,38 +41,83 @@ module Kumi
30
41
  # Compute transitive closure of reverse dependencies
31
42
  transitive_dependents = compute_transitive_closure(reverse_dependencies)
32
43
 
33
- state.with(:dependency_graph, dependency_graph.transform_values(&:freeze).freeze)
34
- .with(:transitive_dependents, transitive_dependents.freeze)
35
- .with(:leaf_map, leaf_map.transform_values(&:freeze).freeze)
44
+ state.with(:dependencies, dependency_graph.transform_values(&:freeze).freeze)
45
+ .with(:dependents, transitive_dependents.freeze)
46
+ .with(:leaves, leaf_map.transform_values(&:freeze).freeze)
36
47
  end
37
48
 
38
49
  private
39
50
 
40
- def process_node(node, decl, graph, reverse_deps, leaves, definitions, input_meta, errors, context)
51
+ def process_node(node, decl, graph, reverse_deps, leaves, definitions, _input_meta, errors, context)
41
52
  case node
42
- when Binding
53
+ when DeclarationReference
43
54
  report_error(errors, "undefined reference to `#{node.name}`", location: node.loc) unless definitions.key?(node.name)
44
- add_dependency_edge(graph, reverse_deps, decl.name, node.name, :ref, context[:via])
45
- when FieldRef
46
- report_error(errors, "undeclared input `#{node.name}`", location: node.loc) unless input_meta.key?(node.name)
55
+
56
+ # Determine if this is a conditional dependency
57
+ conditional = context[:in_cascade_branch] || context[:in_cascade_base] || false
58
+ cascade_owner = conditional ? (context[:cascade_owner] || context[:decl_name]) : nil
59
+
60
+ add_dependency_edge(graph, reverse_deps, decl.name, node.name, :ref, context[:via],
61
+ conditional: conditional,
62
+ cascade_owner: cascade_owner)
63
+ when InputReference
47
64
  add_dependency_edge(graph, reverse_deps, decl.name, node.name, :key, context[:via])
48
- leaves[decl.name] << node # put it back
65
+ leaves[decl.name] << node
66
+ when InputElementReference
67
+ # adds the root input declaration as a dependency
68
+ root_input_declr_name = node.path.first
69
+ add_dependency_edge(graph, reverse_deps, decl.name, root_input_declr_name, :key, context[:via])
49
70
  when Literal
50
71
  leaves[decl.name] << node
51
72
  end
52
73
  end
53
74
 
54
- def add_dependency_edge(graph, reverse_deps, from, to, type, via)
55
- edge = DependencyEdge.new(to: to, type: type, via: via)
75
+ def add_dependency_edge(graph, reverse_deps, from, to, type, via, conditional: false, cascade_owner: nil)
76
+ edge = DependencyEdge.new(
77
+ to: to,
78
+ type: type,
79
+ via: via,
80
+ conditional: conditional,
81
+ cascade_owner: cascade_owner
82
+ )
56
83
  graph[from] << edge
57
84
  reverse_deps[to] << from
58
85
  end
59
86
 
60
- # Compute transitive closure: for each key, find ALL declarations that depend on it
87
+ # Custom visitor that understands cascade structure
88
+ def visit_with_context(node, context = {}, &block)
89
+ return unless node
90
+
91
+ yield(node, context)
92
+
93
+ case node
94
+ when CascadeExpression
95
+ # Visit condition nodes and result expressions (non-base cases)
96
+ node.cases[0...-1].each do |when_case|
97
+ if when_case.condition
98
+ # Visit condition normally
99
+ visit_with_context(when_case.condition, context, &block)
100
+ end
101
+ # Visit result expressions as conditional dependencies
102
+ conditional_context = context.merge(in_cascade_branch: true, cascade_owner: context[:decl_name])
103
+ visit_with_context(when_case.result, conditional_context, &block)
104
+ end
105
+
106
+ # Visit base case with conditional flag
107
+ if node.cases.last
108
+ base_context = context.merge(in_cascade_base: true)
109
+ visit_with_context(node.cases.last.result, base_context, &block)
110
+ end
111
+ when CallExpression
112
+ new_context = context.merge(via: node.fn_name)
113
+ node.children.each { |child| visit_with_context(child, new_context, &block) }
114
+ else
115
+ node.children.each { |child| visit_with_context(child, context, &block) } if node.respond_to?(:children)
116
+ end
117
+ end
118
+
61
119
  def compute_transitive_closure(reverse_dependencies)
62
120
  transitive = {}
63
-
64
- # Collect all keys first to avoid iteration issues
65
121
  all_keys = reverse_dependencies.keys
66
122
 
67
123
  all_keys.each do |key|
@@ -75,7 +131,6 @@ module Kumi
75
131
 
76
132
  visited.add(current)
77
133
 
78
- # Get direct dependents
79
134
  direct_dependents = reverse_dependencies[current] || []
80
135
  direct_dependents.each do |dependent|
81
136
  next if visited.include?(dependent)
@@ -90,21 +145,6 @@ module Kumi
90
145
 
91
146
  transitive
92
147
  end
93
-
94
- # Custom visitor that passes context (like function name) down the tree
95
- def visit_with_context(node, context = {}, &block)
96
- return unless node
97
-
98
- yield(node, context)
99
-
100
- new_context = if node.is_a?(Expressions::CallExpression)
101
- { via: node.fn_name }
102
- else
103
- context
104
- end
105
-
106
- node.children.each { |child| visit_with_context(child, new_context, &block) }
107
- end
108
148
  end
109
149
  end
110
150
  end
@@ -5,17 +5,15 @@ module Kumi
5
5
  module Passes
6
6
  # RESPONSIBILITY: Collect field metadata from input declarations and validate consistency
7
7
  # DEPENDENCIES: :definitions
8
- # PRODUCES: :input_meta - Hash mapping field names to {type:, domain:} metadata
8
+ # PRODUCES: :inputs - Hash mapping field names to {type:, domain:} metadata
9
9
  # INTERFACE: new(schema, state).run(errors)
10
10
  class InputCollector < PassBase
11
- include Syntax::TerminalExpressions
12
-
13
11
  def run(errors)
14
12
  input_meta = {}
15
13
 
16
14
  schema.inputs.each do |field_decl|
17
- unless field_decl.is_a?(FieldDecl)
18
- report_error(errors, "Expected FieldDecl node, got #{field_decl.class}", location: field_decl.loc)
15
+ unless field_decl.is_a?(Kumi::Syntax::InputDeclaration)
16
+ report_error(errors, "Expected InputDeclaration node, got #{field_decl.class}", location: field_decl.loc)
19
17
  next
20
18
  end
21
19
 
@@ -23,40 +21,103 @@ module Kumi
23
21
  existing = input_meta[name]
24
22
 
25
23
  if existing
26
- # Check for compatibility
27
- if existing[:type] != field_decl.type && field_decl.type && existing[:type]
28
- report_error(errors,
29
- "Field :#{name} declared with conflicting types: #{existing[:type]} vs #{field_decl.type}",
30
- location: field_decl.loc)
31
- end
24
+ # Check for compatibility and merge
25
+ merged_meta = merge_field_metadata(existing, field_decl, errors)
26
+ input_meta[name] = merged_meta if merged_meta
27
+ else
28
+ # New field - collect its metadata
29
+ input_meta[name] = collect_field_metadata(field_decl, errors)
30
+ end
31
+ end
32
+
33
+ state.with(:inputs, freeze_nested_hash(input_meta))
34
+ end
32
35
 
33
- if existing[:domain] != field_decl.domain && field_decl.domain && existing[:domain]
34
- report_error(errors,
35
- "Field :#{name} declared with conflicting domains: #{existing[:domain].inspect} vs #{field_decl.domain.inspect}",
36
- location: field_decl.loc)
36
+ private
37
+
38
+ def collect_field_metadata(field_decl, errors)
39
+ validate_domain_type(field_decl, errors) if field_decl.domain
40
+
41
+ metadata = {
42
+ type: field_decl.type,
43
+ domain: field_decl.domain
44
+ }
45
+
46
+ # Process children if present
47
+ if field_decl.children && !field_decl.children.empty?
48
+ children_meta = {}
49
+ field_decl.children.each do |child_decl|
50
+ unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
51
+ report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
52
+ next
37
53
  end
54
+ children_meta[child_decl.name] = collect_field_metadata(child_decl, errors)
55
+ end
56
+ metadata[:children] = children_meta
57
+ end
38
58
 
39
- # Validate domain type if provided
40
- validate_domain_type(field_decl, errors) if field_decl.domain
59
+ metadata
60
+ end
41
61
 
42
- # Merge metadata (later declarations override nil values)
43
- input_meta[name] = {
44
- type: field_decl.type || existing[:type],
45
- domain: field_decl.domain || existing[:domain]
46
- }
47
- else
48
- validate_domain_type(field_decl, errors) if field_decl.domain
49
- input_meta[name] = {
50
- type: field_decl.type,
51
- domain: field_decl.domain
52
- }
62
+ def merge_field_metadata(existing, field_decl, errors)
63
+ name = field_decl.name
64
+
65
+ # Check for type compatibility
66
+ if existing[:type] != field_decl.type && field_decl.type && existing[:type]
67
+ report_error(errors,
68
+ "Field :#{name} declared with conflicting types: #{existing[:type]} vs #{field_decl.type}",
69
+ location: field_decl.loc)
70
+ end
71
+
72
+ # Check for domain compatibility
73
+ if existing[:domain] != field_decl.domain && field_decl.domain && existing[:domain]
74
+ report_error(errors,
75
+ "Field :#{name} declared with conflicting domains: #{existing[:domain].inspect} vs #{field_decl.domain.inspect}",
76
+ location: field_decl.loc)
77
+ end
78
+
79
+ # Validate domain type if provided
80
+ validate_domain_type(field_decl, errors) if field_decl.domain
81
+
82
+ # Merge metadata (later declarations override nil values)
83
+ merged = {
84
+ type: field_decl.type || existing[:type],
85
+ domain: field_decl.domain || existing[:domain]
86
+ }
87
+
88
+ # Merge children if present
89
+ if field_decl.children && !field_decl.children.empty?
90
+ existing_children = existing[:children] || {}
91
+ new_children = {}
92
+
93
+ field_decl.children.each do |child_decl|
94
+ unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
95
+ report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
96
+ next
97
+ end
98
+
99
+ child_name = child_decl.name
100
+ new_children[child_name] = if existing_children[child_name]
101
+ merge_field_metadata(existing_children[child_name], child_decl, errors)
102
+ else
103
+ collect_field_metadata(child_decl, errors)
104
+ end
53
105
  end
106
+
107
+ merged[:children] = new_children
108
+ elsif existing[:children]
109
+ merged[:children] = existing[:children]
54
110
  end
55
111
 
56
- state.with(:input_meta, input_meta.freeze)
112
+ merged
57
113
  end
58
114
 
59
- private
115
+ def freeze_nested_hash(hash)
116
+ hash.each_value do |value|
117
+ freeze_nested_hash(value) if value.is_a?(Hash)
118
+ end
119
+ hash.freeze
120
+ end
60
121
 
61
122
  def validate_domain_type(field_decl, errors)
62
123
  domain = field_decl.domain
@@ -5,7 +5,7 @@ module Kumi
5
5
  module Passes
6
6
  # RESPONSIBILITY: Build definitions index and detect duplicate names
7
7
  # DEPENDENCIES: None (first pass in pipeline)
8
- # PRODUCES: :definitions - Hash mapping names to declaration nodes
8
+ # PRODUCES: :declarations - Hash mapping names to declaration nodes
9
9
  # INTERFACE: new(schema, state).run(errors)
10
10
  class NameIndexer < PassBase
11
11
  def run(errors)
@@ -16,7 +16,7 @@ module Kumi
16
16
  definitions[decl.name] = decl
17
17
  end
18
18
 
19
- state.with(:definitions, definitions.freeze)
19
+ state.with(:declarations, definitions.freeze)
20
20
  end
21
21
  end
22
22
  end
@@ -27,7 +27,7 @@ module Kumi
27
27
  attr_reader :schema, :state
28
28
 
29
29
  # Iterate over all declarations (attributes and traits) in the schema
30
- # @yield [Syntax::Declarations::Attribute|Syntax::Declarations::Trait] Each declaration
30
+ # @yield [Syntax::Attribute|Syntax::Trait] Each declaration
31
31
  def each_decl(&block)
32
32
  schema.attributes.each(&block)
33
33
  schema.traits.each(&block)
@@ -10,7 +10,7 @@ module Kumi
10
10
  #
11
11
  # This pass enforces semantic constraints that must hold regardless of which parser
12
12
  # was used to construct the AST. It validates:
13
- # 1. Cascade conditions are only trait references (Binding nodes)
13
+ # 1. Cascade conditions are only trait references (DeclarationReference nodes)
14
14
  # 2. Trait expressions evaluate to boolean values (CallExpression nodes)
15
15
  # 3. Function names exist in the function registry
16
16
  # 4. Expression types are valid for their context
@@ -24,19 +24,19 @@ module Kumi
24
24
 
25
25
  private
26
26
 
27
- def validate_semantic_constraints(node, decl, errors)
27
+ def validate_semantic_constraints(node, _decl, errors)
28
28
  case node
29
- when Declarations::Trait
29
+ when Kumi::Syntax::TraitDeclaration
30
30
  validate_trait_expression(node, errors)
31
- when Expressions::WhenCaseExpression
31
+ when Kumi::Syntax::CaseExpression
32
32
  validate_cascade_condition(node, errors)
33
- when Expressions::CallExpression
33
+ when Kumi::Syntax::CallExpression
34
34
  validate_function_call(node, errors)
35
35
  end
36
36
  end
37
37
 
38
38
  def validate_trait_expression(trait, errors)
39
- return if trait.expression.is_a?(Expressions::CallExpression)
39
+ return if trait.expression.is_a?(Kumi::Syntax::CallExpression)
40
40
 
41
41
  report_error(
42
42
  errors,
@@ -48,22 +48,22 @@ module Kumi
48
48
 
49
49
  def validate_cascade_condition(when_case, errors)
50
50
  condition = when_case.condition
51
-
51
+
52
52
  case condition
53
- when TerminalExpressions::Binding
53
+ when Kumi::Syntax::DeclarationReference
54
54
  # Valid: trait reference
55
- return
56
- when Expressions::CallExpression
55
+ nil
56
+ when Kumi::Syntax::CallExpression
57
57
  # Valid if it's a boolean composition of traits (all?, any?, none?)
58
58
  return if boolean_trait_composition?(condition)
59
-
59
+
60
60
  # For now, allow other CallExpressions - they'll be validated by other passes
61
- return
62
- when TerminalExpressions::Literal
61
+ nil
62
+ when Kumi::Syntax::Literal
63
63
  # Allow literal conditions (like true/false) - they might be valid
64
- return
64
+ nil
65
65
  else
66
- # Only reject truly invalid conditions like FieldRef or complex expressions
66
+ # Only reject truly invalid conditions like InputReference or complex expressions
67
67
  report_error(
68
68
  errors,
69
69
  "cascade condition must be trait reference",
@@ -75,10 +75,10 @@ module Kumi
75
75
 
76
76
  def validate_function_call(call_expr, errors)
77
77
  fn_name = call_expr.fn_name
78
-
78
+
79
79
  # Skip validation if FunctionRegistry is being mocked for testing
80
80
  return if function_registry_mocked?
81
-
81
+
82
82
  return if FunctionRegistry.supported?(fn_name)
83
83
 
84
84
  report_error(
@@ -96,15 +96,14 @@ module Kumi
96
96
 
97
97
  def function_registry_mocked?
98
98
  # Check if FunctionRegistry is being mocked (for tests)
99
- begin
100
- # Try to access a method that doesn't exist in the real registry
101
- # If it's mocked, this won't raise an error
102
- FunctionRegistry.respond_to?(:confirm_support!)
103
- rescue
104
- false
105
- end
99
+
100
+ # Try to access a method that doesn't exist in the real registry
101
+ # If it's mocked, this won't raise an error
102
+ FunctionRegistry.respond_to?(:confirm_support!)
103
+ rescue StandardError
104
+ false
106
105
  end
107
106
  end
108
107
  end
109
108
  end
110
- end
109
+ end
@@ -3,17 +3,17 @@
3
3
  module Kumi
4
4
  module Analyzer
5
5
  module Passes
6
- # RESPONSIBILITY: Compute topological ordering of declarations from dependency graph
7
- # DEPENDENCIES: :dependency_graph from DependencyResolver, :definitions from NameIndexer
8
- # PRODUCES: :topo_order - Array of declaration names in evaluation order
6
+ # RESPONSIBILITY: Compute topological ordering of declarations, allowing safe conditional cycles
7
+ # DEPENDENCIES: :dependencies from DependencyResolver, :declarations from NameIndexer, :cascades from UnsatDetector
8
+ # PRODUCES: :evaluation_order - Array of declaration names in evaluation order
9
9
  # INTERFACE: new(schema, state).run(errors)
10
10
  class Toposorter < PassBase
11
11
  def run(errors)
12
- dependency_graph = get_state(:dependency_graph, required: false) || {}
13
- definitions = get_state(:definitions, required: false) || {}
12
+ dependency_graph = get_state(:dependencies, required: false) || {}
13
+ definitions = get_state(:declarations, required: false) || {}
14
14
 
15
15
  order = compute_topological_order(dependency_graph, definitions, errors)
16
- state.with(:topo_order, order)
16
+ state.with(:evaluation_order, order)
17
17
  end
18
18
 
19
19
  private
@@ -22,17 +22,26 @@ module Kumi
22
22
  temp_marks = Set.new
23
23
  perm_marks = Set.new
24
24
  order = []
25
+ cascades = get_state(:cascades) || {}
25
26
 
26
- visit_node = lambda do |node|
27
+ visit_node = lambda do |node, path = []|
27
28
  return if perm_marks.include?(node)
28
29
 
29
30
  if temp_marks.include?(node)
31
+ # Check if this is a safe conditional cycle
32
+ cycle_path = path + [node]
33
+ return if safe_conditional_cycle?(cycle_path, graph, cascades)
34
+
35
+ # Allow this cycle - it's safe due to cascade mutual exclusion
36
+
30
37
  report_unexpected_cycle(temp_marks, node, errors)
38
+
31
39
  return
32
40
  end
33
41
 
34
42
  temp_marks << node
35
- Array(graph[node]).each { |edge| visit_node.call(edge.to) }
43
+ current_path = path + [node]
44
+ Array(graph[node]).each { |edge| visit_node.call(edge.to, current_path) }
36
45
  temp_marks.delete(node)
37
46
  perm_marks << node
38
47
 
@@ -50,6 +59,33 @@ module Kumi
50
59
  order.freeze
51
60
  end
52
61
 
62
+ def safe_conditional_cycle?(cycle_path, graph, cascades)
63
+ return false if cycle_path.nil? || cycle_path.size < 2
64
+
65
+ # Find where the cycle starts - look for the first occurrence of the repeated node
66
+ last_node = cycle_path.last
67
+ return false if last_node.nil?
68
+
69
+ cycle_start = cycle_path.index(last_node)
70
+ return false unless cycle_start && cycle_start < cycle_path.size - 1
71
+
72
+ cycle_nodes = cycle_path[cycle_start..]
73
+
74
+ # Check if all edges in the cycle are conditional
75
+ cycle_nodes.each_cons(2) do |from, to|
76
+ edges = graph[from] || []
77
+ edge = edges.find { |e| e.to == to }
78
+
79
+ return false unless edge&.conditional
80
+
81
+ # Check if the cascade has mutually exclusive conditions
82
+ cascade_meta = cascades[edge.cascade_owner]
83
+ return false unless cascade_meta&.dig(:all_mutually_exclusive)
84
+ end
85
+
86
+ true
87
+ end
88
+
53
89
  def report_unexpected_cycle(temp_marks, current_node, errors)
54
90
  cycle_path = temp_marks.to_a.join(" → ") + " → #{current_node}"
55
91
 
@@ -4,12 +4,12 @@ module Kumi
4
4
  module Analyzer
5
5
  module Passes
6
6
  # RESPONSIBILITY: Validate function call arity and argument types against FunctionRegistry
7
- # DEPENDENCIES: :decl_types from TypeInferencer
7
+ # DEPENDENCIES: :inferred_types from TypeInferencer
8
8
  # PRODUCES: None (validation only)
9
9
  # INTERFACE: new(schema, state).run(errors)
10
10
  class TypeChecker < VisitorPass
11
11
  def run(errors)
12
- visit_nodes_of_type(Expressions::CallExpression, errors: errors) do |node, _decl, errs|
12
+ visit_nodes_of_type(Kumi::Syntax::CallExpression, errors: errors) do |node, _decl, errs|
13
13
  validate_function_call(node, errs)
14
14
  end
15
15
  state
@@ -47,11 +47,31 @@ module Kumi
47
47
  types = signature[:param_types]
48
48
  return if types.nil? || (signature[:arity].negative? && node.args.empty?)
49
49
 
50
+ # Skip type checking for vectorized operations
51
+ broadcast_meta = get_state(:broadcasts, required: false)
52
+ return if broadcast_meta && is_part_of_vectorized_operation?(node, broadcast_meta)
53
+
50
54
  node.args.each_with_index do |arg, i|
51
55
  validate_argument_type(arg, i, types[i], node.fn_name, errors)
52
56
  end
53
57
  end
54
58
 
59
+ def is_part_of_vectorized_operation?(node, broadcast_meta)
60
+ # Check if this node is part of a vectorized or reduction operation
61
+ # This is a simplified check - in a real implementation we'd need to track context
62
+ node.args.any? do |arg|
63
+ case arg
64
+ when Kumi::Syntax::DeclarationReference
65
+ broadcast_meta[:vectorized_operations]&.key?(arg.name) ||
66
+ broadcast_meta[:reduction_operations]&.key?(arg.name)
67
+ when Kumi::Syntax::InputElementReference
68
+ broadcast_meta[:array_fields]&.key?(arg.path.first)
69
+ else
70
+ false
71
+ end
72
+ end
73
+ end
74
+
55
75
  def validate_argument_type(arg, index, expected_type, fn_name, errors)
56
76
  return if expected_type.nil? || expected_type == Kumi::Types::ANY
57
77
 
@@ -67,15 +87,15 @@ module Kumi
67
87
 
68
88
  def get_expression_type(expr)
69
89
  case expr
70
- when TerminalExpressions::Literal
90
+ when Kumi::Syntax::Literal
71
91
  # Inferred type from literal value
72
92
  Kumi::Types.infer_from_value(expr.value)
73
93
 
74
- when TerminalExpressions::FieldRef
94
+ when Kumi::Syntax::InputReference
75
95
  # Declared type from input block (user-specified)
76
96
  get_declared_field_type(expr.name)
77
97
 
78
- when TerminalExpressions::Binding
98
+ when Kumi::Syntax::DeclarationReference
79
99
  # Inferred type from type inference results
80
100
  get_inferred_declaration_type(expr.name)
81
101
 
@@ -88,24 +108,24 @@ module Kumi
88
108
 
89
109
  def get_declared_field_type(field_name)
90
110
  # Get explicitly declared type from input metadata
91
- input_meta = get_state(:input_meta, required: false) || {}
111
+ input_meta = get_state(:inputs, required: false) || {}
92
112
  field_meta = input_meta[field_name]
93
113
  field_meta&.dig(:type) || Kumi::Types::ANY
94
114
  end
95
115
 
96
116
  def get_inferred_declaration_type(decl_name)
97
117
  # Get inferred type from type inference results
98
- decl_types = get_state(:decl_types, required: true)
118
+ decl_types = get_state(:inferred_types, required: true)
99
119
  decl_types[decl_name] || Kumi::Types::ANY
100
120
  end
101
121
 
102
122
  def describe_expression_type(expr, type)
103
123
  case expr
104
- when TerminalExpressions::Literal
124
+ when Kumi::Syntax::Literal
105
125
  "`#{expr.value}` of type #{type} (literal value)"
106
126
 
107
- when TerminalExpressions::FieldRef
108
- input_meta = get_state(:input_meta, required: false) || {}
127
+ when Kumi::Syntax::InputReference
128
+ input_meta = get_state(:inputs, required: false) || {}
109
129
  field_meta = input_meta[expr.name]
110
130
 
111
131
  if field_meta&.dig(:type)
@@ -117,17 +137,17 @@ module Kumi
117
137
  "undeclared input field `#{expr.name}` (inferred as #{type})"
118
138
  end
119
139
 
120
- when TerminalExpressions::Binding
140
+ when Kumi::Syntax::DeclarationReference
121
141
  # This type was inferred from the declaration's expression
122
142
  "reference to declaration `#{expr.name}` of inferred type #{type}"
123
143
 
124
- when Expressions::CallExpression
144
+ when Kumi::Syntax::CallExpression
125
145
  "result of function `#{expr.fn_name}` returning #{type}"
126
146
 
127
- when Expressions::ListExpression
147
+ when Kumi::Syntax::ArrayExpression
128
148
  "list expression of type #{type}"
129
149
 
130
- when Expressions::CascadeExpression
150
+ when Kumi::Syntax::CascadeExpression
131
151
  "cascade expression of type #{type}"
132
152
 
133
153
  else
@@ -4,12 +4,12 @@ module Kumi
4
4
  module Analyzer
5
5
  module Passes
6
6
  # RESPONSIBILITY: Validate consistency between declared and inferred types
7
- # DEPENDENCIES: :input_meta from InputCollector, :decl_types from TypeInferencer
7
+ # DEPENDENCIES: :inputs from InputCollector, :inferred_types from TypeInferencer
8
8
  # PRODUCES: None (validation only)
9
9
  # INTERFACE: new(schema, state).run(errors)
10
10
  class TypeConsistencyChecker < PassBase
11
11
  def run(errors)
12
- input_meta = get_state(:input_meta, required: false) || {}
12
+ input_meta = get_state(:inputs, required: false) || {}
13
13
 
14
14
  # First, validate that all declared types are valid
15
15
  validate_declared_types(input_meta, errors)