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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c1aeb3623e8c7a3e7f436ccaeb10323235e792b61b7f5c028373d62d9e199b3
4
- data.tar.gz: 786c9d464c78d6fa67eb28803d6d6d6178f263cc2476e343a9c5f6307ae371da
3
+ metadata.gz: bb490e00af8d7e5b52df8056ed03f5b157670777ef4309b1805218999d71940f
4
+ data.tar.gz: 254c39487da8560617178f30f64ad1650c31e364f29452457c2931a45762e46e
5
5
  SHA512:
6
- metadata.gz: 2b9e61de7438fbfacc2343b820bf5c36c1e66a3f44113af264705c8fee14d7a569dcd408fe1374a0ab0069765a63ee7ea04f44f9b1694862c4523023047eae00
7
- data.tar.gz: f6bb5e93e9f2a7e8e06faa0831b619fdf83435b8b9cdb8555ad270bfb8db560fb344aa949527bfca6935e34049ada0f83b4234a203e3c112ae5ba9dc22cc384d
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.36] – 2026-06-11
4
- ### Fixed
5
- - Keep `rdoc` as a development-only dependency so Ruby 3.1 installs do not resolve `erb >= 6`, which requires Ruby 3.2+.
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.entry_block.instructions.each do |instr|
42
- emit_instruction(instr)
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 emit_instruction(instr)
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
- write "let #{reg(instr.result)} = [];"
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
 
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kumi
4
- VERSION = "0.0.36"
4
+ VERSION = "0.0.37"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kumi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.36
4
+ version: 0.0.37
5
5
  platform: ruby
6
6
  authors:
7
7
  - André Muta