kumi 0.0.11 → 0.0.13
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/.rubocop.yml +1 -1
- data/CHANGELOG.md +20 -0
- data/README.md +4 -4
- data/docs/SYNTAX.md +66 -0
- data/docs/features/hierarchical-broadcasting.md +66 -0
- data/docs/features/input-declaration-system.md +16 -0
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +15 -21
- data/lib/kumi/core/analyzer/passes/input_collector.rb +9 -4
- data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +35 -2
- data/lib/kumi/core/compiler/access_planner.rb +3 -3
- data/lib/kumi/core/input/validator.rb +1 -1
- data/lib/kumi/core/ir/execution_engine.rb +30 -1
- data/lib/kumi/core/ruby_parser/input_builder.rb +25 -4
- data/lib/kumi/core/types/validator.rb +1 -1
- data/lib/kumi/runtime/executable.rb +14 -2
- data/lib/kumi/version.rb +1 -1
- metadata +12 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7af4bdb11ef5da5a25b799cef8387752636a8224634c70656efc203b9d923185
|
4
|
+
data.tar.gz: 4e604513e42dcb8754fab54ead4140d5ca2d8443ae4d0461e62dc3b48bb925d4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dcbd93268081ff8f0bcf112101603b180932196b1d6fcb59aae2f363d6bbfe4e9ea07cf3cbe811290222dcd15cdf132ea04f409bea34f81599412f729211a5fa
|
7
|
+
data.tar.gz: 3e0c2e91609e742c2852457b2dd61a114a03b988ae983e0cfb23c9bbf46bc5c6c1714636961ed46b4b9ae924c6a77a79dc0aab9013cadbb73e49d5374f8b8dbc
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,23 @@
|
|
1
|
+
## [Unreleased]
|
2
|
+
|
3
|
+
## [0.0.13] – 2025-08-14
|
4
|
+
### Added
|
5
|
+
- Runtime performance optimizations for interpreter execution
|
6
|
+
- Input load deduplication to cache loaded input values and avoid redundant operations
|
7
|
+
- Constant folding optimization to evaluate literal expressions during compilation
|
8
|
+
- Accessor memoization with proper cache isolation per input context
|
9
|
+
- Selective cache invalidation for incremental updates
|
10
|
+
|
11
|
+
### Fixed
|
12
|
+
- Cache isolation between different input contexts preventing cross-context pollution
|
13
|
+
- Cascade mutual exclusion tests now pass correctly with proper trait evaluation
|
14
|
+
- Incremental update performance with targeted cache clearing
|
15
|
+
|
16
|
+
## [0.0.12] – 2025-08-14
|
17
|
+
### Added
|
18
|
+
- Hash objects input declarations with `hash :field do ... end` syntax
|
19
|
+
- Complete hash object integration with arrays, nesting, and broadcasting
|
20
|
+
|
1
21
|
## [0.0.11] – 2025-08-13
|
2
22
|
### Added
|
3
23
|
- Intermediate Representation (IR) and slot-based VM interpreter.
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
[](https://github.com/amuta/kumi/actions)
|
4
4
|
[](https://badge.fury.io/rb/kumi)
|
5
5
|
|
6
|
-
Kumi is a
|
6
|
+
**Kumi** is a declarative rules-and-calculation DSL for Ruby. It compiles business logic into a **typed, analyzable dependency graph** with **vector semantics** over nested data, performs **static checks** at definition time, **lowers to a compact IR**, and executes **deterministically**.
|
7
7
|
|
8
8
|
It handles complex, interdependent calculations with validation and consistency checking.
|
9
9
|
|
@@ -436,8 +436,8 @@ runner[:after_tax] # => 196,844.80 (cached)
|
|
436
436
|
- Sequential procedural workflows
|
437
437
|
- High-frequency processing
|
438
438
|
|
439
|
-
## JavaScript Transpiler
|
440
|
-
|
439
|
+
## JavaScript Transpiler (V 0.0.10)
|
440
|
+
Note: On the current 0.0.11 this is disabled but will be back in later versions. reason: IR/Interpreter update.
|
441
441
|
Transpiles compiled schemas to standalone JavaScript code. See [docs/features/javascript-transpiler.md](docs/features/javascript-transpiler.md) for details.
|
442
442
|
|
443
443
|
```ruby
|
@@ -465,7 +465,7 @@ See [docs/features/performance.md](docs/features/performance.md) for detailed be
|
|
465
465
|
|
466
466
|
## What Kumi does not guarantee
|
467
467
|
|
468
|
-
Lambdas (e.g. -> inside the schema), external IO, floating-point vs bignum,
|
468
|
+
Lambdas (e.g. -> inside the schema), external IO, floating-point vs bignum, time-zone math differences, etc.
|
469
469
|
|
470
470
|
## Learn More
|
471
471
|
|
data/docs/SYNTAX.md
CHANGED
@@ -214,12 +214,14 @@ Array broadcasting enables element-wise operations on array fields with automati
|
|
214
214
|
|
215
215
|
```ruby
|
216
216
|
input do
|
217
|
+
# Structured array with defined fields
|
217
218
|
array :line_items do
|
218
219
|
float :price
|
219
220
|
integer :quantity
|
220
221
|
string :category
|
221
222
|
end
|
222
223
|
|
224
|
+
# Nested arrays with hash objects
|
223
225
|
array :orders do
|
224
226
|
array :items do
|
225
227
|
hash :product do
|
@@ -229,6 +231,15 @@ input do
|
|
229
231
|
integer :quantity
|
230
232
|
end
|
231
233
|
end
|
234
|
+
|
235
|
+
# Dynamic arrays with flexible element types
|
236
|
+
array :api_responses do
|
237
|
+
element :any, :response_data # For dynamic/unknown hash structures
|
238
|
+
end
|
239
|
+
|
240
|
+
array :measurements do
|
241
|
+
element :float, :value # For simple scalar arrays
|
242
|
+
end
|
232
243
|
end
|
233
244
|
```
|
234
245
|
|
@@ -290,6 +301,61 @@ value :avg_line_total, fn(:avg, line_totals)
|
|
290
301
|
trait :has_expensive, fn(:any?, expensive_items)
|
291
302
|
```
|
292
303
|
|
304
|
+
### Dynamic Hash Elements with `element :any`
|
305
|
+
|
306
|
+
For arrays containing hash data with unknown or flexible structure, use `element :any` instead of defining explicit hash objects:
|
307
|
+
|
308
|
+
```ruby
|
309
|
+
input do
|
310
|
+
array :api_responses do
|
311
|
+
element :any, :response_data
|
312
|
+
end
|
313
|
+
|
314
|
+
array :user_profiles do
|
315
|
+
element :any, :profile_info
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# Access hash data using fn(:fetch)
|
320
|
+
value :response_codes, fn(:fetch, input.api_responses.response_data, "status")
|
321
|
+
value :user_names, fn(:fetch, input.user_profiles.profile_info, "name")
|
322
|
+
value :user_ages, fn(:fetch, input.user_profiles.profile_info, "age")
|
323
|
+
|
324
|
+
# Mathematical operations on extracted values
|
325
|
+
value :avg_response_time, fn(:mean, fn(:fetch, input.api_responses.response_data, "response_time"))
|
326
|
+
value :total_users, fn(:size, input.user_profiles.profile_info)
|
327
|
+
|
328
|
+
# Traits using dynamic data
|
329
|
+
trait :success_responses, fn(:any?, fn(:fetch, input.api_responses.response_data, "status") == 200)
|
330
|
+
trait :adult_users, fn(:any?, fn(:fetch, input.user_profiles.profile_info, "age") >= 18)
|
331
|
+
```
|
332
|
+
|
333
|
+
**Use Cases for `element :any`:**
|
334
|
+
- API responses with varying schemas
|
335
|
+
- Configuration data with flexible structure
|
336
|
+
- Dynamic hash structures (unknown keys at schema definition time)
|
337
|
+
- Legacy data where hash structure may vary
|
338
|
+
- When you need maximum flexibility without type constraints
|
339
|
+
|
340
|
+
**Comparison: `element :any` vs Hash Objects**
|
341
|
+
|
342
|
+
```ruby
|
343
|
+
# element :any approach (flexible, dynamic)
|
344
|
+
array :users do
|
345
|
+
element :any, :data
|
346
|
+
end
|
347
|
+
value :names, fn(:fetch, input.users.data, "name")
|
348
|
+
|
349
|
+
# hash object approach (typed, structured)
|
350
|
+
array :users do
|
351
|
+
hash :data do
|
352
|
+
string :name
|
353
|
+
integer :age
|
354
|
+
end
|
355
|
+
end
|
356
|
+
value :names, input.users.data.name
|
357
|
+
```
|
358
|
+
|
293
359
|
### Broadcasting Type Inference
|
294
360
|
|
295
361
|
The type system automatically infers appropriate types:
|
@@ -67,6 +67,72 @@ value :cell_data, input.cube.layer.row.cell # 1D values
|
|
67
67
|
- **Ranked polymorphism**: Same operations work across different dimensional arrays
|
68
68
|
- **Clean code**: `fn(:size, input.cube.layer.row)` instead of `fn(:size, fn(:flatten_one, input.cube.layer))`
|
69
69
|
|
70
|
+
### Dynamic Hash Elements with `element :any`
|
71
|
+
|
72
|
+
For arrays containing hash data with flexible or unknown structure, use `element :any` to access dynamic hash content:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
input do
|
76
|
+
array :api_responses do
|
77
|
+
element :any, :response_data # Flexible hash structure
|
78
|
+
end
|
79
|
+
|
80
|
+
array :user_events do
|
81
|
+
element :any, :event_data # Dynamic event properties (includes event_type)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
# Access hash fields using fn(:fetch)
|
86
|
+
value :response_codes, fn(:fetch, input.api_responses.response_data, "status")
|
87
|
+
value :error_messages, fn(:fetch, input.api_responses.response_data, "error")
|
88
|
+
value :event_types, fn(:fetch, input.user_events.event_data, "event_type")
|
89
|
+
value :user_ids, fn(:fetch, input.user_events.event_data, "user_id")
|
90
|
+
value :timestamps, fn(:fetch, input.user_events.event_data, "timestamp")
|
91
|
+
|
92
|
+
# Mathematical operations on extracted values
|
93
|
+
value :avg_response_time, fn(:mean, fn(:fetch, input.api_responses.response_data, "response_time"))
|
94
|
+
value :total_events, fn(:size, input.user_events.event_data)
|
95
|
+
|
96
|
+
# Traits and cascades with dynamic data
|
97
|
+
trait :has_errors, fn(:any?, fn(:fetch, input.api_responses.response_data, "status") >= 400)
|
98
|
+
trait :recent_events, fn(:any?, fn(:fetch, input.user_events.event_data, "timestamp") > 1640995200)
|
99
|
+
|
100
|
+
value :system_status do
|
101
|
+
on has_errors, "Error State"
|
102
|
+
on recent_events, "Active"
|
103
|
+
base "Idle"
|
104
|
+
end
|
105
|
+
```
|
106
|
+
|
107
|
+
**When to use `element :any`:**
|
108
|
+
- API responses with varying JSON schemas
|
109
|
+
- Configuration files with flexible key-value structures
|
110
|
+
- Event data where properties vary by event type
|
111
|
+
- Legacy systems where data structure may change
|
112
|
+
- Prototyping when exact hash structure is unknown
|
113
|
+
|
114
|
+
**Comparison with Hash Objects:**
|
115
|
+
|
116
|
+
| Approach | Use Case | Flexibility | Type Safety |
|
117
|
+
|----------|----------|-------------|-------------|
|
118
|
+
| `hash :field do ... end` | Known structure, strong typing | Limited | High |
|
119
|
+
| `element :any, :field` | Unknown/flexible structure | High | Low |
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
# Known structure - use hash objects
|
123
|
+
array :orders do
|
124
|
+
hash :customer do
|
125
|
+
string :name
|
126
|
+
string :email
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# Unknown/flexible structure - use element :any
|
131
|
+
array :api_calls do
|
132
|
+
element :any, :response # Could be any JSON structure
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
70
136
|
## Business Use Cases
|
71
137
|
|
72
138
|
Element access mode is essential for common business scenarios involving simple nested arrays:
|
@@ -14,10 +14,26 @@ schema do
|
|
14
14
|
array :tags, elem: { type: :string }
|
15
15
|
hash :metadata, key: { type: :string }, val: { type: :any }
|
16
16
|
any :flexible
|
17
|
+
|
18
|
+
# Structured arrays with defined fields
|
19
|
+
array :orders do
|
20
|
+
hash :customer do
|
21
|
+
string :name
|
22
|
+
string :email
|
23
|
+
end
|
24
|
+
float :total
|
25
|
+
end
|
26
|
+
|
27
|
+
# Dynamic arrays with flexible elements
|
28
|
+
array :api_responses do
|
29
|
+
element :any, :response_data # For unknown/flexible hash structures
|
30
|
+
end
|
17
31
|
end
|
18
32
|
|
19
33
|
trait :adult, (input.age >= 18)
|
20
34
|
value :status, input.verified ? "verified" : "pending"
|
35
|
+
value :customer_emails, input.orders.customer.email # Structured access
|
36
|
+
value :response_codes, fn(:fetch, input.api_responses.response_data, "status") # Dynamic access
|
21
37
|
end
|
22
38
|
```
|
23
39
|
|
@@ -86,27 +86,21 @@ puts
|
|
86
86
|
# ------------------------------------------------------------------
|
87
87
|
Benchmark.ips do |x|
|
88
88
|
schemas.each do |d, schema|
|
89
|
-
|
90
|
-
|
91
|
-
x.report("HOT fetch #{d}-deep") do
|
92
|
-
hot[:final_result]
|
93
|
-
end
|
94
|
-
|
95
|
-
# 2) COLD via UPDATE (no memoized result): change a dependent input each iter
|
96
|
-
upd = schema.from(seed: 0)
|
97
|
-
i = 0
|
98
|
-
x.report("COLD update #{d}-deep") do
|
99
|
-
i += 1
|
100
|
-
upd.update(seed: i) # invalidates v0..vN; forces recompute
|
101
|
-
upd[:final_result]
|
102
|
-
end
|
103
|
-
|
104
|
-
# 3) COLD new runner (includes construction)
|
105
|
-
prng = Random.new(42)
|
106
|
-
x.report("COLD new #{d}-deep") do
|
107
|
-
r = schema.from(seed: prng.rand(1_000_000))
|
108
|
-
r[:final_result]
|
109
|
-
end
|
89
|
+
runner = schema.from(seed: 0) # memoised runner
|
90
|
+
x.report("eval #{d}-deep") { runner[:final_result] }
|
110
91
|
end
|
111
92
|
x.compare!
|
112
93
|
end
|
94
|
+
# Warming up --------------------------------------
|
95
|
+
# eval 50-deep 222.000 i/100ms
|
96
|
+
# eval 100-deep 57.000 i/100ms
|
97
|
+
# eval 150-deep 26.000 i/100ms
|
98
|
+
# Calculating -------------------------------------
|
99
|
+
# eval 50-deep 2.166k (± 1.9%) i/s (461.70 μs/i) - 10.878k in 5.024320s
|
100
|
+
# eval 100-deep 561.698 (± 1.4%) i/s (1.78 ms/i) - 2.850k in 5.075057s
|
101
|
+
# eval 150-deep 253.732 (± 0.8%) i/s (3.94 ms/i) - 1.274k in 5.021499s
|
102
|
+
|
103
|
+
# Comparison:
|
104
|
+
# eval 50-deep: 2165.9 i/s
|
105
|
+
# eval 100-deep: 561.7 i/s - 3.86x slower
|
106
|
+
# eval 150-deep: 253.7 i/s - 8.54x slower
|
@@ -85,11 +85,11 @@ module Kumi
|
|
85
85
|
end
|
86
86
|
|
87
87
|
case parent_meta.container
|
88
|
-
when :object
|
88
|
+
when :object, :hash
|
89
89
|
kids.each_value do |child|
|
90
90
|
child.enter_via = :hash
|
91
91
|
child.consume_alias = false
|
92
|
-
child.access_mode
|
92
|
+
child.access_mode ||= :field # Only set if not explicitly specified
|
93
93
|
end
|
94
94
|
|
95
95
|
when :array
|
@@ -139,7 +139,11 @@ module Kumi
|
|
139
139
|
report_error(errors, "access_mode :element only valid for single scalar/array element (at :#{kname})", location: nil)
|
140
140
|
end
|
141
141
|
else
|
142
|
-
|
142
|
+
# Only scalar children under non-array parents are invalid with :element mode
|
143
|
+
# Arrays under hash/object parents can have :element mode (for arrays of scalars)
|
144
|
+
if child.container == :scalar
|
145
|
+
report_error(errors, "access_mode :element only valid under array parent (at :#{kname})", location: nil)
|
146
|
+
end
|
143
147
|
end
|
144
148
|
end
|
145
149
|
end
|
@@ -147,7 +151,8 @@ module Kumi
|
|
147
151
|
|
148
152
|
def kind_from_type(t)
|
149
153
|
return :array if t == :array
|
150
|
-
return :
|
154
|
+
return :hash if t == :hash
|
155
|
+
return :object if t == :field
|
151
156
|
|
152
157
|
:scalar
|
153
158
|
end
|
@@ -139,9 +139,9 @@ module Kumi
|
|
139
139
|
analysis_state = {
|
140
140
|
evaluation_order: evaluation_order,
|
141
141
|
declarations: declarations,
|
142
|
-
|
142
|
+
access_plans: access_plans,
|
143
143
|
join_reduce_plans: join_reduce_plans,
|
144
|
-
|
144
|
+
scope_plans: scope_plans,
|
145
145
|
input_metadata: input_metadata,
|
146
146
|
vec_names: @vec_names,
|
147
147
|
vec_meta: @vec_meta,
|
@@ -379,6 +379,7 @@ module Kumi
|
|
379
379
|
when Syntax::InputReference
|
380
380
|
plan_id = pick_plan_id_for_input([expr.name], access_plans,
|
381
381
|
scope_plan: scope_plan, need_indices: need_indices)
|
382
|
+
|
382
383
|
plans = access_plans.fetch(expr.name.to_s, [])
|
383
384
|
selected = plans.find { |p| p.accessor_key == plan_id }
|
384
385
|
scope = selected ? selected.scope : []
|
@@ -430,6 +431,13 @@ module Kumi
|
|
430
431
|
when Syntax::CallExpression
|
431
432
|
entry = Kumi::Registry.entry(expr.fn_name)
|
432
433
|
|
434
|
+
# Constant folding optimization: evaluate expressions with all literal arguments
|
435
|
+
if can_constant_fold?(expr, entry)
|
436
|
+
folded_value = constant_fold(expr, entry)
|
437
|
+
ops << Kumi::Core::IR::Ops.Const(folded_value)
|
438
|
+
return ops.size - 1
|
439
|
+
end
|
440
|
+
|
433
441
|
if ENV["DEBUG_LOWER"] && has_nested_reducer?(expr)
|
434
442
|
puts " NESTED_REDUCER_DETECTED in #{expr.fn_name} with req_scope=#{required_scope.inspect}"
|
435
443
|
end
|
@@ -986,6 +994,31 @@ module Kumi
|
|
986
994
|
end
|
987
995
|
end
|
988
996
|
end
|
997
|
+
|
998
|
+
# Constant folding optimization helpers
|
999
|
+
def can_constant_fold?(expr, entry)
|
1000
|
+
return false unless entry&.fn # Skip if function not found
|
1001
|
+
return false if entry.reducer # Skip reducer functions for now
|
1002
|
+
return false if expr.args.empty? # Need at least one argument
|
1003
|
+
|
1004
|
+
# Check if all arguments are literals
|
1005
|
+
expr.args.all? { |arg| arg.is_a?(Syntax::Literal) }
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
def constant_fold(expr, entry)
|
1009
|
+
literal_values = expr.args.map(&:value)
|
1010
|
+
|
1011
|
+
begin
|
1012
|
+
# Call the function with literal values at compile time
|
1013
|
+
entry.fn.call(*literal_values)
|
1014
|
+
rescue StandardError => e
|
1015
|
+
# If constant folding fails, fall back to runtime evaluation
|
1016
|
+
# This shouldn't happen with pure functions, but be defensive
|
1017
|
+
puts "Constant folding failed for #{expr.fn_name}: #{e.message}" if ENV["DEBUG_LOWER"]
|
1018
|
+
raise "Cannot constant fold #{expr.fn_name}: #{e.message}"
|
1019
|
+
end
|
1020
|
+
end
|
1021
|
+
|
989
1022
|
end
|
990
1023
|
end
|
991
1024
|
end
|
@@ -143,12 +143,12 @@ module Kumi
|
|
143
143
|
else
|
144
144
|
raise ArgumentError, "Invalid :enter_via '#{enter_via}' for array child '#{seg}'. Must be :hash or :array"
|
145
145
|
end
|
146
|
-
elsif container.nil? || container == :object
|
147
|
-
# Root or
|
146
|
+
elsif container.nil? || container == :object || container == :hash
|
147
|
+
# Root, object, or hash parent - always emit enter_hash
|
148
148
|
ops << enter_hash(seg)
|
149
149
|
puts " Added: enter_hash('#{seg}')" if ENV["DEBUG_ACCESSOR_OPS"]
|
150
150
|
else
|
151
|
-
raise ArgumentError, "Invalid parent :container '#{container}' for segment '#{seg}'. Expected :array, :object, or nil (root)"
|
151
|
+
raise ArgumentError, "Invalid parent :container '#{container}' for segment '#{seg}'. Expected :array, :object, :hash, or nil (root)"
|
152
152
|
end
|
153
153
|
|
154
154
|
parent_meta = node
|
@@ -39,7 +39,7 @@ module Kumi
|
|
39
39
|
end
|
40
40
|
|
41
41
|
private_class_method def self.should_validate_type?(meta)
|
42
|
-
meta[:type] && meta[:type] != :any
|
42
|
+
meta[:type] && meta[:type] != :any && !(meta[:children] && !meta[:children].empty?)
|
43
43
|
end
|
44
44
|
|
45
45
|
private_class_method def self.should_validate_domain?(meta)
|
@@ -42,7 +42,36 @@ module Kumi
|
|
42
42
|
# - DEBUG_GROUP_ROWS=1 prints grouping decisions during Lift.
|
43
43
|
module ExecutionEngine
|
44
44
|
def self.run(ir_module, ctx, accessors:, registry:)
|
45
|
-
|
45
|
+
# Use persistent accessor cache if available, otherwise create temporary one
|
46
|
+
if ctx[:accessor_cache]
|
47
|
+
# Include input data in cache key to avoid cross-context pollution
|
48
|
+
input_key = ctx[:input]&.hash || ctx["input"]&.hash || 0
|
49
|
+
memoized_accessors = add_persistent_memoization(accessors, ctx[:accessor_cache], input_key)
|
50
|
+
else
|
51
|
+
memoized_accessors = add_temporary_memoization(accessors)
|
52
|
+
end
|
53
|
+
|
54
|
+
Interpreter.run(ir_module, ctx, accessors: memoized_accessors, registry: registry)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def self.add_persistent_memoization(accessors, cache, input_key)
|
60
|
+
accessors.map do |plan_id, accessor_fn|
|
61
|
+
[plan_id, lambda do |input_data|
|
62
|
+
cache_key = [plan_id, input_key]
|
63
|
+
cache[cache_key] ||= accessor_fn.call(input_data)
|
64
|
+
end]
|
65
|
+
end.to_h
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.add_temporary_memoization(accessors)
|
69
|
+
cache = {}
|
70
|
+
accessors.map do |plan_id, accessor_fn|
|
71
|
+
[plan_id, lambda do |input_data|
|
72
|
+
cache[plan_id] ||= accessor_fn.call(input_data)
|
73
|
+
end]
|
74
|
+
end.to_h
|
46
75
|
end
|
47
76
|
end
|
48
77
|
end
|
@@ -33,10 +33,14 @@ module Kumi
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
def hash(name_or_key_type, val_type = nil, **kwargs)
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
def hash(name_or_key_type, val_type = nil, **kwargs, &block)
|
37
|
+
if block_given?
|
38
|
+
create_hash_field_with_block(name_or_key_type, kwargs, &block)
|
39
|
+
elsif val_type.nil?
|
40
|
+
create_hash_field(name_or_key_type, kwargs)
|
41
|
+
else
|
42
|
+
Kumi::Core::Types.hash(name_or_key_type, val_type)
|
43
|
+
end
|
40
44
|
end
|
41
45
|
|
42
46
|
def method_missing(method_name, *_args)
|
@@ -174,6 +178,23 @@ module Kumi
|
|
174
178
|
children, = collect_array_children(&block)
|
175
179
|
@context.inputs << Kumi::Syntax::InputDeclaration.new(name, nil, :field, children, nil, loc: @context.current_location)
|
176
180
|
end
|
181
|
+
|
182
|
+
def create_hash_field_with_block(field_name, options, &block)
|
183
|
+
domain = options[:domain]
|
184
|
+
|
185
|
+
# Collect children by creating a nested context (reuse array logic)
|
186
|
+
children, = collect_array_children(&block)
|
187
|
+
|
188
|
+
# Create the InputDeclaration with children and :field access_mode for hash objects
|
189
|
+
@context.inputs << Kumi::Syntax::InputDeclaration.new(
|
190
|
+
field_name,
|
191
|
+
domain,
|
192
|
+
:hash,
|
193
|
+
children,
|
194
|
+
:field,
|
195
|
+
loc: @context.current_location
|
196
|
+
)
|
197
|
+
end
|
177
198
|
end
|
178
199
|
end
|
179
200
|
end
|
@@ -5,7 +5,7 @@ module Kumi
|
|
5
5
|
module Types
|
6
6
|
# Validates type definitions and structures
|
7
7
|
class Validator
|
8
|
-
VALID_TYPES = %i[string integer float boolean any symbol regexp time date datetime array].freeze
|
8
|
+
VALID_TYPES = %i[string integer float boolean any symbol regexp time date datetime array hash].freeze
|
9
9
|
|
10
10
|
def self.valid_type?(type)
|
11
11
|
return true if VALID_TYPES.include?(type)
|
@@ -62,6 +62,7 @@ module Kumi
|
|
62
62
|
@reg = registry
|
63
63
|
@input_metadata = input_metadata.freeze
|
64
64
|
@decl = @ir.decls.map { |d| [d.name, d] }.to_h
|
65
|
+
@accessor_cache = {} # Persistent accessor cache across evaluations
|
65
66
|
end
|
66
67
|
|
67
68
|
def decl?(name) = @decl.key?(name)
|
@@ -85,12 +86,20 @@ module Kumi
|
|
85
86
|
def eval_decl(name, input, mode: :ruby)
|
86
87
|
raise Kumi::Core::Errors::RuntimeError, "unknown decl #{name}" unless decl?(name)
|
87
88
|
|
88
|
-
out = Kumi::Core::IR::ExecutionEngine.run(@ir, { input: input, target: name },
|
89
|
+
out = Kumi::Core::IR::ExecutionEngine.run(@ir, { input: input, target: name, accessor_cache: @accessor_cache },
|
89
90
|
accessors: @acc, registry: @reg).fetch(name)
|
90
91
|
|
91
92
|
mode == :ruby ? unwrap(@decl[name], out) : out
|
92
93
|
end
|
93
94
|
|
95
|
+
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
|
+
}
|
101
|
+
end
|
102
|
+
|
94
103
|
private
|
95
104
|
|
96
105
|
def validate_keys(keys)
|
@@ -153,9 +162,12 @@ module Kumi
|
|
153
162
|
|
154
163
|
# Update the input data
|
155
164
|
@input = deep_merge(@input, { field => value })
|
165
|
+
|
166
|
+
# Clear accessor cache for this specific field
|
167
|
+
@program.clear_field_accessor_cache(field)
|
156
168
|
end
|
157
169
|
|
158
|
-
# Clear cache after all updates
|
170
|
+
# Clear declaration evaluation cache after all updates
|
159
171
|
@cache.clear
|
160
172
|
self
|
161
173
|
end
|
data/lib/kumi/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,14 @@
|
|
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.13
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- André Muta
|
8
|
+
autorequire:
|
8
9
|
bindir: bin
|
9
10
|
cert_chain: []
|
10
|
-
date:
|
11
|
+
date: 2025-08-14 00:00:00.000000000 Z
|
11
12
|
dependencies:
|
12
13
|
- !ruby/object:Gem::Dependency
|
13
14
|
name: zeitwerk
|
@@ -23,6 +24,7 @@ dependencies:
|
|
23
24
|
- - "~>"
|
24
25
|
- !ruby/object:Gem::Version
|
25
26
|
version: 2.6.0
|
27
|
+
description:
|
26
28
|
email:
|
27
29
|
- andremuta@gmail.com
|
28
30
|
executables: []
|
@@ -198,6 +200,7 @@ metadata:
|
|
198
200
|
source_code_uri: https://github.com/amuta/kumi
|
199
201
|
changelog_uri: https://github.com/amuta/kumi/blob/main/CHANGELOG.md
|
200
202
|
rubygems_mfa_required: 'true'
|
203
|
+
post_install_message:
|
201
204
|
rdoc_options: []
|
202
205
|
require_paths:
|
203
206
|
- lib
|
@@ -205,14 +208,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
205
208
|
requirements:
|
206
209
|
- - ">="
|
207
210
|
- !ruby/object:Gem::Version
|
208
|
-
version: 3.
|
211
|
+
version: 3.1.0
|
209
212
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
210
213
|
requirements:
|
211
214
|
- - ">="
|
212
215
|
- !ruby/object:Gem::Version
|
213
216
|
version: '0'
|
214
217
|
requirements: []
|
215
|
-
rubygems_version: 3.
|
218
|
+
rubygems_version: 3.5.22
|
219
|
+
signing_key:
|
216
220
|
specification_version: 4
|
217
|
-
summary:
|
221
|
+
summary: Kumi is a declarative rules-and-calculation DSL for Ruby that compiles your
|
222
|
+
business logic into a typed, analyzable dependency graph with
|
223
|
+
vector semantics over nested data. It does static checks at definition time, lowers
|
224
|
+
to a small IR, and runs with consistent, predictable behavior.
|
218
225
|
test_files: []
|