kumi 0.0.7 → 0.0.9

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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +1 -1
  3. data/README.md +21 -5
  4. data/docs/AST.md +7 -0
  5. data/docs/features/README.md +7 -0
  6. data/docs/features/s-expression-printer.md +77 -0
  7. data/examples/game_of_life.rb +1 -1
  8. data/examples/static_analysis_errors.rb +7 -7
  9. data/lib/kumi/analyzer.rb +15 -15
  10. data/lib/kumi/compiler.rb +6 -6
  11. data/lib/kumi/core/analyzer/analysis_state.rb +39 -0
  12. data/lib/kumi/core/analyzer/constant_evaluator.rb +59 -0
  13. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +248 -0
  14. data/lib/kumi/core/analyzer/passes/declaration_validator.rb +45 -0
  15. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +153 -0
  16. data/lib/kumi/core/analyzer/passes/input_collector.rb +139 -0
  17. data/lib/kumi/core/analyzer/passes/name_indexer.rb +26 -0
  18. data/lib/kumi/core/analyzer/passes/pass_base.rb +52 -0
  19. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +111 -0
  20. data/lib/kumi/core/analyzer/passes/toposorter.rb +110 -0
  21. data/lib/kumi/core/analyzer/passes/type_checker.rb +162 -0
  22. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +48 -0
  23. data/lib/kumi/core/analyzer/passes/type_inferencer.rb +236 -0
  24. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +406 -0
  25. data/lib/kumi/core/analyzer/passes/visitor_pass.rb +44 -0
  26. data/lib/kumi/core/atom_unsat_solver.rb +396 -0
  27. data/lib/kumi/core/compiled_schema.rb +43 -0
  28. data/lib/kumi/core/constraint_relationship_solver.rb +641 -0
  29. data/lib/kumi/core/domain/enum_analyzer.rb +55 -0
  30. data/lib/kumi/core/domain/range_analyzer.rb +85 -0
  31. data/lib/kumi/core/domain/validator.rb +82 -0
  32. data/lib/kumi/core/domain/violation_formatter.rb +42 -0
  33. data/lib/kumi/core/error_reporter.rb +166 -0
  34. data/lib/kumi/core/error_reporting.rb +97 -0
  35. data/lib/kumi/core/errors.rb +120 -0
  36. data/lib/kumi/core/evaluation_wrapper.rb +40 -0
  37. data/lib/kumi/core/explain.rb +295 -0
  38. data/lib/kumi/core/export/deserializer.rb +41 -0
  39. data/lib/kumi/core/export/errors.rb +14 -0
  40. data/lib/kumi/core/export/node_builders.rb +142 -0
  41. data/lib/kumi/core/export/node_registry.rb +54 -0
  42. data/lib/kumi/core/export/node_serializers.rb +158 -0
  43. data/lib/kumi/core/export/serializer.rb +25 -0
  44. data/lib/kumi/core/export.rb +35 -0
  45. data/lib/kumi/core/function_registry/collection_functions.rb +202 -0
  46. data/lib/kumi/core/function_registry/comparison_functions.rb +33 -0
  47. data/lib/kumi/core/function_registry/conditional_functions.rb +38 -0
  48. data/lib/kumi/core/function_registry/function_builder.rb +95 -0
  49. data/lib/kumi/core/function_registry/logical_functions.rb +44 -0
  50. data/lib/kumi/core/function_registry/math_functions.rb +74 -0
  51. data/lib/kumi/core/function_registry/string_functions.rb +57 -0
  52. data/lib/kumi/core/function_registry/type_functions.rb +53 -0
  53. data/lib/kumi/{function_registry.rb → core/function_registry.rb} +28 -36
  54. data/lib/kumi/core/input/type_matcher.rb +97 -0
  55. data/lib/kumi/core/input/validator.rb +51 -0
  56. data/lib/kumi/core/input/violation_creator.rb +52 -0
  57. data/lib/kumi/core/json_schema/generator.rb +65 -0
  58. data/lib/kumi/core/json_schema/validator.rb +27 -0
  59. data/lib/kumi/core/json_schema.rb +16 -0
  60. data/lib/kumi/core/ruby_parser/build_context.rb +27 -0
  61. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +38 -0
  62. data/lib/kumi/core/ruby_parser/dsl.rb +14 -0
  63. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +138 -0
  64. data/lib/kumi/core/ruby_parser/expression_converter.rb +128 -0
  65. data/lib/kumi/core/ruby_parser/guard_rails.rb +45 -0
  66. data/lib/kumi/core/ruby_parser/input_builder.rb +127 -0
  67. data/lib/kumi/core/ruby_parser/input_field_proxy.rb +48 -0
  68. data/lib/kumi/core/ruby_parser/input_proxy.rb +31 -0
  69. data/lib/kumi/core/ruby_parser/nested_input.rb +17 -0
  70. data/lib/kumi/core/ruby_parser/parser.rb +71 -0
  71. data/lib/kumi/core/ruby_parser/schema_builder.rb +175 -0
  72. data/lib/kumi/core/ruby_parser/sugar.rb +263 -0
  73. data/lib/kumi/core/ruby_parser.rb +12 -0
  74. data/lib/kumi/core/schema_instance.rb +111 -0
  75. data/lib/kumi/core/types/builder.rb +23 -0
  76. data/lib/kumi/core/types/compatibility.rb +96 -0
  77. data/lib/kumi/core/types/formatter.rb +26 -0
  78. data/lib/kumi/core/types/inference.rb +42 -0
  79. data/lib/kumi/core/types/normalizer.rb +72 -0
  80. data/lib/kumi/core/types/validator.rb +37 -0
  81. data/lib/kumi/core/types.rb +66 -0
  82. data/lib/kumi/core/vectorization_metadata.rb +110 -0
  83. data/lib/kumi/errors.rb +1 -112
  84. data/lib/kumi/registry.rb +37 -0
  85. data/lib/kumi/schema.rb +5 -5
  86. data/lib/kumi/schema_metadata.rb +3 -3
  87. data/lib/kumi/support/s_expression_printer.rb +161 -0
  88. data/lib/kumi/syntax/array_expression.rb +6 -6
  89. data/lib/kumi/syntax/call_expression.rb +4 -4
  90. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  91. data/lib/kumi/syntax/case_expression.rb +4 -4
  92. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  93. data/lib/kumi/syntax/hash_expression.rb +4 -4
  94. data/lib/kumi/syntax/input_declaration.rb +5 -5
  95. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  96. data/lib/kumi/syntax/input_reference.rb +5 -5
  97. data/lib/kumi/syntax/literal.rb +4 -4
  98. data/lib/kumi/syntax/node.rb +34 -34
  99. data/lib/kumi/syntax/root.rb +6 -6
  100. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  101. data/lib/kumi/syntax/value_declaration.rb +4 -4
  102. data/lib/kumi/version.rb +1 -1
  103. data/migrate_to_core_iterative.rb +938 -0
  104. data/scripts/generate_function_docs.rb +9 -9
  105. metadata +77 -72
  106. data/lib/kumi/analyzer/analysis_state.rb +0 -37
  107. data/lib/kumi/analyzer/constant_evaluator.rb +0 -57
  108. data/lib/kumi/analyzer/passes/broadcast_detector.rb +0 -246
  109. data/lib/kumi/analyzer/passes/declaration_validator.rb +0 -43
  110. data/lib/kumi/analyzer/passes/dependency_resolver.rb +0 -151
  111. data/lib/kumi/analyzer/passes/input_collector.rb +0 -137
  112. data/lib/kumi/analyzer/passes/name_indexer.rb +0 -24
  113. data/lib/kumi/analyzer/passes/pass_base.rb +0 -50
  114. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +0 -109
  115. data/lib/kumi/analyzer/passes/toposorter.rb +0 -108
  116. data/lib/kumi/analyzer/passes/type_checker.rb +0 -160
  117. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +0 -46
  118. data/lib/kumi/analyzer/passes/type_inferencer.rb +0 -232
  119. data/lib/kumi/analyzer/passes/unsat_detector.rb +0 -404
  120. data/lib/kumi/analyzer/passes/visitor_pass.rb +0 -42
  121. data/lib/kumi/atom_unsat_solver.rb +0 -394
  122. data/lib/kumi/compiled_schema.rb +0 -41
  123. data/lib/kumi/constraint_relationship_solver.rb +0 -638
  124. data/lib/kumi/domain/enum_analyzer.rb +0 -53
  125. data/lib/kumi/domain/range_analyzer.rb +0 -83
  126. data/lib/kumi/domain/validator.rb +0 -80
  127. data/lib/kumi/domain/violation_formatter.rb +0 -40
  128. data/lib/kumi/error_reporter.rb +0 -164
  129. data/lib/kumi/error_reporting.rb +0 -95
  130. data/lib/kumi/evaluation_wrapper.rb +0 -38
  131. data/lib/kumi/explain.rb +0 -293
  132. data/lib/kumi/export/deserializer.rb +0 -39
  133. data/lib/kumi/export/errors.rb +0 -12
  134. data/lib/kumi/export/node_builders.rb +0 -140
  135. data/lib/kumi/export/node_registry.rb +0 -52
  136. data/lib/kumi/export/node_serializers.rb +0 -156
  137. data/lib/kumi/export/serializer.rb +0 -23
  138. data/lib/kumi/export.rb +0 -33
  139. data/lib/kumi/function_registry/collection_functions.rb +0 -200
  140. data/lib/kumi/function_registry/comparison_functions.rb +0 -31
  141. data/lib/kumi/function_registry/conditional_functions.rb +0 -36
  142. data/lib/kumi/function_registry/function_builder.rb +0 -93
  143. data/lib/kumi/function_registry/logical_functions.rb +0 -42
  144. data/lib/kumi/function_registry/math_functions.rb +0 -72
  145. data/lib/kumi/function_registry/string_functions.rb +0 -54
  146. data/lib/kumi/function_registry/type_functions.rb +0 -51
  147. data/lib/kumi/input/type_matcher.rb +0 -95
  148. data/lib/kumi/input/validator.rb +0 -49
  149. data/lib/kumi/input/violation_creator.rb +0 -50
  150. data/lib/kumi/json_schema/generator.rb +0 -63
  151. data/lib/kumi/json_schema/validator.rb +0 -25
  152. data/lib/kumi/json_schema.rb +0 -14
  153. data/lib/kumi/ruby_parser/build_context.rb +0 -25
  154. data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +0 -36
  155. data/lib/kumi/ruby_parser/dsl.rb +0 -12
  156. data/lib/kumi/ruby_parser/dsl_cascade_builder.rb +0 -136
  157. data/lib/kumi/ruby_parser/expression_converter.rb +0 -126
  158. data/lib/kumi/ruby_parser/guard_rails.rb +0 -43
  159. data/lib/kumi/ruby_parser/input_builder.rb +0 -125
  160. data/lib/kumi/ruby_parser/input_field_proxy.rb +0 -46
  161. data/lib/kumi/ruby_parser/input_proxy.rb +0 -29
  162. data/lib/kumi/ruby_parser/nested_input.rb +0 -15
  163. data/lib/kumi/ruby_parser/parser.rb +0 -69
  164. data/lib/kumi/ruby_parser/schema_builder.rb +0 -173
  165. data/lib/kumi/ruby_parser/sugar.rb +0 -261
  166. data/lib/kumi/ruby_parser.rb +0 -10
  167. data/lib/kumi/schema_instance.rb +0 -109
  168. data/lib/kumi/types/builder.rb +0 -21
  169. data/lib/kumi/types/compatibility.rb +0 -94
  170. data/lib/kumi/types/formatter.rb +0 -24
  171. data/lib/kumi/types/inference.rb +0 -40
  172. data/lib/kumi/types/normalizer.rb +0 -70
  173. data/lib/kumi/types/validator.rb +0 -35
  174. data/lib/kumi/types.rb +0 -64
  175. data/lib/kumi/vectorization_metadata.rb +0 -108
@@ -0,0 +1,248 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ # Detects which operations should be broadcast over arrays
8
+ # DEPENDENCIES: :inputs, :declarations
9
+ # PRODUCES: :broadcasts
10
+ class BroadcastDetector < PassBase
11
+ def run(errors)
12
+ input_meta = get_state(:inputs) || {}
13
+ definitions = get_state(:declarations) || {}
14
+
15
+ # Find array fields with their element types
16
+ array_fields = find_array_fields(input_meta)
17
+
18
+ # Build compiler metadata
19
+ compiler_metadata = {
20
+ array_fields: array_fields,
21
+ vectorized_operations: {},
22
+ reduction_operations: {}
23
+ }
24
+
25
+ # Track which values are vectorized for type inference
26
+ vectorized_values = {}
27
+
28
+ # Analyze traits first, then values (to handle dependencies)
29
+ traits = definitions.select { |_name, decl| decl.is_a?(Kumi::Syntax::TraitDeclaration) }
30
+ values = definitions.select { |_name, decl| decl.is_a?(Kumi::Syntax::ValueDeclaration) }
31
+
32
+ (traits.to_a + values.to_a).each do |name, decl|
33
+ result = analyze_value_vectorization(name, decl.expression, array_fields, vectorized_values, errors)
34
+
35
+ case result[:type]
36
+ when :vectorized
37
+ compiler_metadata[:vectorized_operations][name] = result[:info]
38
+ # Store array source information for dimension checking
39
+ array_source = extract_array_source(result[:info], array_fields)
40
+ vectorized_values[name] = { vectorized: true, array_source: array_source }
41
+ when :reduction
42
+ compiler_metadata[:reduction_operations][name] = result[:info]
43
+ # Reduction produces scalar, not vectorized
44
+ vectorized_values[name] = { vectorized: false }
45
+ end
46
+ end
47
+
48
+ state.with(:broadcasts, compiler_metadata.freeze)
49
+ end
50
+
51
+ private
52
+
53
+ def find_array_fields(input_meta)
54
+ result = {}
55
+ input_meta.each do |name, meta|
56
+ next unless meta[:type] == :array && meta[:children]
57
+
58
+ result[name] = {
59
+ element_fields: meta[:children].keys,
60
+ element_types: meta[:children].transform_values { |v| v[:type] || :any }
61
+ }
62
+ end
63
+ result
64
+ end
65
+
66
+ def analyze_value_vectorization(name, expr, array_fields, vectorized_values, errors)
67
+ case expr
68
+ when Kumi::Syntax::InputElementReference
69
+ if array_fields.key?(expr.path.first)
70
+ { type: :vectorized, info: { source: :array_field_access, path: expr.path } }
71
+ else
72
+ { type: :scalar }
73
+ end
74
+
75
+ when Kumi::Syntax::DeclarationReference
76
+ # Check if this references a vectorized value
77
+ vector_info = vectorized_values[expr.name]
78
+ if vector_info && vector_info[:vectorized]
79
+ { type: :vectorized, info: { source: :vectorized_declaration, name: expr.name } }
80
+ else
81
+ { type: :scalar }
82
+ end
83
+
84
+ when Kumi::Syntax::CallExpression
85
+ analyze_call_vectorization(name, expr, array_fields, vectorized_values, errors)
86
+
87
+ when Kumi::Syntax::CascadeExpression
88
+ analyze_cascade_vectorization(name, expr, array_fields, vectorized_values, errors)
89
+
90
+ else
91
+ { type: :scalar }
92
+ end
93
+ end
94
+
95
+ def analyze_call_vectorization(_name, expr, array_fields, vectorized_values, errors)
96
+ # Check if this is a reduction function using function registry metadata
97
+ if Kumi::Registry.reducer?(expr.fn_name)
98
+ # Only treat as reduction if the argument is actually vectorized
99
+ arg_info = analyze_argument_vectorization(expr.args.first, array_fields, vectorized_values)
100
+ if arg_info[:vectorized]
101
+ { type: :reduction, info: { function: expr.fn_name, source: arg_info[:source] } }
102
+ else
103
+ # Not a vectorized reduction - just a regular function call
104
+ { type: :scalar }
105
+ end
106
+
107
+ else
108
+ # Special case: all?, any?, none? functions with vectorized trait arguments should be treated as vectorized
109
+ # for cascade condition purposes (they get transformed during compilation)
110
+ if %i[all? any? none?].include?(expr.fn_name) && expr.args.length == 1
111
+ arg = expr.args.first
112
+ if arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.length == 1
113
+ trait_ref = arg.elements.first
114
+ if trait_ref.is_a?(Kumi::Syntax::DeclarationReference) && vectorized_values[trait_ref.name]&.[](:vectorized)
115
+ return { type: :vectorized, info: { source: :cascade_condition_with_vectorized_trait, trait: trait_ref.name } }
116
+ end
117
+ end
118
+ end
119
+
120
+ # ANY function with vectorized arguments becomes vectorized (with broadcasting)
121
+ arg_infos = expr.args.map { |arg| analyze_argument_vectorization(arg, array_fields, vectorized_values) }
122
+
123
+ if arg_infos.any? { |info| info[:vectorized] }
124
+ # Check for dimension mismatches when multiple arguments are vectorized
125
+ vectorized_sources = arg_infos.select { |info| info[:vectorized] }.filter_map { |info| info[:array_source] }.uniq
126
+
127
+ if vectorized_sources.length > 1
128
+ # Multiple different array sources - this is a dimension mismatch
129
+ # Generate enhanced error message with type information
130
+ enhanced_message = build_dimension_mismatch_error(expr, arg_infos, array_fields, vectorized_sources)
131
+
132
+ report_error(errors, enhanced_message, location: expr.loc, type: :semantic)
133
+ return { type: :scalar } # Treat as scalar to prevent further errors
134
+ end
135
+
136
+ # This is a vectorized operation - ANY function supports broadcasting
137
+ { type: :vectorized, info: {
138
+ operation: expr.fn_name,
139
+ vectorized_args: arg_infos.map.with_index { |info, i| [i, info[:vectorized]] }.to_h
140
+ } }
141
+ else
142
+ { type: :scalar }
143
+ end
144
+ end
145
+ end
146
+
147
+ def analyze_argument_vectorization(arg, array_fields, vectorized_values)
148
+ case arg
149
+ when Kumi::Syntax::InputElementReference
150
+ if array_fields.key?(arg.path.first)
151
+ { vectorized: true, source: :array_field, array_source: arg.path.first }
152
+ else
153
+ { vectorized: false }
154
+ end
155
+
156
+ when Kumi::Syntax::DeclarationReference
157
+ # Check if this references a vectorized value
158
+ vector_info = vectorized_values[arg.name]
159
+ if vector_info && vector_info[:vectorized]
160
+ array_source = vector_info[:array_source]
161
+ { vectorized: true, source: :vectorized_value, array_source: array_source }
162
+ else
163
+ { vectorized: false }
164
+ end
165
+
166
+ when Kumi::Syntax::CallExpression
167
+ # Recursively check
168
+ result = analyze_value_vectorization(nil, arg, array_fields, vectorized_values, [])
169
+ { vectorized: result[:type] == :vectorized, source: :expression }
170
+
171
+ else
172
+ { vectorized: false }
173
+ end
174
+ end
175
+
176
+ def extract_array_source(info, _array_fields)
177
+ case info[:source]
178
+ when :array_field_access
179
+ info[:path]&.first
180
+ when :cascade_condition_with_vectorized_trait
181
+ # For cascades, we'd need to trace back to the original source
182
+ nil # TODO: Could be enhanced to trace through trait dependencies
183
+ end
184
+ end
185
+
186
+ def analyze_cascade_vectorization(_name, expr, array_fields, vectorized_values, errors)
187
+ # A cascade is vectorized if:
188
+ # 1. Any of its result expressions are vectorized, OR
189
+ # 2. Any of its conditions reference vectorized values (traits or arrays)
190
+ vectorized_results = []
191
+ vectorized_conditions = []
192
+
193
+ expr.cases.each do |case_expr|
194
+ # Check if result is vectorized
195
+ result_info = analyze_value_vectorization(nil, case_expr.result, array_fields, vectorized_values, errors)
196
+ vectorized_results << (result_info[:type] == :vectorized)
197
+
198
+ # Check if condition is vectorized
199
+ condition_info = analyze_value_vectorization(nil, case_expr.condition, array_fields, vectorized_values, errors)
200
+ vectorized_conditions << (condition_info[:type] == :vectorized)
201
+ end
202
+
203
+ if vectorized_results.any? || vectorized_conditions.any?
204
+ { type: :vectorized, info: { source: :cascade_with_vectorized_conditions_or_results } }
205
+ else
206
+ { type: :scalar }
207
+ end
208
+ end
209
+
210
+ def build_dimension_mismatch_error(_expr, arg_infos, array_fields, vectorized_sources)
211
+ # Build detailed error message with type information
212
+ summary = "Cannot broadcast operation across arrays from different sources: #{vectorized_sources.join(', ')}. "
213
+
214
+ problem_desc = "Problem: Multiple operands are arrays from different sources:\n"
215
+
216
+ vectorized_args = arg_infos.select { |info| info[:vectorized] }
217
+ vectorized_args.each_with_index do |arg_info, index|
218
+ array_source = arg_info[:array_source]
219
+ next unless array_source && array_fields[array_source]
220
+
221
+ # Determine the type based on array field metadata
222
+ type_desc = determine_array_type(array_source, array_fields)
223
+ problem_desc += " - Operand #{index + 1} resolves to #{type_desc} from array '#{array_source}'\n"
224
+ end
225
+
226
+ explanation = "Direct operations on arrays from different sources is ambiguous and not supported. " \
227
+ "Vectorized operations can only work on fields from the same array input."
228
+
229
+ "#{summary}#{problem_desc}#{explanation}"
230
+ end
231
+
232
+ def determine_array_type(array_source, array_fields)
233
+ field_info = array_fields[array_source]
234
+ return "array(any)" unless field_info[:element_types]
235
+
236
+ # For nested arrays (like items.name where items is an array), this represents array(element_type)
237
+ element_types = field_info[:element_types].values.uniq
238
+ if element_types.length == 1
239
+ "array(#{element_types.first})"
240
+ else
241
+ "array(mixed)"
242
+ end
243
+ end
244
+ end
245
+ end
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ # RESPONSIBILITY: Perform local structural validation on each declaration
8
+ # DEPENDENCIES: :definitions
9
+ # PRODUCES: None (validation only)
10
+ # INTERFACE: new(schema, state).run(errors)
11
+ class DeclarationValidator < VisitorPass
12
+ def run(errors)
13
+ each_decl do |decl|
14
+ visit(decl) { |node| validate_node(node, errors) }
15
+ end
16
+ state
17
+ end
18
+
19
+ private
20
+
21
+ def validate_node(node, errors)
22
+ case node
23
+ when Kumi::Syntax::ValueDeclaration
24
+ validate_attribute(node, errors)
25
+ when Kumi::Syntax::TraitDeclaration
26
+ validate_trait(node, errors)
27
+ end
28
+ end
29
+
30
+ def validate_attribute(node, errors)
31
+ return unless node.expression.nil?
32
+
33
+ report_error(errors, "attribute `#{node.name}` requires an expression", location: node.loc)
34
+ end
35
+
36
+ def validate_trait(node, errors)
37
+ return if node.expression.is_a?(Kumi::Syntax::CallExpression)
38
+
39
+ report_error(errors, "trait `#{node.name}` must wrap a CallExpression", location: node.loc)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ # RESPONSIBILITY: Build dependency graph and detect conditional dependencies in cascades
8
+ # DEPENDENCIES: :declarations from NameIndexer, :inputs from InputCollector
9
+ # PRODUCES: :dependencies, :dependents, :leaves - Dependency analysis results
10
+ # INTERFACE: new(schema, state).run(errors)
11
+ class DependencyResolver < PassBase
12
+ # Enhanced edge with conditional flag and cascade metadata
13
+ class DependencyEdge
14
+ attr_reader :to, :type, :via, :conditional, :cascade_owner
15
+
16
+ def initialize(to:, type:, via:, conditional: false, cascade_owner: nil)
17
+ @to = to
18
+ @type = type
19
+ @via = via
20
+ @conditional = conditional
21
+ @cascade_owner = cascade_owner
22
+ end
23
+ end
24
+
25
+ include Syntax
26
+
27
+ def run(errors)
28
+ definitions = get_state(:declarations)
29
+ input_meta = get_state(:inputs)
30
+
31
+ dependency_graph = Hash.new { |h, k| h[k] = [] }
32
+ reverse_dependencies = Hash.new { |h, k| h[k] = [] }
33
+ leaf_map = Hash.new { |h, k| h[k] = Set.new }
34
+
35
+ each_decl do |decl|
36
+ # Traverse the expression for each declaration, passing context down
37
+ visit_with_context(decl.expression, { decl_name: decl.name }) do |node, context|
38
+ process_node(node, decl, dependency_graph, reverse_dependencies, leaf_map, definitions, input_meta, errors, context)
39
+ end
40
+ end
41
+
42
+ # Compute transitive closure of reverse dependencies
43
+ transitive_dependents = compute_transitive_closure(reverse_dependencies)
44
+
45
+ state.with(:dependencies, dependency_graph.transform_values(&:freeze).freeze)
46
+ .with(:dependents, transitive_dependents.freeze)
47
+ .with(:leaves, leaf_map.transform_values(&:freeze).freeze)
48
+ end
49
+
50
+ private
51
+
52
+ def process_node(node, decl, graph, reverse_deps, leaves, definitions, _input_meta, errors, context)
53
+ case node
54
+ when DeclarationReference
55
+ report_error(errors, "undefined reference to `#{node.name}`", location: node.loc) unless definitions.key?(node.name)
56
+
57
+ # Determine if this is a conditional dependency
58
+ conditional = context[:in_cascade_branch] || context[:in_cascade_base] || false
59
+ cascade_owner = conditional ? (context[:cascade_owner] || context[:decl_name]) : nil
60
+
61
+ add_dependency_edge(graph, reverse_deps, decl.name, node.name, :ref, context[:via],
62
+ conditional: conditional,
63
+ cascade_owner: cascade_owner)
64
+ when InputReference
65
+ add_dependency_edge(graph, reverse_deps, decl.name, node.name, :key, context[:via])
66
+ leaves[decl.name] << node
67
+ when InputElementReference
68
+ # adds the root input declaration as a dependency
69
+ root_input_declr_name = node.path.first
70
+ add_dependency_edge(graph, reverse_deps, decl.name, root_input_declr_name, :key, context[:via])
71
+ when Literal
72
+ leaves[decl.name] << node
73
+ end
74
+ end
75
+
76
+ def add_dependency_edge(graph, reverse_deps, from, to, type, via, conditional: false, cascade_owner: nil)
77
+ edge = DependencyEdge.new(
78
+ to: to,
79
+ type: type,
80
+ via: via,
81
+ conditional: conditional,
82
+ cascade_owner: cascade_owner
83
+ )
84
+ graph[from] << edge
85
+ reverse_deps[to] << from
86
+ end
87
+
88
+ # Custom visitor that understands cascade structure
89
+ def visit_with_context(node, context = {}, &block)
90
+ return unless node
91
+
92
+ yield(node, context)
93
+
94
+ case node
95
+ when CascadeExpression
96
+ # Visit condition nodes and result expressions (non-base cases)
97
+ node.cases[0...-1].each do |when_case|
98
+ if when_case.condition
99
+ # Visit condition normally
100
+ visit_with_context(when_case.condition, context, &block)
101
+ end
102
+ # Visit result expressions as conditional dependencies
103
+ conditional_context = context.merge(in_cascade_branch: true, cascade_owner: context[:decl_name])
104
+ visit_with_context(when_case.result, conditional_context, &block)
105
+ end
106
+
107
+ # Visit base case with conditional flag
108
+ if node.cases.last
109
+ base_context = context.merge(in_cascade_base: true)
110
+ visit_with_context(node.cases.last.result, base_context, &block)
111
+ end
112
+ when CallExpression
113
+ new_context = context.merge(via: node.fn_name)
114
+ node.children.each { |child| visit_with_context(child, new_context, &block) }
115
+ else
116
+ node.children.each { |child| visit_with_context(child, context, &block) } if node.respond_to?(:children)
117
+ end
118
+ end
119
+
120
+ def compute_transitive_closure(reverse_dependencies)
121
+ transitive = {}
122
+ all_keys = reverse_dependencies.keys
123
+
124
+ all_keys.each do |key|
125
+ visited = Set.new
126
+ to_visit = [key]
127
+ dependents = Set.new
128
+
129
+ while to_visit.any?
130
+ current = to_visit.shift
131
+ next if visited.include?(current)
132
+
133
+ visited.add(current)
134
+
135
+ direct_dependents = reverse_dependencies[current] || []
136
+ direct_dependents.each do |dependent|
137
+ next if visited.include?(dependent)
138
+
139
+ dependents << dependent
140
+ to_visit << dependent
141
+ end
142
+ end
143
+
144
+ transitive[key] = dependents.to_a
145
+ end
146
+
147
+ transitive
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ # RESPONSIBILITY: Collect field metadata from input declarations and validate consistency
8
+ # DEPENDENCIES: :definitions
9
+ # PRODUCES: :inputs - Hash mapping field names to {type:, domain:} metadata
10
+ # INTERFACE: new(schema, state).run(errors)
11
+ class InputCollector < PassBase
12
+ def run(errors)
13
+ input_meta = {}
14
+
15
+ schema.inputs.each do |field_decl|
16
+ unless field_decl.is_a?(Kumi::Syntax::InputDeclaration)
17
+ report_error(errors, "Expected InputDeclaration node, got #{field_decl.class}", location: field_decl.loc)
18
+ next
19
+ end
20
+
21
+ name = field_decl.name
22
+ existing = input_meta[name]
23
+
24
+ if existing
25
+ # Check for compatibility and merge
26
+ merged_meta = merge_field_metadata(existing, field_decl, errors)
27
+ input_meta[name] = merged_meta if merged_meta
28
+ else
29
+ # New field - collect its metadata
30
+ input_meta[name] = collect_field_metadata(field_decl, errors)
31
+ end
32
+ end
33
+
34
+ state.with(:inputs, freeze_nested_hash(input_meta))
35
+ end
36
+
37
+ private
38
+
39
+ def collect_field_metadata(field_decl, errors)
40
+ validate_domain_type(field_decl, errors) if field_decl.domain
41
+
42
+ metadata = {
43
+ type: field_decl.type,
44
+ domain: field_decl.domain
45
+ }
46
+
47
+ # Process children if present
48
+ if field_decl.children && !field_decl.children.empty?
49
+ children_meta = {}
50
+ field_decl.children.each do |child_decl|
51
+ unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
52
+ report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
53
+ next
54
+ end
55
+ children_meta[child_decl.name] = collect_field_metadata(child_decl, errors)
56
+ end
57
+ metadata[:children] = children_meta
58
+ end
59
+
60
+ metadata
61
+ end
62
+
63
+ def merge_field_metadata(existing, field_decl, errors)
64
+ name = field_decl.name
65
+
66
+ # Check for type compatibility
67
+ if existing[:type] != field_decl.type && field_decl.type && existing[:type]
68
+ report_error(errors,
69
+ "Field :#{name} declared with conflicting types: #{existing[:type]} vs #{field_decl.type}",
70
+ location: field_decl.loc)
71
+ end
72
+
73
+ # Check for domain compatibility
74
+ if existing[:domain] != field_decl.domain && field_decl.domain && existing[:domain]
75
+ report_error(errors,
76
+ "Field :#{name} declared with conflicting domains: #{existing[:domain].inspect} vs #{field_decl.domain.inspect}",
77
+ location: field_decl.loc)
78
+ end
79
+
80
+ # Validate domain type if provided
81
+ validate_domain_type(field_decl, errors) if field_decl.domain
82
+
83
+ # Merge metadata (later declarations override nil values)
84
+ merged = {
85
+ type: field_decl.type || existing[:type],
86
+ domain: field_decl.domain || existing[:domain]
87
+ }
88
+
89
+ # Merge children if present
90
+ if field_decl.children && !field_decl.children.empty?
91
+ existing_children = existing[:children] || {}
92
+ new_children = {}
93
+
94
+ field_decl.children.each do |child_decl|
95
+ unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
96
+ report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
97
+ next
98
+ end
99
+
100
+ child_name = child_decl.name
101
+ new_children[child_name] = if existing_children[child_name]
102
+ merge_field_metadata(existing_children[child_name], child_decl, errors)
103
+ else
104
+ collect_field_metadata(child_decl, errors)
105
+ end
106
+ end
107
+
108
+ merged[:children] = new_children
109
+ elsif existing[:children]
110
+ merged[:children] = existing[:children]
111
+ end
112
+
113
+ merged
114
+ end
115
+
116
+ def freeze_nested_hash(hash)
117
+ hash.each_value do |value|
118
+ freeze_nested_hash(value) if value.is_a?(Hash)
119
+ end
120
+ hash.freeze
121
+ end
122
+
123
+ def validate_domain_type(field_decl, errors)
124
+ domain = field_decl.domain
125
+ return if valid_domain_type?(domain)
126
+
127
+ report_error(errors,
128
+ "Field :#{field_decl.name} has invalid domain constraint: #{domain.inspect}. Domain must be a Range, Array, or Proc",
129
+ location: field_decl.loc)
130
+ end
131
+
132
+ def valid_domain_type?(domain)
133
+ domain.is_a?(Range) || domain.is_a?(Array) || domain.is_a?(Proc)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ # RESPONSIBILITY: Build definitions index and detect duplicate names
8
+ # DEPENDENCIES: None (first pass in pipeline)
9
+ # PRODUCES: :declarations - Hash mapping names to declaration nodes
10
+ # INTERFACE: new(schema, state).run(errors)
11
+ class NameIndexer < PassBase
12
+ def run(errors)
13
+ definitions = {}
14
+
15
+ each_decl do |decl|
16
+ report_error(errors, "duplicated definition `#{decl.name}`", location: decl.loc) if definitions.key?(decl.name)
17
+ definitions[decl.name] = decl
18
+ end
19
+
20
+ state.with(:declarations, definitions.freeze)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ # Base class for analyzer passes with simple immutable state
8
+ class PassBase
9
+ include Kumi::Syntax
10
+ include Kumi::Core::ErrorReporting
11
+
12
+ # @param schema [Syntax::Root] The schema to analyze
13
+ # @param state [AnalysisState] Current analysis state
14
+ def initialize(schema, state)
15
+ @schema = schema
16
+ @state = state
17
+ end
18
+
19
+ # Main pass execution - subclasses implement this
20
+ # @param errors [Array] Error accumulator array
21
+ # @return [AnalysisState] New state after pass execution
22
+ def run(errors)
23
+ raise NotImplementedError, "#{self.class.name} must implement #run"
24
+ end
25
+
26
+ protected
27
+
28
+ attr_reader :schema, :state
29
+
30
+ # Iterate over all declarations (attributes and traits) in the schema
31
+ # @yield [Syntax::Attribute|Syntax::Trait] Each declaration
32
+ def each_decl(&block)
33
+ schema.attributes.each(&block)
34
+ schema.traits.each(&block)
35
+ end
36
+
37
+ # Get state value - compatible with old interface
38
+ def get_state(key, required: true)
39
+ raise StandardError, "Required state key '#{key}' not found" if required && !state.key?(key)
40
+
41
+ state[key]
42
+ end
43
+
44
+ # Add error to the error list
45
+ def add_error(errors, location, message)
46
+ errors << ErrorReporter.create_error(message, location: location, type: :semantic)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end