kumi 0.0.7 → 0.0.8

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 (171) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +1 -1
  3. data/README.md +8 -5
  4. data/examples/game_of_life.rb +1 -1
  5. data/examples/static_analysis_errors.rb +7 -7
  6. data/lib/kumi/analyzer.rb +15 -15
  7. data/lib/kumi/compiler.rb +6 -6
  8. data/lib/kumi/core/analyzer/analysis_state.rb +39 -0
  9. data/lib/kumi/core/analyzer/constant_evaluator.rb +59 -0
  10. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +248 -0
  11. data/lib/kumi/core/analyzer/passes/declaration_validator.rb +45 -0
  12. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +153 -0
  13. data/lib/kumi/core/analyzer/passes/input_collector.rb +139 -0
  14. data/lib/kumi/core/analyzer/passes/name_indexer.rb +26 -0
  15. data/lib/kumi/core/analyzer/passes/pass_base.rb +52 -0
  16. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +111 -0
  17. data/lib/kumi/core/analyzer/passes/toposorter.rb +110 -0
  18. data/lib/kumi/core/analyzer/passes/type_checker.rb +162 -0
  19. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +48 -0
  20. data/lib/kumi/core/analyzer/passes/type_inferencer.rb +236 -0
  21. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +406 -0
  22. data/lib/kumi/core/analyzer/passes/visitor_pass.rb +44 -0
  23. data/lib/kumi/core/atom_unsat_solver.rb +396 -0
  24. data/lib/kumi/core/compiled_schema.rb +43 -0
  25. data/lib/kumi/core/constraint_relationship_solver.rb +641 -0
  26. data/lib/kumi/core/domain/enum_analyzer.rb +55 -0
  27. data/lib/kumi/core/domain/range_analyzer.rb +85 -0
  28. data/lib/kumi/core/domain/validator.rb +82 -0
  29. data/lib/kumi/core/domain/violation_formatter.rb +42 -0
  30. data/lib/kumi/core/error_reporter.rb +166 -0
  31. data/lib/kumi/core/error_reporting.rb +97 -0
  32. data/lib/kumi/core/errors.rb +120 -0
  33. data/lib/kumi/core/evaluation_wrapper.rb +40 -0
  34. data/lib/kumi/core/explain.rb +295 -0
  35. data/lib/kumi/core/export/deserializer.rb +41 -0
  36. data/lib/kumi/core/export/errors.rb +14 -0
  37. data/lib/kumi/core/export/node_builders.rb +142 -0
  38. data/lib/kumi/core/export/node_registry.rb +54 -0
  39. data/lib/kumi/core/export/node_serializers.rb +158 -0
  40. data/lib/kumi/core/export/serializer.rb +25 -0
  41. data/lib/kumi/core/export.rb +35 -0
  42. data/lib/kumi/core/function_registry/collection_functions.rb +202 -0
  43. data/lib/kumi/core/function_registry/comparison_functions.rb +33 -0
  44. data/lib/kumi/core/function_registry/conditional_functions.rb +38 -0
  45. data/lib/kumi/core/function_registry/function_builder.rb +95 -0
  46. data/lib/kumi/core/function_registry/logical_functions.rb +44 -0
  47. data/lib/kumi/core/function_registry/math_functions.rb +74 -0
  48. data/lib/kumi/core/function_registry/string_functions.rb +57 -0
  49. data/lib/kumi/core/function_registry/type_functions.rb +53 -0
  50. data/lib/kumi/{function_registry.rb → core/function_registry.rb} +28 -36
  51. data/lib/kumi/core/input/type_matcher.rb +97 -0
  52. data/lib/kumi/core/input/validator.rb +51 -0
  53. data/lib/kumi/core/input/violation_creator.rb +52 -0
  54. data/lib/kumi/core/json_schema/generator.rb +65 -0
  55. data/lib/kumi/core/json_schema/validator.rb +27 -0
  56. data/lib/kumi/core/json_schema.rb +16 -0
  57. data/lib/kumi/core/ruby_parser/build_context.rb +27 -0
  58. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +38 -0
  59. data/lib/kumi/core/ruby_parser/dsl.rb +14 -0
  60. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +138 -0
  61. data/lib/kumi/core/ruby_parser/expression_converter.rb +128 -0
  62. data/lib/kumi/core/ruby_parser/guard_rails.rb +45 -0
  63. data/lib/kumi/core/ruby_parser/input_builder.rb +127 -0
  64. data/lib/kumi/core/ruby_parser/input_field_proxy.rb +48 -0
  65. data/lib/kumi/core/ruby_parser/input_proxy.rb +31 -0
  66. data/lib/kumi/core/ruby_parser/nested_input.rb +17 -0
  67. data/lib/kumi/core/ruby_parser/parser.rb +71 -0
  68. data/lib/kumi/core/ruby_parser/schema_builder.rb +175 -0
  69. data/lib/kumi/core/ruby_parser/sugar.rb +263 -0
  70. data/lib/kumi/core/ruby_parser.rb +12 -0
  71. data/lib/kumi/core/schema_instance.rb +111 -0
  72. data/lib/kumi/core/types/builder.rb +23 -0
  73. data/lib/kumi/core/types/compatibility.rb +96 -0
  74. data/lib/kumi/core/types/formatter.rb +26 -0
  75. data/lib/kumi/core/types/inference.rb +42 -0
  76. data/lib/kumi/core/types/normalizer.rb +72 -0
  77. data/lib/kumi/core/types/validator.rb +37 -0
  78. data/lib/kumi/core/types.rb +66 -0
  79. data/lib/kumi/core/vectorization_metadata.rb +110 -0
  80. data/lib/kumi/errors.rb +1 -112
  81. data/lib/kumi/registry.rb +37 -0
  82. data/lib/kumi/schema.rb +5 -5
  83. data/lib/kumi/schema_metadata.rb +3 -3
  84. data/lib/kumi/syntax/array_expression.rb +6 -6
  85. data/lib/kumi/syntax/call_expression.rb +4 -4
  86. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  87. data/lib/kumi/syntax/case_expression.rb +4 -4
  88. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  89. data/lib/kumi/syntax/hash_expression.rb +4 -4
  90. data/lib/kumi/syntax/input_declaration.rb +5 -5
  91. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  92. data/lib/kumi/syntax/input_reference.rb +5 -5
  93. data/lib/kumi/syntax/literal.rb +4 -4
  94. data/lib/kumi/syntax/node.rb +34 -34
  95. data/lib/kumi/syntax/root.rb +6 -6
  96. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  97. data/lib/kumi/syntax/value_declaration.rb +4 -4
  98. data/lib/kumi/version.rb +1 -1
  99. data/migrate_to_core_iterative.rb +938 -0
  100. data/scripts/generate_function_docs.rb +9 -9
  101. metadata +75 -72
  102. data/lib/kumi/analyzer/analysis_state.rb +0 -37
  103. data/lib/kumi/analyzer/constant_evaluator.rb +0 -57
  104. data/lib/kumi/analyzer/passes/broadcast_detector.rb +0 -246
  105. data/lib/kumi/analyzer/passes/declaration_validator.rb +0 -43
  106. data/lib/kumi/analyzer/passes/dependency_resolver.rb +0 -151
  107. data/lib/kumi/analyzer/passes/input_collector.rb +0 -137
  108. data/lib/kumi/analyzer/passes/name_indexer.rb +0 -24
  109. data/lib/kumi/analyzer/passes/pass_base.rb +0 -50
  110. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +0 -109
  111. data/lib/kumi/analyzer/passes/toposorter.rb +0 -108
  112. data/lib/kumi/analyzer/passes/type_checker.rb +0 -160
  113. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +0 -46
  114. data/lib/kumi/analyzer/passes/type_inferencer.rb +0 -232
  115. data/lib/kumi/analyzer/passes/unsat_detector.rb +0 -404
  116. data/lib/kumi/analyzer/passes/visitor_pass.rb +0 -42
  117. data/lib/kumi/atom_unsat_solver.rb +0 -394
  118. data/lib/kumi/compiled_schema.rb +0 -41
  119. data/lib/kumi/constraint_relationship_solver.rb +0 -638
  120. data/lib/kumi/domain/enum_analyzer.rb +0 -53
  121. data/lib/kumi/domain/range_analyzer.rb +0 -83
  122. data/lib/kumi/domain/validator.rb +0 -80
  123. data/lib/kumi/domain/violation_formatter.rb +0 -40
  124. data/lib/kumi/error_reporter.rb +0 -164
  125. data/lib/kumi/error_reporting.rb +0 -95
  126. data/lib/kumi/evaluation_wrapper.rb +0 -38
  127. data/lib/kumi/explain.rb +0 -293
  128. data/lib/kumi/export/deserializer.rb +0 -39
  129. data/lib/kumi/export/errors.rb +0 -12
  130. data/lib/kumi/export/node_builders.rb +0 -140
  131. data/lib/kumi/export/node_registry.rb +0 -52
  132. data/lib/kumi/export/node_serializers.rb +0 -156
  133. data/lib/kumi/export/serializer.rb +0 -23
  134. data/lib/kumi/export.rb +0 -33
  135. data/lib/kumi/function_registry/collection_functions.rb +0 -200
  136. data/lib/kumi/function_registry/comparison_functions.rb +0 -31
  137. data/lib/kumi/function_registry/conditional_functions.rb +0 -36
  138. data/lib/kumi/function_registry/function_builder.rb +0 -93
  139. data/lib/kumi/function_registry/logical_functions.rb +0 -42
  140. data/lib/kumi/function_registry/math_functions.rb +0 -72
  141. data/lib/kumi/function_registry/string_functions.rb +0 -54
  142. data/lib/kumi/function_registry/type_functions.rb +0 -51
  143. data/lib/kumi/input/type_matcher.rb +0 -95
  144. data/lib/kumi/input/validator.rb +0 -49
  145. data/lib/kumi/input/violation_creator.rb +0 -50
  146. data/lib/kumi/json_schema/generator.rb +0 -63
  147. data/lib/kumi/json_schema/validator.rb +0 -25
  148. data/lib/kumi/json_schema.rb +0 -14
  149. data/lib/kumi/ruby_parser/build_context.rb +0 -25
  150. data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +0 -36
  151. data/lib/kumi/ruby_parser/dsl.rb +0 -12
  152. data/lib/kumi/ruby_parser/dsl_cascade_builder.rb +0 -136
  153. data/lib/kumi/ruby_parser/expression_converter.rb +0 -126
  154. data/lib/kumi/ruby_parser/guard_rails.rb +0 -43
  155. data/lib/kumi/ruby_parser/input_builder.rb +0 -125
  156. data/lib/kumi/ruby_parser/input_field_proxy.rb +0 -46
  157. data/lib/kumi/ruby_parser/input_proxy.rb +0 -29
  158. data/lib/kumi/ruby_parser/nested_input.rb +0 -15
  159. data/lib/kumi/ruby_parser/parser.rb +0 -69
  160. data/lib/kumi/ruby_parser/schema_builder.rb +0 -173
  161. data/lib/kumi/ruby_parser/sugar.rb +0 -261
  162. data/lib/kumi/ruby_parser.rb +0 -10
  163. data/lib/kumi/schema_instance.rb +0 -109
  164. data/lib/kumi/types/builder.rb +0 -21
  165. data/lib/kumi/types/compatibility.rb +0 -94
  166. data/lib/kumi/types/formatter.rb +0 -24
  167. data/lib/kumi/types/inference.rb +0 -40
  168. data/lib/kumi/types/normalizer.rb +0 -70
  169. data/lib/kumi/types/validator.rb +0 -35
  170. data/lib/kumi/types.rb +0 -64
  171. data/lib/kumi/vectorization_metadata.rb +0 -108
@@ -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
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ # RESPONSIBILITY: Validate DSL semantic constraints at the AST level
8
+ # DEPENDENCIES: :definitions
9
+ # PRODUCES: None (validation only)
10
+ # INTERFACE: new(schema, state).run(errors)
11
+ #
12
+ # This pass enforces semantic constraints that must hold regardless of which parser
13
+ # was used to construct the AST. It validates:
14
+ # 1. Cascade conditions are only trait references (DeclarationReference nodes)
15
+ # 2. Trait expressions evaluate to boolean values (CallExpression nodes)
16
+ # 3. Function names exist in the function registry
17
+ # 4. Expression types are valid for their context
18
+ class SemanticConstraintValidator < VisitorPass
19
+ def run(errors)
20
+ each_decl do |decl|
21
+ visit(decl) { |node| validate_semantic_constraints(node, decl, errors) }
22
+ end
23
+ state
24
+ end
25
+
26
+ private
27
+
28
+ def validate_semantic_constraints(node, _decl, errors)
29
+ case node
30
+ when Kumi::Syntax::TraitDeclaration
31
+ validate_trait_expression(node, errors)
32
+ when Kumi::Syntax::CaseExpression
33
+ validate_cascade_condition(node, errors)
34
+ when Kumi::Syntax::CallExpression
35
+ validate_function_call(node, errors)
36
+ end
37
+ end
38
+
39
+ def validate_trait_expression(trait, errors)
40
+ return if trait.expression.is_a?(Kumi::Syntax::CallExpression)
41
+
42
+ report_error(
43
+ errors,
44
+ "trait `#{trait.name}` must have a boolean expression",
45
+ location: trait.loc,
46
+ type: :semantic
47
+ )
48
+ end
49
+
50
+ def validate_cascade_condition(when_case, errors)
51
+ condition = when_case.condition
52
+
53
+ case condition
54
+ when Kumi::Syntax::DeclarationReference
55
+ # Valid: trait reference
56
+ nil
57
+ when Kumi::Syntax::CallExpression
58
+ # Valid if it's a boolean composition of traits (all?, any?, none?)
59
+ return if boolean_trait_composition?(condition)
60
+
61
+ # For now, allow other CallExpressions - they'll be validated by other passes
62
+ nil
63
+ when Kumi::Syntax::Literal
64
+ # Allow literal conditions (like true/false) - they might be valid
65
+ nil
66
+ else
67
+ # Only reject truly invalid conditions like InputReference or complex expressions
68
+ report_error(
69
+ errors,
70
+ "cascade condition must be trait reference",
71
+ location: when_case.loc,
72
+ type: :semantic
73
+ )
74
+ end
75
+ end
76
+
77
+ def validate_function_call(call_expr, errors)
78
+ fn_name = call_expr.fn_name
79
+
80
+ # Skip validation if Kumi::Registry.is being mocked for testing
81
+ return if function_registry_mocked?
82
+
83
+ return if Kumi::Registry.supported?(fn_name)
84
+
85
+ report_error(
86
+ errors,
87
+ "unknown function `#{fn_name}`",
88
+ location: call_expr.loc,
89
+ type: :semantic
90
+ )
91
+ end
92
+
93
+ def boolean_trait_composition?(call_expr)
94
+ # Allow boolean composition functions that operate on trait collections
95
+ %i[all? any? none?].include?(call_expr.fn_name)
96
+ end
97
+
98
+ def function_registry_mocked?
99
+ # Check if Kumi::Registry.is being mocked (for tests)
100
+
101
+ # Try to access a method that doesn't exist in the real registry
102
+ # If it's mocked, this won't raise an error
103
+ Kumi::Registry.respond_to?(:confirm_support!)
104
+ rescue StandardError
105
+ false
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module Analyzer
6
+ module Passes
7
+ # RESPONSIBILITY: Compute topological ordering of declarations, allowing safe conditional cycles
8
+ # DEPENDENCIES: :dependencies from DependencyResolver, :declarations from NameIndexer, :cascades from UnsatDetector
9
+ # PRODUCES: :evaluation_order - Array of declaration names in evaluation order
10
+ # INTERFACE: new(schema, state).run(errors)
11
+ class Toposorter < PassBase
12
+ def run(errors)
13
+ dependency_graph = get_state(:dependencies, required: false) || {}
14
+ definitions = get_state(:declarations, required: false) || {}
15
+
16
+ order = compute_topological_order(dependency_graph, definitions, errors)
17
+ state.with(:evaluation_order, order)
18
+ end
19
+
20
+ private
21
+
22
+ def compute_topological_order(graph, definitions, errors)
23
+ temp_marks = Set.new
24
+ perm_marks = Set.new
25
+ order = []
26
+ cascades = get_state(:cascades) || {}
27
+
28
+ visit_node = lambda do |node, path = []|
29
+ return if perm_marks.include?(node)
30
+
31
+ if temp_marks.include?(node)
32
+ # Check if this is a safe conditional cycle
33
+ cycle_path = path + [node]
34
+ return if safe_conditional_cycle?(cycle_path, graph, cascades)
35
+
36
+ # Allow this cycle - it's safe due to cascade mutual exclusion
37
+
38
+ report_unexpected_cycle(temp_marks, node, errors)
39
+
40
+ return
41
+ end
42
+
43
+ temp_marks << node
44
+ current_path = path + [node]
45
+ Array(graph[node]).each { |edge| visit_node.call(edge.to, current_path) }
46
+ temp_marks.delete(node)
47
+ perm_marks << node
48
+
49
+ # Only include declaration nodes in the final order
50
+ order << node if definitions.key?(node)
51
+ end
52
+
53
+ # Visit all nodes in the graph
54
+ graph.each_key { |node| visit_node.call(node) }
55
+
56
+ # Also visit any definitions that aren't in the dependency graph
57
+ # (i.e., declarations with no dependencies)
58
+ definitions.each_key { |node| visit_node.call(node) }
59
+
60
+ order.freeze
61
+ end
62
+
63
+ def safe_conditional_cycle?(cycle_path, graph, cascades)
64
+ return false if cycle_path.nil? || cycle_path.size < 2
65
+
66
+ # Find where the cycle starts - look for the first occurrence of the repeated node
67
+ last_node = cycle_path.last
68
+ return false if last_node.nil?
69
+
70
+ cycle_start = cycle_path.index(last_node)
71
+ return false unless cycle_start && cycle_start < cycle_path.size - 1
72
+
73
+ cycle_nodes = cycle_path[cycle_start..]
74
+
75
+ # Check if all edges in the cycle are conditional
76
+ cycle_nodes.each_cons(2) do |from, to|
77
+ edges = graph[from] || []
78
+ edge = edges.find { |e| e.to == to }
79
+
80
+ return false unless edge&.conditional
81
+
82
+ # Check if the cascade has mutually exclusive conditions
83
+ cascade_meta = cascades[edge.cascade_owner]
84
+ return false unless cascade_meta&.dig(:all_mutually_exclusive)
85
+ end
86
+
87
+ true
88
+ end
89
+
90
+ def report_unexpected_cycle(temp_marks, current_node, errors)
91
+ cycle_path = temp_marks.to_a.join(" → ") + " → #{current_node}"
92
+
93
+ # Try to find the first declaration in the cycle for location info
94
+ first_decl = find_declaration_by_name(temp_marks.first || current_node)
95
+ location = first_decl&.loc
96
+
97
+ report_error(errors, "cycle detected: #{cycle_path}", location: location)
98
+ end
99
+
100
+ def find_declaration_by_name(name)
101
+ return nil unless schema
102
+
103
+ schema.attributes.find { |attr| attr.name == name } ||
104
+ schema.traits.find { |trait| trait.name == name }
105
+ end
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end