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.
- checksums.yaml +4 -4
- data/CLAUDE.md +160 -8
- data/README.md +278 -200
- data/{documents → docs}/AST.md +29 -29
- data/{documents → docs}/DSL.md +3 -3
- data/{documents → docs}/SYNTAX.md +107 -24
- data/docs/features/README.md +45 -0
- data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
- data/docs/features/analysis-type-inference.md +42 -0
- data/docs/features/analysis-unsat-detection.md +71 -0
- data/docs/features/array-broadcasting.md +170 -0
- data/docs/features/input-declaration-system.md +42 -0
- data/docs/features/performance.md +16 -0
- data/examples/federal_tax_calculator_2024.rb +43 -40
- data/examples/game_of_life.rb +97 -0
- data/examples/simple_rpg_game.rb +1000 -0
- data/examples/static_analysis_errors.rb +178 -0
- data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
- data/lib/kumi/analyzer/analysis_state.rb +37 -0
- data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
- data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
- data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +8 -7
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +106 -26
- data/lib/kumi/analyzer/passes/input_collector.rb +105 -23
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/pass_base.rb +11 -28
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
- data/lib/kumi/analyzer/passes/toposorter.rb +45 -9
- data/lib/kumi/analyzer/passes/type_checker.rb +34 -11
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
- data/lib/kumi/analyzer/passes/type_inferencer.rb +128 -21
- data/lib/kumi/analyzer/passes/unsat_detector.rb +312 -13
- data/lib/kumi/analyzer/passes/visitor_pass.rb +4 -3
- data/lib/kumi/analyzer.rb +41 -24
- data/lib/kumi/atom_unsat_solver.rb +45 -0
- data/lib/kumi/cli.rb +449 -0
- data/lib/kumi/compiler.rb +194 -16
- data/lib/kumi/constraint_relationship_solver.rb +638 -0
- data/lib/kumi/domain/validator.rb +0 -4
- data/lib/kumi/error_reporter.rb +6 -6
- data/lib/kumi/evaluation_wrapper.rb +20 -4
- data/lib/kumi/explain.rb +28 -28
- data/lib/kumi/export/node_registry.rb +26 -12
- data/lib/kumi/export/node_serializers.rb +1 -1
- data/lib/kumi/function_registry/collection_functions.rb +117 -9
- data/lib/kumi/function_registry/function_builder.rb +4 -3
- data/lib/kumi/function_registry.rb +8 -2
- data/lib/kumi/input/type_matcher.rb +3 -0
- data/lib/kumi/input/validator.rb +0 -3
- data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
- data/lib/kumi/parser/dsl_cascade_builder.rb +19 -8
- data/lib/kumi/parser/expression_converter.rb +80 -12
- data/lib/kumi/parser/input_builder.rb +40 -9
- data/lib/kumi/parser/input_field_proxy.rb +46 -0
- data/lib/kumi/parser/input_proxy.rb +3 -3
- data/lib/kumi/parser/nested_input.rb +15 -0
- data/lib/kumi/parser/parser.rb +2 -0
- data/lib/kumi/parser/schema_builder.rb +10 -9
- data/lib/kumi/parser/sugar.rb +171 -18
- data/lib/kumi/schema.rb +3 -1
- data/lib/kumi/schema_instance.rb +69 -3
- data/lib/kumi/syntax/array_expression.rb +15 -0
- data/lib/kumi/syntax/call_expression.rb +11 -0
- data/lib/kumi/syntax/cascade_expression.rb +11 -0
- data/lib/kumi/syntax/case_expression.rb +11 -0
- data/lib/kumi/syntax/declaration_reference.rb +11 -0
- data/lib/kumi/syntax/hash_expression.rb +11 -0
- data/lib/kumi/syntax/input_declaration.rb +12 -0
- data/lib/kumi/syntax/input_element_reference.rb +12 -0
- data/lib/kumi/syntax/input_reference.rb +12 -0
- data/lib/kumi/syntax/literal.rb +11 -0
- data/lib/kumi/syntax/root.rb +1 -0
- data/lib/kumi/syntax/trait_declaration.rb +11 -0
- data/lib/kumi/syntax/value_declaration.rb +11 -0
- data/lib/kumi/types/compatibility.rb +8 -0
- data/lib/kumi/types/validator.rb +1 -1
- data/lib/kumi/vectorization_metadata.rb +108 -0
- data/lib/kumi/version.rb +1 -1
- data/scripts/generate_function_docs.rb +22 -10
- metadata +38 -17
- data/CHANGELOG.md +0 -25
- data/lib/kumi/domain.rb +0 -8
- data/lib/kumi/input.rb +0 -8
- data/lib/kumi/syntax/declarations.rb +0 -23
- data/lib/kumi/syntax/expressions.rb +0 -30
- data/lib/kumi/syntax/terminal_expressions.rb +0 -27
- data/lib/kumi/syntax.rb +0 -9
- data/test_impossible_cascade.rb +0 -51
- /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
|
data/lib/kumi/error_reporter.rb
CHANGED
@@ -17,7 +17,7 @@ module Kumi
|
|
17
17
|
"#{location_str}: #{message}"
|
18
18
|
end
|
19
19
|
|
20
|
-
def
|
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
|
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(&:
|
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(&:
|
133
|
-
errors_without_location: errors.reject(&:
|
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.
|
141
|
+
expected_with_location.include?(error.type) && !error.location?
|
142
142
|
end
|
143
143
|
|
144
144
|
report
|