kumi 0.0.14 → 0.0.15
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 +33 -0
- data/README.md +0 -27
- data/docs/dev/vm-profiling.md +95 -0
- data/docs/features/README.md +0 -7
- data/lib/kumi/analyzer.rb +5 -2
- data/lib/kumi/compiler.rb +6 -5
- data/lib/kumi/core/analyzer/passes/ir_dependency_pass.rb +67 -0
- data/lib/kumi/core/analyzer/passes/toposorter.rb +3 -35
- data/lib/kumi/core/ir/execution_engine/interpreter.rb +42 -30
- data/lib/kumi/core/ir/execution_engine/profiler.rb +139 -11
- data/lib/kumi/core/ir/execution_engine.rb +6 -15
- data/lib/kumi/dev/profile_aggregator.rb +301 -0
- data/lib/kumi/dev/profile_runner.rb +199 -0
- data/lib/kumi/dev/runner.rb +3 -1
- data/lib/kumi/dev.rb +14 -0
- data/lib/kumi/runtime/executable.rb +61 -29
- data/lib/kumi/schema.rb +9 -3
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +1 -0
- metadata +6 -2
- data/docs/features/analysis-cascade-mutual-exclusion.md +0 -89
@@ -0,0 +1,199 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "fileutils"
|
5
|
+
require "benchmark"
|
6
|
+
|
7
|
+
module Kumi
|
8
|
+
module Dev
|
9
|
+
module ProfileRunner
|
10
|
+
module_function
|
11
|
+
|
12
|
+
def run(script_path, opts = {})
|
13
|
+
# Validate script exists
|
14
|
+
unless File.exist?(script_path)
|
15
|
+
puts "Error: Script not found: #{script_path}"
|
16
|
+
return false
|
17
|
+
end
|
18
|
+
|
19
|
+
# Set up profiling environment
|
20
|
+
setup_profiler_env(opts)
|
21
|
+
|
22
|
+
puts "Profiling: #{script_path}"
|
23
|
+
puts "Configuration:"
|
24
|
+
puts " Output: #{ENV['KUMI_PROFILE_FILE']}"
|
25
|
+
puts " Phases: enabled"
|
26
|
+
puts " Operations: #{ENV['KUMI_PROFILE_OPS'] == '1' ? 'enabled' : 'disabled'}"
|
27
|
+
puts " Sampling: #{ENV['KUMI_PROFILE_SAMPLE'] || '1'}"
|
28
|
+
puts " Persistent: #{ENV['KUMI_PROFILE_PERSISTENT'] == '1' ? 'yes' : 'no'}"
|
29
|
+
puts " Memory snapshots: #{opts[:memory] ? 'enabled' : 'disabled'}"
|
30
|
+
puts
|
31
|
+
|
32
|
+
# Initialize profiler
|
33
|
+
Dev::Profiler.init_persistent! if ENV["KUMI_PROFILE_PERSISTENT"] == "1"
|
34
|
+
|
35
|
+
# Add memory snapshot before execution
|
36
|
+
Dev::Profiler.memory_snapshot("script_start") if opts[:memory]
|
37
|
+
|
38
|
+
# Execute the script
|
39
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
40
|
+
begin
|
41
|
+
result = Dev::Profiler.phase("script_execution", script: File.basename(script_path)) do
|
42
|
+
# Execute in a clean environment to avoid polluting the current process
|
43
|
+
load(File.expand_path(script_path))
|
44
|
+
end
|
45
|
+
rescue StandardError => e
|
46
|
+
puts "Error executing script: #{e.message}"
|
47
|
+
puts e.backtrace.first(5).join("\n")
|
48
|
+
return false
|
49
|
+
ensure
|
50
|
+
execution_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
|
51
|
+
end
|
52
|
+
|
53
|
+
# Add memory snapshot after execution
|
54
|
+
Dev::Profiler.memory_snapshot("script_end") if opts[:memory]
|
55
|
+
|
56
|
+
# Finalize profiler to get aggregated data
|
57
|
+
Dev::Profiler.finalize!
|
58
|
+
|
59
|
+
puts "Script completed in #{execution_time.round(4)}s"
|
60
|
+
|
61
|
+
# Show analysis unless quiet
|
62
|
+
show_analysis(opts) unless opts[:quiet]
|
63
|
+
|
64
|
+
true
|
65
|
+
rescue LoadError => e
|
66
|
+
puts "Error loading script: #{e.message}"
|
67
|
+
false
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def self.setup_profiler_env(opts)
|
73
|
+
# Always enable profiling
|
74
|
+
ENV["KUMI_PROFILE"] = "1"
|
75
|
+
|
76
|
+
# Output file
|
77
|
+
output_file = opts[:output] || "tmp/profile.jsonl"
|
78
|
+
ENV["KUMI_PROFILE_FILE"] = output_file
|
79
|
+
|
80
|
+
# Truncate if requested
|
81
|
+
ENV["KUMI_PROFILE_TRUNCATE"] = opts[:truncate] ? "1" : "0"
|
82
|
+
|
83
|
+
# Streaming
|
84
|
+
ENV["KUMI_PROFILE_STREAM"] = opts[:stream] ? "1" : "0"
|
85
|
+
|
86
|
+
# Operations profiling
|
87
|
+
if opts[:phases_only]
|
88
|
+
ENV["KUMI_PROFILE_OPS"] = "0"
|
89
|
+
elsif opts[:ops]
|
90
|
+
ENV["KUMI_PROFILE_OPS"] = "1"
|
91
|
+
else
|
92
|
+
# Default: phases only
|
93
|
+
ENV["KUMI_PROFILE_OPS"] = "0"
|
94
|
+
end
|
95
|
+
|
96
|
+
# Sampling
|
97
|
+
ENV["KUMI_PROFILE_SAMPLE"] = opts[:sample].to_s if opts[:sample]
|
98
|
+
|
99
|
+
# Persistent mode
|
100
|
+
ENV["KUMI_PROFILE_PERSISTENT"] = opts[:persistent] ? "1" : "0"
|
101
|
+
|
102
|
+
# Ensure output directory exists
|
103
|
+
FileUtils.mkdir_p(File.dirname(output_file))
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.show_analysis(opts)
|
107
|
+
output_file = ENV["KUMI_PROFILE_FILE"]
|
108
|
+
|
109
|
+
unless File.exist?(output_file)
|
110
|
+
puts "No profile data generated"
|
111
|
+
return
|
112
|
+
end
|
113
|
+
|
114
|
+
puts "\n=== Profiling Analysis ==="
|
115
|
+
|
116
|
+
# Use ProfileAggregator for comprehensive analysis
|
117
|
+
require_relative "profile_aggregator"
|
118
|
+
aggregator = ProfileAggregator.new(output_file)
|
119
|
+
|
120
|
+
if opts[:json]
|
121
|
+
# Export full analysis to JSON and display
|
122
|
+
json_output = opts[:json_file] || "/tmp/profile_analysis.json"
|
123
|
+
aggregator.export_summary(json_output)
|
124
|
+
puts File.read(json_output)
|
125
|
+
return
|
126
|
+
end
|
127
|
+
|
128
|
+
# Show comprehensive analysis using ProfileAggregator
|
129
|
+
if opts[:detailed]
|
130
|
+
aggregator.detailed_report(limit: opts[:limit] || 15)
|
131
|
+
else
|
132
|
+
# Show summary + key insights
|
133
|
+
aggregator.summary_report
|
134
|
+
|
135
|
+
# Add some key insights for CLI users
|
136
|
+
puts
|
137
|
+
puts "=== KEY INSIGHTS ==="
|
138
|
+
|
139
|
+
# Show top hotspots
|
140
|
+
hotspots = aggregator.hotspot_analysis(limit: 3)
|
141
|
+
if hotspots.any?
|
142
|
+
puts "Top Performance Bottlenecks:"
|
143
|
+
hotspots.each_with_index do |(key, stats), i|
|
144
|
+
puts " #{i+1}. #{stats[:decl]} (#{stats[:tag]}): #{stats[:total_ms]}ms"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
# Reference analysis summary
|
149
|
+
ref_analysis = aggregator.reference_operation_analysis
|
150
|
+
if ref_analysis[:operations] > 0
|
151
|
+
puts "Reference Operation Impact: #{(ref_analysis[:total_time] / aggregator.vm_execution_time * 100).round(1)}% of VM time"
|
152
|
+
end
|
153
|
+
|
154
|
+
# Memory impact
|
155
|
+
mem = aggregator.memory_analysis
|
156
|
+
if mem
|
157
|
+
puts "Memory Impact: #{mem[:growth][:heap_growth_pct]}% heap growth, #{mem[:growth][:rss_growth_pct]}% RSS growth"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
puts
|
162
|
+
puts "Full profile: #{output_file}"
|
163
|
+
puts "For detailed analysis: bin/kumi profile #{ARGV.join(' ')} --detailed"
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.analyze_phases(phase_events)
|
167
|
+
phase_events.group_by { |e| e["name"] }.transform_values do |events|
|
168
|
+
{
|
169
|
+
count: events.length,
|
170
|
+
total_ms: events.sum { |e| e["wall_ms"] }.round(3),
|
171
|
+
avg_ms: (events.sum { |e| e["wall_ms"] } / events.length).round(4)
|
172
|
+
}
|
173
|
+
end.sort_by { |_, stats| -stats[:total_ms] }.to_h
|
174
|
+
end
|
175
|
+
|
176
|
+
def self.analyze_events(events)
|
177
|
+
{
|
178
|
+
summary: {
|
179
|
+
total_events: events.length,
|
180
|
+
phase_events: events.count { |e| e["kind"] == "phase" },
|
181
|
+
memory_events: events.count { |e| e["kind"] == "mem" },
|
182
|
+
operation_events: events.count { |e| !%w[phase mem summary final_summary cache_analysis].include?(e["kind"]) }
|
183
|
+
},
|
184
|
+
phases: analyze_phases(events.select { |e| e["kind"] == "phase" }),
|
185
|
+
memory_snapshots: events.select { |e| e["kind"] == "mem" }.map do |e|
|
186
|
+
{
|
187
|
+
label: e["label"],
|
188
|
+
heap_live: e["heap_live"],
|
189
|
+
rss_mb: e["rss_mb"],
|
190
|
+
timestamp: e["ts"]
|
191
|
+
}
|
192
|
+
end,
|
193
|
+
final_analysis: events.find { |e| e["kind"] == "final_summary" }&.dig("data"),
|
194
|
+
cache_analysis: events.find { |e| e["kind"] == "cache_analysis" }&.dig("data")
|
195
|
+
}
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
data/lib/kumi/dev/runner.rb
CHANGED
@@ -19,7 +19,9 @@ module Kumi
|
|
19
19
|
errors = []
|
20
20
|
|
21
21
|
begin
|
22
|
-
final_state =
|
22
|
+
final_state = Dev::Profiler.phase("text.analyzer") do
|
23
|
+
Kumi::Analyzer.run_analysis_passes(schema, Kumi::Analyzer::DEFAULT_PASSES, state, errors)
|
24
|
+
end
|
23
25
|
ir = final_state[:ir_module]
|
24
26
|
|
25
27
|
result = Result.new(
|
data/lib/kumi/dev.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Dev
|
5
|
+
# Alias to the execution engine profiler for cross-layer access
|
6
|
+
Profiler = Kumi::Core::IR::ExecutionEngine::Profiler
|
7
|
+
|
8
|
+
# Load profile runner for CLI
|
9
|
+
autoload :ProfileRunner, "kumi/dev/profile_runner"
|
10
|
+
|
11
|
+
# Load profile aggregator for data analysis
|
12
|
+
autoload :ProfileAggregator, "kumi/dev/profile_aggregator"
|
13
|
+
end
|
14
|
+
end
|
@@ -37,12 +37,16 @@ module Kumi
|
|
37
37
|
# - DEBUG_VM_ARGS=1 to trace VM execution
|
38
38
|
# - Accessors can be debugged independently with DEBUG_ACCESSOR_OPS=1
|
39
39
|
class Executable
|
40
|
-
def self.from_analysis(state, registry: nil)
|
40
|
+
def self.from_analysis(state, registry: nil, schema_name: nil)
|
41
41
|
ir = state.fetch(:ir_module)
|
42
42
|
access_plans = state.fetch(:access_plans)
|
43
43
|
input_metadata = state[:input_metadata] || {}
|
44
44
|
dependents = state[:dependents] || {}
|
45
|
-
|
45
|
+
ir_dependencies = state[:ir_dependencies] || {} # <-- from IR dependency pass
|
46
|
+
name_index = state[:name_index] || {} # <-- from IR dependency pass
|
47
|
+
accessors = Dev::Profiler.phase("compiler.access_builder") do
|
48
|
+
Kumi::Core::Compiler::AccessBuilder.build(access_plans)
|
49
|
+
end
|
46
50
|
|
47
51
|
access_meta = {}
|
48
52
|
field_to_plan_ids = Hash.new { |h, k| h[k] = [] }
|
@@ -60,10 +64,12 @@ module Kumi
|
|
60
64
|
# Use the internal functions hash that VM expects
|
61
65
|
registry ||= Kumi::Registry.functions
|
62
66
|
new(ir: ir, accessors: accessors, access_meta: access_meta, registry: registry,
|
63
|
-
input_metadata: input_metadata, field_to_plan_ids: field_to_plan_ids, dependents: dependents
|
67
|
+
input_metadata: input_metadata, field_to_plan_ids: field_to_plan_ids, dependents: dependents,
|
68
|
+
ir_dependencies: ir_dependencies, name_index: name_index, schema_name: schema_name)
|
64
69
|
end
|
65
70
|
|
66
|
-
def initialize(ir:, accessors:, access_meta:, registry:, input_metadata:, field_to_plan_ids: {}, dependents: {}
|
71
|
+
def initialize(ir:, accessors:, access_meta:, registry:, input_metadata:, field_to_plan_ids: {}, dependents: {}, ir_dependencies: {},
|
72
|
+
name_index: {}, schema_name: nil)
|
67
73
|
@ir = ir.freeze
|
68
74
|
@acc = accessors.freeze
|
69
75
|
@meta = access_meta.freeze
|
@@ -71,6 +77,9 @@ module Kumi
|
|
71
77
|
@input_metadata = input_metadata.freeze
|
72
78
|
@field_to_plan_ids = field_to_plan_ids.freeze
|
73
79
|
@dependents = dependents.freeze
|
80
|
+
@ir_dependencies = ir_dependencies.freeze # decl -> [stored_bindings_it_references]
|
81
|
+
@name_index = name_index.freeze # store_name -> producing decl
|
82
|
+
@schema_name = schema_name
|
74
83
|
@decl = @ir.decls.map { |d| [d.name, d] }.to_h
|
75
84
|
@accessor_cache = {} # Persistent accessor cache across evaluations
|
76
85
|
end
|
@@ -96,14 +105,22 @@ module Kumi
|
|
96
105
|
def eval_decl(name, input, mode: :ruby, declaration_cache: nil)
|
97
106
|
raise Kumi::Core::Errors::RuntimeError, "unknown decl #{name}" unless decl?(name)
|
98
107
|
|
99
|
-
|
100
|
-
|
101
|
-
|
108
|
+
# If the caller asked for a specific binding, schedule deps once
|
109
|
+
decls_to_run = topo_closure_for_target(name)
|
110
|
+
|
111
|
+
vm_context = {
|
112
|
+
input: input,
|
102
113
|
accessor_cache: @accessor_cache,
|
103
|
-
declaration_cache: declaration_cache
|
114
|
+
declaration_cache: declaration_cache || {}, # run-local cache
|
115
|
+
decls_to_run: decls_to_run, # <-- explicit schedule
|
116
|
+
strict_refs: true, # <-- refs must be precomputed
|
117
|
+
name_index: @name_index, # for error messages, twins, etc.
|
118
|
+
schema_name: @schema_name
|
104
119
|
}
|
105
|
-
|
106
|
-
out =
|
120
|
+
|
121
|
+
out = Dev::Profiler.phase("vm.run", target: name) do
|
122
|
+
Kumi::Core::IR::ExecutionEngine.run(@ir, vm_context, accessors: @acc, registry: @reg).fetch(name)
|
123
|
+
end
|
107
124
|
|
108
125
|
mode == :ruby ? unwrap(@decl[name], out) : out
|
109
126
|
end
|
@@ -119,6 +136,39 @@ module Kumi
|
|
119
136
|
v[:k] == :scalar ? v[:v] : v # no grouping needed
|
120
137
|
end
|
121
138
|
|
139
|
+
def topo_closure_for_target(store_name)
|
140
|
+
target_decl = @name_index[store_name]
|
141
|
+
raise "Unknown target store #{store_name}" unless target_decl
|
142
|
+
|
143
|
+
# DFS collect closure of decl names using pre-computed IR-level dependencies
|
144
|
+
seen = {}
|
145
|
+
order = []
|
146
|
+
visiting = {}
|
147
|
+
|
148
|
+
visit = lambda do |dname|
|
149
|
+
return if seen[dname]
|
150
|
+
raise "Cycle detected in DAG scheduler: #{dname}. Mutual recursion should be caught earlier by UnsatDetector." if visiting[dname]
|
151
|
+
|
152
|
+
visiting[dname] = true
|
153
|
+
|
154
|
+
# Visit declarations that produce the bindings this decl references
|
155
|
+
Array(@ir_dependencies[dname]).each do |ref_binding|
|
156
|
+
# Find which declaration produces this binding
|
157
|
+
producer = @name_index[ref_binding]
|
158
|
+
visit.call(producer.name) if producer
|
159
|
+
end
|
160
|
+
|
161
|
+
visiting.delete(dname)
|
162
|
+
seen[dname] = true
|
163
|
+
order << dname
|
164
|
+
end
|
165
|
+
|
166
|
+
visit.call(target_decl.name)
|
167
|
+
|
168
|
+
# 'order' is postorder; it already yields producers before consumers
|
169
|
+
order.map { |dname| @decl[dname] }
|
170
|
+
end
|
171
|
+
|
122
172
|
private
|
123
173
|
|
124
174
|
def validate_keys(keys)
|
@@ -146,7 +196,7 @@ module Kumi
|
|
146
196
|
# Store VM format for cross-VM caching
|
147
197
|
@cache[name] = vm_result
|
148
198
|
end
|
149
|
-
|
199
|
+
|
150
200
|
# Convert to requested format when returning
|
151
201
|
vm_result = @cache[name]
|
152
202
|
@mode == :wrapped ? vm_result : @program.unwrap(nil, vm_result)
|
@@ -203,18 +253,6 @@ module Kumi
|
|
203
253
|
self
|
204
254
|
end
|
205
255
|
|
206
|
-
def wrapped!
|
207
|
-
@mode = :wrapped
|
208
|
-
@cache.clear
|
209
|
-
self
|
210
|
-
end
|
211
|
-
|
212
|
-
def ruby!
|
213
|
-
@mode = :ruby
|
214
|
-
@cache.clear
|
215
|
-
self
|
216
|
-
end
|
217
|
-
|
218
256
|
private
|
219
257
|
|
220
258
|
def input_field_exists?(field)
|
@@ -245,12 +283,6 @@ module Kumi
|
|
245
283
|
false
|
246
284
|
end
|
247
285
|
end
|
248
|
-
|
249
|
-
def deep_merge(a, b)
|
250
|
-
return b unless a.is_a?(Hash) && b.is_a?(Hash)
|
251
|
-
|
252
|
-
a.merge(b) { |_k, v1, v2| deep_merge(v1, v2) }
|
253
|
-
end
|
254
286
|
end
|
255
287
|
end
|
256
288
|
end
|
data/lib/kumi/schema.rb
CHANGED
@@ -49,12 +49,18 @@ module Kumi
|
|
49
49
|
def schema(&)
|
50
50
|
# from_location = caller_locations(1, 1).first
|
51
51
|
# raise "Called from #{from_location.path}:#{from_location.lineno}"
|
52
|
-
@__syntax_tree__ =
|
52
|
+
@__syntax_tree__ = Dev::Profiler.phase("frontend.parse") do
|
53
|
+
Core::RubyParser::Dsl.build_syntax_tree(&).freeze
|
54
|
+
end
|
53
55
|
|
54
56
|
puts Support::SExpressionPrinter.print(@__syntax_tree__, indent: 2) if ENV["KUMI_DEBUG"] || ENV["KUMI_PRINT_SYNTAX_TREE"]
|
55
57
|
|
56
|
-
@__analyzer_result__ =
|
57
|
-
|
58
|
+
@__analyzer_result__ = Dev::Profiler.phase("analyzer") do
|
59
|
+
Analyzer.analyze!(@__syntax_tree__).freeze
|
60
|
+
end
|
61
|
+
@__compiled_schema__ = Dev::Profiler.phase("compiler") do
|
62
|
+
Compiler.compile(@__syntax_tree__, analyzer: @__analyzer_result__, schema_name: self.name).freeze
|
63
|
+
end
|
58
64
|
|
59
65
|
Inspector.new(@__syntax_tree__, @__analyzer_result__, @__compiled_schema__)
|
60
66
|
end
|
data/lib/kumi/version.rb
CHANGED
data/lib/kumi.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: kumi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.15
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- André Muta
|
@@ -48,10 +48,10 @@ files:
|
|
48
48
|
- docs/compiler_design_principles.md
|
49
49
|
- docs/dev/analyzer-debug.md
|
50
50
|
- docs/dev/parse-command.md
|
51
|
+
- docs/dev/vm-profiling.md
|
51
52
|
- docs/development/README.md
|
52
53
|
- docs/development/error-reporting.md
|
53
54
|
- docs/features/README.md
|
54
|
-
- docs/features/analysis-cascade-mutual-exclusion.md
|
55
55
|
- docs/features/analysis-type-inference.md
|
56
56
|
- docs/features/analysis-unsat-detection.md
|
57
57
|
- docs/features/hierarchical-broadcasting.md
|
@@ -93,6 +93,7 @@ files:
|
|
93
93
|
- lib/kumi/core/analyzer/passes/function_signature_pass.rb
|
94
94
|
- lib/kumi/core/analyzer/passes/input_access_planner_pass.rb
|
95
95
|
- lib/kumi/core/analyzer/passes/input_collector.rb
|
96
|
+
- lib/kumi/core/analyzer/passes/ir_dependency_pass.rb
|
96
97
|
- lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb
|
97
98
|
- lib/kumi/core/analyzer/passes/load_input_cse.rb
|
98
99
|
- lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb
|
@@ -188,8 +189,11 @@ files:
|
|
188
189
|
- lib/kumi/core/types/inference.rb
|
189
190
|
- lib/kumi/core/types/normalizer.rb
|
190
191
|
- lib/kumi/core/types/validator.rb
|
192
|
+
- lib/kumi/dev.rb
|
191
193
|
- lib/kumi/dev/ir.rb
|
192
194
|
- lib/kumi/dev/parse.rb
|
195
|
+
- lib/kumi/dev/profile_aggregator.rb
|
196
|
+
- lib/kumi/dev/profile_runner.rb
|
193
197
|
- lib/kumi/dev/runner.rb
|
194
198
|
- lib/kumi/errors.rb
|
195
199
|
- lib/kumi/frontends.rb
|
@@ -1,89 +0,0 @@
|
|
1
|
-
# Cascade Mutual Exclusion Detection
|
2
|
-
|
3
|
-
Analyzes cascade expressions to allow safe recursive patterns when conditions are mutually exclusive.
|
4
|
-
|
5
|
-
## Overview
|
6
|
-
|
7
|
-
The cascade mutual exclusion detector identifies when all conditions in a cascade expression cannot be true simultaneously, enabling safe mutual recursion patterns that would otherwise be rejected as cycles.
|
8
|
-
|
9
|
-
## Core Mechanism
|
10
|
-
|
11
|
-
The system performs three-stage analysis:
|
12
|
-
|
13
|
-
1. **Conditional Dependency Tracking** - DependencyResolver marks base case dependencies as conditional
|
14
|
-
2. **Mutual Exclusion Analysis** - UnsatDetector determines if cascade conditions are mutually exclusive
|
15
|
-
3. **Safe Cycle Detection** - Toposorter allows cycles where all edges are conditional and conditions are mutually exclusive
|
16
|
-
|
17
|
-
## Example: Processing Workflow
|
18
|
-
|
19
|
-
```ruby
|
20
|
-
schema do
|
21
|
-
input do
|
22
|
-
string :operation # "forward", "reverse", "unknown"
|
23
|
-
integer :value
|
24
|
-
end
|
25
|
-
|
26
|
-
trait :is_forward, input.operation == "forward"
|
27
|
-
trait :is_reverse, input.operation == "reverse"
|
28
|
-
|
29
|
-
# Safe mutual recursion - conditions are mutually exclusive
|
30
|
-
value :forward_processor do
|
31
|
-
on is_forward, input.value * 2 # Direct calculation
|
32
|
-
on is_reverse, reverse_processor + 10 # Delegates to reverse (safe)
|
33
|
-
base "invalid operation" # Fallback for unknown operations
|
34
|
-
end
|
35
|
-
|
36
|
-
value :reverse_processor do
|
37
|
-
on is_forward, forward_processor - 5 # Delegates to forward (safe)
|
38
|
-
on is_reverse, input.value / 2 # Direct calculation
|
39
|
-
base "invalid operation" # Fallback for unknown operations
|
40
|
-
end
|
41
|
-
end
|
42
|
-
```
|
43
|
-
|
44
|
-
## Safety Guarantees
|
45
|
-
|
46
|
-
**Allowed**: Cycles where conditions are mutually exclusive
|
47
|
-
- `is_forward` and `is_reverse` cannot both be true (operation has single value)
|
48
|
-
- Each recursion executes exactly one step before hitting direct calculation
|
49
|
-
- Bounded recursion with guaranteed termination
|
50
|
-
|
51
|
-
**Rejected**: Cycles with overlapping conditions
|
52
|
-
```ruby
|
53
|
-
# This would be rejected - conditions can overlap
|
54
|
-
value :unsafe_cycle do
|
55
|
-
on input.n > 0, "positive"
|
56
|
-
on input.n > 5, "large" # Both can be true!
|
57
|
-
base fn(:not, unsafe_cycle)
|
58
|
-
end
|
59
|
-
```
|
60
|
-
|
61
|
-
## Implementation Details
|
62
|
-
|
63
|
-
### Conditional Dependencies
|
64
|
-
Base case dependencies are marked as conditional because they only execute when no explicit conditions match.
|
65
|
-
|
66
|
-
### Mutual Exclusion Analysis
|
67
|
-
Conditions are analyzed for mutual exclusion:
|
68
|
-
- Same field equality comparisons: `field == value1` vs `field == value2`
|
69
|
-
- Domain constraints ensuring impossibility
|
70
|
-
- All condition pairs must be mutually exclusive
|
71
|
-
|
72
|
-
### Metadata Generation
|
73
|
-
Analysis results stored in `cascade_metadata` state:
|
74
|
-
```ruby
|
75
|
-
{
|
76
|
-
condition_traits: [:is_forward, :is_reverse],
|
77
|
-
condition_count: 2,
|
78
|
-
all_mutually_exclusive: true,
|
79
|
-
exclusive_pairs: 1,
|
80
|
-
total_pairs: 1
|
81
|
-
}
|
82
|
-
```
|
83
|
-
|
84
|
-
## Use Cases
|
85
|
-
|
86
|
-
- Processing workflows with bidirectional logic
|
87
|
-
- State machine fallback patterns
|
88
|
-
- Recursive decision trees with termination conditions
|
89
|
-
- Complex business rules with safe delegation patterns
|