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
@@ -1,151 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kumi
4
- module Analyzer
5
- module Passes
6
- # RESPONSIBILITY: Build dependency graph and detect conditional dependencies in cascades
7
- # DEPENDENCIES: :declarations from NameIndexer, :inputs from InputCollector
8
- # PRODUCES: :dependencies, :dependents, :leaves - Dependency analysis results
9
- # INTERFACE: new(schema, state).run(errors)
10
- class DependencyResolver < PassBase
11
- # Enhanced edge with conditional flag and cascade metadata
12
- class DependencyEdge
13
- attr_reader :to, :type, :via, :conditional, :cascade_owner
14
-
15
- def initialize(to:, type:, via:, conditional: false, cascade_owner: nil)
16
- @to = to
17
- @type = type
18
- @via = via
19
- @conditional = conditional
20
- @cascade_owner = cascade_owner
21
- end
22
- end
23
-
24
- include Syntax
25
-
26
- def run(errors)
27
- definitions = get_state(:declarations)
28
- input_meta = get_state(:inputs)
29
-
30
- dependency_graph = Hash.new { |h, k| h[k] = [] }
31
- reverse_dependencies = Hash.new { |h, k| h[k] = [] }
32
- leaf_map = Hash.new { |h, k| h[k] = Set.new }
33
-
34
- each_decl do |decl|
35
- # Traverse the expression for each declaration, passing context down
36
- visit_with_context(decl.expression, { decl_name: decl.name }) do |node, context|
37
- process_node(node, decl, dependency_graph, reverse_dependencies, leaf_map, definitions, input_meta, errors, context)
38
- end
39
- end
40
-
41
- # Compute transitive closure of reverse dependencies
42
- transitive_dependents = compute_transitive_closure(reverse_dependencies)
43
-
44
- state.with(:dependencies, dependency_graph.transform_values(&:freeze).freeze)
45
- .with(:dependents, transitive_dependents.freeze)
46
- .with(:leaves, leaf_map.transform_values(&:freeze).freeze)
47
- end
48
-
49
- private
50
-
51
- def process_node(node, decl, graph, reverse_deps, leaves, definitions, _input_meta, errors, context)
52
- case node
53
- when DeclarationReference
54
- report_error(errors, "undefined reference to `#{node.name}`", location: node.loc) unless definitions.key?(node.name)
55
-
56
- # Determine if this is a conditional dependency
57
- conditional = context[:in_cascade_branch] || context[:in_cascade_base] || false
58
- cascade_owner = conditional ? (context[:cascade_owner] || context[:decl_name]) : nil
59
-
60
- add_dependency_edge(graph, reverse_deps, decl.name, node.name, :ref, context[:via],
61
- conditional: conditional,
62
- cascade_owner: cascade_owner)
63
- when InputReference
64
- add_dependency_edge(graph, reverse_deps, decl.name, node.name, :key, context[:via])
65
- leaves[decl.name] << node
66
- when InputElementReference
67
- # adds the root input declaration as a dependency
68
- root_input_declr_name = node.path.first
69
- add_dependency_edge(graph, reverse_deps, decl.name, root_input_declr_name, :key, context[:via])
70
- when Literal
71
- leaves[decl.name] << node
72
- end
73
- end
74
-
75
- def add_dependency_edge(graph, reverse_deps, from, to, type, via, conditional: false, cascade_owner: nil)
76
- edge = DependencyEdge.new(
77
- to: to,
78
- type: type,
79
- via: via,
80
- conditional: conditional,
81
- cascade_owner: cascade_owner
82
- )
83
- graph[from] << edge
84
- reverse_deps[to] << from
85
- end
86
-
87
- # Custom visitor that understands cascade structure
88
- def visit_with_context(node, context = {}, &block)
89
- return unless node
90
-
91
- yield(node, context)
92
-
93
- case node
94
- when CascadeExpression
95
- # Visit condition nodes and result expressions (non-base cases)
96
- node.cases[0...-1].each do |when_case|
97
- if when_case.condition
98
- # Visit condition normally
99
- visit_with_context(when_case.condition, context, &block)
100
- end
101
- # Visit result expressions as conditional dependencies
102
- conditional_context = context.merge(in_cascade_branch: true, cascade_owner: context[:decl_name])
103
- visit_with_context(when_case.result, conditional_context, &block)
104
- end
105
-
106
- # Visit base case with conditional flag
107
- if node.cases.last
108
- base_context = context.merge(in_cascade_base: true)
109
- visit_with_context(node.cases.last.result, base_context, &block)
110
- end
111
- when CallExpression
112
- new_context = context.merge(via: node.fn_name)
113
- node.children.each { |child| visit_with_context(child, new_context, &block) }
114
- else
115
- node.children.each { |child| visit_with_context(child, context, &block) } if node.respond_to?(:children)
116
- end
117
- end
118
-
119
- def compute_transitive_closure(reverse_dependencies)
120
- transitive = {}
121
- all_keys = reverse_dependencies.keys
122
-
123
- all_keys.each do |key|
124
- visited = Set.new
125
- to_visit = [key]
126
- dependents = Set.new
127
-
128
- while to_visit.any?
129
- current = to_visit.shift
130
- next if visited.include?(current)
131
-
132
- visited.add(current)
133
-
134
- direct_dependents = reverse_dependencies[current] || []
135
- direct_dependents.each do |dependent|
136
- next if visited.include?(dependent)
137
-
138
- dependents << dependent
139
- to_visit << dependent
140
- end
141
- end
142
-
143
- transitive[key] = dependents.to_a
144
- end
145
-
146
- transitive
147
- end
148
- end
149
- end
150
- end
151
- end
@@ -1,137 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kumi
4
- module Analyzer
5
- module Passes
6
- # RESPONSIBILITY: Collect field metadata from input declarations and validate consistency
7
- # DEPENDENCIES: :definitions
8
- # PRODUCES: :inputs - Hash mapping field names to {type:, domain:} metadata
9
- # INTERFACE: new(schema, state).run(errors)
10
- class InputCollector < PassBase
11
- def run(errors)
12
- input_meta = {}
13
-
14
- schema.inputs.each do |field_decl|
15
- unless field_decl.is_a?(Kumi::Syntax::InputDeclaration)
16
- report_error(errors, "Expected InputDeclaration node, got #{field_decl.class}", location: field_decl.loc)
17
- next
18
- end
19
-
20
- name = field_decl.name
21
- existing = input_meta[name]
22
-
23
- if existing
24
- # Check for compatibility and merge
25
- merged_meta = merge_field_metadata(existing, field_decl, errors)
26
- input_meta[name] = merged_meta if merged_meta
27
- else
28
- # New field - collect its metadata
29
- input_meta[name] = collect_field_metadata(field_decl, errors)
30
- end
31
- end
32
-
33
- state.with(:inputs, freeze_nested_hash(input_meta))
34
- end
35
-
36
- private
37
-
38
- def collect_field_metadata(field_decl, errors)
39
- validate_domain_type(field_decl, errors) if field_decl.domain
40
-
41
- metadata = {
42
- type: field_decl.type,
43
- domain: field_decl.domain
44
- }
45
-
46
- # Process children if present
47
- if field_decl.children && !field_decl.children.empty?
48
- children_meta = {}
49
- field_decl.children.each do |child_decl|
50
- unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
51
- report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
52
- next
53
- end
54
- children_meta[child_decl.name] = collect_field_metadata(child_decl, errors)
55
- end
56
- metadata[:children] = children_meta
57
- end
58
-
59
- metadata
60
- end
61
-
62
- def merge_field_metadata(existing, field_decl, errors)
63
- name = field_decl.name
64
-
65
- # Check for type compatibility
66
- if existing[:type] != field_decl.type && field_decl.type && existing[:type]
67
- report_error(errors,
68
- "Field :#{name} declared with conflicting types: #{existing[:type]} vs #{field_decl.type}",
69
- location: field_decl.loc)
70
- end
71
-
72
- # Check for domain compatibility
73
- if existing[:domain] != field_decl.domain && field_decl.domain && existing[:domain]
74
- report_error(errors,
75
- "Field :#{name} declared with conflicting domains: #{existing[:domain].inspect} vs #{field_decl.domain.inspect}",
76
- location: field_decl.loc)
77
- end
78
-
79
- # Validate domain type if provided
80
- validate_domain_type(field_decl, errors) if field_decl.domain
81
-
82
- # Merge metadata (later declarations override nil values)
83
- merged = {
84
- type: field_decl.type || existing[:type],
85
- domain: field_decl.domain || existing[:domain]
86
- }
87
-
88
- # Merge children if present
89
- if field_decl.children && !field_decl.children.empty?
90
- existing_children = existing[:children] || {}
91
- new_children = {}
92
-
93
- field_decl.children.each do |child_decl|
94
- unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
95
- report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
96
- next
97
- end
98
-
99
- child_name = child_decl.name
100
- new_children[child_name] = if existing_children[child_name]
101
- merge_field_metadata(existing_children[child_name], child_decl, errors)
102
- else
103
- collect_field_metadata(child_decl, errors)
104
- end
105
- end
106
-
107
- merged[:children] = new_children
108
- elsif existing[:children]
109
- merged[:children] = existing[:children]
110
- end
111
-
112
- merged
113
- end
114
-
115
- def freeze_nested_hash(hash)
116
- hash.each_value do |value|
117
- freeze_nested_hash(value) if value.is_a?(Hash)
118
- end
119
- hash.freeze
120
- end
121
-
122
- def validate_domain_type(field_decl, errors)
123
- domain = field_decl.domain
124
- return if valid_domain_type?(domain)
125
-
126
- report_error(errors,
127
- "Field :#{field_decl.name} has invalid domain constraint: #{domain.inspect}. Domain must be a Range, Array, or Proc",
128
- location: field_decl.loc)
129
- end
130
-
131
- def valid_domain_type?(domain)
132
- domain.is_a?(Range) || domain.is_a?(Array) || domain.is_a?(Proc)
133
- end
134
- end
135
- end
136
- end
137
- end
@@ -1,24 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kumi
4
- module Analyzer
5
- module Passes
6
- # RESPONSIBILITY: Build definitions index and detect duplicate names
7
- # DEPENDENCIES: None (first pass in pipeline)
8
- # PRODUCES: :declarations - Hash mapping names to declaration nodes
9
- # INTERFACE: new(schema, state).run(errors)
10
- class NameIndexer < PassBase
11
- def run(errors)
12
- definitions = {}
13
-
14
- each_decl do |decl|
15
- report_error(errors, "duplicated definition `#{decl.name}`", location: decl.loc) if definitions.key?(decl.name)
16
- definitions[decl.name] = decl
17
- end
18
-
19
- state.with(:declarations, definitions.freeze)
20
- end
21
- end
22
- end
23
- end
24
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kumi
4
- module Analyzer
5
- module Passes
6
- # Base class for analyzer passes with simple immutable state
7
- class PassBase
8
- include Kumi::Syntax
9
- include Kumi::ErrorReporting
10
-
11
- # @param schema [Syntax::Root] The schema to analyze
12
- # @param state [AnalysisState] Current analysis state
13
- def initialize(schema, state)
14
- @schema = schema
15
- @state = state
16
- end
17
-
18
- # Main pass execution - subclasses implement this
19
- # @param errors [Array] Error accumulator array
20
- # @return [AnalysisState] New state after pass execution
21
- def run(errors)
22
- raise NotImplementedError, "#{self.class.name} must implement #run"
23
- end
24
-
25
- protected
26
-
27
- attr_reader :schema, :state
28
-
29
- # Iterate over all declarations (attributes and traits) in the schema
30
- # @yield [Syntax::Attribute|Syntax::Trait] Each declaration
31
- def each_decl(&block)
32
- schema.attributes.each(&block)
33
- schema.traits.each(&block)
34
- end
35
-
36
- # Get state value - compatible with old interface
37
- def get_state(key, required: true)
38
- raise StandardError, "Required state key '#{key}' not found" if required && !state.key?(key)
39
-
40
- state[key]
41
- end
42
-
43
- # Add error to the error list
44
- def add_error(errors, location, message)
45
- errors << ErrorReporter.create_error(message, location: location, type: :semantic)
46
- end
47
- end
48
- end
49
- end
50
- end
@@ -1,109 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kumi
4
- module Analyzer
5
- module Passes
6
- # RESPONSIBILITY: Validate DSL semantic constraints at the AST level
7
- # DEPENDENCIES: :definitions
8
- # PRODUCES: None (validation only)
9
- # INTERFACE: new(schema, state).run(errors)
10
- #
11
- # This pass enforces semantic constraints that must hold regardless of which parser
12
- # was used to construct the AST. It validates:
13
- # 1. Cascade conditions are only trait references (DeclarationReference nodes)
14
- # 2. Trait expressions evaluate to boolean values (CallExpression nodes)
15
- # 3. Function names exist in the function registry
16
- # 4. Expression types are valid for their context
17
- class SemanticConstraintValidator < VisitorPass
18
- def run(errors)
19
- each_decl do |decl|
20
- visit(decl) { |node| validate_semantic_constraints(node, decl, errors) }
21
- end
22
- state
23
- end
24
-
25
- private
26
-
27
- def validate_semantic_constraints(node, _decl, errors)
28
- case node
29
- when Kumi::Syntax::TraitDeclaration
30
- validate_trait_expression(node, errors)
31
- when Kumi::Syntax::CaseExpression
32
- validate_cascade_condition(node, errors)
33
- when Kumi::Syntax::CallExpression
34
- validate_function_call(node, errors)
35
- end
36
- end
37
-
38
- def validate_trait_expression(trait, errors)
39
- return if trait.expression.is_a?(Kumi::Syntax::CallExpression)
40
-
41
- report_error(
42
- errors,
43
- "trait `#{trait.name}` must have a boolean expression",
44
- location: trait.loc,
45
- type: :semantic
46
- )
47
- end
48
-
49
- def validate_cascade_condition(when_case, errors)
50
- condition = when_case.condition
51
-
52
- case condition
53
- when Kumi::Syntax::DeclarationReference
54
- # Valid: trait reference
55
- nil
56
- when Kumi::Syntax::CallExpression
57
- # Valid if it's a boolean composition of traits (all?, any?, none?)
58
- return if boolean_trait_composition?(condition)
59
-
60
- # For now, allow other CallExpressions - they'll be validated by other passes
61
- nil
62
- when Kumi::Syntax::Literal
63
- # Allow literal conditions (like true/false) - they might be valid
64
- nil
65
- else
66
- # Only reject truly invalid conditions like InputReference or complex expressions
67
- report_error(
68
- errors,
69
- "cascade condition must be trait reference",
70
- location: when_case.loc,
71
- type: :semantic
72
- )
73
- end
74
- end
75
-
76
- def validate_function_call(call_expr, errors)
77
- fn_name = call_expr.fn_name
78
-
79
- # Skip validation if FunctionRegistry is being mocked for testing
80
- return if function_registry_mocked?
81
-
82
- return if FunctionRegistry.supported?(fn_name)
83
-
84
- report_error(
85
- errors,
86
- "unknown function `#{fn_name}`",
87
- location: call_expr.loc,
88
- type: :semantic
89
- )
90
- end
91
-
92
- def boolean_trait_composition?(call_expr)
93
- # Allow boolean composition functions that operate on trait collections
94
- %i[all? any? none?].include?(call_expr.fn_name)
95
- end
96
-
97
- def function_registry_mocked?
98
- # Check if FunctionRegistry is being mocked (for tests)
99
-
100
- # Try to access a method that doesn't exist in the real registry
101
- # If it's mocked, this won't raise an error
102
- FunctionRegistry.respond_to?(:confirm_support!)
103
- rescue StandardError
104
- false
105
- end
106
- end
107
- end
108
- end
109
- end
@@ -1,108 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kumi
4
- module Analyzer
5
- module Passes
6
- # RESPONSIBILITY: Compute topological ordering of declarations, allowing safe conditional cycles
7
- # DEPENDENCIES: :dependencies from DependencyResolver, :declarations from NameIndexer, :cascades from UnsatDetector
8
- # PRODUCES: :evaluation_order - Array of declaration names in evaluation order
9
- # INTERFACE: new(schema, state).run(errors)
10
- class Toposorter < PassBase
11
- def run(errors)
12
- dependency_graph = get_state(:dependencies, required: false) || {}
13
- definitions = get_state(:declarations, required: false) || {}
14
-
15
- order = compute_topological_order(dependency_graph, definitions, errors)
16
- state.with(:evaluation_order, order)
17
- end
18
-
19
- private
20
-
21
- def compute_topological_order(graph, definitions, errors)
22
- temp_marks = Set.new
23
- perm_marks = Set.new
24
- order = []
25
- cascades = get_state(:cascades) || {}
26
-
27
- visit_node = lambda do |node, path = []|
28
- return if perm_marks.include?(node)
29
-
30
- if temp_marks.include?(node)
31
- # Check if this is a safe conditional cycle
32
- cycle_path = path + [node]
33
- return if safe_conditional_cycle?(cycle_path, graph, cascades)
34
-
35
- # Allow this cycle - it's safe due to cascade mutual exclusion
36
-
37
- report_unexpected_cycle(temp_marks, node, errors)
38
-
39
- return
40
- end
41
-
42
- temp_marks << node
43
- current_path = path + [node]
44
- Array(graph[node]).each { |edge| visit_node.call(edge.to, current_path) }
45
- temp_marks.delete(node)
46
- perm_marks << node
47
-
48
- # Only include declaration nodes in the final order
49
- order << node if definitions.key?(node)
50
- end
51
-
52
- # Visit all nodes in the graph
53
- graph.each_key { |node| visit_node.call(node) }
54
-
55
- # Also visit any definitions that aren't in the dependency graph
56
- # (i.e., declarations with no dependencies)
57
- definitions.each_key { |node| visit_node.call(node) }
58
-
59
- order.freeze
60
- end
61
-
62
- def safe_conditional_cycle?(cycle_path, graph, cascades)
63
- return false if cycle_path.nil? || cycle_path.size < 2
64
-
65
- # Find where the cycle starts - look for the first occurrence of the repeated node
66
- last_node = cycle_path.last
67
- return false if last_node.nil?
68
-
69
- cycle_start = cycle_path.index(last_node)
70
- return false unless cycle_start && cycle_start < cycle_path.size - 1
71
-
72
- cycle_nodes = cycle_path[cycle_start..]
73
-
74
- # Check if all edges in the cycle are conditional
75
- cycle_nodes.each_cons(2) do |from, to|
76
- edges = graph[from] || []
77
- edge = edges.find { |e| e.to == to }
78
-
79
- return false unless edge&.conditional
80
-
81
- # Check if the cascade has mutually exclusive conditions
82
- cascade_meta = cascades[edge.cascade_owner]
83
- return false unless cascade_meta&.dig(:all_mutually_exclusive)
84
- end
85
-
86
- true
87
- end
88
-
89
- def report_unexpected_cycle(temp_marks, current_node, errors)
90
- cycle_path = temp_marks.to_a.join(" → ") + " → #{current_node}"
91
-
92
- # Try to find the first declaration in the cycle for location info
93
- first_decl = find_declaration_by_name(temp_marks.first || current_node)
94
- location = first_decl&.loc
95
-
96
- report_error(errors, "cycle detected: #{cycle_path}", location: location)
97
- end
98
-
99
- def find_declaration_by_name(name)
100
- return nil unless schema
101
-
102
- schema.attributes.find { |attr| attr.name == name } ||
103
- schema.traits.find { |trait| trait.name == name }
104
- end
105
- end
106
- end
107
- end
108
- end