kumi 0.0.36 → 0.0.37
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/CHANGELOG.md +3 -3
- data/README.md +72 -1
- data/docs/SYNTAX.md +18 -0
- data/lib/kumi/core/analyzer/passes/codegen/loop/js/emitter.rb +43 -5
- data/lib/kumi/core/analyzer/passes/codegen/loop_js_pass.rb +2 -1
- data/lib/kumi/core/ruby_parser/build_context.rb +6 -1
- data/lib/kumi/core/ruby_parser/parser.rb +1 -1
- data/lib/kumi/core/ruby_parser/schema_builder.rb +18 -1
- data/lib/kumi/syntax/root.rb +1 -1
- data/lib/kumi/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bb490e00af8d7e5b52df8056ed03f5b157670777ef4309b1805218999d71940f
|
|
4
|
+
data.tar.gz: 254c39487da8560617178f30f64ad1650c31e364f29452457c2931a45762e46e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 17ee44edcb98ae17de0ba57b422469769a0ef59f06b2c09d03b138b401dae63774b845532b0948c3abdd4173abb75e0d77c36af327d1630ac0362e7d791ebcc3
|
|
7
|
+
data.tar.gz: 78fd6d67671e81059ba9dfa6f10a5e4393e6fcef852409a3cccb93ee107a8f09d8c48a4366e4f3f3b4fb66fde8832925e3c089ffbf282ece6cc3fd694a802bfe
|
data/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
-
## [0.0.
|
|
4
|
-
###
|
|
5
|
-
-
|
|
3
|
+
## [0.0.37] – 2026-06-11
|
|
4
|
+
### Added
|
|
5
|
+
- Streaming hints for JS codegen
|
|
6
6
|
|
|
7
7
|
## [0.0.35] – 2026-06-11
|
|
8
8
|
### Added
|
data/README.md
CHANGED
|
@@ -93,11 +93,82 @@ Double.write_source("output.mjs", platform: :javascript)
|
|
|
93
93
|
You can also override the compilation strategy without touching code by setting
|
|
94
94
|
`KUMI_COMPILATION_MODE` to `jit` or `aot` (e.g. `export KUMI_COMPILATION_MODE=aot`).
|
|
95
95
|
|
|
96
|
+
## Composing Schemas
|
|
97
|
+
|
|
98
|
+
Schemas can import other schemas — and the compiler **inlines everything at compile time**. The generated code has no runtime dependency on the imported module; imported logic participates in broadcasting and loop fusion like any locally written expression.
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
module TaxPolicy
|
|
102
|
+
extend Kumi::Schema
|
|
103
|
+
|
|
104
|
+
schema do
|
|
105
|
+
input { decimal :amount }
|
|
106
|
+
value :tax, input.amount * 0.15
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
module Order
|
|
111
|
+
extend Kumi::Schema
|
|
112
|
+
|
|
113
|
+
schema do
|
|
114
|
+
import :tax, from: TaxPolicy
|
|
115
|
+
|
|
116
|
+
input do
|
|
117
|
+
array :items do
|
|
118
|
+
hash :item do
|
|
119
|
+
decimal :price
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# TaxPolicy only knows a scalar :amount — passing a vectorized
|
|
125
|
+
# argument applies it per item automatically.
|
|
126
|
+
value :item_taxes, fn(:tax, amount: input.items.item.price)
|
|
127
|
+
value :total_tax, fn(:sum, item_taxes)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
Order.from(items: [{ price: 100 }, { price: 200 }, { price: 300 }])[:total_tax]
|
|
132
|
+
# => 90.0
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The generated JavaScript for `total_tax` shows what "inline everything" means — the imported tax rule **and** the `sum` reduction are fused into one loop, with no call to `TaxPolicy`, no intermediate array, and no runtime to ship:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
export function _total_tax(input) {
|
|
139
|
+
let t13 = input["items"];
|
|
140
|
+
let acc18 = 0.0;
|
|
141
|
+
for (let items_i15 = 0; items_i15 < t13.length; items_i15++) {
|
|
142
|
+
let items_el14 = t13[items_i15];
|
|
143
|
+
let t16 = items_el14["price"];
|
|
144
|
+
let t17 = 0.15;
|
|
145
|
+
let t12 = t16 * t17;
|
|
146
|
+
acc18 += t12;
|
|
147
|
+
}
|
|
148
|
+
let t6 = acc18;
|
|
149
|
+
return t6;
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
See [Schema Imports](docs/SCHEMA_IMPORTS.md) for renamed inputs, multiple imports, and imports over nested arrays.
|
|
154
|
+
|
|
155
|
+
## Performance
|
|
156
|
+
|
|
157
|
+
Compiled schemas are straight-line code with fused loops — there is no interpreter, rule engine, or library call in the artifact, so the runtime cost is whatever the host language charges for a `for` loop.
|
|
158
|
+
|
|
159
|
+
Two playground examples make this tangible:
|
|
160
|
+
|
|
161
|
+
- **[Payroll at Scale](https://kumi-play-web.fly.dev/?example=payroll-at-scale)** — a full payroll run (overtime split, progressive withholding bands, ten aggregates) over **10,000 employees in ~0.2 ms per call** in desktop Chrome. Press **Benchmark ×100** in the Execute tab to measure median / p95 on your own machine.
|
|
162
|
+
- **[Game of Life XL](https://kumi-play-web.fly.dev/?example=life-xl)** — Conway's rules over an **83,000-cell grid**, stepping live in a worker with the nine neighbor reads fused into a single pass — millions of cell updates per second, rendered in real time with a live counter.
|
|
163
|
+
|
|
96
164
|
## Examples
|
|
97
165
|
|
|
98
166
|
- **US Tax Calculator (2024)** — a single schema computes federal, state, and FICA taxes across multiple filing statuses. [Open in the demo](https://kumi-play-web.fly.dev/?example=us-federal-tax-2024).
|
|
167
|
+
- **Payroll at Scale** — overtime, withholding bands, and aggregates over 10,000 employees with per-run timing. [Open in the demo](https://kumi-play-web.fly.dev/?example=payroll-at-scale).
|
|
99
168
|
- **Monte Carlo Portfolio** — probabilistic simulations and table visualizations. [Open in the demo](https://kumi-play-web.fly.dev/?example=monte-carlo-simulation).
|
|
100
|
-
- **Conway's Game of Life** — array operations powering a grid-based simulation. [Open in the demo](https://kumi-play-web.fly.dev/?example=game-of-life).
|
|
169
|
+
- **Conway's Game of Life** — array operations powering a grid-based simulation ([XL version](https://kumi-play-web.fly.dev/?example=life-xl) runs 83k cells live). [Open in the demo](https://kumi-play-web.fly.dev/?example=game-of-life).
|
|
170
|
+
|
|
171
|
+
The playground's example picker groups these by theme (language tour, business logic, scale & speed, simulations) — the [Language Intro](https://kumi-play-web.fly.dev/?example=language-intro) is the best starting point.
|
|
101
172
|
|
|
102
173
|
---
|
|
103
174
|
|
data/docs/SYNTAX.md
CHANGED
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
### File Structure
|
|
6
6
|
```kumi
|
|
7
7
|
schema do
|
|
8
|
+
# Optional schema-level generation hints
|
|
9
|
+
codegen streaming: true
|
|
10
|
+
|
|
8
11
|
input do
|
|
9
12
|
# Input shape declarations
|
|
10
13
|
end
|
|
@@ -30,6 +33,21 @@ value :name, expr # Output value
|
|
|
30
33
|
trait :name, expr # Boolean mask
|
|
31
34
|
```
|
|
32
35
|
|
|
36
|
+
### Codegen Hints
|
|
37
|
+
```kumi
|
|
38
|
+
schema do
|
|
39
|
+
codegen streaming: true
|
|
40
|
+
|
|
41
|
+
input do
|
|
42
|
+
# ...
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
value :next_state, ...
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`codegen streaming: true` keeps normal JavaScript exports and adds `_name_stream(input, target = {})` exports for each output. For direct array outputs, pass an array target to reuse it in-place, or pass an object target to receive `target["name"]`.
|
|
50
|
+
|
|
33
51
|
### Operators
|
|
34
52
|
|
|
35
53
|
**Arithmetic:** `+` `-` `*` `/` `**` `%`
|
|
@@ -16,11 +16,12 @@ module Kumi
|
|
|
16
16
|
@indent = 0
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def emit(loop_module, schema_digest: nil)
|
|
19
|
+
def emit(loop_module, schema_digest: nil, streaming: false)
|
|
20
20
|
reset!
|
|
21
21
|
|
|
22
22
|
loop_module.each_function do |fn|
|
|
23
23
|
emit_function(fn)
|
|
24
|
+
emit_streaming_function(fn) if streaming
|
|
24
25
|
end
|
|
25
26
|
|
|
26
27
|
to_s
|
|
@@ -38,15 +39,42 @@ module Kumi
|
|
|
38
39
|
def emit_function(fn)
|
|
39
40
|
write "export function _#{fn.name}(input) {"
|
|
40
41
|
indented do
|
|
41
|
-
fn
|
|
42
|
-
|
|
42
|
+
emit_instructions(fn)
|
|
43
|
+
write "return #{reg(fn.return_reg)};"
|
|
44
|
+
end
|
|
45
|
+
write "}\n"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def emit_streaming_function(fn)
|
|
49
|
+
write "export function _#{fn.name}_stream(input, target = {}) {"
|
|
50
|
+
indented do
|
|
51
|
+
return_array_reg = direct_return_array_reg(fn)
|
|
52
|
+
if return_array_reg
|
|
53
|
+
write "let __streamTarget = Array.isArray(target) ? target : target[\"#{fn.name}\"];"
|
|
54
|
+
write "if (!Array.isArray(__streamTarget)) {"
|
|
55
|
+
indented do
|
|
56
|
+
write "__streamTarget = [];"
|
|
57
|
+
write "if (target && typeof target === \"object\" && !Array.isArray(target)) target[\"#{fn.name}\"] = __streamTarget;"
|
|
58
|
+
end
|
|
59
|
+
write "} else {"
|
|
60
|
+
indented { write "__streamTarget.length = 0;" }
|
|
61
|
+
write "}"
|
|
43
62
|
end
|
|
63
|
+
|
|
64
|
+
emit_instructions(fn, stream_return_array_reg: return_array_reg)
|
|
65
|
+
write "if (target && typeof target === \"object\" && !Array.isArray(target)) target[\"#{fn.name}\"] = #{reg(fn.return_reg)};"
|
|
44
66
|
write "return #{reg(fn.return_reg)};"
|
|
45
67
|
end
|
|
46
68
|
write "}\n"
|
|
47
69
|
end
|
|
48
70
|
|
|
49
|
-
def
|
|
71
|
+
def emit_instructions(fn, stream_return_array_reg: nil)
|
|
72
|
+
fn.entry_block.instructions.each do |instr|
|
|
73
|
+
emit_instruction(instr, stream_return_array_reg: stream_return_array_reg)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def emit_instruction(instr, stream_return_array_reg: nil)
|
|
50
78
|
case instr.opcode
|
|
51
79
|
when :constant
|
|
52
80
|
write "let #{reg(instr.result)} = #{format_literal(instr.attributes[:value])};"
|
|
@@ -79,7 +107,11 @@ module Kumi
|
|
|
79
107
|
@indent -= 1
|
|
80
108
|
write "}"
|
|
81
109
|
when :array_init
|
|
82
|
-
|
|
110
|
+
if stream_return_array_reg == instr.result
|
|
111
|
+
write "let #{reg(instr.result)} = __streamTarget;"
|
|
112
|
+
else
|
|
113
|
+
write "let #{reg(instr.result)} = [];"
|
|
114
|
+
end
|
|
83
115
|
when :array_push
|
|
84
116
|
write "#{reg(instr.inputs[0])}.push(#{reg(instr.inputs[1])});"
|
|
85
117
|
when :array_len
|
|
@@ -106,6 +138,12 @@ module Kumi
|
|
|
106
138
|
end
|
|
107
139
|
end
|
|
108
140
|
|
|
141
|
+
def direct_return_array_reg(fn)
|
|
142
|
+
fn.entry_block.instructions.any? do |instr|
|
|
143
|
+
instr.opcode == :array_init && instr.result == fn.return_reg
|
|
144
|
+
end ? fn.return_reg : nil
|
|
145
|
+
end
|
|
146
|
+
|
|
109
147
|
def emit_shift_read(instr)
|
|
110
148
|
array, index, length = instr.inputs.map { reg(_1) }
|
|
111
149
|
out = reg(instr.result)
|
|
@@ -10,9 +10,10 @@ module Kumi
|
|
|
10
10
|
loop_module = get_state(:loop_module, required: true)
|
|
11
11
|
registry = get_state(:registry, required: true)
|
|
12
12
|
schema_digest = get_state(:schema_digest)
|
|
13
|
+
streaming = schema.hints.dig(:codegen, :streaming) == true
|
|
13
14
|
|
|
14
15
|
emitter = Codegen::Loop::Js::Emitter.new(registry)
|
|
15
|
-
src = emitter.emit(loop_module, schema_digest: schema_digest)
|
|
16
|
+
src = emitter.emit(loop_module, schema_digest: schema_digest, streaming: streaming)
|
|
16
17
|
|
|
17
18
|
state.with(:javascript_codegen_files, { "codegen.mjs" => src })
|
|
18
19
|
end
|
|
@@ -4,7 +4,7 @@ module Kumi
|
|
|
4
4
|
module Core
|
|
5
5
|
module RubyParser
|
|
6
6
|
class BuildContext
|
|
7
|
-
attr_reader :inputs, :values, :traits, :imports, :imported_names
|
|
7
|
+
attr_reader :inputs, :values, :traits, :imports, :imported_names, :root_hints
|
|
8
8
|
attr_accessor :current_location
|
|
9
9
|
|
|
10
10
|
def initialize
|
|
@@ -13,9 +13,14 @@ module Kumi
|
|
|
13
13
|
@traits = []
|
|
14
14
|
@imports = []
|
|
15
15
|
@imported_names = Set.new
|
|
16
|
+
@root_hints = {}
|
|
16
17
|
@input_block_defined = false
|
|
17
18
|
end
|
|
18
19
|
|
|
20
|
+
def merge_root_hint(namespace, values)
|
|
21
|
+
@root_hints[namespace] = (@root_hints[namespace] || {}).merge(values)
|
|
22
|
+
end
|
|
23
|
+
|
|
19
24
|
def input_block_defined?
|
|
20
25
|
@input_block_defined
|
|
21
26
|
end
|
|
@@ -45,7 +45,7 @@ module Kumi
|
|
|
45
45
|
end
|
|
46
46
|
|
|
47
47
|
def build_syntax_tree
|
|
48
|
-
Root.new(@context.inputs, @context.values, @context.traits, @context.imports)
|
|
48
|
+
Root.new(@context.inputs, @context.values, @context.traits, @context.imports, hints: @context.root_hints)
|
|
49
49
|
end
|
|
50
50
|
|
|
51
51
|
def handle_parse_error(error)
|
|
@@ -8,7 +8,7 @@ module Kumi
|
|
|
8
8
|
include Syntax
|
|
9
9
|
include ErrorReporting
|
|
10
10
|
|
|
11
|
-
DSL_METHODS = %i[value trait input ref literal fn select shift roll import].freeze
|
|
11
|
+
DSL_METHODS = %i[value trait input ref literal fn select shift roll import codegen].freeze
|
|
12
12
|
|
|
13
13
|
def initialize(context)
|
|
14
14
|
@context = context
|
|
@@ -72,6 +72,23 @@ module Kumi
|
|
|
72
72
|
@context.imported_names.merge(names)
|
|
73
73
|
end
|
|
74
74
|
|
|
75
|
+
def codegen(**kwargs)
|
|
76
|
+
update_location
|
|
77
|
+
unknown = kwargs.keys - %i[streaming]
|
|
78
|
+
unless unknown.empty?
|
|
79
|
+
raise_syntax_error(
|
|
80
|
+
"unknown codegen option(s): #{unknown.map(&:inspect).join(', ')}",
|
|
81
|
+
location: @context.current_location
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if kwargs.key?(:streaming) && ![true, false].include?(kwargs[:streaming])
|
|
86
|
+
raise_syntax_error("codegen streaming: must be true or false", location: @context.current_location)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
@context.merge_root_hint(:codegen, kwargs)
|
|
90
|
+
end
|
|
91
|
+
|
|
75
92
|
def fn(fn_name, *args, **kwargs)
|
|
76
93
|
update_location
|
|
77
94
|
|
data/lib/kumi/syntax/root.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Kumi
|
|
|
13
13
|
# The digest must be stable and depend on anything that could change the
|
|
14
14
|
# compiled output. This includes the AST, the Kumi version (compiler changes),
|
|
15
15
|
# and the Ruby version (runtime behavior changes).
|
|
16
|
-
digest_input = "#{Kumi::VERSION}-#{RUBY_VERSION}-#{self}"
|
|
16
|
+
digest_input = "#{Kumi::VERSION}-#{RUBY_VERSION}-#{self}-#{hints.inspect}"
|
|
17
17
|
|
|
18
18
|
# Ruby constants cannot start with a number, so we add a prefix.
|
|
19
19
|
"KUMI_#{Digest::SHA256.hexdigest(digest_input)}"
|
data/lib/kumi/version.rb
CHANGED