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
@@ -4,14 +4,17 @@ module Kumi
4
4
  module Analyzer
5
5
  module Passes
6
6
  # RESPONSIBILITY: Infer types for all declarations based on expression analysis
7
- # DEPENDENCIES: Toposorter (needs topo_order), DefinitionValidator (needs definitions)
8
- # PRODUCES: decl_types hash mapping declaration names to inferred types
7
+ # DEPENDENCIES: Toposorter (needs evaluation_order), DeclarationValidator (needs declarations)
8
+ # PRODUCES: inferred_types hash mapping declaration names to inferred types
9
9
  # INTERFACE: new(schema, state).run(errors)
10
10
  class TypeInferencer < PassBase
11
11
  def run(errors)
12
12
  types = {}
13
- topo_order = get_state(:topo_order)
14
- definitions = get_state(:definitions)
13
+ topo_order = get_state(:evaluation_order)
14
+ definitions = get_state(:declarations)
15
+
16
+ # Get broadcast metadata from broadcast detector
17
+ broadcast_meta = get_state(:broadcasts, required: false) || {}
15
18
 
16
19
  # Process declarations in topological order to ensure dependencies are resolved
17
20
  topo_order.each do |name|
@@ -19,44 +22,67 @@ module Kumi
19
22
  next unless decl
20
23
 
21
24
  begin
22
- inferred_type = infer_expression_type(decl.expression, types)
23
- types[name] = inferred_type
25
+ # Check if this declaration is marked as vectorized
26
+ if broadcast_meta[:vectorized_operations]&.key?(name)
27
+ # Infer the element type and wrap in array
28
+ element_type = infer_vectorized_element_type(decl.expression, types, broadcast_meta)
29
+ types[name] = decl.is_a?(Kumi::Syntax::TraitDeclaration) ? { array: :boolean } : { array: element_type }
30
+ else
31
+ # Normal type inference
32
+ inferred_type = infer_expression_type(decl.expression, types, broadcast_meta, name)
33
+ types[name] = inferred_type
34
+ end
24
35
  rescue StandardError => e
25
36
  report_type_error(errors, "Type inference failed: #{e.message}", location: decl&.loc)
26
37
  end
27
38
  end
28
39
 
29
- state.with(:decl_types, types)
40
+ state.with(:inferred_types, types)
30
41
  end
31
42
 
32
43
  private
33
44
 
34
- def infer_expression_type(expr, type_context = {})
45
+ def infer_expression_type(expr, type_context = {}, broadcast_metadata = {}, current_decl_name = nil)
35
46
  case expr
36
47
  when Literal
37
48
  Types.infer_from_value(expr.value)
38
- when FieldRef
49
+ when InputReference
39
50
  # Look up type from field metadata
40
- input_meta = get_state(:input_meta, required: false) || {}
51
+ input_meta = get_state(:inputs, required: false) || {}
41
52
  meta = input_meta[expr.name]
42
53
  meta&.dig(:type) || :any
43
- when Binding
54
+ when DeclarationReference
44
55
  type_context[expr.name] || :any
45
56
  when CallExpression
46
- infer_call_type(expr, type_context)
47
- when ListExpression
48
- infer_list_type(expr, type_context)
57
+ infer_call_type(expr, type_context, broadcast_metadata, current_decl_name)
58
+ when ArrayExpression
59
+ infer_list_type(expr, type_context, broadcast_metadata, current_decl_name)
49
60
  when CascadeExpression
50
- infer_cascade_type(expr, type_context)
61
+ infer_cascade_type(expr, type_context, broadcast_metadata, current_decl_name)
62
+ when InputElementReference
63
+ # Element reference returns the field type
64
+ infer_element_reference_type(expr)
51
65
  else
52
66
  :any
53
67
  end
54
68
  end
55
69
 
56
- def infer_call_type(call_expr, type_context)
70
+ def infer_call_type(call_expr, type_context, broadcast_metadata = {}, current_decl_name = nil)
57
71
  fn_name = call_expr.fn_name
58
72
  args = call_expr.args
59
73
 
74
+ # Check broadcast metadata first
75
+ if current_decl_name && broadcast_metadata[:vectorized_values]&.key?(current_decl_name)
76
+ # This declaration is marked as vectorized, so it produces an array
77
+ element_type = infer_vectorized_element_type(call_expr, type_context, broadcast_metadata)
78
+ return { array: element_type }
79
+ end
80
+
81
+ if current_decl_name && broadcast_metadata[:reducer_values]&.key?(current_decl_name)
82
+ # This declaration is marked as a reducer, get the result from the function
83
+ return infer_function_return_type(fn_name, args, type_context, broadcast_metadata)
84
+ end
85
+
60
86
  # Check if function exists in registry
61
87
  unless FunctionRegistry.supported?(fn_name)
62
88
  # Don't push error here - let existing TypeChecker handle it
@@ -72,7 +98,7 @@ module Kumi
72
98
  end
73
99
 
74
100
  # Infer argument types
75
- arg_types = args.map { |arg| infer_expression_type(arg, type_context) }
101
+ arg_types = args.map { |arg| infer_expression_type(arg, type_context, broadcast_metadata, current_decl_name) }
76
102
 
77
103
  # Validate parameter types (warn but don't fail)
78
104
  param_types = signature[:param_types] || []
@@ -90,10 +116,29 @@ module Kumi
90
116
  signature[:return_type] || :any
91
117
  end
92
118
 
93
- def infer_list_type(list_expr, type_context)
119
+ def infer_vectorized_element_type(call_expr, _type_context, _broadcast_metadata)
120
+ # For vectorized arithmetic operations, infer the element type
121
+ # For now, assume arithmetic operations on floats produce floats
122
+ case call_expr.fn_name
123
+ when :multiply, :add, :subtract, :divide
124
+ :float
125
+ else
126
+ :any
127
+ end
128
+ end
129
+
130
+ def infer_function_return_type(fn_name, _args, _type_context, _broadcast_metadata)
131
+ # Get the function signature
132
+ return :any unless FunctionRegistry.supported?(fn_name)
133
+
134
+ signature = FunctionRegistry.signature(fn_name)
135
+ signature[:return_type] || :any
136
+ end
137
+
138
+ def infer_list_type(list_expr, type_context, broadcast_metadata = {}, current_decl_name = nil)
94
139
  return Types.array(:any) if list_expr.elements.empty?
95
140
 
96
- element_types = list_expr.elements.map { |elem| infer_expression_type(elem, type_context) }
141
+ element_types = list_expr.elements.map { |elem| infer_expression_type(elem, type_context, broadcast_metadata, current_decl_name) }
97
142
 
98
143
  # Try to unify all element types
99
144
  unified_type = element_types.reduce { |acc, type| Types.unify(acc, type) }
@@ -103,11 +148,75 @@ module Kumi
103
148
  Types.array(:any)
104
149
  end
105
150
 
106
- def infer_cascade_type(cascade_expr, type_context)
151
+ def infer_vectorized_element_type(expr, type_context, vectorization_meta)
152
+ # For vectorized operations, we need to infer the element type
153
+ case expr
154
+ when InputElementReference
155
+ # Get the field type from metadata
156
+ input_meta = get_state(:inputs, required: false) || {}
157
+ array_name = expr.path.first
158
+ field_name = expr.path[1]
159
+
160
+ array_meta = input_meta[array_name]
161
+ return :any unless array_meta&.dig(:type) == :array
162
+
163
+ array_meta.dig(:children, field_name, :type) || :any
164
+
165
+ when CallExpression
166
+ # For arithmetic operations, infer from operands
167
+ if %i[add subtract multiply divide].include?(expr.fn_name)
168
+ # Get types of operands
169
+ arg_types = expr.args.map do |arg|
170
+ if arg.is_a?(InputElementReference)
171
+ infer_vectorized_element_type(arg, type_context, vectorization_meta)
172
+ elsif arg.is_a?(DeclarationReference)
173
+ # Get the element type if it's vectorized
174
+ ref_type = type_context[arg.name]
175
+ if ref_type.is_a?(Hash) && ref_type.key?(:array)
176
+ ref_type[:array]
177
+ else
178
+ ref_type || :any
179
+ end
180
+ else
181
+ infer_expression_type(arg, type_context, vectorization_meta)
182
+ end
183
+ end
184
+
185
+ # Unify types for arithmetic
186
+ Types.unify(*arg_types) || :float
187
+ else
188
+ :any
189
+ end
190
+
191
+ else
192
+ :any
193
+ end
194
+ end
195
+
196
+ def infer_element_reference_type(expr)
197
+ # Get array field metadata
198
+ input_meta = get_state(:inputs, required: false) || {}
199
+
200
+ return :any unless expr.path.size >= 2
201
+
202
+ array_name = expr.path.first
203
+ field_name = expr.path[1]
204
+
205
+ array_meta = input_meta[array_name]
206
+ return :any unless array_meta&.dig(:type) == :array
207
+
208
+ # Get the field type from children metadata
209
+ field_type = array_meta.dig(:children, field_name, :type) || :any
210
+
211
+ # Return array of field type (vectorized)
212
+ { array: field_type }
213
+ end
214
+
215
+ def infer_cascade_type(cascade_expr, type_context, broadcast_metadata = {}, current_decl_name = nil)
107
216
  return :any if cascade_expr.cases.empty?
108
217
 
109
218
  result_types = cascade_expr.cases.map do |case_stmt|
110
- infer_expression_type(case_stmt.result, type_context)
219
+ infer_expression_type(case_stmt.result, type_context, broadcast_metadata, current_decl_name)
111
220
  end
112
221
 
113
222
  # Reduce all possible types into a single unified type
@@ -3,6 +3,10 @@
3
3
  module Kumi
4
4
  module Analyzer
5
5
  module Passes
6
+ # RESPONSIBILITY: Detect unsatisfiable constraints and analyze cascade mutual exclusion
7
+ # DEPENDENCIES: :declarations from NameIndexer, :inputs from InputCollector
8
+ # PRODUCES: :cascades - Hash of cascade mutual exclusion analysis results
9
+ # INTERFACE: new(schema, state).run(errors)
6
10
  class UnsatDetector < VisitorPass
7
11
  include Syntax
8
12
 
@@ -10,68 +14,143 @@ module Kumi
10
14
  Atom = Kumi::AtomUnsatSolver::Atom
11
15
 
12
16
  def run(errors)
13
- definitions = get_state(:definitions)
14
- @input_meta = get_state(:input_meta) || {}
17
+ definitions = get_state(:declarations)
18
+ @input_meta = get_state(:inputs) || {}
15
19
  @definitions = definitions
16
20
  @evaluator = ConstantEvaluator.new(definitions)
17
21
 
22
+ # First pass: analyze cascade conditions for mutual exclusion
23
+ cascades = {}
18
24
  each_decl do |decl|
25
+ cascades[decl.name] = analyze_cascade_mutual_exclusion(decl, definitions) if decl.expression.is_a?(CascadeExpression)
26
+
27
+ # Store cascade metadata for later passes
28
+
29
+ # Second pass: check for unsatisfiable constraints
19
30
  if decl.expression.is_a?(CascadeExpression)
20
31
  # Special handling for cascade expressions
21
32
  check_cascade_expression(decl, definitions, errors)
22
- else
33
+ elsif decl.expression.is_a?(CallExpression) && decl.expression.fn_name == :or
23
34
  # Check for OR expressions which need special disjunctive handling
24
- if decl.expression.is_a?(CallExpression) && decl.expression.fn_name == :or
25
- impossible = check_or_expression(decl.expression, definitions, errors)
26
- report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
27
- else
28
- # Normal handling for non-cascade expressions
29
- atoms = gather_atoms(decl.expression, definitions, Set.new)
30
- next if atoms.empty?
31
-
32
- # Use enhanced solver that can detect cross-variable mathematical constraints
33
- impossible = if definitions && !definitions.empty?
34
- Kumi::ConstraintRelationshipSolver.unsat?(atoms, definitions, input_meta: @input_meta)
35
- else
36
- Kumi::AtomUnsatSolver.unsat?(atoms)
37
- end
35
+ impossible = check_or_expression(decl.expression, definitions, errors)
36
+ report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
37
+ else
38
+ # Normal handling for non-cascade expressions
39
+ atoms = gather_atoms(decl.expression, definitions, Set.new)
40
+ next if atoms.empty?
41
+
42
+ # Use enhanced solver that can detect cross-variable mathematical constraints
43
+ impossible = if definitions && !definitions.empty?
44
+ Kumi::ConstraintRelationshipSolver.unsat?(atoms, definitions, input_meta: @input_meta)
45
+ else
46
+ Kumi::AtomUnsatSolver.unsat?(atoms)
47
+ end
48
+
49
+ report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
50
+ end
51
+ end
52
+ state.with(:cascades, cascades)
53
+ end
54
+
55
+ private
38
56
 
39
- report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
57
+ def analyze_cascade_mutual_exclusion(decl, definitions)
58
+ conditions = []
59
+ condition_traits = []
60
+
61
+ # Extract all cascade conditions (except base case)
62
+ decl.expression.cases[0...-1].each do |when_case|
63
+ next unless when_case.condition
64
+
65
+ next unless when_case.condition.fn_name == :all?
66
+
67
+ when_case.condition.args.each do |arg|
68
+ next unless arg.is_a?(ArrayExpression)
69
+
70
+ arg.elements.each do |element|
71
+ next unless element.is_a?(DeclarationReference)
72
+
73
+ trait_name = element.name
74
+ trait = definitions[trait_name]
75
+ if trait
76
+ conditions << trait.expression
77
+ condition_traits << trait_name
78
+ end
40
79
  end
41
80
  end
81
+ # end
82
+ end
83
+
84
+ # Check mutual exclusion for all pairs
85
+ total_pairs = conditions.size * (conditions.size - 1) / 2
86
+ exclusive_pairs = 0
87
+
88
+ if conditions.size >= 2
89
+ conditions.combination(2).each do |cond1, cond2|
90
+ exclusive_pairs += 1 if conditions_mutually_exclusive?(cond1, cond2)
91
+ end
42
92
  end
43
- state
93
+
94
+ all_mutually_exclusive = total_pairs.positive? && (exclusive_pairs == total_pairs)
95
+
96
+ {
97
+ condition_traits: condition_traits,
98
+ condition_count: conditions.size,
99
+ all_mutually_exclusive: all_mutually_exclusive,
100
+ exclusive_pairs: exclusive_pairs,
101
+ total_pairs: total_pairs
102
+ }
44
103
  end
45
104
 
46
- private
105
+ def conditions_mutually_exclusive?(cond1, cond2)
106
+ if cond1.is_a?(CallExpression) && cond1.fn_name == :== &&
107
+ cond2.is_a?(CallExpression) && cond2.fn_name == :==
108
+
109
+ c1_field, c1_value = cond1.args
110
+ c2_field, c2_value = cond2.args
111
+
112
+ # Same field, different values = mutually exclusive
113
+ return true if same_field?(c1_field, c2_field) && different_values?(c1_value, c2_value)
114
+ end
47
115
 
48
- def check_or_expression(or_expr, definitions, errors)
116
+ false
117
+ end
118
+
119
+ def same_field?(field1, field2)
120
+ return false unless field1.is_a?(InputReference) && field2.is_a?(InputReference)
121
+
122
+ field1.name == field2.name
123
+ end
124
+
125
+ def different_values?(val1, val2)
126
+ return false unless val1.is_a?(Literal) && val2.is_a?(Literal)
127
+
128
+ val1.value != val2.value
129
+ end
130
+
131
+ def check_or_expression(or_expr, definitions, _errors)
49
132
  # For OR expressions: A | B is impossible only if BOTH A AND B are impossible
50
133
  # If either side is satisfiable, the OR is satisfiable
51
134
  left_side, right_side = or_expr.args
52
135
 
53
136
  # Check if left side is impossible
54
137
  left_atoms = gather_atoms(left_side, definitions, Set.new)
55
- left_impossible = if !left_atoms.empty?
56
- if definitions && !definitions.empty?
57
- Kumi::ConstraintRelationshipSolver.unsat?(left_atoms, definitions, input_meta: @input_meta)
58
- else
59
- Kumi::AtomUnsatSolver.unsat?(left_atoms)
60
- end
61
- else
138
+ left_impossible = if left_atoms.empty?
62
139
  false
140
+ elsif definitions && !definitions.empty?
141
+ Kumi::ConstraintRelationshipSolver.unsat?(left_atoms, definitions, input_meta: @input_meta)
142
+ else
143
+ Kumi::AtomUnsatSolver.unsat?(left_atoms)
63
144
  end
64
145
 
65
146
  # Check if right side is impossible
66
147
  right_atoms = gather_atoms(right_side, definitions, Set.new)
67
- right_impossible = if !right_atoms.empty?
68
- if definitions && !definitions.empty?
69
- Kumi::ConstraintRelationshipSolver.unsat?(right_atoms, definitions, input_meta: @input_meta)
70
- else
71
- Kumi::AtomUnsatSolver.unsat?(right_atoms)
72
- end
73
- else
148
+ right_impossible = if right_atoms.empty?
74
149
  false
150
+ elsif definitions && !definitions.empty?
151
+ Kumi::ConstraintRelationshipSolver.unsat?(right_atoms, definitions, input_meta: @input_meta)
152
+ else
153
+ Kumi::AtomUnsatSolver.unsat?(right_atoms)
75
154
  end
76
155
 
77
156
  # OR is impossible only if BOTH sides are impossible
@@ -106,10 +185,10 @@ module Kumi
106
185
  elsif current.is_a?(CallExpression) && current.fn_name == :all?
107
186
  # For all? function, add all trait arguments to the stack
108
187
  current.args.each { |arg| stack << arg }
109
- elsif current.is_a?(ListExpression)
110
- # For ListExpression, add all elements to the stack
188
+ elsif current.is_a?(ArrayExpression)
189
+ # For ArrayExpression, add all elements to the stack
111
190
  current.elements.each { |elem| stack << elem }
112
- elsif current.is_a?(Binding)
191
+ elsif current.is_a?(DeclarationReference)
113
192
  name = current.name
114
193
  unless visited.include?(name)
115
194
  visited << name
@@ -141,8 +220,8 @@ module Kumi
141
220
 
142
221
  # Skip single-trait 'on' branches: trait-level unsat detection covers these
143
222
  if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
144
- # Handle both ListExpression (old format) and multiple args (new format)
145
- if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(ListExpression)
223
+ # Handle both ArrayExpression (old format) and multiple args (new format)
224
+ if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(ArrayExpression)
146
225
  list = when_case.condition.args.first
147
226
  next if list.elements.size == 1
148
227
  elsif when_case.condition.args.size == 1
@@ -154,7 +233,6 @@ module Kumi
154
233
  condition_atoms = gather_atoms(when_case.condition, definitions, Set.new, [])
155
234
  # DEBUG
156
235
  # if when_case.condition.is_a?(CallExpression) && [:all?, :any?, :none?].include?(when_case.condition.fn_name)
157
- # puts "DEBUG: Processing #{when_case.condition.fn_name} condition"
158
236
  # puts " Args: #{when_case.condition.args.inspect}"
159
237
  # puts " Atoms found: #{condition_atoms.inspect}"
160
238
  # end
@@ -174,14 +252,14 @@ module Kumi
174
252
 
175
253
  # For multi-trait on-clauses, report the trait names rather than the value name
176
254
  if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
177
- # Handle both ListExpression (old format) and multiple args (new format)
178
- trait_bindings = if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(ListExpression)
255
+ # Handle both ArrayExpression (old format) and multiple args (new format)
256
+ trait_bindings = if when_case.condition.args.size == 1 && when_case.condition.args.first.is_a?(ArrayExpression)
179
257
  when_case.condition.args.first.elements
180
258
  else
181
259
  when_case.condition.args
182
260
  end
183
261
 
184
- if trait_bindings.all?(Binding)
262
+ if trait_bindings.all?(DeclarationReference)
185
263
  traits = trait_bindings.map(&:name).join(" AND ")
186
264
  report_error(errors, "conjunction `#{traits}` is impossible", location: decl.loc)
187
265
  next
@@ -193,7 +271,7 @@ module Kumi
193
271
 
194
272
  def term(node, _defs)
195
273
  case node
196
- when FieldRef, Binding
274
+ when InputReference, DeclarationReference
197
275
  val = @evaluator.evaluate(node)
198
276
  val == :unknown ? node.name : val
199
277
  when Literal
@@ -205,14 +283,14 @@ module Kumi
205
283
 
206
284
  def check_domain_constraints(node, definitions, errors)
207
285
  case node
208
- when FieldRef
209
- # Check if FieldRef points to a field with domain constraints
286
+ when InputReference
287
+ # Check if InputReference points to a field with domain constraints
210
288
  field_meta = @input_meta[node.name]
211
289
  nil unless field_meta&.dig(:domain)
212
290
 
213
- # For FieldRef, the constraint comes from trait conditions
214
- # We don't flag here since the FieldRef itself is valid
215
- when Binding
291
+ # For InputReference, the constraint comes from trait conditions
292
+ # We don't flag here since the InputReference itself is valid
293
+ when DeclarationReference
216
294
  # Check if this binding evaluates to a value that violates domain constraints
217
295
  definition = definitions[node.name]
218
296
  return unless definition
@@ -254,18 +332,18 @@ module Kumi
254
332
  end
255
333
 
256
334
  def impossible_constraint?(lhs, rhs, operator)
257
- # Case 1: FieldRef compared against value outside its domain
258
- if lhs.is_a?(FieldRef) && rhs.is_a?(Literal)
335
+ # Case 1: InputReference compared against value outside its domain
336
+ if lhs.is_a?(InputReference) && rhs.is_a?(Literal)
259
337
  return field_literal_impossible?(lhs, rhs, operator)
260
- elsif rhs.is_a?(FieldRef) && lhs.is_a?(Literal)
338
+ elsif rhs.is_a?(InputReference) && lhs.is_a?(Literal)
261
339
  # Reverse case: literal compared to field
262
340
  return field_literal_impossible?(rhs, lhs, flip_operator(operator))
263
341
  end
264
342
 
265
- # Case 2: Binding that evaluates to literal compared against impossible value
266
- if lhs.is_a?(Binding) && rhs.is_a?(Literal)
343
+ # Case 2: DeclarationReference that evaluates to literal compared against impossible value
344
+ if lhs.is_a?(DeclarationReference) && rhs.is_a?(Literal)
267
345
  return binding_literal_impossible?(lhs, rhs, operator)
268
- elsif rhs.is_a?(Binding) && lhs.is_a?(Literal)
346
+ elsif rhs.is_a?(DeclarationReference) && lhs.is_a?(Literal)
269
347
  return binding_literal_impossible?(rhs, lhs, flip_operator(operator))
270
348
  end
271
349
 
@@ -20,7 +20,7 @@ module Kumi
20
20
 
21
21
  # Helper to visit each declaration's expression tree
22
22
  # @param errors [Array] Error accumulator
23
- # @yield [Syntax::Node, Syntax::Declarations::Base] Each node and its containing declaration
23
+ # @yield [Syntax::Node, Syntax::Base] Each node and its containing declaration
24
24
  def visit_all_expressions(errors)
25
25
  each_decl do |decl|
26
26
  visit(decl.expression) { |node| yield(node, decl, errors) }
@@ -30,7 +30,7 @@ module Kumi
30
30
  # Helper to visit only specific node types
31
31
  # @param node_types [Array<Class>] Node types to match
32
32
  # @param errors [Array] Error accumulator
33
- # @yield [Syntax::Node, Syntax::Declarations::Base] Matching nodes and their declarations
33
+ # @yield [Syntax::Node, Syntax::Base] Matching nodes and their declarations
34
34
  def visit_nodes_of_type(*node_types, errors:)
35
35
  visit_all_expressions(errors) do |node, decl, errs|
36
36
  yield(node, decl, errs) if node_types.any? { |type| node.is_a?(type) }
data/lib/kumi/analyzer.rb CHANGED
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "analyzer/analysis_state"
4
-
5
3
  module Kumi
6
4
  module Analyzer
7
5
  Result = Struct.new(:definitions, :dependency_graph, :leaf_map, :topo_order, :decl_types, :state, keyword_init: true)
@@ -9,16 +7,17 @@ module Kumi
9
7
  module_function
10
8
 
11
9
  DEFAULT_PASSES = [
12
- Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
13
- Passes::InputCollector, # 2. Collects field metadata from input declarations.
14
- Passes::DefinitionValidator, # 3. Checks the basic structure of each rule.
15
- Passes::SemanticConstraintValidator, # 4. Validates DSL semantic constraints at AST level.
16
- Passes::DependencyResolver, # 5. Builds the dependency graph.
17
- Passes::UnsatDetector, # 6. Detects unsatisfiable constraints in rules.
18
- Passes::Toposorter, # 7. Creates the final evaluation order.
19
- Passes::TypeInferencer, # 8. Infers types for all declarations (pure annotation).
20
- Passes::TypeConsistencyChecker, # 9. Validates declared vs inferred type consistency.
21
- Passes::TypeChecker # 10. Validates types using inferred information.
10
+ Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
11
+ Passes::InputCollector, # 2. Collects field metadata from input declarations.
12
+ Passes::DeclarationValidator, # 3. Checks the basic structure of each rule.
13
+ Passes::SemanticConstraintValidator, # 4. Validates DSL semantic constraints at AST level.
14
+ Passes::DependencyResolver, # 5. Builds the dependency graph with conditional dependencies.
15
+ Passes::UnsatDetector, # 6. Detects unsatisfiable constraints and analyzes cascade mutual exclusion.
16
+ Passes::Toposorter, # 7. Creates the final evaluation order, allowing safe cycles.
17
+ Passes::BroadcastDetector, # 8. Detects which operations should be broadcast over arrays (must run before type inference).
18
+ Passes::TypeInferencer, # 9. Infers types for all declarations (uses vectorization metadata).
19
+ Passes::TypeConsistencyChecker, # 10. Validates declared vs inferred type consistency.
20
+ Passes::TypeChecker # 11. Validates types using inferred information.
22
21
  ].freeze
23
22
 
24
23
  def analyze!(schema, passes: DEFAULT_PASSES, **opts)
@@ -53,11 +52,11 @@ module Kumi
53
52
 
54
53
  def self.create_analysis_result(state)
55
54
  Result.new(
56
- definitions: state[:definitions],
57
- dependency_graph: state[:dependency_graph],
58
- leaf_map: state[:leaf_map],
59
- topo_order: state[:topo_order],
60
- decl_types: state[:decl_types],
55
+ definitions: state[:declarations],
56
+ dependency_graph: state[:dependencies],
57
+ leaf_map: state[:leaves],
58
+ topo_order: state[:evaluation_order],
59
+ decl_types: state[:inferred_types],
61
60
  state: state.to_h
62
61
  )
63
62
  end