kumi 0.0.11 → 0.0.12
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 +5 -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 +2 -2
- data/lib/kumi/core/compiler/access_planner.rb +3 -3
- data/lib/kumi/core/input/validator.rb +1 -1
- data/lib/kumi/core/ruby_parser/input_builder.rb +25 -4
- data/lib/kumi/core/types/validator.rb +1 -1
- 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: dc6d1a550925563fbffcc0f4f96e6609d70734dfff031d9e6e2bfd405ecf45f8
|
4
|
+
data.tar.gz: '090999f0550fcd66a79cb3f683b8f27680b5921166267bbdf57dd8c593d3c6fd'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9c18d000d924168fbef1abcc6a19f59f18af8a82fae6d4d7bb14e475ce5b34032f031a14ae3d20faa8deadf96f7e1da9bb2190fca0251da74d629c38f637c139
|
7
|
+
data.tar.gz: 37f124928afe975bb7138c2856093849b6c15fb23694d77a7a0d1628b2c67f7e262576cb734a522105b8f83253fe112f6cbacc00895ad2daba4212eb7a337384
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
## [0.0.12] – 2025-08-14
|
2
|
+
### Added
|
3
|
+
- Hash objects input declarations with `hash :field do ... end` syntax
|
4
|
+
- Complete hash object integration with arrays, nesting, and broadcasting
|
5
|
+
|
1
6
|
## [0.0.11] – 2025-08-13
|
2
7
|
### Added
|
3
8
|
- 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,
|
@@ -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)
|
@@ -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)
|
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.12
|
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: []
|