kumi 0.0.16 → 0.0.18
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/CHANGELOG.md +9 -0
- data/golden/cascade_logic/schema.kumi +3 -1
- data/lib/kumi/analyzer.rb +8 -11
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +0 -81
- data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +0 -36
- data/lib/kumi/core/analyzer/passes/toposorter.rb +1 -36
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +8 -191
- data/lib/kumi/core/compiler/access_builder.rb +5 -8
- data/lib/kumi/version.rb +1 -1
- metadata +2 -25
- data/BACKLOG.md +0 -34
- data/config/functions.yaml +0 -352
- data/docs/functions/analyzer_integration.md +0 -199
- data/docs/functions/signatures.md +0 -171
- data/examples/hash_objects_demo.rb +0 -138
- data/lib/kumi/core/analyzer/passes/function_signature_pass.rb +0 -199
- data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +0 -48
- data/lib/kumi/core/functions/dimension.rb +0 -98
- data/lib/kumi/core/functions/dtypes.rb +0 -20
- data/lib/kumi/core/functions/errors.rb +0 -11
- data/lib/kumi/core/functions/kernel_adapter.rb +0 -45
- data/lib/kumi/core/functions/loader.rb +0 -119
- data/lib/kumi/core/functions/registry_v2.rb +0 -68
- data/lib/kumi/core/functions/shape.rb +0 -70
- data/lib/kumi/core/functions/signature.rb +0 -122
- data/lib/kumi/core/functions/signature_parser.rb +0 -86
- data/lib/kumi/core/functions/signature_resolver.rb +0 -272
- data/lib/kumi/kernels/ruby/aggregate_core.rb +0 -105
- data/lib/kumi/kernels/ruby/datetime_scalar.rb +0 -21
- data/lib/kumi/kernels/ruby/mask_scalar.rb +0 -15
- data/lib/kumi/kernels/ruby/scalar_core.rb +0 -63
- data/lib/kumi/kernels/ruby/string_scalar.rb +0 -19
- data/lib/kumi/kernels/ruby/vector_struct.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f48b3c1d3dc46f7725b1f138fd4eafd2f1c197ed9bdcc0465a26206ed0b3ec56
|
4
|
+
data.tar.gz: 758a5ed295dda7ab9ca97f8caa888b502c0485abfb80ad13ccf6e0a0c03a716e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ebdd2e94fe3e3146018fbb4487601b8cbb5c10b1574c3fe7858cc230d8b69f1884d4bab7e8f9f352523754e4a6f1ed39d512659812569248f41584a4c692b341
|
7
|
+
data.tar.gz: 3d588dac5c01a4c6773d43383b3f0ed54ea21c43bf6d8aeefbc933518b74d00cd2b5b3b02cac5ce86cf633d930c4733c8a55d851e770063325b4a94d56f64d15
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,14 @@
|
|
1
1
|
## [Unreleased]
|
2
2
|
|
3
|
+
## [0.0.18] – 2025-09-03
|
4
|
+
- Fixed bug missing updated Gemfile.lock
|
5
|
+
|
6
|
+
## [0.0.17] – 2025-09-03
|
7
|
+
|
8
|
+
### Removed
|
9
|
+
- Reverted experimental function registry v2 implementation
|
10
|
+
- Cleaned up unused analyzer passes and simplified unsat detector logic
|
11
|
+
|
3
12
|
## [0.0.16] – 2025-08-22
|
4
13
|
|
5
14
|
### Performance
|
@@ -6,9 +6,11 @@ schema do
|
|
6
6
|
|
7
7
|
trait :x_positive, input.x > 0
|
8
8
|
trait :y_positive, input.y > 0
|
9
|
+
|
10
|
+
trait :both_positive, y_positive & x_positive
|
9
11
|
|
10
12
|
value :status do
|
11
|
-
on
|
13
|
+
on both_positive, "both positive"
|
12
14
|
on x_positive, "x positive"
|
13
15
|
on y_positive, "y positive"
|
14
16
|
base "neither positive"
|
data/lib/kumi/analyzer.rb
CHANGED
@@ -15,17 +15,14 @@ module Kumi
|
|
15
15
|
Core::Analyzer::Passes::Toposorter, # 8. Creates the final evaluation order, allowing safe cycles.
|
16
16
|
Core::Analyzer::Passes::BroadcastDetector, # 9. Detects which operations should be broadcast over arrays.
|
17
17
|
Core::Analyzer::Passes::TypeInferencerPass, # 10. Infers types for all declarations (uses vectorization metadata).
|
18
|
-
Core::Analyzer::Passes::
|
19
|
-
Core::Analyzer::Passes::
|
20
|
-
Core::Analyzer::Passes::
|
21
|
-
Core::Analyzer::Passes::
|
22
|
-
Core::Analyzer::Passes::
|
23
|
-
Core::Analyzer::Passes::
|
24
|
-
Core::Analyzer::Passes::
|
25
|
-
Core::Analyzer::Passes::
|
26
|
-
Core::Analyzer::Passes::IRDependencyPass, # 19. Extracts IR-level dependencies for VM execution optimization
|
27
|
-
Core::Analyzer::Passes::IRExecutionSchedulePass # 20. Builds a precomputed execution schedule.
|
28
|
-
|
18
|
+
Core::Analyzer::Passes::TypeChecker, # 11. Validates types using inferred information.
|
19
|
+
Core::Analyzer::Passes::InputAccessPlannerPass, # 12. Plans access strategies for input fields.
|
20
|
+
Core::Analyzer::Passes::ScopeResolutionPass, # 13. Plans execution scope and lifting needs for declarations.
|
21
|
+
Core::Analyzer::Passes::JoinReducePlanningPass, # 14. Plans join/reduce operations (Generates IR Structs)
|
22
|
+
Core::Analyzer::Passes::LowerToIRPass, # 15. Lowers the schema to IR (Generates IR Structs)
|
23
|
+
Core::Analyzer::Passes::LoadInputCSE, # 16. Eliminates redundant load_input operations
|
24
|
+
Core::Analyzer::Passes::IRDependencyPass, # 17. Extracts IR-level dependencies for VM execution optimization
|
25
|
+
Core::Analyzer::Passes::IRExecutionSchedulePass # 18. Builds a precomputed execution schedule.
|
29
26
|
].freeze
|
30
27
|
|
31
28
|
def self.analyze!(schema, passes: DEFAULT_PASSES, **opts)
|
@@ -656,87 +656,6 @@ module Kumi
|
|
656
656
|
end
|
657
657
|
end
|
658
658
|
|
659
|
-
def extract_dimensional_info_with_context(info, _array_fields, _nested_paths, vectorized_values)
|
660
|
-
case info[:source]
|
661
|
-
when :array_field_access, :nested_array_access
|
662
|
-
# Direct array field access - use the path
|
663
|
-
source = info[:path]&.first
|
664
|
-
dimension = info[:path]
|
665
|
-
[source, dimension]
|
666
|
-
when :vectorized_declaration
|
667
|
-
# Reference to another vectorized declaration - look it up
|
668
|
-
if info[:name] && vectorized_values[info[:name]]
|
669
|
-
vectorized_info = vectorized_values[info[:name]]
|
670
|
-
if vectorized_info[:array_source]
|
671
|
-
# This declaration references an array field, use that source
|
672
|
-
[vectorized_info[:array_source], [vectorized_info[:array_source]]]
|
673
|
-
else
|
674
|
-
# This is a derived vectorized value, try to trace its source
|
675
|
-
[:vectorized_reference, [:vectorized_reference]]
|
676
|
-
end
|
677
|
-
else
|
678
|
-
[:unknown_declaration, [:unknown_declaration]]
|
679
|
-
end
|
680
|
-
else
|
681
|
-
# Operations and other cases - try to extract from operation args
|
682
|
-
if info[:operation] && info[:vectorized_args]
|
683
|
-
# This is an operation result - trace the vectorized arguments
|
684
|
-
# For now, assume operations inherit the dimension of their first vectorized arg
|
685
|
-
[:operation_result, [:operation_result]]
|
686
|
-
else
|
687
|
-
[:unknown, [:unknown]]
|
688
|
-
end
|
689
|
-
end
|
690
|
-
end
|
691
|
-
|
692
|
-
def extract_dimensional_source(info, _array_fields)
|
693
|
-
case info[:source]
|
694
|
-
when :array_field_access
|
695
|
-
info[:path]&.first
|
696
|
-
when :nested_array_access
|
697
|
-
info[:path]&.first
|
698
|
-
when :vectorized_declaration, :vectorized_value
|
699
|
-
# Try to extract from the vectorized value info if available
|
700
|
-
if info[:name] && info.dig(:info, :path)
|
701
|
-
info[:info][:path].first
|
702
|
-
else
|
703
|
-
:vectorized_reference
|
704
|
-
end
|
705
|
-
else
|
706
|
-
# For operations and other cases, try to infer from vectorized args
|
707
|
-
if info[:vectorized_args]
|
708
|
-
# This is likely an operation - we should look at its arguments
|
709
|
-
:operation_result
|
710
|
-
else
|
711
|
-
:unknown
|
712
|
-
end
|
713
|
-
end
|
714
|
-
end
|
715
|
-
|
716
|
-
def extract_dimensions(info, _array_fields, _nested_paths)
|
717
|
-
case info[:source]
|
718
|
-
when :array_field_access
|
719
|
-
info[:path]
|
720
|
-
when :nested_array_access
|
721
|
-
info[:path]
|
722
|
-
when :vectorized_declaration, :vectorized_value
|
723
|
-
# Try to extract from the vectorized value info if available
|
724
|
-
if info[:name] && info.dig(:info, :path)
|
725
|
-
info[:info][:path]
|
726
|
-
else
|
727
|
-
[:vectorized_reference]
|
728
|
-
end
|
729
|
-
else
|
730
|
-
# For operations, try to infer from the operation context
|
731
|
-
if info[:vectorized_args]
|
732
|
-
# This is likely an operation - we should trace its arguments
|
733
|
-
[:operation_result]
|
734
|
-
else
|
735
|
-
[:unknown]
|
736
|
-
end
|
737
|
-
end
|
738
|
-
end
|
739
|
-
|
740
659
|
def extract_nested_paths_from_dimensions(dimension, nested_paths)
|
741
660
|
return nil unless dimension.is_a?(Array)
|
742
661
|
|
@@ -426,9 +426,6 @@ module Kumi
|
|
426
426
|
when Syntax::CallExpression
|
427
427
|
entry = Kumi::Registry.entry(expr.fn_name)
|
428
428
|
|
429
|
-
# Validate signature metadata from FunctionSignaturePass (read-only assertions)
|
430
|
-
validate_signature_metadata(expr, entry)
|
431
|
-
|
432
429
|
# Constant folding optimization: evaluate expressions with all literal arguments
|
433
430
|
if can_constant_fold?(expr, entry)
|
434
431
|
folded_value = constant_fold(expr, entry)
|
@@ -888,39 +885,6 @@ module Kumi
|
|
888
885
|
expr.args.all? { |arg| arg.is_a?(Syntax::Literal) }
|
889
886
|
end
|
890
887
|
|
891
|
-
def validate_signature_metadata(expr, entry)
|
892
|
-
# Get the node index to access signature metadata
|
893
|
-
node_index = get_state(:node_index, required: false)
|
894
|
-
return unless node_index
|
895
|
-
|
896
|
-
node_entry = node_index[expr.object_id]
|
897
|
-
return unless node_entry
|
898
|
-
|
899
|
-
metadata = node_entry[:metadata]
|
900
|
-
return unless metadata
|
901
|
-
|
902
|
-
# Validate that dropped axes make sense for reduction functions
|
903
|
-
if entry&.reducer && metadata[:dropped_axes]
|
904
|
-
dropped_axes = metadata[:dropped_axes]
|
905
|
-
unless dropped_axes.is_a?(Array)
|
906
|
-
raise "Invalid dropped_axes metadata for reducer #{expr.fn_name}: expected Array, got #{dropped_axes.class}"
|
907
|
-
end
|
908
|
-
|
909
|
-
# For reductions, we should have at least one dropped axis (or empty for scalar reductions)
|
910
|
-
puts " SIGNATURE[#{expr.fn_name}] dropped_axes: #{dropped_axes.inspect}" if ENV["DEBUG_LOWER"]
|
911
|
-
end
|
912
|
-
|
913
|
-
# Validate join_policy is recognized
|
914
|
-
if metadata[:join_policy] && !%i[zip product].include?(metadata[:join_policy])
|
915
|
-
raise "Invalid join_policy for #{expr.fn_name}: #{metadata[:join_policy].inspect}"
|
916
|
-
end
|
917
|
-
|
918
|
-
# Warn about join_policy when no join op exists yet (future integration point)
|
919
|
-
return unless metadata[:join_policy] && ENV["DEBUG_LOWER"]
|
920
|
-
|
921
|
-
puts " SIGNATURE[#{expr.fn_name}] join_policy: #{metadata[:join_policy]} (join op not yet implemented)"
|
922
|
-
end
|
923
|
-
|
924
888
|
def constant_fold(expr, entry)
|
925
889
|
literal_values = expr.args.map(&:value)
|
926
890
|
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "pry"
|
4
3
|
module Kumi
|
5
4
|
module Core
|
6
5
|
module Analyzer
|
@@ -8,52 +7,18 @@ module Kumi
|
|
8
7
|
# RESPONSIBILITY: Compute topological ordering of declarations, blocking all cycles
|
9
8
|
# DEPENDENCIES: :dependencies from DependencyResolver, :declarations from NameIndexer
|
10
9
|
# PRODUCES: :evaluation_order - Array of declaration names in evaluation order
|
11
|
-
# :node_index - Hash mapping object_id to node metadata for later passes
|
12
10
|
# INTERFACE: new(schema, state).run(errors)
|
13
11
|
class Toposorter < PassBase
|
14
12
|
def run(errors)
|
15
13
|
dependency_graph = get_state(:dependencies, required: false) || {}
|
16
14
|
definitions = get_state(:declarations, required: false) || {}
|
17
15
|
|
18
|
-
# Create node index for later passes to use
|
19
|
-
node_index = build_node_index(definitions)
|
20
16
|
order = compute_topological_order(dependency_graph, definitions, errors)
|
21
|
-
|
22
|
-
state.with(:evaluation_order, order).with(:node_index, node_index)
|
17
|
+
state.with(:evaluation_order, order)
|
23
18
|
end
|
24
19
|
|
25
20
|
private
|
26
21
|
|
27
|
-
def build_node_index(definitions)
|
28
|
-
index = {}
|
29
|
-
|
30
|
-
# Walk all declarations and their expressions to index every node
|
31
|
-
definitions.each_value do |decl|
|
32
|
-
index_node_recursive(decl, index)
|
33
|
-
end
|
34
|
-
|
35
|
-
index
|
36
|
-
end
|
37
|
-
|
38
|
-
def index_node_recursive(node, index)
|
39
|
-
return unless node
|
40
|
-
|
41
|
-
# Index this node by its object_id
|
42
|
-
index[node.object_id] = {
|
43
|
-
node: node,
|
44
|
-
type: node.class.name.split("::").last,
|
45
|
-
metadata: {}
|
46
|
-
}
|
47
|
-
|
48
|
-
# Use the same approach as the visitor pattern - recursively index all children
|
49
|
-
node.children.each { |child| index_node_recursive(child, index) } if node.respond_to?(:children)
|
50
|
-
|
51
|
-
# Index expression for declaration nodes
|
52
|
-
return unless node.respond_to?(:expression)
|
53
|
-
|
54
|
-
index_node_recursive(node.expression, index)
|
55
|
-
end
|
56
|
-
|
57
22
|
def compute_topological_order(graph, definitions, errors)
|
58
23
|
temp_marks = Set.new
|
59
24
|
perm_marks = Set.new
|
@@ -6,7 +6,6 @@ module Kumi
|
|
6
6
|
module Passes
|
7
7
|
# RESPONSIBILITY: Detect unsatisfiable constraints and analyze cascade mutual exclusion
|
8
8
|
# DEPENDENCIES: :declarations from NameIndexer, :input_metadata from InputCollector
|
9
|
-
# PRODUCES: :cascades - Hash of cascade mutual exclusion analysis results
|
10
9
|
# INTERFACE: new(schema, state).run(errors)
|
11
10
|
class UnsatDetector < VisitorPass
|
12
11
|
include Syntax
|
@@ -20,142 +19,31 @@ module Kumi
|
|
20
19
|
@definitions = definitions
|
21
20
|
@evaluator = ConstantEvaluator.new(definitions)
|
22
21
|
|
23
|
-
# First pass: analyze cascade conditions for mutual exclusion
|
24
|
-
cascades = {}
|
25
22
|
each_decl do |decl|
|
26
|
-
cascades[decl.name] = analyze_cascade_mutual_exclusion(decl, definitions) if decl.expression.is_a?(CascadeExpression)
|
27
|
-
|
28
|
-
# Store cascade metadata for later passes
|
29
|
-
|
30
|
-
# Second pass: check for unsatisfiable constraints
|
31
23
|
if decl.expression.is_a?(CascadeExpression)
|
32
|
-
# Special handling for cascade expressions
|
33
24
|
check_cascade_expression(decl, definitions, errors)
|
34
25
|
elsif decl.expression.is_a?(CallExpression) && decl.expression.fn_name == :or
|
35
|
-
# Check for OR expressions which need special disjunctive handling
|
36
26
|
impossible = check_or_expression(decl.expression, definitions, errors)
|
37
27
|
report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
|
38
28
|
else
|
39
|
-
# Normal handling for non-cascade expressions
|
40
29
|
atoms = gather_atoms(decl.expression, definitions, Set.new)
|
41
30
|
next if atoms.empty?
|
42
31
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
puts " Input meta: #{@input_meta.keys.inspect}" if @input_meta
|
49
|
-
end
|
50
|
-
|
51
|
-
# Use enhanced solver that can detect cross-variable mathematical constraints
|
52
|
-
if definitions && !definitions.empty?
|
53
|
-
result = Kumi::Core::ConstraintRelationshipSolver.unsat?(atoms, definitions, input_meta: @input_meta)
|
54
|
-
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
55
|
-
puts " Enhanced solver result: #{result}"
|
56
|
-
end
|
57
|
-
else
|
58
|
-
result = Kumi::Core::AtomUnsatSolver.unsat?(atoms)
|
59
|
-
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
60
|
-
puts " Basic solver result: #{result}"
|
61
|
-
end
|
62
|
-
end
|
32
|
+
result = if definitions && !definitions.empty?
|
33
|
+
Kumi::Core::ConstraintRelationshipSolver.unsat?(atoms, definitions, input_meta: @input_meta)
|
34
|
+
else
|
35
|
+
Kumi::Core::AtomUnsatSolver.unsat?(atoms)
|
36
|
+
end
|
63
37
|
impossible = result
|
64
38
|
|
65
|
-
if impossible && (ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257"))
|
66
|
-
puts " -> FLAGGING AS IMPOSSIBLE: #{decl.name}"
|
67
|
-
end
|
68
|
-
|
69
39
|
report_error(errors, "conjunction `#{decl.name}` is impossible", location: decl.loc) if impossible
|
70
40
|
end
|
71
41
|
end
|
72
|
-
state.with(:cascades, cascades)
|
73
|
-
end
|
74
|
-
|
75
|
-
private
|
76
|
-
|
77
|
-
def analyze_cascade_mutual_exclusion(decl, definitions)
|
78
|
-
conditions = []
|
79
|
-
condition_traits = []
|
80
|
-
|
81
|
-
# Extract all cascade conditions (except base case)
|
82
|
-
decl.expression.cases[0...-1].each do |when_case|
|
83
|
-
next unless when_case.condition
|
84
|
-
|
85
|
-
next unless when_case.condition.fn_name == :cascade_and
|
86
|
-
|
87
|
-
when_case.condition.args.each do |arg|
|
88
|
-
if arg.is_a?(ArrayExpression)
|
89
|
-
# Handle array elements (for array broadcasting)
|
90
|
-
arg.elements.each do |element|
|
91
|
-
next unless element.is_a?(DeclarationReference)
|
92
|
-
|
93
|
-
trait_name = element.name
|
94
|
-
trait = definitions[trait_name]
|
95
|
-
if trait
|
96
|
-
conditions << trait.expression
|
97
|
-
condition_traits << trait_name
|
98
|
-
end
|
99
|
-
end
|
100
|
-
elsif arg.is_a?(DeclarationReference)
|
101
|
-
# Handle direct trait references (simple case)
|
102
|
-
trait_name = arg.name
|
103
|
-
trait = definitions[trait_name]
|
104
|
-
if trait
|
105
|
-
conditions << trait.expression
|
106
|
-
condition_traits << trait_name
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
# end
|
111
|
-
end
|
112
|
-
|
113
|
-
# Check mutual exclusion for all pairs
|
114
|
-
total_pairs = conditions.size * (conditions.size - 1) / 2
|
115
|
-
exclusive_pairs = 0
|
116
|
-
|
117
|
-
if conditions.size >= 2
|
118
|
-
conditions.combination(2).each do |cond1, cond2|
|
119
|
-
exclusive_pairs += 1 if conditions_mutually_exclusive?(cond1, cond2)
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
all_mutually_exclusive = total_pairs.positive? && (exclusive_pairs == total_pairs)
|
124
|
-
|
125
|
-
{
|
126
|
-
condition_traits: condition_traits,
|
127
|
-
condition_count: conditions.size,
|
128
|
-
all_mutually_exclusive: all_mutually_exclusive,
|
129
|
-
exclusive_pairs: exclusive_pairs,
|
130
|
-
total_pairs: total_pairs
|
131
|
-
}
|
132
|
-
end
|
133
|
-
|
134
|
-
def conditions_mutually_exclusive?(cond1, cond2)
|
135
|
-
if cond1.is_a?(CallExpression) && cond1.fn_name == :== &&
|
136
|
-
cond2.is_a?(CallExpression) && cond2.fn_name == :==
|
137
|
-
|
138
|
-
c1_field, c1_value = cond1.args
|
139
|
-
c2_field, c2_value = cond2.args
|
140
|
-
|
141
|
-
# Same field, different values = mutually exclusive
|
142
|
-
return true if same_field?(c1_field, c2_field) && different_values?(c1_value, c2_value)
|
143
|
-
end
|
144
42
|
|
145
|
-
|
43
|
+
state
|
146
44
|
end
|
147
45
|
|
148
|
-
|
149
|
-
return false unless field1.is_a?(InputReference) && field2.is_a?(InputReference)
|
150
|
-
|
151
|
-
field1.name == field2.name
|
152
|
-
end
|
153
|
-
|
154
|
-
def different_values?(val1, val2)
|
155
|
-
return false unless val1.is_a?(Literal) && val2.is_a?(Literal)
|
156
|
-
|
157
|
-
val1.value != val2.value
|
158
|
-
end
|
46
|
+
private
|
159
47
|
|
160
48
|
def check_or_expression(or_expr, definitions, _errors)
|
161
49
|
# For OR expressions: A | B is impossible only if BOTH A AND B are impossible
|
@@ -199,23 +87,16 @@ module Kumi
|
|
199
87
|
if current.is_a?(CallExpression) && COMPARATORS.include?(current.fn_name)
|
200
88
|
lhs, rhs = current.args
|
201
89
|
|
202
|
-
# Check for domain constraint violations before creating atom
|
203
90
|
list << if impossible_constraint?(lhs, rhs, current.fn_name)
|
204
|
-
# Create a special impossible atom that will always trigger unsat
|
205
91
|
Atom.new(:==, :__impossible__, true)
|
206
92
|
else
|
207
93
|
Atom.new(current.fn_name, term(lhs, defs), term(rhs, defs))
|
208
94
|
end
|
209
95
|
elsif current.is_a?(CallExpression) && current.fn_name == :or
|
210
|
-
# Special handling for OR expressions - they are disjunctive, not conjunctive
|
211
|
-
# We should NOT add OR children to the stack as they would be treated as AND
|
212
|
-
# OR expressions need separate analysis in the main run() method
|
213
96
|
next
|
214
97
|
elsif current.is_a?(CallExpression) && current.fn_name == :cascade_and
|
215
|
-
# cascade_and takes individual arguments (not wrapped in array)
|
216
98
|
current.args.each { |arg| stack << arg }
|
217
99
|
elsif current.is_a?(ArrayExpression)
|
218
|
-
# For ArrayExpression, add all elements to the stack
|
219
100
|
current.elements.each { |elem| stack << elem }
|
220
101
|
elsif current.is_a?(DeclarationReference)
|
221
102
|
name = current.name
|
@@ -225,10 +106,6 @@ module Kumi
|
|
225
106
|
end
|
226
107
|
end
|
227
108
|
|
228
|
-
# Add children to stack for processing
|
229
|
-
# IMPORTANT: Skip CascadeExpression children to avoid false positives
|
230
|
-
# Cascades are handled separately by check_cascade_expression() and are disjunctive,
|
231
|
-
# but gather_atoms() treats all collected atoms as conjunctive
|
232
109
|
current.children.each { |child| stack << child } if current.respond_to?(:children) && !current.is_a?(CascadeExpression)
|
233
110
|
end
|
234
111
|
|
@@ -247,32 +124,24 @@ module Kumi
|
|
247
124
|
end
|
248
125
|
|
249
126
|
decl.expression.cases.each_with_index do |when_case, index|
|
250
|
-
# DEBUG: Log each case
|
251
127
|
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
252
128
|
puts " Case #{index}: condition=#{when_case.condition.inspect}"
|
253
129
|
end
|
254
130
|
|
255
|
-
# Skip the base case (it's typically a literal true condition)
|
256
131
|
next if when_case.condition.is_a?(Literal) && when_case.condition.value == true
|
257
132
|
|
258
|
-
# Skip non-conjunctive conditions (any?, none?) as they are disjunctive
|
259
133
|
next if when_case.condition.is_a?(CallExpression) && %i[any? none?].include?(when_case.condition.fn_name)
|
260
134
|
|
261
|
-
# Skip single-trait 'on' branches: trait-level unsat detection covers these
|
262
135
|
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :cascade_and && (when_case.condition.args.size == 1)
|
263
|
-
# cascade_and uses individual arguments - skip if only one trait
|
264
136
|
next
|
265
137
|
end
|
266
138
|
|
267
|
-
# Gather atoms from this individual condition only
|
268
139
|
condition_atoms = gather_atoms(when_case.condition, definitions, Set.new, [])
|
269
140
|
|
270
|
-
# DEBUG: Add detailed logging for hierarchical broadcasting debugging
|
271
141
|
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
272
142
|
puts " Condition atoms: #{condition_atoms.map(&:inspect)}"
|
273
143
|
end
|
274
144
|
|
275
|
-
# Use enhanced solver for cascade conditions too
|
276
145
|
if definitions && !definitions.empty?
|
277
146
|
result = Kumi::Core::ConstraintRelationshipSolver.unsat?(condition_atoms, definitions, input_meta: @input_meta)
|
278
147
|
if ENV["DEBUG_UNSAT"] || decl.loc&.to_s&.include?("hierarchical_broadcasting_spec.rb:257")
|
@@ -287,9 +156,7 @@ module Kumi
|
|
287
156
|
impossible = result
|
288
157
|
next unless !condition_atoms.empty? && impossible
|
289
158
|
|
290
|
-
# For multi-trait on-clauses, report the trait names rather than the value name
|
291
159
|
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :cascade_and
|
292
|
-
# cascade_and uses individual arguments
|
293
160
|
trait_bindings = when_case.condition.args
|
294
161
|
|
295
162
|
if trait_bindings.all?(DeclarationReference)
|
@@ -314,9 +181,6 @@ module Kumi
|
|
314
181
|
val = @evaluator.evaluate(node)
|
315
182
|
val == :unknown ? node.name : val
|
316
183
|
when InputElementReference
|
317
|
-
# For hierarchical paths like input.companies.regions.offices.teams.department,
|
318
|
-
# create a unique identifier that represents the specific path
|
319
|
-
# This prevents false positives where different paths are treated as the same :unknown
|
320
184
|
path_identifier = node.path.join(".").to_s
|
321
185
|
path_identifier.to_sym
|
322
186
|
when Literal
|
@@ -326,51 +190,10 @@ module Kumi
|
|
326
190
|
end
|
327
191
|
end
|
328
192
|
|
329
|
-
def check_domain_constraints(node, definitions, errors)
|
330
|
-
case node
|
331
|
-
when InputReference
|
332
|
-
# Check if InputReference points to a field with domain constraints
|
333
|
-
field_meta = @input_meta[node.name]
|
334
|
-
nil unless field_meta&.dig(:domain)
|
335
|
-
|
336
|
-
# For InputReference, the constraint comes from trait conditions
|
337
|
-
# We don't flag here since the InputReference itself is valid
|
338
|
-
when DeclarationReference
|
339
|
-
# Check if this binding evaluates to a value that violates domain constraints
|
340
|
-
definition = definitions[node.name]
|
341
|
-
return unless definition
|
342
|
-
|
343
|
-
if definition.expression.is_a?(Literal)
|
344
|
-
literal_value = definition.expression.value
|
345
|
-
check_value_against_domains(node.name, literal_value, errors, definition.loc)
|
346
|
-
end
|
347
|
-
end
|
348
|
-
end
|
349
|
-
|
350
|
-
def check_value_against_domains(_var_name, value, _errors, _location)
|
351
|
-
# Check if this value violates any input domain constraints
|
352
|
-
@input_meta.each_value do |field_meta|
|
353
|
-
domain = field_meta[:domain]
|
354
|
-
next unless domain
|
355
|
-
|
356
|
-
if violates_domain?(value, domain)
|
357
|
-
# This indicates a constraint that can never be satisfied
|
358
|
-
# Rather than flagging the cascade, flag the impossible condition
|
359
|
-
return true
|
360
|
-
end
|
361
|
-
end
|
362
|
-
false
|
363
|
-
end
|
364
|
-
|
365
193
|
def violates_domain?(value, domain)
|
366
194
|
case domain
|
367
|
-
when Range
|
368
|
-
!domain.include?(value)
|
369
|
-
when Array
|
195
|
+
when Range, Array
|
370
196
|
!domain.include?(value)
|
371
|
-
when Proc
|
372
|
-
# For Proc domains, we can't statically analyze
|
373
|
-
false
|
374
197
|
else
|
375
198
|
false
|
376
199
|
end
|
@@ -381,7 +204,6 @@ module Kumi
|
|
381
204
|
if lhs.is_a?(InputReference) && rhs.is_a?(Literal)
|
382
205
|
return field_literal_impossible?(lhs, rhs, operator)
|
383
206
|
elsif rhs.is_a?(InputReference) && lhs.is_a?(Literal)
|
384
|
-
# Reverse case: literal compared to field
|
385
207
|
return field_literal_impossible?(rhs, lhs, flip_operator(operator))
|
386
208
|
end
|
387
209
|
|
@@ -404,13 +226,10 @@ module Kumi
|
|
404
226
|
|
405
227
|
case operator
|
406
228
|
when :==
|
407
|
-
# field == value where value is not in domain
|
408
229
|
violates_domain?(literal_value, domain)
|
409
230
|
when :!=
|
410
|
-
# field != value where value is not in domain is always true (not impossible)
|
411
231
|
false
|
412
232
|
else
|
413
|
-
# For other operators, we'd need more sophisticated analysis
|
414
233
|
false
|
415
234
|
end
|
416
235
|
end
|
@@ -424,10 +243,8 @@ module Kumi
|
|
424
243
|
|
425
244
|
case operator
|
426
245
|
when :==
|
427
|
-
# binding == value where binding evaluates to different value
|
428
246
|
evaluated_value != literal_value
|
429
247
|
else
|
430
|
-
# For other operators, we could add more sophisticated checking
|
431
248
|
false
|
432
249
|
end
|
433
250
|
end
|
@@ -3,17 +3,14 @@ module Kumi
|
|
3
3
|
module Compiler
|
4
4
|
class AccessBuilder
|
5
5
|
class << self
|
6
|
-
attr_accessor :
|
6
|
+
attr_accessor :yjit
|
7
7
|
end
|
8
|
+
self.yjit = defined?(RubyVM::YJIT) && RubyVM::YJIT.enabled?
|
8
9
|
|
9
|
-
self.strategy
|
10
|
-
|
11
|
-
else
|
12
|
-
:codegen
|
13
|
-
end
|
14
|
-
|
15
|
-
def self.build(plans)
|
10
|
+
def self.build(plans, strategy: nil)
|
11
|
+
strategy ||= yjit ? :interp : :codegen
|
16
12
|
accessors = {}
|
13
|
+
|
17
14
|
plans.each_value do |variants|
|
18
15
|
variants.each do |plan|
|
19
16
|
accessors[plan.accessor_key] =
|
data/lib/kumi/version.rb
CHANGED