kumi 0.0.13 → 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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +0 -1
  3. data/BACKLOG.md +34 -0
  4. data/CHANGELOG.md +33 -0
  5. data/CLAUDE.md +4 -6
  6. data/README.md +0 -45
  7. data/config/functions.yaml +352 -0
  8. data/docs/dev/analyzer-debug.md +52 -0
  9. data/docs/dev/parse-command.md +64 -0
  10. data/docs/dev/vm-profiling.md +95 -0
  11. data/docs/features/README.md +0 -7
  12. data/docs/functions/analyzer_integration.md +199 -0
  13. data/docs/functions/signatures.md +171 -0
  14. data/examples/hash_objects_demo.rb +138 -0
  15. data/golden/array_operations/schema.kumi +17 -0
  16. data/golden/cascade_logic/schema.kumi +16 -0
  17. data/golden/mixed_nesting/schema.kumi +42 -0
  18. data/golden/simple_math/schema.kumi +10 -0
  19. data/lib/kumi/analyzer.rb +76 -22
  20. data/lib/kumi/compiler.rb +6 -5
  21. data/lib/kumi/core/analyzer/checkpoint.rb +72 -0
  22. data/lib/kumi/core/analyzer/debug.rb +167 -0
  23. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +1 -3
  24. data/lib/kumi/core/analyzer/passes/function_signature_pass.rb +199 -0
  25. data/lib/kumi/core/analyzer/passes/ir_dependency_pass.rb +67 -0
  26. data/lib/kumi/core/analyzer/passes/load_input_cse.rb +120 -0
  27. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +72 -157
  28. data/lib/kumi/core/analyzer/passes/toposorter.rb +40 -36
  29. data/lib/kumi/core/analyzer/state_serde.rb +64 -0
  30. data/lib/kumi/core/analyzer/structs/access_plan.rb +12 -10
  31. data/lib/kumi/core/compiler/access_planner.rb +3 -2
  32. data/lib/kumi/core/function_registry/collection_functions.rb +3 -1
  33. data/lib/kumi/core/functions/dimension.rb +98 -0
  34. data/lib/kumi/core/functions/dtypes.rb +20 -0
  35. data/lib/kumi/core/functions/errors.rb +11 -0
  36. data/lib/kumi/core/functions/kernel_adapter.rb +45 -0
  37. data/lib/kumi/core/functions/loader.rb +119 -0
  38. data/lib/kumi/core/functions/registry_v2.rb +68 -0
  39. data/lib/kumi/core/functions/shape.rb +70 -0
  40. data/lib/kumi/core/functions/signature.rb +122 -0
  41. data/lib/kumi/core/functions/signature_parser.rb +86 -0
  42. data/lib/kumi/core/functions/signature_resolver.rb +272 -0
  43. data/lib/kumi/core/ir/execution_engine/interpreter.rb +110 -7
  44. data/lib/kumi/core/ir/execution_engine/profiler.rb +330 -0
  45. data/lib/kumi/core/ir/execution_engine.rb +6 -15
  46. data/lib/kumi/dev/ir.rb +75 -0
  47. data/lib/kumi/dev/parse.rb +105 -0
  48. data/lib/kumi/dev/profile_aggregator.rb +301 -0
  49. data/lib/kumi/dev/profile_runner.rb +199 -0
  50. data/lib/kumi/dev/runner.rb +85 -0
  51. data/lib/kumi/dev.rb +14 -0
  52. data/lib/kumi/frontends/ruby.rb +28 -0
  53. data/lib/kumi/frontends/text.rb +46 -0
  54. data/lib/kumi/frontends.rb +29 -0
  55. data/lib/kumi/kernels/ruby/aggregate_core.rb +105 -0
  56. data/lib/kumi/kernels/ruby/datetime_scalar.rb +21 -0
  57. data/lib/kumi/kernels/ruby/mask_scalar.rb +15 -0
  58. data/lib/kumi/kernels/ruby/scalar_core.rb +63 -0
  59. data/lib/kumi/kernels/ruby/string_scalar.rb +19 -0
  60. data/lib/kumi/kernels/ruby/vector_struct.rb +39 -0
  61. data/lib/kumi/runtime/executable.rb +108 -45
  62. data/lib/kumi/schema.rb +12 -6
  63. data/lib/kumi/support/diff.rb +22 -0
  64. data/lib/kumi/support/ir_render.rb +61 -0
  65. data/lib/kumi/version.rb +1 -1
  66. data/lib/kumi.rb +3 -0
  67. data/performance_results.txt +63 -0
  68. data/scripts/test_mixed_nesting_performance.rb +206 -0
  69. metadata +50 -6
  70. data/docs/features/analysis-cascade-mutual-exclusion.md +0 -89
  71. data/docs/features/javascript-transpiler.md +0 -148
  72. data/lib/kumi/js.rb +0 -23
  73. data/lib/kumi/support/ir_dump.rb +0 -491
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Frontends
5
+ def self.load(path:, inputs: {})
6
+ mode = (ENV["KUMI_PARSER"] || "auto") # auto|text|ruby
7
+ ext = File.extname(path)
8
+
9
+ # Explicit mode selection
10
+ return Text.load(path:, inputs:) if mode == "text"
11
+ return Ruby.load(path:, inputs:) if mode == "ruby"
12
+
13
+ # Auto mode: prefer .kumi if present
14
+ if mode == "auto" && ext == ".rb"
15
+ kumi_path = path.sub(/\.rb\z/, ".kumi")
16
+ if File.exist?(kumi_path)
17
+ return Text.load(path: kumi_path, inputs: inputs)
18
+ end
19
+ end
20
+
21
+ # File extension based selection
22
+ return Text.load(path:, inputs:) if ext == ".kumi"
23
+ return Ruby.load(path:, inputs:) if ext == ".rb"
24
+
25
+ # Default fallback
26
+ Ruby.load(path:, inputs:)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Kernels
5
+ module Ruby
6
+ module AggregateCore
7
+ module_function
8
+
9
+ def kumi_sum(enum, skip_nulls: true, min_count: 0)
10
+ total = 0
11
+ count = 0
12
+ enum.each do |x|
13
+ next if skip_nulls && x.nil?
14
+
15
+ total += x
16
+ count += 1
17
+ end
18
+ return nil if count < min_count
19
+
20
+ total
21
+ end
22
+
23
+ def kumi_min(enum, skip_nulls: true, min_count: 0)
24
+ best = nil
25
+ count = 0
26
+ enum.each do |x|
27
+ next if skip_nulls && x.nil?
28
+
29
+ best = x if best.nil? || x < best
30
+ count += 1
31
+ end
32
+ return nil if count < min_count
33
+
34
+ best
35
+ end
36
+
37
+ def kumi_max(enum, skip_nulls: true, min_count: 0)
38
+ best = nil
39
+ count = 0
40
+ enum.each do |x|
41
+ next if skip_nulls && x.nil?
42
+
43
+ best = x if best.nil? || x > best
44
+ count += 1
45
+ end
46
+ return nil if count < min_count
47
+
48
+ best
49
+ end
50
+
51
+ def kumi_mean(enum, skip_nulls: true, min_count: 0)
52
+ total = 0.0
53
+ count = 0
54
+ enum.each do |x|
55
+ next if skip_nulls && x.nil?
56
+
57
+ total += x
58
+ count += 1
59
+ end
60
+ return nil if count < [min_count, 1].max
61
+
62
+ total / count
63
+ end
64
+
65
+ def kumi_any(enum, skip_nulls: true, min_count: 0)
66
+ count = 0
67
+ enum.each do |x|
68
+ next if skip_nulls && x.nil?
69
+
70
+ return true if x
71
+ count += 1
72
+ end
73
+ return nil if count < min_count
74
+
75
+ false
76
+ end
77
+
78
+ def kumi_all(enum, skip_nulls: true, min_count: 0)
79
+ count = 0
80
+ enum.each do |x|
81
+ next if skip_nulls && x.nil?
82
+
83
+ return false unless x
84
+ count += 1
85
+ end
86
+ return nil if count < min_count
87
+
88
+ true
89
+ end
90
+
91
+ def kumi_count(enum, skip_nulls: true, min_count: 0)
92
+ count = 0
93
+ enum.each do |x|
94
+ next if skip_nulls && x.nil?
95
+
96
+ count += 1
97
+ end
98
+ return nil if count < min_count
99
+
100
+ count
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module Kumi
6
+ module Kernels
7
+ module Ruby
8
+ module DatetimeScalar
9
+ module_function
10
+
11
+ def dt_add_days(d, n)
12
+ d + n
13
+ end
14
+
15
+ def dt_diff_days(d1, d2)
16
+ (d1 - d2).to_i
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Kernels
5
+ module Ruby
6
+ module MaskScalar
7
+ module_function
8
+
9
+ def where(condition, if_true, if_false)
10
+ condition ? if_true : if_false
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Kernels
5
+ module Ruby
6
+ module ScalarCore
7
+ module_function
8
+
9
+ def kumi_add(a, b)
10
+ a + b
11
+ end
12
+
13
+ def kumi_sub(a, b)
14
+ a - b
15
+ end
16
+
17
+ def kumi_mul(a, b)
18
+ a * b
19
+ end
20
+
21
+ def kumi_div(a, b)
22
+ a / b.to_f
23
+ end
24
+
25
+ def kumi_eq(a, b)
26
+ a == b
27
+ end
28
+
29
+ def kumi_gt(a, b)
30
+ a > b
31
+ end
32
+
33
+ def kumi_gte(a, b)
34
+ a >= b
35
+ end
36
+
37
+ def kumi_lt(a, b)
38
+ a < b
39
+ end
40
+
41
+ def kumi_lte(a, b)
42
+ a <= b
43
+ end
44
+
45
+ def kumi_ne(a, b)
46
+ a != b
47
+ end
48
+
49
+ def kumi_and(a, b)
50
+ a && b
51
+ end
52
+
53
+ def kumi_or(a, b)
54
+ a || b
55
+ end
56
+
57
+ def kumi_not(a)
58
+ !a
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Kernels
5
+ module Ruby
6
+ module StringScalar
7
+ module_function
8
+
9
+ def str_concat(a, b)
10
+ "#{a}#{b}"
11
+ end
12
+
13
+ def str_length(s)
14
+ s&.length
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Kernels
5
+ module Ruby
6
+ module VectorStruct
7
+ module_function
8
+
9
+ def size(vec)
10
+ vec&.size
11
+ end
12
+
13
+ def join_zip(left, right)
14
+ raise NotImplementedError, "join operations should be implemented in IR/VM"
15
+ end
16
+
17
+ def join_product(left, right)
18
+ raise NotImplementedError, "join operations should be implemented in IR/VM"
19
+ end
20
+
21
+ def align_to(vec, target_axes)
22
+ raise NotImplementedError, "align_to should be implemented in IR/VM"
23
+ end
24
+
25
+ def lift(vec, indices)
26
+ raise NotImplementedError, "lift should be implemented in IR/VM"
27
+ end
28
+
29
+ def flatten(*args)
30
+ raise NotImplementedError, "flatten should be implemented in IR/VM"
31
+ end
32
+
33
+ def take(values, indices)
34
+ raise NotImplementedError, "take should be implemented in IR/VM"
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -37,30 +37,49 @@ 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
- accessors = Kumi::Core::Compiler::AccessBuilder.build(access_plans)
44
+ dependents = state[:dependents] || {}
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
45
50
 
46
51
  access_meta = {}
52
+ field_to_plan_ids = Hash.new { |h, k| h[k] = [] }
53
+
47
54
  access_plans.each_value do |plans|
48
55
  plans.each do |p|
49
56
  access_meta[p.accessor_key] = { mode: p.mode, scope: p.scope }
57
+
58
+ # Build precise field -> plan_ids mapping for invalidation
59
+ root_field = p.accessor_key.to_s.split(":").first.split(".").first.to_sym
60
+ field_to_plan_ids[root_field] << p.accessor_key
50
61
  end
51
62
  end
52
63
 
53
64
  # Use the internal functions hash that VM expects
54
65
  registry ||= Kumi::Registry.functions
55
- new(ir: ir, accessors: accessors, access_meta: access_meta, registry: registry, input_metadata: input_metadata)
66
+ new(ir: ir, accessors: accessors, access_meta: access_meta, registry: registry,
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)
56
69
  end
57
70
 
58
- def initialize(ir:, accessors:, access_meta:, registry:, input_metadata:)
71
+ def initialize(ir:, accessors:, access_meta:, registry:, input_metadata:, field_to_plan_ids: {}, dependents: {}, ir_dependencies: {},
72
+ name_index: {}, schema_name: nil)
59
73
  @ir = ir.freeze
60
74
  @acc = accessors.freeze
61
75
  @meta = access_meta.freeze
62
76
  @reg = registry
63
77
  @input_metadata = input_metadata.freeze
78
+ @field_to_plan_ids = field_to_plan_ids.freeze
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
64
83
  @decl = @ir.decls.map { |d| [d.name, d] }.to_h
65
84
  @accessor_cache = {} # Persistent accessor cache across evaluations
66
85
  end
@@ -68,14 +87,14 @@ module Kumi
68
87
  def decl?(name) = @decl.key?(name)
69
88
 
70
89
  def read(input, mode: :ruby)
71
- Run.new(self, input, mode: mode, input_metadata: @input_metadata)
90
+ Run.new(self, input, mode: mode, input_metadata: @input_metadata, dependents: @dependents)
72
91
  end
73
92
 
74
93
  # API compatibility for backward compatibility
75
94
  def evaluate(ctx, *key_names)
76
95
  target_keys = key_names.empty? ? @decl.keys : validate_keys(key_names)
77
96
 
78
- # Handle context wrapping for backward compatibility
97
+ # Handle context wrapping for backward compatibility
79
98
  input = ctx.respond_to?(:ctx) ? ctx.ctx : ctx
80
99
 
81
100
  target_keys.each_with_object({}) do |key, result|
@@ -83,21 +102,71 @@ module Kumi
83
102
  end
84
103
  end
85
104
 
86
- def eval_decl(name, input, mode: :ruby)
105
+ def eval_decl(name, input, mode: :ruby, declaration_cache: nil)
87
106
  raise Kumi::Core::Errors::RuntimeError, "unknown decl #{name}" unless decl?(name)
88
107
 
89
- out = Kumi::Core::IR::ExecutionEngine.run(@ir, { input: input, target: name, accessor_cache: @accessor_cache },
90
- accessors: @acc, registry: @reg).fetch(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,
113
+ accessor_cache: @accessor_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
119
+ }
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
91
124
 
92
125
  mode == :ruby ? unwrap(@decl[name], out) : out
93
126
  end
94
127
 
95
128
  def clear_field_accessor_cache(field_name)
96
- # Clear cache entries for all accessor plans related to this field
97
- # Cache keys are now [plan_id, input_key] arrays
98
- @accessor_cache.delete_if { |cache_key, _|
99
- cache_key.is_a?(Array) && cache_key[0].to_s.start_with?("#{field_name}:")
100
- }
129
+ # Use precise field -> plan_ids mapping for exact invalidation
130
+ plan_ids = @field_to_plan_ids[field_name] || []
131
+ # Cache keys are [plan_id, input_object_id] arrays
132
+ @accessor_cache.delete_if { |(pid, _), _| plan_ids.include?(pid) }
133
+ end
134
+
135
+ def unwrap(_decl, v)
136
+ v[:k] == :scalar ? v[:v] : v # no grouping needed
137
+ end
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] }
101
170
  end
102
171
 
103
172
  private
@@ -108,25 +177,29 @@ module Kumi
108
177
 
109
178
  raise Kumi::Errors::RuntimeError, "No binding named #{unknown_keys.first}"
110
179
  end
111
-
112
- private
113
-
114
- def unwrap(_decl, v)
115
- v[:k] == :scalar ? v[:v] : v # no grouping needed
116
- end
117
180
  end
118
181
 
119
182
  class Run
120
- def initialize(program, input, mode:, input_metadata:)
183
+ def initialize(program, input, mode:, input_metadata:, dependents:)
121
184
  @program = program
122
185
  @input = input
123
186
  @mode = mode
124
187
  @input_metadata = input_metadata
188
+ @dependents = dependents
125
189
  @cache = {}
126
190
  end
127
191
 
128
192
  def get(name)
129
- @cache[name] ||= @program.eval_decl(name, @input, mode: @mode)
193
+ unless @cache.key?(name)
194
+ # Get the result in VM internal format
195
+ vm_result = @program.eval_decl(name, @input, mode: :wrapped, declaration_cache: @cache)
196
+ # Store VM format for cross-VM caching
197
+ @cache[name] = vm_result
198
+ end
199
+
200
+ # Convert to requested format when returning
201
+ vm_result = @cache[name]
202
+ @mode == :wrapped ? vm_result : @program.unwrap(nil, vm_result)
130
203
  end
131
204
 
132
205
  def [](name)
@@ -135,6 +208,7 @@ module Kumi
135
208
 
136
209
  def slice(*keys)
137
210
  return {} if keys.empty?
211
+
138
212
  keys.each_with_object({}) { |key, result| result[key] = get(key) }
139
213
  end
140
214
 
@@ -142,7 +216,7 @@ module Kumi
142
216
  @program
143
217
  end
144
218
 
145
- def method_missing(sym, *args, **kwargs, &blk)
219
+ def method_missing(sym, *args, **kwargs, &)
146
220
  return super unless args.empty? && kwargs.empty? && @program.decl?(sym)
147
221
 
148
222
  get(sym)
@@ -153,6 +227,8 @@ module Kumi
153
227
  end
154
228
 
155
229
  def update(**changes)
230
+ affected_declarations = Set.new
231
+
156
232
  changes.each do |field, value|
157
233
  # Validate field exists
158
234
  raise ArgumentError, "unknown input field: #{field}" unless input_field_exists?(field)
@@ -160,27 +236,20 @@ module Kumi
160
236
  # Validate domain constraints
161
237
  validate_domain_constraint(field, value)
162
238
 
163
- # Update the input data
164
- @input = deep_merge(@input, { field => value })
165
-
239
+ # Update the input data IN-PLACE to preserve object_id for cache keys
240
+ @input[field] = value
241
+
166
242
  # Clear accessor cache for this specific field
167
243
  @program.clear_field_accessor_cache(field)
168
- end
169
244
 
170
- # Clear declaration evaluation cache after all updates
171
- @cache.clear
172
- self
173
- end
245
+ # Collect all declarations that depend on this input field
246
+ field_dependents = @dependents[field] || []
247
+ affected_declarations.merge(field_dependents)
248
+ end
174
249
 
175
- def wrapped!
176
- @mode = :wrapped
177
- @cache.clear
178
- self
179
- end
250
+ # Only clear cache for affected declarations, not all declarations
251
+ affected_declarations.each { |decl| @cache.delete(decl) }
180
252
 
181
- def ruby!
182
- @mode = :ruby
183
- @cache.clear
184
253
  self
185
254
  end
186
255
 
@@ -214,12 +283,6 @@ module Kumi
214
283
  false
215
284
  end
216
285
  end
217
-
218
- def deep_merge(a, b)
219
- return b unless a.is_a?(Hash) && b.is_a?(Hash)
220
-
221
- a.merge(b) { |_k, v1, v2| deep_merge(v1, v2) }
222
- end
223
286
  end
224
287
  end
225
288
  end
data/lib/kumi/schema.rb CHANGED
@@ -42,19 +42,25 @@ module Kumi
42
42
  nil
43
43
  end
44
44
 
45
- def build_syntax_tree(&block)
46
- @__syntax_tree__ = Core::RubyParser::Dsl.build_syntax_tree(&block).freeze
45
+ def build_syntax_tree(&)
46
+ @__syntax_tree__ = Core::RubyParser::Dsl.build_syntax_tree(&).freeze
47
47
  end
48
48
 
49
- def schema(&block)
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(&block).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
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Support
5
+ module Diff
6
+ module_function
7
+
8
+ def unified(a_str, b_str)
9
+ a = a_str.lines
10
+ b = b_str.lines
11
+ out = []
12
+ max = [a.size, b.size].max
13
+ (0...max).each do |i|
14
+ next if a[i] == b[i]
15
+ out << format("%4d- %s", i + 1, a[i] || "")
16
+ out << format("%4d+ %s", i + 1, b[i] || "")
17
+ end
18
+ out.join
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Kumi
6
+ module Support
7
+ module IRRender
8
+ module_function
9
+
10
+ # Stable JSON for goldens (simple canonical serialization)
11
+ def to_json(ir_module, pretty: true)
12
+ raise "nil IR" unless ir_module
13
+
14
+ data = {
15
+ inputs: ir_module.inputs,
16
+ decls: ir_module.decls.map do |decl|
17
+ {
18
+ name: decl.name,
19
+ kind: decl.kind,
20
+ shape: decl.shape,
21
+ ops: decl.ops.map do |op|
22
+ {
23
+ tag: op.tag,
24
+ attrs: op.attrs,
25
+ args: op.args
26
+ }
27
+ end
28
+ }
29
+ end
30
+ }
31
+
32
+ if pretty
33
+ JSON.pretty_generate(data)
34
+ else
35
+ JSON.generate(data)
36
+ end
37
+ end
38
+
39
+ # Human pretty text (using IRDump)
40
+ def to_text(ir_module, analysis_state: nil)
41
+ raise "nil IR" unless ir_module
42
+
43
+ if defined?(Kumi::Support::IRDump)
44
+ # Convert AnalysisState to hash if needed
45
+ state_hash = analysis_state.to_h
46
+ else
47
+ # Fallback: simple text representation
48
+ lines = []
49
+ lines << "IR Module (#{ir_module.decls.size} declarations):"
50
+ ir_module.decls.each_with_index do |decl, i|
51
+ lines << " [#{i}] #{decl.kind.upcase} #{decl.name} (#{decl.ops.size} ops)"
52
+ decl.ops.each_with_index do |op, j|
53
+ lines << " #{j}: #{op.tag.upcase} #{op.attrs.inspect} #{op.args.inspect}"
54
+ end
55
+ end
56
+ lines.join("\n")
57
+ end
58
+ end
59
+ end
60
+ end
61
+ 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.13"
4
+ VERSION = "0.0.15"
5
5
  end