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.
@@ -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
@@ -19,7 +19,9 @@ module Kumi
19
19
  errors = []
20
20
 
21
21
  begin
22
- final_state = Kumi::Analyzer.run_analysis_passes(schema, Kumi::Analyzer::DEFAULT_PASSES, state, errors)
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
- accessors = Kumi::Core::Compiler::AccessBuilder.build(access_plans)
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
- vm_context = {
100
- input: input,
101
- target: name,
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 = Kumi::Core::IR::ExecutionEngine.run(@ir, vm_context, accessors: @acc, registry: @reg).fetch(name)
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__ = Core::RubyParser::Dsl.build_syntax_tree(&).freeze
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__ = Analyzer.analyze!(@__syntax_tree__).freeze
57
- @__compiled_schema__ = Compiler.compile(@__syntax_tree__, analyzer: @__analyzer_result__).freeze
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kumi
4
- VERSION = "0.0.14"
4
+ VERSION = "0.0.15"
5
5
  end
data/lib/kumi.rb CHANGED
@@ -8,6 +8,7 @@ loader.ignore("#{__dir__}/kumi-cli")
8
8
  loader.inflector.inflect(
9
9
  "lower_to_ir_pass" => "LowerToIRPass",
10
10
  "load_input_cse" => "LoadInputCSE",
11
+ "ir_dependency_pass" => "IRDependencyPass",
11
12
  "vm" => "VM",
12
13
  "ir" => "IR",
13
14
  'ir_dump' => 'IRDump',
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.14
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