kumi 0.0.4 → 0.0.6

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +160 -8
  3. data/README.md +278 -200
  4. data/{documents → docs}/AST.md +29 -29
  5. data/{documents → docs}/DSL.md +3 -3
  6. data/{documents → docs}/SYNTAX.md +107 -24
  7. data/docs/features/README.md +45 -0
  8. data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
  9. data/docs/features/analysis-type-inference.md +42 -0
  10. data/docs/features/analysis-unsat-detection.md +71 -0
  11. data/docs/features/array-broadcasting.md +170 -0
  12. data/docs/features/input-declaration-system.md +42 -0
  13. data/docs/features/performance.md +16 -0
  14. data/examples/federal_tax_calculator_2024.rb +43 -40
  15. data/examples/game_of_life.rb +97 -0
  16. data/examples/simple_rpg_game.rb +1000 -0
  17. data/examples/static_analysis_errors.rb +178 -0
  18. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
  19. data/lib/kumi/analyzer/analysis_state.rb +37 -0
  20. data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
  21. data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
  22. data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +8 -7
  23. data/lib/kumi/analyzer/passes/dependency_resolver.rb +106 -26
  24. data/lib/kumi/analyzer/passes/input_collector.rb +105 -23
  25. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  26. data/lib/kumi/analyzer/passes/pass_base.rb +11 -28
  27. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
  28. data/lib/kumi/analyzer/passes/toposorter.rb +45 -9
  29. data/lib/kumi/analyzer/passes/type_checker.rb +34 -11
  30. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
  31. data/lib/kumi/analyzer/passes/type_inferencer.rb +128 -21
  32. data/lib/kumi/analyzer/passes/unsat_detector.rb +312 -13
  33. data/lib/kumi/analyzer/passes/visitor_pass.rb +4 -3
  34. data/lib/kumi/analyzer.rb +41 -24
  35. data/lib/kumi/atom_unsat_solver.rb +45 -0
  36. data/lib/kumi/cli.rb +449 -0
  37. data/lib/kumi/compiler.rb +194 -16
  38. data/lib/kumi/constraint_relationship_solver.rb +638 -0
  39. data/lib/kumi/domain/validator.rb +0 -4
  40. data/lib/kumi/error_reporter.rb +6 -6
  41. data/lib/kumi/evaluation_wrapper.rb +20 -4
  42. data/lib/kumi/explain.rb +28 -28
  43. data/lib/kumi/export/node_registry.rb +26 -12
  44. data/lib/kumi/export/node_serializers.rb +1 -1
  45. data/lib/kumi/function_registry/collection_functions.rb +117 -9
  46. data/lib/kumi/function_registry/function_builder.rb +4 -3
  47. data/lib/kumi/function_registry.rb +8 -2
  48. data/lib/kumi/input/type_matcher.rb +3 -0
  49. data/lib/kumi/input/validator.rb +0 -3
  50. data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
  51. data/lib/kumi/parser/dsl_cascade_builder.rb +19 -8
  52. data/lib/kumi/parser/expression_converter.rb +80 -12
  53. data/lib/kumi/parser/input_builder.rb +40 -9
  54. data/lib/kumi/parser/input_field_proxy.rb +46 -0
  55. data/lib/kumi/parser/input_proxy.rb +3 -3
  56. data/lib/kumi/parser/nested_input.rb +15 -0
  57. data/lib/kumi/parser/parser.rb +2 -0
  58. data/lib/kumi/parser/schema_builder.rb +10 -9
  59. data/lib/kumi/parser/sugar.rb +171 -18
  60. data/lib/kumi/schema.rb +3 -1
  61. data/lib/kumi/schema_instance.rb +69 -3
  62. data/lib/kumi/syntax/array_expression.rb +15 -0
  63. data/lib/kumi/syntax/call_expression.rb +11 -0
  64. data/lib/kumi/syntax/cascade_expression.rb +11 -0
  65. data/lib/kumi/syntax/case_expression.rb +11 -0
  66. data/lib/kumi/syntax/declaration_reference.rb +11 -0
  67. data/lib/kumi/syntax/hash_expression.rb +11 -0
  68. data/lib/kumi/syntax/input_declaration.rb +12 -0
  69. data/lib/kumi/syntax/input_element_reference.rb +12 -0
  70. data/lib/kumi/syntax/input_reference.rb +12 -0
  71. data/lib/kumi/syntax/literal.rb +11 -0
  72. data/lib/kumi/syntax/root.rb +1 -0
  73. data/lib/kumi/syntax/trait_declaration.rb +11 -0
  74. data/lib/kumi/syntax/value_declaration.rb +11 -0
  75. data/lib/kumi/types/compatibility.rb +8 -0
  76. data/lib/kumi/types/validator.rb +1 -1
  77. data/lib/kumi/vectorization_metadata.rb +108 -0
  78. data/lib/kumi/version.rb +1 -1
  79. data/scripts/generate_function_docs.rb +22 -10
  80. metadata +38 -17
  81. data/CHANGELOG.md +0 -25
  82. data/lib/kumi/domain.rb +0 -8
  83. data/lib/kumi/input.rb +0 -8
  84. data/lib/kumi/syntax/declarations.rb +0 -23
  85. data/lib/kumi/syntax/expressions.rb +0 -30
  86. data/lib/kumi/syntax/terminal_expressions.rb +0 -27
  87. data/lib/kumi/syntax.rb +0 -9
  88. data/test_impossible_cascade.rb +0 -51
  89. /data/{documents → docs}/FUNCTIONS.md +0 -0
@@ -0,0 +1,638 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ # Enhanced constraint solver that can detect mathematical impossibilities
5
+ # across dependency chains by tracking variable relationships.
6
+ #
7
+ # This solver extends the basic AtomUnsatSolver by:
8
+ # 1. Building a graph of mathematical relationships between variables
9
+ # 2. Iteratively propagating constraints through multi-step dependency chains
10
+ # 3. Detecting contradictions that span multiple variables across complex relationships
11
+ #
12
+ # Capabilities:
13
+ # - Single-step relationships: y = x + 10, x == 50, y == 40 (impossible)
14
+ # - Multi-step chains: x -> y -> z -> w, with constraints on x and w
15
+ # - Identity relationships: y = x, handles both bindings and field references
16
+ # - Mathematical operations: add, subtract, multiply, divide
17
+ # - Forward and reverse constraint propagation
18
+ #
19
+ # @example Multi-step chain detection
20
+ # # Given: v1 = seed + 1, v2 = v1 + 2, v3 = v2 + 3, seed == 0, v3 == 10
21
+ # # The solver detects this is impossible since v3 must equal 6
22
+ module ConstraintRelationshipSolver
23
+ # Represents a mathematical relationship between variables
24
+ # @!attribute [r] target
25
+ # @return [Symbol] the dependent variable
26
+ # @!attribute [r] operation
27
+ # @return [Symbol] the mathematical operation (:add, :subtract, :multiply, :divide)
28
+ # @!attribute [r] operands
29
+ # @return [Array] the operands (variables or constants)
30
+ Relationship = Struct.new(:target, :operation, :operands)
31
+
32
+ # Represents a derived constraint from propagating through relationships
33
+ # @!attribute [r] variable
34
+ # @return [Symbol] the variable being constrained
35
+ # @!attribute [r] operation
36
+ # @return [Symbol] the constraint operation (:==, :>, :<, etc.)
37
+ # @!attribute [r] value
38
+ # @return [Object] the constraint value
39
+ # @!attribute [r] derivation_path
40
+ # @return [Array<Symbol>] the variables this constraint was derived through
41
+ DerivedConstraint = Struct.new(:variable, :operation, :value, :derivation_path)
42
+
43
+ module_function
44
+
45
+ # Enhanced unsatisfiability check that includes relationship analysis
46
+ #
47
+ # @param atoms [Array<Atom>] basic constraint atoms
48
+ # @param definitions [Hash] variable definitions for relationship building
49
+ # @param input_meta [Hash] input field metadata with domain constraints
50
+ # @param debug [Boolean] enable debug output
51
+ # @return [Boolean] true if constraints are unsatisfiable
52
+ def unsat?(atoms, definitions, input_meta: {}, debug: false)
53
+ # First run the standard unsat solver
54
+ return true if Kumi::AtomUnsatSolver.unsat?(atoms, debug: debug)
55
+
56
+ # Then check for relationship-based contradictions
57
+ relationships = build_relationships(definitions)
58
+ return false if relationships.empty?
59
+
60
+ # Check for range-based impossibilities
61
+ return true if range_violation?(atoms, relationships, input_meta, debug: debug)
62
+
63
+ # Propagate constraints through relationships
64
+ derived_constraints = propagate_constraints(atoms, relationships, debug: debug)
65
+
66
+ # Check if any derived constraints violate domain constraints
67
+ return true if domain_constraint_violation?(derived_constraints, input_meta, debug: debug)
68
+
69
+ # Check if any derived constraints create contradictions
70
+ all_constraints = atoms + derived_constraints.map do |dc|
71
+ Kumi::AtomUnsatSolver::Atom.new(dc.operation, dc.variable, dc.value)
72
+ end
73
+
74
+ Kumi::AtomUnsatSolver.unsat?(all_constraints, debug: debug)
75
+ end
76
+
77
+ # Builds mathematical relationships from variable definitions
78
+ #
79
+ # @param definitions [Hash] variable name to AST node mapping
80
+ # @return [Array<Relationship>] mathematical relationships between variables
81
+ def build_relationships(definitions)
82
+ relationships = []
83
+
84
+ definitions.each do |var_name, definition|
85
+ next unless definition&.expression
86
+
87
+ relationship = extract_relationship(var_name, definition.expression)
88
+ relationships << relationship if relationship
89
+ end
90
+
91
+ relationships
92
+ end
93
+
94
+ # Extracts mathematical relationship from an AST expression
95
+ #
96
+ # @param target [Symbol] the variable being defined
97
+ # @param expression [Object] the AST expression defining the variable
98
+ # @return [Relationship, nil] the relationship or nil if not extractable
99
+ def extract_relationship(target, expression)
100
+ case expression
101
+ when Kumi::Syntax::CallExpression
102
+ extract_call_relationship(target, expression)
103
+ when Kumi::Syntax::DeclarationReference
104
+ # Simple alias: target = other_variable
105
+ Relationship.new(target, :identity, [expression.name])
106
+ when Kumi::Syntax::InputReference
107
+ # Direct field reference: target = input.field
108
+ # Create identity relationship so we can propagate constraints
109
+ Relationship.new(target, :identity, [expression.name])
110
+ end
111
+ end
112
+
113
+ # Extracts relationship from a function call expression
114
+ #
115
+ # @param target [Symbol] the variable being defined
116
+ # @param call_expr [CallExpression] the function call expression
117
+ # @return [Relationship, nil] the relationship or nil if not supported
118
+ def extract_call_relationship(target, call_expr)
119
+ case call_expr.fn_name
120
+ when :add
121
+ operands = extract_operands(call_expr.args)
122
+ return nil unless operands
123
+
124
+ Relationship.new(target, :add, operands)
125
+ when :subtract
126
+ operands = extract_operands(call_expr.args)
127
+ return nil unless operands
128
+
129
+ Relationship.new(target, :subtract, operands)
130
+ when :multiply
131
+ operands = extract_operands(call_expr.args)
132
+ return nil unless operands
133
+
134
+ Relationship.new(target, :multiply, operands)
135
+ when :divide
136
+ operands = extract_operands(call_expr.args)
137
+ return nil unless operands
138
+
139
+ Relationship.new(target, :divide, operands)
140
+ else
141
+ # Unsupported operation for relationship extraction
142
+ nil
143
+ end
144
+ end
145
+
146
+ # Extracts operands from function arguments
147
+ #
148
+ # @param args [Array] function call arguments
149
+ # @return [Array, nil] operands (variables as symbols, constants as values) or nil if not extractable
150
+ def extract_operands(args)
151
+ return nil if args.empty?
152
+
153
+ args.map do |arg|
154
+ case arg
155
+ when Kumi::Syntax::DeclarationReference
156
+ arg.name
157
+ when Kumi::Syntax::Literal
158
+ arg.value
159
+ when Kumi::Syntax::InputReference
160
+ # Use the field name directly to match how atoms represent input fields
161
+ arg.name
162
+ else
163
+ # Unknown operand type
164
+ return nil
165
+ end
166
+ end
167
+ end
168
+
169
+ # Propagates constraints through mathematical relationships to derive new constraints
170
+ # Uses iterative propagation to handle multi-step dependency chains
171
+ #
172
+ # @param atoms [Array<Atom>] original constraint atoms
173
+ # @param relationships [Array<Relationship>] mathematical relationships
174
+ # @param debug [Boolean] enable debug output
175
+ # @return [Array<DerivedConstraint>] derived constraints
176
+ def propagate_constraints(atoms, relationships, debug: false)
177
+ all_derived_constraints = []
178
+ current_atoms = atoms.dup
179
+ max_iterations = relationships.size + 1 # Prevent infinite loops
180
+ iteration = 0
181
+
182
+ loop do
183
+ iteration += 1
184
+ constraint_map = build_constraint_map(current_atoms)
185
+ round_derived = []
186
+
187
+ # Forward propagation: from operand constraints to target constraints
188
+ relationships.each do |rel|
189
+ derived = derive_constraints_for_relationship(rel, constraint_map, debug: debug)
190
+ round_derived.concat(derived)
191
+
192
+ # Reverse propagation: from target constraints to operand constraints
193
+ derived = reverse_derive_constraints(rel, constraint_map, debug: debug)
194
+ round_derived.concat(derived)
195
+ end
196
+
197
+ # Check if we derived any new constraints this round
198
+ new_constraints = round_derived.reject do |dc|
199
+ # Check if this constraint already exists in current_atoms or all_derived_constraints
200
+ existing_atom = current_atoms.find do |atom|
201
+ atom.lhs == dc.variable && atom.op == dc.operation && atom.rhs == dc.value
202
+ end
203
+ existing_derived = all_derived_constraints.find do |existing_dc|
204
+ existing_dc.variable == dc.variable &&
205
+ existing_dc.operation == dc.operation &&
206
+ existing_dc.value == dc.value
207
+ end
208
+ existing_atom || existing_derived
209
+ end
210
+
211
+ break if new_constraints.empty? || iteration > max_iterations
212
+
213
+ puts "Iteration #{iteration}: derived #{new_constraints.size} new constraints" if debug
214
+
215
+ # Add new constraints to our working set for next iteration
216
+ new_atoms = new_constraints.map do |dc|
217
+ Kumi::AtomUnsatSolver::Atom.new(dc.operation, dc.variable, dc.value)
218
+ end
219
+ current_atoms.concat(new_atoms)
220
+ all_derived_constraints.concat(new_constraints)
221
+ end
222
+
223
+ puts "Total derived #{all_derived_constraints.size} constraints in #{iteration} iterations" if debug
224
+ all_derived_constraints
225
+ end
226
+
227
+ # Builds a map from variables to their constraints
228
+ #
229
+ # @param atoms [Array<Atom>] constraint atoms
230
+ # @return [Hash] variable name to array of constraints
231
+ def build_constraint_map(atoms)
232
+ constraint_map = Hash.new { |h, k| h[k] = [] }
233
+
234
+ atoms.each do |atom|
235
+ constraint_map[atom.lhs] << atom if atom.lhs.is_a?(Symbol)
236
+ end
237
+
238
+ constraint_map
239
+ end
240
+
241
+ # Derives constraints on target variable from operand constraints
242
+ #
243
+ # @param relationship [Relationship] the mathematical relationship
244
+ # @param constraint_map [Hash] variable to constraints mapping
245
+ # @param debug [Boolean] enable debug output
246
+ # @return [Array<DerivedConstraint>] derived constraints on target
247
+ def derive_constraints_for_relationship(relationship, constraint_map, debug: false)
248
+ derived = []
249
+
250
+ # Handle different operand patterns
251
+ if relationship.operands.size == 2
252
+ # Case 1: One variable and one constant (e.g., x + 5)
253
+ var_operand = relationship.operands.find { |op| op.is_a?(Symbol) }
254
+ const_operand = relationship.operands.find { |op| op.is_a?(Numeric) }
255
+
256
+ if var_operand && const_operand && constraint_map[var_operand].any?
257
+ constraint_map[var_operand].each do |constraint|
258
+ next unless constraint.op == :==
259
+
260
+ derived_value = apply_operation(relationship.operation, constraint.rhs, const_operand, var_operand == relationship.operands[0])
261
+ next unless derived_value
262
+
263
+ derived << DerivedConstraint.new(
264
+ relationship.target,
265
+ :==,
266
+ derived_value,
267
+ [var_operand]
268
+ )
269
+ puts "Derived: #{relationship.target} == #{derived_value} (from #{var_operand} == #{constraint.rhs})" if debug
270
+ end
271
+ end
272
+
273
+ # Case 2: Two variables (e.g., x + y) - handle when one has an equality constraint
274
+ var1 = relationship.operands[0] if relationship.operands[0].is_a?(Symbol)
275
+ var2 = relationship.operands[1] if relationship.operands[1].is_a?(Symbol)
276
+
277
+ if var1 && var2 && var1 != var2
278
+ # If we have constraints on var1, try to derive constraints involving var2
279
+ constraint_map[var1].each do |constraint|
280
+ next unless constraint.op == :==
281
+
282
+ # For now, only handle addition with two variables: target = var1 + var2
283
+ # If var1 == value, then target == value + var2
284
+ next unless relationship.operation == :add && constraint_map[var2].any?
285
+
286
+ constraint_map[var2].each do |var2_constraint|
287
+ next unless var2_constraint.op == :==
288
+
289
+ derived_value = constraint.rhs + var2_constraint.rhs
290
+ derived << DerivedConstraint.new(
291
+ relationship.target,
292
+ :==,
293
+ derived_value,
294
+ [var1, var2]
295
+ )
296
+ if debug
297
+ puts "Derived: #{relationship.target} == #{derived_value} (from #{var1} == #{constraint.rhs} and #{var2} == #{var2_constraint.rhs})"
298
+ end
299
+ end
300
+ end
301
+ end
302
+ elsif relationship.operands.size == 1
303
+ # Case 3: Identity relationship (target = operand)
304
+ operand = relationship.operands[0]
305
+ if operand.is_a?(Symbol) && constraint_map[operand].any?
306
+ constraint_map[operand].each do |constraint|
307
+ next unless constraint.op == :==
308
+
309
+ derived << DerivedConstraint.new(
310
+ relationship.target,
311
+ :==,
312
+ constraint.rhs,
313
+ [operand]
314
+ )
315
+ puts "Derived: #{relationship.target} == #{constraint.rhs} (identity from #{operand})" if debug
316
+ end
317
+ end
318
+ end
319
+
320
+ derived
321
+ end
322
+
323
+ # Derives constraints on operand variables from target constraints (reverse propagation)
324
+ #
325
+ # @param relationship [Relationship] the mathematical relationship
326
+ # @param constraint_map [Hash] variable to constraints mapping
327
+ # @param debug [Boolean] enable debug output
328
+ # @return [Array<DerivedConstraint>] derived constraints on operands
329
+ def reverse_derive_constraints(relationship, constraint_map, debug: false)
330
+ derived = []
331
+
332
+ # For simple operations with one variable and one constant
333
+ if relationship.operands.size == 2
334
+ var_operand = relationship.operands.find { |op| op.is_a?(Symbol) }
335
+ const_operand = relationship.operands.find { |op| op.is_a?(Numeric) }
336
+
337
+ if var_operand && const_operand && constraint_map[relationship.target].any?
338
+ constraint_map[relationship.target].each do |constraint|
339
+ next unless constraint.op == :==
340
+
341
+ derived_value = reverse_operation(relationship.operation, constraint.rhs, const_operand,
342
+ var_operand == relationship.operands[0])
343
+ next unless derived_value
344
+
345
+ derived << DerivedConstraint.new(
346
+ var_operand,
347
+ :==,
348
+ derived_value,
349
+ [relationship.target]
350
+ )
351
+ puts "Reverse derived: #{var_operand} == #{derived_value} (from #{relationship.target} == #{constraint.rhs})" if debug
352
+ end
353
+ end
354
+ elsif relationship.operands.size == 1 && relationship.operation == :identity
355
+ # Handle reverse propagation for identity relationships
356
+ operand = relationship.operands[0]
357
+ if operand.is_a?(Symbol) && constraint_map[relationship.target].any?
358
+ constraint_map[relationship.target].each do |constraint|
359
+ next unless constraint.op == :==
360
+
361
+ derived << DerivedConstraint.new(
362
+ operand,
363
+ :==,
364
+ constraint.rhs,
365
+ [relationship.target]
366
+ )
367
+ puts "Reverse derived: #{operand} == #{constraint.rhs} (identity from #{relationship.target})" if debug
368
+ end
369
+ end
370
+ end
371
+
372
+ derived
373
+ end
374
+
375
+ # Applies mathematical operation to derive target value
376
+ #
377
+ # @param operation [Symbol] the operation (:add, :subtract, :multiply, :divide)
378
+ # @param var_value [Numeric] the value of the variable operand
379
+ # @param const_value [Numeric] the constant operand value
380
+ # @param var_is_first [Boolean] whether variable is first operand
381
+ # @return [Numeric, nil] the derived value or nil if not computable
382
+ def apply_operation(operation, var_value, const_value, var_is_first)
383
+ case operation
384
+ when :add
385
+ var_value + const_value
386
+ when :subtract
387
+ var_is_first ? var_value - const_value : const_value - var_value
388
+ when :multiply
389
+ var_value * const_value
390
+ when :divide
391
+ return nil if const_value.zero?
392
+
393
+ var_is_first ? var_value / const_value : const_value / var_value
394
+ end
395
+ end
396
+
397
+ # Applies reverse mathematical operation to derive operand value
398
+ #
399
+ # @param operation [Symbol] the operation (:add, :subtract, :multiply, :divide)
400
+ # @param target_value [Numeric] the value of the target variable
401
+ # @param const_value [Numeric] the constant operand value
402
+ # @param var_is_first [Boolean] whether variable is first operand
403
+ # @return [Numeric, nil] the derived operand value or nil if not computable
404
+ def reverse_operation(operation, target_value, const_value, var_is_first)
405
+ case operation
406
+ when :add
407
+ target_value - const_value
408
+ when :subtract
409
+ var_is_first ? target_value + const_value : const_value - target_value
410
+ when :multiply
411
+ return nil if const_value.zero?
412
+
413
+ target_value / const_value
414
+ when :divide
415
+ var_is_first ? target_value * const_value : const_value / target_value
416
+ end
417
+ end
418
+
419
+ # Checks for range violations through mathematical operations
420
+ #
421
+ # @param atoms [Array<Atom>] basic constraint atoms
422
+ # @param relationships [Array<Relationship>] mathematical relationships
423
+ # @param input_meta [Hash] input field metadata with domain constraints
424
+ # @param debug [Boolean] enable debug output
425
+ # @return [Boolean] true if range violation exists
426
+ def range_violation?(atoms, relationships, input_meta, debug: false)
427
+ return false if input_meta.empty?
428
+
429
+ # Build range map for all variables
430
+ range_map = build_range_map(relationships, input_meta, debug: debug)
431
+ return false if range_map.empty?
432
+
433
+ # Check if any constraint violates computed ranges
434
+ atoms.each do |atom|
435
+ next unless atom.lhs.is_a?(Symbol) && atom.rhs.is_a?(Numeric)
436
+ next unless %i[> < >= <=].include?(atom.op)
437
+
438
+ range = range_map[atom.lhs]
439
+ next unless range
440
+
441
+ if range_constraint_impossible?(range, atom.op, atom.rhs)
442
+ puts "Range violation: #{atom.lhs} #{atom.op} #{atom.rhs} impossible with range #{range}" if debug
443
+ return true
444
+ end
445
+ end
446
+
447
+ false
448
+ end
449
+
450
+ # Builds a map of variable ranges based on mathematical relationships and input domains
451
+ #
452
+ # @param relationships [Array<Relationship>] mathematical relationships
453
+ # @param input_meta [Hash] input field metadata with domain constraints
454
+ # @param debug [Boolean] enable debug output
455
+ # @return [Hash] variable name to range mapping
456
+ def build_range_map(relationships, input_meta, debug: false)
457
+ range_map = {}
458
+
459
+ # Start with input field ranges
460
+ input_meta.each do |field_name, meta|
461
+ domain = meta[:domain]
462
+ next unless domain.is_a?(Range) && domain.first.is_a?(Numeric) && domain.last.is_a?(Numeric)
463
+
464
+ range_map[field_name] = domain
465
+ puts "Input range: #{field_name} -> #{domain}" if debug
466
+ end
467
+
468
+ # Propagate ranges through mathematical relationships
469
+ # Use iterative approach to handle chains
470
+ max_iterations = relationships.size + 1
471
+ iteration = 0
472
+
473
+ loop do
474
+ iteration += 1
475
+ new_ranges = {}
476
+
477
+ relationships.each do |rel|
478
+ range = compute_range_for_relationship(rel, range_map)
479
+ next unless range
480
+
481
+ if range_map[rel.target] != range
482
+ new_ranges[rel.target] = range
483
+ puts "Computed range: #{rel.target} -> #{range} (from #{rel.operation})" if debug
484
+ end
485
+ end
486
+
487
+ break if new_ranges.empty? || iteration > max_iterations
488
+
489
+ range_map.merge!(new_ranges)
490
+ end
491
+
492
+ range_map
493
+ end
494
+
495
+ # Computes the range for a variable based on its mathematical relationship
496
+ #
497
+ # @param relationship [Relationship] the mathematical relationship
498
+ # @param range_map [Hash] current variable to range mapping
499
+ # @return [Range, nil] computed range or nil if not computable
500
+ def compute_range_for_relationship(relationship, range_map)
501
+ # Handle identity relationships (simple variable copy)
502
+ if relationship.operation == :identity && relationship.operands.size == 1
503
+ var_operand = relationship.operands.first
504
+ return range_map[var_operand] if range_map.key?(var_operand)
505
+
506
+ return nil
507
+ end
508
+
509
+ return nil unless relationship.operands.size == 2
510
+
511
+ # Handle variable + constant pattern
512
+ var_operand = relationship.operands.find { |op| op.is_a?(Symbol) }
513
+ const_operand = relationship.operands.find { |op| op.is_a?(Numeric) }
514
+
515
+ return nil unless var_operand && const_operand
516
+
517
+ var_range = range_map[var_operand]
518
+ return nil unless var_range
519
+
520
+ transform_range(var_range, relationship.operation, const_operand, var_operand == relationship.operands[0])
521
+ end
522
+
523
+ # Transforms a range through a mathematical operation
524
+ #
525
+ # @param range [Range] the input range
526
+ # @param operation [Symbol] the mathematical operation
527
+ # @param constant [Numeric] the constant operand
528
+ # @param var_is_first [Boolean] whether variable is first operand
529
+ # @return [Range, nil] transformed range or nil if not computable
530
+ def transform_range(range, operation, constant, var_is_first)
531
+ min_val = range.first
532
+ max_val = range.last
533
+
534
+ case operation
535
+ when :add
536
+ (min_val + constant)..(max_val + constant)
537
+ when :subtract
538
+ if var_is_first
539
+ (min_val - constant)..(max_val - constant)
540
+ else
541
+ (constant - max_val)..(constant - min_val)
542
+ end
543
+ when :multiply
544
+ if constant >= 0
545
+ (min_val * constant)..(max_val * constant)
546
+ else
547
+ (max_val * constant)..(min_val * constant)
548
+ end
549
+ when :divide
550
+ return nil if constant.zero?
551
+
552
+ if var_is_first
553
+ if constant.positive?
554
+ (min_val / constant)..(max_val / constant)
555
+ else
556
+ (max_val / constant)..(min_val / constant)
557
+ end
558
+ else
559
+ # constant / range - more complex, skip for now
560
+ nil
561
+ end
562
+ end
563
+ end
564
+
565
+ # Checks if a constraint is impossible given a variable's range
566
+ #
567
+ # @param range [Range] the variable's possible range
568
+ # @param operator [Symbol] the constraint operator
569
+ # @param value [Numeric] the constraint value
570
+ # @return [Boolean] true if constraint is impossible
571
+ def range_constraint_impossible?(range, operator, value)
572
+ case operator
573
+ when :>
574
+ # x > value is impossible if max(x) <= value
575
+ range.last <= value
576
+ when :>=
577
+ # x >= value is impossible if max(x) < value
578
+ range.last < value
579
+ when :<
580
+ # x < value is impossible if min(x) >= value
581
+ range.first >= value
582
+ when :<=
583
+ # x <= value is impossible if min(x) > value
584
+ range.first > value
585
+ else
586
+ false
587
+ end
588
+ end
589
+
590
+ # Checks if any derived constraints violate input field domain constraints
591
+ #
592
+ # @param derived_constraints [Array<DerivedConstraint>] constraints derived from relationships
593
+ # @param input_meta [Hash] input field metadata with domain constraints
594
+ # @param debug [Boolean] enable debug output
595
+ # @return [Boolean] true if domain constraint violation exists
596
+ def domain_constraint_violation?(derived_constraints, input_meta, debug: false)
597
+ return false if input_meta.empty?
598
+
599
+ derived_constraints.each do |constraint|
600
+ # Only check equality constraints for now
601
+ next unless constraint.operation == :==
602
+
603
+ # Check if this variable corresponds to an input field with domain constraints
604
+ field_meta = input_meta[constraint.variable]
605
+ next unless field_meta&.dig(:domain)
606
+
607
+ domain = field_meta[:domain]
608
+ value = constraint.value
609
+
610
+ if violates_domain?(value, domain)
611
+ puts "Domain violation detected: #{constraint.variable} == #{value} violates domain #{domain.inspect}" if debug
612
+ return true
613
+ end
614
+ end
615
+
616
+ false
617
+ end
618
+
619
+ # Checks if a value violates a domain constraint
620
+ #
621
+ # @param value [Object] the value to check
622
+ # @param domain [Range, Array, Proc] the domain constraint
623
+ # @return [Boolean] true if value violates domain
624
+ def violates_domain?(value, domain)
625
+ case domain
626
+ when Range
627
+ !domain.include?(value)
628
+ when Array
629
+ !domain.include?(value)
630
+ when Proc
631
+ # For Proc domains, we can't statically analyze
632
+ false
633
+ else
634
+ false
635
+ end
636
+ end
637
+ end
638
+ end
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "range_analyzer"
4
- require_relative "enum_analyzer"
5
- require_relative "violation_formatter"
6
-
7
3
  module Kumi
8
4
  module Domain
9
5
  class Validator
@@ -17,7 +17,7 @@ module Kumi
17
17
  "#{location_str}: #{message}"
18
18
  end
19
19
 
20
- def has_location?
20
+ def location?
21
21
  location && !location.is_a?(Symbol)
22
22
  end
23
23
 
@@ -32,7 +32,7 @@ module Kumi
32
32
  when Syntax::Location
33
33
  "at #{loc.file}:#{loc.line}:#{loc.column}"
34
34
  else
35
- "at #{loc}"
35
+ "at <unknown>"
36
36
  end
37
37
  end
38
38
  end
@@ -102,7 +102,7 @@ module Kumi
102
102
  # @param errors [Array<ErrorEntry>] Array of error entries
103
103
  # @return [Array<ErrorEntry>] Errors without location info
104
104
  def missing_location_errors(errors)
105
- errors.reject(&:has_location?)
105
+ errors.reject(&:location?)
106
106
  end
107
107
 
108
108
  # Enhanced error reporting with suggestions and context
@@ -129,8 +129,8 @@ module Kumi
129
129
  def validate_error_locations(errors, expected_with_location: %i[syntax semantic type])
130
130
  report = {
131
131
  total_errors: errors.size,
132
- errors_with_location: errors.count(&:has_location?),
133
- errors_without_location: errors.reject(&:has_location?),
132
+ errors_with_location: errors.count(&:location?),
133
+ errors_without_location: errors.reject(&:location?),
134
134
  location_coverage: 0.0
135
135
  }
136
136
 
@@ -138,7 +138,7 @@ module Kumi
138
138
 
139
139
  # Check specific types that should have locations
140
140
  report[:problematic_errors] = errors.select do |error|
141
- expected_with_location.include?(error.type) && !error.has_location?
141
+ expected_with_location.include?(error.type) && !error.location?
142
142
  end
143
143
 
144
144
  report