kumi 0.0.0 → 0.0.4

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +113 -3
  3. data/CHANGELOG.md +21 -1
  4. data/CLAUDE.md +387 -0
  5. data/README.md +270 -20
  6. data/docs/development/README.md +120 -0
  7. data/docs/development/error-reporting.md +361 -0
  8. data/documents/AST.md +126 -0
  9. data/documents/DSL.md +154 -0
  10. data/documents/FUNCTIONS.md +132 -0
  11. data/documents/SYNTAX.md +367 -0
  12. data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +106 -0
  13. data/examples/federal_tax_calculator_2024.rb +112 -0
  14. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +80 -0
  15. data/lib/generators/trait_engine/templates/schema_spec.rb.erb +27 -0
  16. data/lib/kumi/analyzer/constant_evaluator.rb +51 -0
  17. data/lib/kumi/analyzer/passes/definition_validator.rb +42 -0
  18. data/lib/kumi/analyzer/passes/dependency_resolver.rb +71 -0
  19. data/lib/kumi/analyzer/passes/input_collector.rb +55 -0
  20. data/lib/kumi/analyzer/passes/name_indexer.rb +24 -0
  21. data/lib/kumi/analyzer/passes/pass_base.rb +67 -0
  22. data/lib/kumi/analyzer/passes/toposorter.rb +72 -0
  23. data/lib/kumi/analyzer/passes/type_checker.rb +139 -0
  24. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +45 -0
  25. data/lib/kumi/analyzer/passes/type_inferencer.rb +125 -0
  26. data/lib/kumi/analyzer/passes/unsat_detector.rb +107 -0
  27. data/lib/kumi/analyzer/passes/visitor_pass.rb +41 -0
  28. data/lib/kumi/analyzer.rb +54 -0
  29. data/lib/kumi/atom_unsat_solver.rb +349 -0
  30. data/lib/kumi/compiled_schema.rb +41 -0
  31. data/lib/kumi/compiler.rb +127 -0
  32. data/lib/kumi/domain/enum_analyzer.rb +53 -0
  33. data/lib/kumi/domain/range_analyzer.rb +83 -0
  34. data/lib/kumi/domain/validator.rb +84 -0
  35. data/lib/kumi/domain/violation_formatter.rb +40 -0
  36. data/lib/kumi/domain.rb +8 -0
  37. data/lib/kumi/error_reporter.rb +164 -0
  38. data/lib/kumi/error_reporting.rb +95 -0
  39. data/lib/kumi/errors.rb +116 -0
  40. data/lib/kumi/evaluation_wrapper.rb +22 -0
  41. data/lib/kumi/explain.rb +281 -0
  42. data/lib/kumi/export/deserializer.rb +39 -0
  43. data/lib/kumi/export/errors.rb +12 -0
  44. data/lib/kumi/export/node_builders.rb +140 -0
  45. data/lib/kumi/export/node_registry.rb +38 -0
  46. data/lib/kumi/export/node_serializers.rb +156 -0
  47. data/lib/kumi/export/serializer.rb +23 -0
  48. data/lib/kumi/export.rb +33 -0
  49. data/lib/kumi/function_registry/collection_functions.rb +92 -0
  50. data/lib/kumi/function_registry/comparison_functions.rb +31 -0
  51. data/lib/kumi/function_registry/conditional_functions.rb +36 -0
  52. data/lib/kumi/function_registry/function_builder.rb +92 -0
  53. data/lib/kumi/function_registry/logical_functions.rb +42 -0
  54. data/lib/kumi/function_registry/math_functions.rb +72 -0
  55. data/lib/kumi/function_registry/string_functions.rb +54 -0
  56. data/lib/kumi/function_registry/type_functions.rb +51 -0
  57. data/lib/kumi/function_registry.rb +138 -0
  58. data/lib/kumi/input/type_matcher.rb +92 -0
  59. data/lib/kumi/input/validator.rb +52 -0
  60. data/lib/kumi/input/violation_creator.rb +50 -0
  61. data/lib/kumi/input.rb +8 -0
  62. data/lib/kumi/parser/build_context.rb +25 -0
  63. data/lib/kumi/parser/dsl.rb +12 -0
  64. data/lib/kumi/parser/dsl_cascade_builder.rb +125 -0
  65. data/lib/kumi/parser/expression_converter.rb +58 -0
  66. data/lib/kumi/parser/guard_rails.rb +43 -0
  67. data/lib/kumi/parser/input_builder.rb +94 -0
  68. data/lib/kumi/parser/input_proxy.rb +29 -0
  69. data/lib/kumi/parser/parser.rb +66 -0
  70. data/lib/kumi/parser/schema_builder.rb +172 -0
  71. data/lib/kumi/parser/sugar.rb +108 -0
  72. data/lib/kumi/schema.rb +49 -0
  73. data/lib/kumi/schema_instance.rb +43 -0
  74. data/lib/kumi/syntax/declarations.rb +23 -0
  75. data/lib/kumi/syntax/expressions.rb +30 -0
  76. data/lib/kumi/syntax/node.rb +46 -0
  77. data/lib/kumi/syntax/root.rb +12 -0
  78. data/lib/kumi/syntax/terminal_expressions.rb +27 -0
  79. data/lib/kumi/syntax.rb +9 -0
  80. data/lib/kumi/types/builder.rb +21 -0
  81. data/lib/kumi/types/compatibility.rb +86 -0
  82. data/lib/kumi/types/formatter.rb +24 -0
  83. data/lib/kumi/types/inference.rb +40 -0
  84. data/lib/kumi/types/normalizer.rb +70 -0
  85. data/lib/kumi/types/validator.rb +35 -0
  86. data/lib/kumi/types.rb +64 -0
  87. data/lib/kumi/version.rb +1 -1
  88. data/lib/kumi.rb +7 -3
  89. data/scripts/generate_function_docs.rb +59 -0
  90. data/test_impossible_cascade.rb +51 -0
  91. metadata +93 -10
  92. data/sig/kumi.rbs +0 -4
@@ -0,0 +1,349 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ # AtomUnsatSolver detects logical contradictions in constraint systems using three analysis passes:
5
+ # 1. Numerical bounds checking for symbol-numeric inequalities (e.g. x > 5, x < 3)
6
+ # 2. Equality contradiction detection for same-type comparisons
7
+ # 3. Strict inequality cycle detection using Kahn's topological sort (stack-safe)
8
+ #
9
+ # @example Basic usage
10
+ # atoms = [Atom.new(:>, :x, 5), Atom.new(:<, :x, 3)]
11
+ # AtomUnsatSolver.unsat?(atoms) #=> true (contradiction: x > 5 AND x < 3)
12
+ #
13
+ # @example Cycle detection
14
+ # atoms = [Atom.new(:<, :x, :y), Atom.new(:<, :y, :z), Atom.new(:<, :z, :x)]
15
+ # AtomUnsatSolver.unsat?(atoms) #=> true (cycle: x < y < z < x)
16
+ module AtomUnsatSolver
17
+ # Represents a constraint atom with operator and operands
18
+ # @!attribute [r] op
19
+ # @return [Symbol] comparison operator (:>, :<, :>=, :<=, :==)
20
+ # @!attribute [r] lhs
21
+ # @return [Object] left-hand side operand
22
+ # @!attribute [r] rhs
23
+ # @return [Object] right-hand side operand
24
+ Atom = Struct.new(:op, :lhs, :rhs)
25
+
26
+ # Represents a directed edge in the strict inequality graph
27
+ # @!attribute [r] from
28
+ # @return [Symbol] source vertex
29
+ # @!attribute [r] to
30
+ # @return [Symbol] target vertex
31
+ Edge = Struct.new(:from, :to)
32
+
33
+ module_function
34
+
35
+ # Main entry point: checks if the given constraint atoms are unsatisfiable
36
+ #
37
+ # @param atoms [Array<Atom>] constraint atoms to analyze
38
+ # @param debug [Boolean] enable debug output
39
+ # @return [Boolean] true if constraints are unsatisfiable
40
+ def unsat?(atoms, debug: false)
41
+ # Pass 1: Check numerical bound contradictions (symbol vs numeric)
42
+ return true if numerical_contradiction?(atoms, debug: debug)
43
+
44
+ # Pass 2: Check equality contradictions (same-type comparisons)
45
+ return true if equality_contradiction?(atoms, debug: debug)
46
+
47
+ # Pass 3: Check strict inequality cycles using stack-safe Kahn's algorithm
48
+ edges = build_strict_inequality_edges(atoms)
49
+ puts "edges: #{edges.map { |e| "#{e.from}→#{e.to}" }.join(', ')}" if debug
50
+
51
+ StrictInequalitySolver.cycle?(edges, debug: debug)
52
+ end
53
+
54
+ # Pass 1: Detects numerical bound contradictions using interval analysis
55
+ # Handles cases like x > 5 AND x < 3 (contradictory bounds)
56
+ # Also detects always-false comparisons like 100 < 100 or 5 > 5
57
+ #
58
+ # @param atoms [Array<Atom>] constraint atoms
59
+ # @param debug [Boolean] enable debug output
60
+ # @return [Boolean] true if numerical contradiction exists
61
+ def numerical_contradiction?(atoms, debug: false)
62
+ return true if always_false_constraints_exist?(atoms, debug)
63
+
64
+ check_bound_contradictions(atoms, debug)
65
+ end
66
+
67
+ def always_false_constraints_exist?(atoms, debug)
68
+ atoms.any? do |atom|
69
+ next false unless always_false_comparison?(atom)
70
+
71
+ puts "always-false comparison detected: #{atom.lhs} #{atom.op} #{atom.rhs}" if debug
72
+ true
73
+ end
74
+ end
75
+
76
+ def check_bound_contradictions(atoms, debug)
77
+ lowers = Hash.new(-Float::INFINITY)
78
+ uppers = Hash.new(Float::INFINITY)
79
+
80
+ atoms.each do |atom|
81
+ sym, num, op = extract_symbol_numeric_pair(atom)
82
+ next unless sym
83
+
84
+ update_bounds(lowers, uppers, sym, num, op)
85
+ end
86
+
87
+ contradiction = uppers.any? { |sym, hi| hi < lowers[sym] }
88
+ puts "numerical contradiction detected" if contradiction && debug
89
+ contradiction
90
+ end
91
+
92
+ # Pass 2: Detects equality contradictions using union-find equivalence classes
93
+ # Handles cases like x == y AND x > y (equality vs strict inequality)
94
+ #
95
+ # @param atoms [Array<Atom>] constraint atoms
96
+ # @param debug [Boolean] enable debug output
97
+ # @return [Boolean] true if equality contradiction exists
98
+ def equality_contradiction?(atoms, debug: false)
99
+ equal_pairs, strict_pairs = collect_equality_pairs(atoms)
100
+
101
+ return true if direct_equality_contradiction?(equal_pairs, strict_pairs, debug)
102
+
103
+ transitive_equality_contradiction?(equal_pairs, strict_pairs, debug)
104
+ end
105
+
106
+ # Extracts symbol-numeric pairs and normalizes operator direction
107
+ # @param atom [Atom] constraint atom
108
+ # @return [Array(Symbol, Numeric, Symbol)] normalized [symbol, number, operator] or [nil, nil, nil]
109
+ def extract_symbol_numeric_pair(atom)
110
+ if atom.lhs.is_a?(Symbol) && atom.rhs.is_a?(Numeric)
111
+ [atom.lhs, atom.rhs, atom.op]
112
+ elsif atom.rhs.is_a?(Symbol) && atom.lhs.is_a?(Numeric)
113
+ [atom.rhs, atom.lhs, flip_operator(atom.op)]
114
+ else
115
+ [nil, nil, nil]
116
+ end
117
+ end
118
+
119
+ # Updates variable bounds based on constraint
120
+ # @param lowers [Hash] lower bounds by symbol
121
+ # @param uppers [Hash] upper bounds by symbol
122
+ # @param sym [Symbol] variable symbol
123
+ # @param num [Numeric] constraint value
124
+ # @param operator [Symbol] constraint operator
125
+ def update_bounds(lowers, uppers, sym, num, operator)
126
+ case operator
127
+ when :> then lowers[sym] = [lowers[sym], num + 1].max # x > 5 means x >= 6
128
+ when :>= then lowers[sym] = [lowers[sym], num].max # x >= 5 means x >= 5
129
+ when :< then uppers[sym] = [uppers[sym], num - 1].min # x < 5 means x <= 4
130
+ when :<= then uppers[sym] = [uppers[sym], num].min # x <= 5 means x <= 5
131
+ end
132
+ end
133
+
134
+ # Flips comparison operator for normalization
135
+ # @param operator [Symbol] original operator
136
+ # @return [Symbol] flipped operator
137
+ def flip_operator(operator)
138
+ { :> => :<, :>= => :<=, :< => :>, :<= => :>= }[operator]
139
+ end
140
+
141
+ # Detects always-false comparisons like 5 > 5, 100 < 100, etc.
142
+ # These represent impossible conditions since they can never be true
143
+ # @param atom [Atom] constraint atom to check
144
+ # @return [Boolean] true if comparison is always false
145
+ def always_false_comparison?(atom)
146
+ return false unless atom.lhs.is_a?(Numeric) && atom.rhs.is_a?(Numeric)
147
+
148
+ lhs = atom.lhs
149
+ rhs = atom.rhs
150
+ case atom.op
151
+ when :> then lhs <= rhs # 5 > 5 is always false
152
+ when :< then lhs >= rhs # 5 < 5 is always false
153
+ when :>= then lhs < rhs # 5 >= 6 is always false
154
+ when :<= then lhs > rhs # 6 <= 5 is always false
155
+ when :== then lhs != rhs # 5 == 6 is always false
156
+ when :!= then lhs == rhs # 5 != 5 is always false
157
+ else false
158
+ end
159
+ end
160
+
161
+ # Builds directed edges for strict inequality cycle detection
162
+ # Only creates edges when both endpoints are symbols (variables)
163
+ # Filters out symbol-numeric pairs (handled by numerical_contradiction?)
164
+ #
165
+ # @param atoms [Array<Atom>] constraint atoms
166
+ # @return [Array<Edge>] directed edges for cycle detection
167
+ def build_strict_inequality_edges(atoms)
168
+ atoms.filter_map do |atom|
169
+ next unless atom.lhs.is_a?(Symbol) && atom.rhs.is_a?(Symbol)
170
+
171
+ case atom.op
172
+ when :> then Edge.new(atom.rhs, atom.lhs) # x > y ⇒ edge y → x
173
+ when :< then Edge.new(atom.lhs, atom.rhs) # x < y ⇒ edge x → y
174
+ end
175
+ end
176
+ end
177
+
178
+ # Collects equality and strict inequality pairs for same-type operands
179
+ # @param atoms [Array<Atom>] constraint atoms
180
+ # @return [Array(Set, Set)] [equality pairs, strict inequality pairs]
181
+ def collect_equality_pairs(atoms)
182
+ equal_pairs = Set.new
183
+ strict_pairs = Set.new
184
+
185
+ atoms.each do |atom|
186
+ next unless atom.lhs.instance_of?(atom.rhs.class)
187
+
188
+ pair = [atom.lhs, atom.rhs].sort
189
+ case atom.op
190
+ when :==
191
+ equal_pairs << pair
192
+ when :>, :<
193
+ strict_pairs << pair
194
+ end
195
+ end
196
+
197
+ [equal_pairs, strict_pairs]
198
+ end
199
+
200
+ # Checks for direct equality contradictions (x == y AND x > y)
201
+ # @param equal_pairs [Set] equality constraint pairs
202
+ # @param strict_pairs [Set] strict inequality pairs
203
+ # @param debug [Boolean] enable debug output
204
+ # @return [Boolean] true if direct contradiction exists
205
+ def direct_equality_contradiction?(equal_pairs, strict_pairs, debug)
206
+ conflicting_pairs = equal_pairs & strict_pairs
207
+ return false unless conflicting_pairs.any?
208
+
209
+ puts "equality contradiction detected" if debug
210
+ true
211
+ end
212
+
213
+ # Checks for transitive equality contradictions using union-find
214
+ # @param equal_pairs [Set] equality constraint pairs
215
+ # @param strict_pairs [Set] strict inequality pairs
216
+ # @param debug [Boolean] enable debug output
217
+ # @return [Boolean] true if transitive contradiction exists
218
+ def transitive_equality_contradiction?(equal_pairs, strict_pairs, debug)
219
+ equiv_classes = build_equivalence_classes(equal_pairs)
220
+ equiv_classes.each do |equiv_class|
221
+ equiv_class.combination(2).each do |var1, var2|
222
+ pair = [var1, var2].sort
223
+ next unless strict_pairs.include?(pair)
224
+
225
+ puts "transitive equality contradiction detected" if debug
226
+ return true
227
+ end
228
+ end
229
+ false
230
+ end
231
+
232
+ # Builds equivalence classes using union-find algorithm
233
+ # @param equal_pairs [Set] equality constraint pairs
234
+ # @return [Array<Array>] equivalence classes (groups of equal variables)
235
+ def build_equivalence_classes(equal_pairs)
236
+ parent = Hash.new { |h, k| h[k] = k }
237
+
238
+ equal_pairs.each do |pair|
239
+ root1 = find_root(pair[0], parent)
240
+ root2 = find_root(pair[1], parent)
241
+ parent[root1] = root2
242
+ end
243
+
244
+ group_variables_by_root(parent)
245
+ end
246
+
247
+ # Finds root of equivalence class with path compression
248
+ # @param element [Object] element to find root for
249
+ # @param parent [Hash] parent pointers for union-find
250
+ # @return [Object] root element of equivalence class
251
+ def find_root(element, parent)
252
+ return element if parent[element] == element
253
+
254
+ parent[element] = find_root(parent[element], parent)
255
+ parent[element]
256
+ end
257
+
258
+ # Groups variables by their equivalence class root
259
+ # @param parent [Hash] parent pointers for union-find
260
+ # @return [Array<Array>] equivalence classes with multiple elements
261
+ def group_variables_by_root(parent)
262
+ groups = Hash.new { |h, k| h[k] = [] }
263
+ parent.each_key do |var|
264
+ groups[find_root(var, parent)] << var
265
+ end
266
+ groups.values.select { |group| group.size > 1 }
267
+ end
268
+ end
269
+
270
+ # Stack-safe strict inequality cycle detector using Kahn's topological sort algorithm
271
+ #
272
+ # This module implements iterative cycle detection to avoid SystemStackError on deep graphs.
273
+ # Uses Kahn's algorithm: if topological sort cannot order all vertices, a cycle exists.
274
+ module StrictInequalitySolver
275
+ module_function
276
+
277
+ # Detects cycles in directed graph using stack-safe Kahn's topological sort
278
+ #
279
+ # @param edges [Array<Edge>] directed edges representing strict inequalities
280
+ # @param debug [Boolean] enable debug output
281
+ # @return [Boolean] true if cycle exists
282
+ def cycle?(edges, debug: false)
283
+ return false if edges.empty?
284
+
285
+ graph, in_degree = build_graph_with_degrees(edges)
286
+ processed_count = kahns_algorithm(graph, in_degree)
287
+
288
+ detect_cycle_from_processing_count(processed_count, graph.size, debug)
289
+ end
290
+
291
+ def kahns_algorithm(graph, in_degree)
292
+ queue = graph.keys.select { |v| in_degree[v].zero? }
293
+ processed_count = 0
294
+
295
+ until queue.empty?
296
+ vertex = queue.shift
297
+ processed_count += 1
298
+
299
+ graph[vertex].each do |neighbor|
300
+ in_degree[neighbor] -= 1
301
+ queue << neighbor if in_degree[neighbor].zero?
302
+ end
303
+ end
304
+
305
+ processed_count
306
+ end
307
+
308
+ def detect_cycle_from_processing_count(processed_count, total_vertices, debug)
309
+ has_cycle = processed_count < total_vertices
310
+ puts "cycle detected in strict inequality graph" if has_cycle && debug
311
+ has_cycle
312
+ end
313
+
314
+ # Builds adjacency list graph and in-degree counts from edges
315
+ # Pre-populates all vertices (including those with no outgoing edges) to avoid mutation during iteration
316
+ #
317
+ # @param edges [Array<Edge>] directed edges
318
+ # @return [Array(Hash, Hash)] [adjacency_list, in_degree_counts]
319
+ def build_graph_with_degrees(edges)
320
+ vertices = collect_all_vertices(edges)
321
+ graph, in_degree = initialize_graph_structures(vertices)
322
+ populate_graph_data(edges, graph, in_degree)
323
+ [graph, in_degree]
324
+ end
325
+
326
+ def collect_all_vertices(edges)
327
+ vertices = Set.new
328
+ edges.each { |e| vertices << e.from << e.to }
329
+ vertices
330
+ end
331
+
332
+ def initialize_graph_structures(vertices)
333
+ graph = Hash.new { |h, k| h[k] = [] }
334
+ in_degree = Hash.new(0)
335
+ vertices.each do |v|
336
+ graph[v]
337
+ in_degree[v] = 0
338
+ end
339
+ [graph, in_degree]
340
+ end
341
+
342
+ def populate_graph_data(edges, graph, in_degree)
343
+ edges.each do |edge|
344
+ graph[edge.from] << edge.to
345
+ in_degree[edge.to] += 1
346
+ end
347
+ end
348
+ end
349
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ class CompiledSchema
5
+ attr_reader :bindings
6
+
7
+ def initialize(bindings)
8
+ @bindings = bindings.freeze
9
+ end
10
+
11
+ def evaluate(ctx, *key_names)
12
+ target_keys = key_names.empty? ? @bindings.keys : validate_keys(key_names)
13
+
14
+ target_keys.each_with_object({}) do |key, result|
15
+ result[key] = evaluate_binding(key, ctx)
16
+ end
17
+ end
18
+
19
+ def evaluate_binding(key, ctx)
20
+ memo = ctx.instance_variable_get(:@__schema_cache__)
21
+ return memo[key] if memo&.key?(key)
22
+
23
+ value = @bindings[key][1].call(ctx)
24
+ memo[key] = value if memo
25
+ value
26
+ end
27
+
28
+ private
29
+
30
+ def hash_like?(obj)
31
+ obj.respond_to?(:key?) && obj.respond_to?(:[])
32
+ end
33
+
34
+ def validate_keys(keys)
35
+ unknown_keys = keys - @bindings.keys
36
+ return keys if unknown_keys.empty?
37
+
38
+ raise Kumi::Errors::RuntimeError, "No binding named #{unknown_keys.first}"
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ # Compiles an analyzed schema into executable lambdas
5
+ class Compiler
6
+ # ExprCompilers holds per-node compile implementations
7
+ module ExprCompilers
8
+ def compile_literal(expr)
9
+ v = expr.value
10
+ ->(_ctx) { v }
11
+ end
12
+
13
+ def compile_field_node(expr)
14
+ compile_field(expr)
15
+ end
16
+
17
+ def compile_binding_node(expr)
18
+ fn = @bindings[expr.name].last
19
+ ->(ctx) { fn.call(ctx) }
20
+ end
21
+
22
+ def compile_list(expr)
23
+ fns = expr.elements.map { |e| compile_expr(e) }
24
+ ->(ctx) { fns.map { |fn| fn.call(ctx) } }
25
+ end
26
+
27
+ def compile_call(expr)
28
+ fn_name = expr.fn_name
29
+ arg_fns = expr.args.map { |a| compile_expr(a) }
30
+ ->(ctx) { invoke_function(fn_name, arg_fns, ctx, expr.loc) }
31
+ end
32
+
33
+ def compile_cascade(expr)
34
+ pairs = expr.cases.map { |c| [compile_expr(c.condition), compile_expr(c.result)] }
35
+ lambda do |ctx|
36
+ pairs.each { |cond, res| return res.call(ctx) if cond.call(ctx) }
37
+ nil
38
+ end
39
+ end
40
+ end
41
+
42
+ include ExprCompilers
43
+
44
+ # Map node classes to compiler methods
45
+ DISPATCH = {
46
+ Syntax::TerminalExpressions::Literal => :compile_literal,
47
+ Syntax::TerminalExpressions::FieldRef => :compile_field_node,
48
+ Syntax::TerminalExpressions::Binding => :compile_binding_node,
49
+ Syntax::Expressions::ListExpression => :compile_list,
50
+ Syntax::Expressions::CallExpression => :compile_call,
51
+ Syntax::Expressions::CascadeExpression => :compile_cascade
52
+ }.freeze
53
+
54
+ def self.compile(schema, analyzer:)
55
+ new(schema, analyzer).compile
56
+ end
57
+
58
+ def initialize(schema, analyzer)
59
+ @schema = schema
60
+ @analysis = analyzer
61
+ @bindings = {}
62
+ end
63
+
64
+ def compile
65
+ build_index
66
+ @analysis.topo_order.each do |name|
67
+ decl = @index[name] or raise("Unknown binding #{name}")
68
+ compile_declaration(decl)
69
+ end
70
+
71
+ CompiledSchema.new(@bindings.freeze)
72
+ end
73
+
74
+ private
75
+
76
+ def build_index
77
+ @index = {}
78
+ @schema.attributes.each { |a| @index[a.name] = a }
79
+ @schema.traits.each { |t| @index[t.name] = t }
80
+ end
81
+
82
+ def compile_declaration(decl)
83
+ kind = decl.is_a?(Syntax::Declarations::Trait) ? :trait : :attr
84
+ fn = compile_expr(decl.expression)
85
+ @bindings[decl.name] = [kind, fn]
86
+ end
87
+
88
+ # Dispatch to the appropriate compile_* method
89
+ def compile_expr(expr)
90
+ method = DISPATCH.fetch(expr.class)
91
+ send(method, expr)
92
+ end
93
+
94
+ # Existing helpers unchanged
95
+ def compile_field(node)
96
+ name = node.name
97
+ loc = node.loc
98
+ lambda do |ctx|
99
+ return ctx[name] if ctx.respond_to?(:key?) && ctx.key?(name)
100
+
101
+ raise Errors::RuntimeError,
102
+ "Key '#{name}' not found at #{loc}. Available: #{ctx.respond_to?(:keys) ? ctx.keys.join(', ') : 'N/A'}"
103
+ end
104
+ end
105
+
106
+ def invoke_function(name, arg_fns, ctx, loc)
107
+ fn = FunctionRegistry.fetch(name)
108
+ values = arg_fns.map { |fn| fn.call(ctx) }
109
+ fn.call(*values)
110
+ rescue StandardError => e
111
+ # Preserve original error class and backtrace while adding context
112
+ enhanced_message = "Error calling fn(:#{name}) at #{loc}: #{e.message}"
113
+
114
+ if e.is_a?(Kumi::Errors::Error)
115
+ # Re-raise Kumi errors with enhanced message but preserve type
116
+ e.define_singleton_method(:message) { enhanced_message }
117
+ raise e
118
+ else
119
+ # For non-Kumi errors, wrap in RuntimeError but preserve original error info
120
+ runtime_error = Errors::RuntimeError.new(enhanced_message)
121
+ runtime_error.set_backtrace(e.backtrace)
122
+ runtime_error.define_singleton_method(:cause) { e }
123
+ raise runtime_error
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Domain
5
+ class EnumAnalyzer
6
+ def self.analyze(enum)
7
+ {
8
+ type: :enumeration,
9
+ values: enum,
10
+ size: enum.size,
11
+ sample_values: generate_samples(enum),
12
+ invalid_samples: generate_invalid_samples(enum)
13
+ }
14
+ end
15
+
16
+ def self.generate_samples(enum)
17
+ enum.sample([enum.size, 3].min)
18
+ end
19
+
20
+ def self.generate_invalid_samples(enum)
21
+ case enum.first
22
+ when String
23
+ generate_string_invalid_samples(enum)
24
+ when Integer
25
+ generate_integer_invalid_samples(enum)
26
+ when Symbol
27
+ generate_symbol_invalid_samples(enum)
28
+ else
29
+ generate_default_invalid_samples
30
+ end
31
+ end
32
+
33
+ private_class_method def self.generate_string_invalid_samples(enum)
34
+ candidates = ["invalid_string", "", "not_in_list"]
35
+ candidates.reject { |v| enum.include?(v) }
36
+ end
37
+
38
+ private_class_method def self.generate_integer_invalid_samples(enum)
39
+ candidates = [-999, 0, 999]
40
+ candidates.reject { |v| enum.include?(v) }
41
+ end
42
+
43
+ private_class_method def self.generate_symbol_invalid_samples(enum)
44
+ candidates = %i[invalid_symbol not_in_list]
45
+ candidates.reject { |v| enum.include?(v) }
46
+ end
47
+
48
+ private_class_method def self.generate_default_invalid_samples
49
+ [nil, "invalid", -1]
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Domain
5
+ class RangeAnalyzer
6
+ def self.analyze(range)
7
+ {
8
+ type: :range,
9
+ min: range.begin,
10
+ max: range.end,
11
+ exclusive_end: range.exclude_end?,
12
+ size: calculate_size(range),
13
+ sample_values: generate_samples(range),
14
+ boundary_values: [range.begin, range.end],
15
+ invalid_samples: generate_invalid_samples(range)
16
+ }
17
+ end
18
+
19
+ def self.calculate_size(range)
20
+ return :infinite if range.begin.nil? || range.end.nil?
21
+ return :large if range.end - range.begin > 1000
22
+
23
+ if integer_range?(range)
24
+ range.exclude_end? ? range.end - range.begin : range.end - range.begin + 1
25
+ else
26
+ :continuous
27
+ end
28
+ end
29
+
30
+ def self.generate_samples(range)
31
+ samples = [range.begin]
32
+
33
+ samples << calculate_midpoint(range) if numeric_range?(range)
34
+
35
+ samples << calculate_endpoint(range)
36
+ samples.uniq
37
+ end
38
+
39
+ def self.generate_invalid_samples(range)
40
+ invalid = []
41
+
42
+ invalid << calculate_before_start(range) if range.begin.is_a?(Numeric)
43
+
44
+ invalid << calculate_after_end(range) if range.end.is_a?(Numeric)
45
+
46
+ invalid
47
+ end
48
+
49
+ private_class_method def self.integer_range?(range)
50
+ range.begin.is_a?(Integer) && range.end.is_a?(Integer)
51
+ end
52
+
53
+ private_class_method def self.numeric_range?(range)
54
+ range.begin.is_a?(Numeric) && range.end.is_a?(Numeric)
55
+ end
56
+
57
+ private_class_method def self.calculate_midpoint(range)
58
+ mid = (range.begin + range.end) / 2.0
59
+ range.begin.is_a?(Integer) ? mid.round : mid
60
+ end
61
+
62
+ private_class_method def self.calculate_endpoint(range)
63
+ if range.exclude_end?
64
+ range.end - (range.begin.is_a?(Integer) ? 1 : 0.1)
65
+ else
66
+ range.end
67
+ end
68
+ end
69
+
70
+ private_class_method def self.calculate_before_start(range)
71
+ range.begin - (range.begin.is_a?(Integer) ? 1 : 0.1)
72
+ end
73
+
74
+ private_class_method def self.calculate_after_end(range)
75
+ if range.exclude_end?
76
+ range.end
77
+ else
78
+ range.end + (range.end.is_a?(Integer) ? 1 : 0.1)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end