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.
- checksums.yaml +4 -4
- data/.rspec +0 -1
- data/BACKLOG.md +34 -0
- data/CHANGELOG.md +33 -0
- data/CLAUDE.md +4 -6
- data/README.md +0 -45
- data/config/functions.yaml +352 -0
- data/docs/dev/analyzer-debug.md +52 -0
- data/docs/dev/parse-command.md +64 -0
- data/docs/dev/vm-profiling.md +95 -0
- data/docs/features/README.md +0 -7
- data/docs/functions/analyzer_integration.md +199 -0
- data/docs/functions/signatures.md +171 -0
- data/examples/hash_objects_demo.rb +138 -0
- data/golden/array_operations/schema.kumi +17 -0
- data/golden/cascade_logic/schema.kumi +16 -0
- data/golden/mixed_nesting/schema.kumi +42 -0
- data/golden/simple_math/schema.kumi +10 -0
- data/lib/kumi/analyzer.rb +76 -22
- data/lib/kumi/compiler.rb +6 -5
- data/lib/kumi/core/analyzer/checkpoint.rb +72 -0
- data/lib/kumi/core/analyzer/debug.rb +167 -0
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +1 -3
- data/lib/kumi/core/analyzer/passes/function_signature_pass.rb +199 -0
- data/lib/kumi/core/analyzer/passes/ir_dependency_pass.rb +67 -0
- data/lib/kumi/core/analyzer/passes/load_input_cse.rb +120 -0
- data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +72 -157
- data/lib/kumi/core/analyzer/passes/toposorter.rb +40 -36
- data/lib/kumi/core/analyzer/state_serde.rb +64 -0
- data/lib/kumi/core/analyzer/structs/access_plan.rb +12 -10
- data/lib/kumi/core/compiler/access_planner.rb +3 -2
- data/lib/kumi/core/function_registry/collection_functions.rb +3 -1
- data/lib/kumi/core/functions/dimension.rb +98 -0
- data/lib/kumi/core/functions/dtypes.rb +20 -0
- data/lib/kumi/core/functions/errors.rb +11 -0
- data/lib/kumi/core/functions/kernel_adapter.rb +45 -0
- data/lib/kumi/core/functions/loader.rb +119 -0
- data/lib/kumi/core/functions/registry_v2.rb +68 -0
- data/lib/kumi/core/functions/shape.rb +70 -0
- data/lib/kumi/core/functions/signature.rb +122 -0
- data/lib/kumi/core/functions/signature_parser.rb +86 -0
- data/lib/kumi/core/functions/signature_resolver.rb +272 -0
- data/lib/kumi/core/ir/execution_engine/interpreter.rb +110 -7
- data/lib/kumi/core/ir/execution_engine/profiler.rb +330 -0
- data/lib/kumi/core/ir/execution_engine.rb +6 -15
- data/lib/kumi/dev/ir.rb +75 -0
- data/lib/kumi/dev/parse.rb +105 -0
- data/lib/kumi/dev/profile_aggregator.rb +301 -0
- data/lib/kumi/dev/profile_runner.rb +199 -0
- data/lib/kumi/dev/runner.rb +85 -0
- data/lib/kumi/dev.rb +14 -0
- data/lib/kumi/frontends/ruby.rb +28 -0
- data/lib/kumi/frontends/text.rb +46 -0
- data/lib/kumi/frontends.rb +29 -0
- data/lib/kumi/kernels/ruby/aggregate_core.rb +105 -0
- data/lib/kumi/kernels/ruby/datetime_scalar.rb +21 -0
- data/lib/kumi/kernels/ruby/mask_scalar.rb +15 -0
- data/lib/kumi/kernels/ruby/scalar_core.rb +63 -0
- data/lib/kumi/kernels/ruby/string_scalar.rb +19 -0
- data/lib/kumi/kernels/ruby/vector_struct.rb +39 -0
- data/lib/kumi/runtime/executable.rb +108 -45
- data/lib/kumi/schema.rb +12 -6
- data/lib/kumi/support/diff.rb +22 -0
- data/lib/kumi/support/ir_render.rb +61 -0
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +3 -0
- data/performance_results.txt +63 -0
- data/scripts/test_mixed_nesting_performance.rb +206 -0
- metadata +50 -6
- data/docs/features/analysis-cascade-mutual-exclusion.md +0 -89
- data/docs/features/javascript-transpiler.md +0 -148
- data/lib/kumi/js.rb +0 -23
- 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,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,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
|
-
|
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,
|
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
|
-
|
90
|
-
|
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
|
-
#
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
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, &
|
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 =
|
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
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
176
|
-
@
|
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(&
|
46
|
-
@__syntax_tree__ = Core::RubyParser::Dsl.build_syntax_tree(&
|
45
|
+
def build_syntax_tree(&)
|
46
|
+
@__syntax_tree__ = Core::RubyParser::Dsl.build_syntax_tree(&).freeze
|
47
47
|
end
|
48
48
|
|
49
|
-
def schema(&
|
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
|
@@ -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