kumi 0.0.9 → 0.0.11

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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +18 -258
  4. data/README.md +188 -121
  5. data/docs/AST.md +1 -1
  6. data/docs/FUNCTIONS.md +52 -8
  7. data/docs/VECTOR_SEMANTICS.md +286 -0
  8. data/docs/compiler_design_principles.md +86 -0
  9. data/docs/features/README.md +15 -2
  10. data/docs/features/hierarchical-broadcasting.md +349 -0
  11. data/docs/features/javascript-transpiler.md +148 -0
  12. data/docs/features/performance.md +1 -3
  13. data/docs/features/s-expression-printer.md +2 -2
  14. data/docs/schema_metadata.md +7 -7
  15. data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
  16. data/examples/game_of_life.rb +2 -4
  17. data/lib/kumi/analyzer.rb +34 -14
  18. data/lib/kumi/compiler.rb +4 -283
  19. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +717 -66
  20. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
  21. data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
  22. data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -99
  23. data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
  24. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
  25. data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
  26. data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
  27. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +28 -0
  28. data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
  29. data/lib/kumi/core/analyzer/passes/type_checker.rb +9 -5
  30. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
  31. data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
  32. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +92 -48
  33. data/lib/kumi/core/analyzer/plans.rb +52 -0
  34. data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
  35. data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
  36. data/lib/kumi/core/compiler/access_builder.rb +36 -0
  37. data/lib/kumi/core/compiler/access_planner.rb +219 -0
  38. data/lib/kumi/core/compiler/accessors/base.rb +69 -0
  39. data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
  40. data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
  41. data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
  42. data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
  43. data/lib/kumi/core/compiler_base.rb +137 -0
  44. data/lib/kumi/core/error_reporter.rb +6 -5
  45. data/lib/kumi/core/errors.rb +4 -0
  46. data/lib/kumi/core/explain.rb +157 -205
  47. data/lib/kumi/core/export/node_builders.rb +2 -2
  48. data/lib/kumi/core/export/node_serializers.rb +1 -1
  49. data/lib/kumi/core/function_registry/collection_functions.rb +100 -6
  50. data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
  51. data/lib/kumi/core/function_registry/function_builder.rb +142 -53
  52. data/lib/kumi/core/function_registry/logical_functions.rb +173 -3
  53. data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
  54. data/lib/kumi/core/function_registry.rb +138 -98
  55. data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
  56. data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
  57. data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
  58. data/lib/kumi/core/ir/execution_engine.rb +50 -0
  59. data/lib/kumi/core/ir.rb +58 -0
  60. data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
  61. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
  62. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +37 -16
  63. data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
  64. data/lib/kumi/core/ruby_parser/parser.rb +1 -1
  65. data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
  66. data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
  67. data/lib/kumi/errors.rb +2 -0
  68. data/lib/kumi/js.rb +23 -0
  69. data/lib/kumi/registry.rb +17 -22
  70. data/lib/kumi/runtime/executable.rb +213 -0
  71. data/lib/kumi/schema.rb +15 -4
  72. data/lib/kumi/schema_metadata.rb +2 -2
  73. data/lib/kumi/support/ir_dump.rb +491 -0
  74. data/lib/kumi/support/s_expression_printer.rb +17 -16
  75. data/lib/kumi/syntax/array_expression.rb +6 -6
  76. data/lib/kumi/syntax/call_expression.rb +4 -4
  77. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  78. data/lib/kumi/syntax/case_expression.rb +4 -4
  79. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  80. data/lib/kumi/syntax/hash_expression.rb +4 -4
  81. data/lib/kumi/syntax/input_declaration.rb +6 -5
  82. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  83. data/lib/kumi/syntax/input_reference.rb +5 -5
  84. data/lib/kumi/syntax/literal.rb +4 -4
  85. data/lib/kumi/syntax/location.rb +5 -0
  86. data/lib/kumi/syntax/node.rb +33 -34
  87. data/lib/kumi/syntax/root.rb +6 -6
  88. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  89. data/lib/kumi/syntax/value_declaration.rb +4 -4
  90. data/lib/kumi/version.rb +1 -1
  91. data/lib/kumi.rb +6 -15
  92. data/scripts/analyze_broadcast_methods.rb +68 -0
  93. data/scripts/analyze_cascade_methods.rb +74 -0
  94. data/scripts/check_broadcasting_coverage.rb +51 -0
  95. data/scripts/find_dead_code.rb +114 -0
  96. metadata +36 -9
  97. data/docs/features/array-broadcasting.md +0 -170
  98. data/lib/kumi/cli.rb +0 -449
  99. data/lib/kumi/core/compiled_schema.rb +0 -43
  100. data/lib/kumi/core/evaluation_wrapper.rb +0 -40
  101. data/lib/kumi/core/schema_instance.rb +0 -111
  102. data/lib/kumi/core/vectorization_metadata.rb +0 -110
  103. data/migrate_to_core_iterative.rb +0 -938
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  Kumi is a Declarative logic and rules engine framework with static analysis for Ruby.
7
7
 
8
- It is well-suited for scenarios with complex, interdependent calculations, enforcing validation and consistency across your business rules while maintaining performance.
8
+ It handles complex, interdependent calculations with validation and consistency checking.
9
9
 
10
10
 
11
11
  ## What can you build?
@@ -59,14 +59,14 @@ result[:total_tax] # => 21,491.00
59
59
  result[:after_tax] # => 78,509.00
60
60
  ```
61
61
 
62
- Real tax calculation with brackets, deductions, and FICA caps. Validation happens during schema definition.
62
+ Real tax calculation with brackets, deductions, and FICA caps.
63
63
 
64
- Kumi is well-suited for scenarios with complex, interdependent calculations, enforcing validation and consistency across your business rules while maintaining performance.
64
+ Validation happens during schema definition.
65
65
 
66
66
  ## Installation
67
67
 
68
68
  ```bash
69
- # Requires Ruby 3.0+
69
+ # Requires Ruby 3.1+
70
70
  # No external dependencies
71
71
  gem install kumi
72
72
  ```
@@ -74,7 +74,7 @@ gem install kumi
74
74
  ## Core Features
75
75
 
76
76
  <details>
77
- <summary><strong>📊 Schema Primitives</strong> - Four building blocks for business logic</summary>
77
+ <summary><strong>Schema Primitives</strong> - Four building blocks for business logic</summary>
78
78
 
79
79
  ### Schema Primitives
80
80
 
@@ -96,7 +96,7 @@ value :tax_rate, 0.08
96
96
  value :tax_amount, subtotal * tax_rate
97
97
  ```
98
98
 
99
- **Traits** are boolean conditions that enable branching logic:
99
+ **Traits** are boolean conditions for branching logic:
100
100
  ```ruby
101
101
  trait :bulk_order, input.quantity >= 100
102
102
  trait :premium_customer, input.tier == "premium"
@@ -109,7 +109,7 @@ value :discount do
109
109
  end
110
110
  ```
111
111
 
112
- **Functions** provide computational building blocks:
112
+ **Functions** are computational building blocks:
113
113
 
114
114
  ```ruby
115
115
  value :final_price, [subtotal - discount_amount, 0].max
@@ -120,9 +120,13 @@ Note: You can find a list all core functions in [docs/FUNCTIONS.md](docs/FUNCTIO
120
120
  </details>
121
121
 
122
122
  <details>
123
- <summary><strong>🔍 Static Analysis</strong> - Catch errors at definition time and provides rich metadata</summary>
123
+ <summary><strong>Analysis & Introspection</strong> - Static verification, runtime inspection, and metadata extraction</summary>
124
124
 
125
- ### Static Analysis
125
+ ### Analysis & Introspection
126
+
127
+ Kumi provides comprehensive analysis capabilities - catching errors at definition time and exposing schema structure for tooling and debugging.
128
+
129
+ #### **Static Analysis: Catch Errors Early**
126
130
 
127
131
  Kumi catches many types of business logic errors that cause runtime failures or silent bugs:
128
132
 
@@ -203,7 +207,7 @@ end
203
207
  # ❌ Function arity error: divide expects 2 arguments, got 1
204
208
  ```
205
209
 
206
- **Bounded Recursion**: Kumi allows mutual recursion when cascade conditions are mutually exclusive:
210
+ **Mutual Recursion**: Kumi supports mutual recursion when cascade conditions are mutually exclusive:
207
211
 
208
212
  ```ruby
209
213
  trait :is_forward, input.operation == "forward"
@@ -212,7 +216,7 @@ trait :is_reverse, input.operation == "reverse"
212
216
  # Safe mutual recursion - conditions are mutually exclusive
213
217
  value :forward_processor do
214
218
  on is_forward, input.value * 2 # Direct calculation
215
- on is_reverse, reverse_processor + 10 # Delegates to reverse (safe)
219
+ on is_reverse, reveAnalysisrse_processor + 10 # Delegates to reverse (safe)
216
220
  base "invalid operation"
217
221
  end
218
222
 
@@ -230,168 +234,225 @@ end
230
234
 
231
235
  This compiles because `operation` can only be "forward" or "reverse", never both. Each recursion executes one step before hitting a direct calculation.
232
236
 
233
- </details>
237
+ #### **Runtime Introspection: Debug and Understand**
234
238
 
235
- <details>
236
- <summary><strong>🔍 Array Broadcasting</strong> - Automatic vectorization over array fields</summary>
237
-
238
- ### Array Broadcasting
239
-
240
- Kumi broadcasts operations over array fields for element-wise computation:
239
+ **Explainability**: Trace exactly how any value is computed, step-by-step. This is invaluable for debugging complex logic and auditing results.
241
240
 
242
241
  ```ruby
243
- schema do
244
- input do
245
- array :line_items do
246
- float :price
247
- integer :quantity
248
- string :category
249
- end
250
- float :tax_rate
251
- end
252
-
253
- # Element-wise computation - broadcasts over each item
254
- value :subtotals, input.line_items.price * input.line_items.quantity
255
-
256
- # Element-wise traits - applied to each item
257
- trait :is_taxable, (input.line_items.category != "digital")
258
-
259
- # Aggregation operations - consume arrays to produce scalars
260
- value :total_subtotal, fn(:sum, subtotals)
261
- value :item_count, fn(:size, input.line_items)
262
- end
242
+ Kumi::Explain.call(FederalTax2024, :fed_tax, inputs: {income: 100_000, filing_status: "single"})
243
+ # => fed_tax = fed_calc[0]
244
+ # = (fed_calc = piecewise_sum(taxable_income, fed_breaks, fed_rates)
245
+ # = piecewise_sum(85_400, [11_600, 47_150, ...], [0.10, 0.12, ...])
246
+ # = [15_099.50, 0.22])
247
+ # = 15_099.50
263
248
  ```
264
249
 
265
- **Dimension Mismatch Detection**: Operations across different arrays generate error messages:
250
+ #### **Schema Metadata API: Build Tooling**
266
251
 
267
- ```ruby
268
- schema do
269
- input do
270
- array :items do
271
- string :name
272
- end
273
- array :logs do
274
- string :user_name
275
- end
276
- end
252
+ Programmatically access the analyzed structure of your schema to build tools like form generators, documentation sites, or custom validators.
277
253
 
278
- # This generates an error
279
- trait :same_name, input.items.name == input.logs.user_name
280
- end
254
+ ```ruby
255
+ metadata = FederalTax2024.schema_metadata
281
256
 
282
- # Error:
283
- # Cannot broadcast operation across arrays from different sources: items, logs.
284
- # Problem: Multiple operands are arrays from different sources:
285
- # - Operand 1 resolves to array(string) from array 'items'
286
- # - Operand 2 resolves to array(string) from array 'logs'
287
- # Direct operations on arrays from different sources is ambiguous and not supported.
288
- ```
257
+ # Processed, tool-friendly metadata
258
+ metadata.inputs # => { name: { type: :string, domain: ... } }
259
+ metadata.values # => { name: { dependencies: [...], expression: "..." } }
260
+ metadata.traits # => { name: { condition: "...", dependencies: [...] } }
289
261
 
290
- </details>
262
+ # Raw analyzer state for deep analysis
263
+ metadata.dependencies # Dependency graph between all declarations
264
+ metadata.evaluation_order # Topologically sorted computation order
291
265
 
292
- <details>
293
- <summary><strong>💾 Automatic Memoization</strong> - Each value computed exactly once</summary>
266
+ # Export to standard formats
267
+ metadata.to_h # => Serializable hash for JSON/APIs
268
+ metadata.to_json_schema # => JSON Schema for input validation
269
+ ```
294
270
 
295
- ### Automatic Memoization
271
+ #### **AST Visualization: See the Structure**
296
272
 
297
- Each value is computed exactly once:
273
+ For deep debugging, you can print the raw Abstract Syntax Tree (AST) of a schema.
298
274
 
299
275
  ```ruby
300
- runner = FederalTax2024.from(income: 250_000, filing_status: "married_joint")
301
-
302
- # First access computes full dependency chain
303
- runner[:total_tax] # => 53,155.20
304
-
305
- # Subsequent access uses cached values
306
- runner[:fed_tax] # => 39,077.00 (cached)
307
- runner[:after_tax] # => 196,844.80 (cached)
276
+ puts Kumi::Support::SExpressionPrinter.print(FederalTax2024.__syntax_tree__)
277
+ # => (Root
278
+ # inputs: [
279
+ # (InputDeclaration :income :float)
280
+ # (InputDeclaration :filing_status :string domain: ["single", "married_joint"])
281
+ # ]
282
+ # traits: [...]
283
+ # attributes: [...])
308
284
  ```
309
285
 
310
286
  </details>
311
287
 
312
288
  <details>
313
- <summary><strong>🔍 Introspection</strong> - See exactly how values are calculated</summary>
289
+ <summary><strong>Hierarchical Broadcasting</strong> - Vectorization over nested data structures</summary>
290
+
291
+ ### Hierarchical Broadcasting
292
+
293
+ Kumi broadcasts operations over hierarchical data structures with two complementary access modes for maximum flexibility.
314
294
 
315
- ### Introspection
295
+ See [docs/features/hierarchical-broadcasting.md](docs/features/hierarchical-broadcasting.md) for detailed documentation.
316
296
 
317
- Show how values are calculated:
297
+ **Business Scenario**: E-commerce checkout with dynamic pricing rules
318
298
 
299
+ > **"As an e-commerce platform, I need to calculate order totals with complex discount rules:**
300
+ > - Premium members get 15% off electronics
301
+ > - Bulk orders (5+ items) get 10% off that item
302
+ > - Free shipping on orders over $100
303
+ > - Calculate: item subtotals, total discounts, shipping, final total
304
+ >
305
+ > **The challenge:** Each order has different items, quantities, categories, and customer tiers. The discount logic involves multiple conditions - some items qualify for multiple discounts, others for none. Traditional pricing code requires nested if-statements and manual calculations."
306
+
307
+ **Kumi Solution** (16 lines of declarative pricing logic):
319
308
  ```ruby
320
- Kumi::Explain.call(FederalTax2024, :fed_tax, inputs: {income: 100_000, filing_status: "single"})
321
- # => fed_tax = fed_calc[0]
322
- # = (fed_calc = piecewise_sum(taxable_income, fed_breaks, fed_rates)
323
- # = piecewise_sum(85,400, [11,600, 47,150, ...], [0.10, 0.12, ...])
324
- # = [15,099.50, 0.22])
325
- # = 15,099.50
309
+ module OrderPricing
310
+ extend Kumi::Schema
311
+
312
+ schema do
313
+ input do
314
+ array :items do
315
+ float :price
316
+ integer :quantity
317
+ string :category
318
+ end
319
+ string :customer_tier
320
+ float :shipping_threshold
321
+ end
322
+
323
+ # Calculate item subtotals and discount eligibility
324
+ value :subtotals, input.items.price * input.items.quantity
325
+ trait :electronics, input.items.category == "electronics"
326
+ trait :bulk_item, input.items.quantity >= 5
327
+ trait :premium_customer, input.customer_tier == "premium"
328
+
329
+ # Apply layered discounts (premium + bulk can stack)
330
+ trait :premium_electronics, premium_customer & electronics
331
+ trait :stacked_discount, premium_electronics & bulk_item
332
+
333
+ value :discounted_prices do
334
+ on stacked_discount, input.items.price * 0.75 # 15% + 10% = 25% off
335
+ on premium_electronics, input.items.price * 0.85 # 15% off
336
+ on bulk_item, input.items.price * 0.90 # 10% off
337
+ base input.items.price # No discount
338
+ end
339
+
340
+ value :final_subtotals, discounted_prices * input.items.quantity
341
+
342
+ # Order totals and conditional shipping
343
+ value :subtotal, fn(:sum, final_subtotals)
344
+ value :total_savings, fn(:sum, subtotals) - subtotal
345
+ value :shipping, subtotal > input.shipping_threshold ? 0.0 : 9.99
346
+ value :total, subtotal + shipping
347
+ end
348
+ end
326
349
  ```
327
350
 
328
- **Debug AST Structure**: Visualize the parsed schema as S-expressions:
351
+ **Mixed Access Modes**: Both object and element access can be used together in the same schema:
329
352
 
330
353
  ```ruby
331
- puts Kumi::Support::SExpressionPrinter.print(FederalTax2024.__syntax_tree__)
332
- # => (Root
333
- # inputs: [
334
- # (InputDeclaration :income :float)
335
- # (InputDeclaration :filing_status :string domain: ["single", "married_joint"])
336
- # ]
337
- # traits: [...]
338
- # attributes: [...])
354
+ module UserAnalytics
355
+ extend Kumi::Schema
356
+
357
+ schema do
358
+ input do
359
+ # Object access mode - structured business data
360
+ array :users do
361
+ string :name
362
+ integer :age
363
+ end
364
+
365
+ # Element access mode - multi-dimensional raw arrays
366
+ array :recent_purchases do
367
+ element :integer, :days_ago
368
+ end
369
+ end
370
+
371
+ # Object access works normally
372
+ value :user_names, input.users.name
373
+ value :avg_age, fn(:avg, input.users.age)
374
+
375
+ # Element access handles nested arrays with progressive path traversal
376
+ value :all_purchase_days, fn(:flatten, input.recent_purchases.days_ago)
377
+ value :recent_flags, input.recent_purchases.days_ago < 5
378
+ trait :has_recent_purchase, fn(:any?, fn(:flatten, recent_flags))
379
+
380
+ # Progressive dimensional analysis - each path level goes deeper
381
+ value :purchase_dimensions, [
382
+ fn(:size, input.recent_purchases), # Number of purchase groups
383
+ fn(:size, input.recent_purchases.days_ago) # Total individual purchase days
384
+ ]
385
+
386
+ # Mixed usage in conditions
387
+ trait :adult_users, input.users.age >= 18
388
+ value :adult_count, fn(:count_if, adult_users)
389
+ end
390
+ end
391
+
392
+ # Works with mixed data structures
393
+ result = UserAnalytics.from({
394
+ users: [{ name: "Alice", age: 25 }, { name: "Bob", age: 17 }],
395
+ recent_purchases: [[1, 3, 7], [2, 4], [10, 15]] # Nested arrays
396
+ })
397
+
398
+ result[:user_names] # => ["Alice", "Bob"]
399
+ result[:has_recent_purchase] # => true (some purchases < 5 days ago)
400
+ result[:adult_count] # => 1
401
+ result[:purchase_dimensions] # => [3, 8] (3 groups, 8 total days)
339
402
  ```
340
403
 
341
404
  </details>
342
405
 
343
406
  <details>
344
- <summary><strong>📋 Schema Metadata</strong> - Extract structured information for tooling</summary>
407
+ <summary><strong>Memoization</strong> - Each value computed exactly once</summary>
345
408
 
346
- ### Schema Metadata
409
+ ### Memoization
347
410
 
348
- Access structured metadata for building tools like form generators and dependency analyzers:
411
+ Each value is computed exactly once:
349
412
 
350
413
  ```ruby
351
- metadata = FederalTax2024.schema_metadata
352
-
353
- # Processed metadata (tool-friendly)
354
- metadata.inputs # Input field types and domains
355
- metadata.values # Value declarations with dependencies
356
- metadata.traits # Trait conditions and metadata
357
- metadata.functions # Function registry information
414
+ runner = FederalTax2024.from(income: 250_000, filing_status: "married_joint")
358
415
 
359
- # Raw analyzer state (advanced usage)
360
- metadata.dependencies # Dependency graph between declarations
361
- metadata.evaluation_order # Topologically sorted computation order
362
- metadata.inferred_types # Type inference results
363
- metadata.declarations # Raw AST declaration nodes
416
+ # First access computes full dependency chain
417
+ runner[:total_tax] # => 53,155.20
364
418
 
365
- # Export formats
366
- metadata.to_h # Serializable hash for JSON/APIs
367
- metadata.to_json_schema # JSON Schema for input validation
419
+ # Subsequent access uses cached values
420
+ runner[:fed_tax] # => 39,077.00 (cached)
421
+ runner[:after_tax] # => 196,844.80 (cached)
368
422
  ```
369
-
370
- The SchemaMetadata interface provides both processed metadata for tool development and raw analyzer state for advanced use cases. Complete documentation available in the SchemaMetadata class and [docs/schema_metadata.md](docs/schema_metadata.md).
371
-
372
423
  </details>
373
424
 
374
- ## Beyond Rules: What the Metadata Unlocks
375
- * **Auto-generated forms** – compile schema → field spec → React form
376
- * **Scenario explorer** – derive all trait combinations, Monte Carlo outcomes
377
- * **Coverage dashboard** – flag branches never hit in prod
378
- * **Schema diff** – highlight behaviour changes across versions
379
-
380
425
  ## Usage
381
426
 
382
427
  **Suitable for:**
383
428
  - Complex interdependent business rules
384
429
  - Tax calculation engines
385
430
  - Insurance premium calculators
386
- - Loan amortization schedules
387
431
  - Commission structures with complex tiers
388
432
  - Pricing engines with multiple discount rules
389
433
 
390
434
  **Not suitable for:**
391
- - Simple conditional statements
435
+ - Basic conditional statements
392
436
  - Sequential procedural workflows
393
- - Rules that change during execution
394
- - High-frequency real-time processing
437
+ - High-frequency processing
438
+
439
+ ## JavaScript Transpiler
440
+
441
+ Transpiles compiled schemas to standalone JavaScript code. See [docs/features/javascript-transpiler.md](docs/features/javascript-transpiler.md) for details.
442
+
443
+ ```ruby
444
+ Kumi::Js.export_to_file(FederalTax2024, "federal-tax-2024.js")
445
+ ```
446
+
447
+ ```javascript
448
+ const { schema } = require('./federal-tax-2024.js');
449
+ const calculator = schema.from({ income: 100_000, filing_status: "single" });
450
+ console.log(calculator.fetch('total_tax')); // 21491
451
+ ```
452
+
453
+ Generated JavaScript includes only functions used by the schema.
454
+
455
+ All tests run in dual mode to verify compiled schemas produce identical results in both Ruby and JavaScript.
395
456
 
396
457
  ## Performance
397
458
 
@@ -400,6 +461,12 @@ Benchmarks on Linux with Ruby 3.3.8 on a Dell Latitude 7450:
400
461
  - 1,000 attributes: **131,000/sec** (analysis <50ms)
401
462
  - 10,000 attributes: **14,200/sec** (analysis ~300ms)
402
463
 
464
+ See [docs/features/performance.md](docs/features/performance.md) for detailed benchmarks.
465
+
466
+ ## What Kumi does not guarantee
467
+
468
+ Lambdas (e.g. -> inside the schema), external IO, floating-point vs bignum, JS transpiler edge cases, time-zone math differences, etc.
469
+
403
470
  ## Learn More
404
471
 
405
472
  - [DSL Syntax Reference](docs/SYNTAX.md)
data/docs/AST.md CHANGED
@@ -77,7 +77,7 @@ CaseExpression = Struct.new(:condition, :result)
77
77
  ```
78
78
 
79
79
  **Case type mappings**:
80
- - `on :a, :b, result` → `condition: fn(:all?, ref(:a), ref(:b))`
80
+ - `on :a, :b, result` → `condition: fn(:cascade_and, ref(:a), ref(:b))`
81
81
  - `on_any :a, :b, result` → `condition: fn(:any?, ref(:a), ref(:b))`
82
82
  - `base result` → `condition: literal(true)`
83
83
 
data/docs/FUNCTIONS.md CHANGED
@@ -10,6 +10,8 @@ Kumi provides a rich library of built-in functions for use within `value` and `t
10
10
  * **Usage**: `fn(:and, boolean1, boolean2, ...)` → `boolean`
11
11
  * **`any?`**: Check if any element in collection is truthy
12
12
  * **Usage**: `fn(:any?, array(any) arg1)` → `boolean`
13
+ * **`cascade_and`**: Element-wise AND for arrays with same nested structure
14
+ * **Usage**: `fn(:cascade_and, boolean1, boolean2, ...)` → `boolean`
13
15
  * **`none?`**: Check if no elements in collection are truthy
14
16
  * **Usage**: `fn(:none?, array(any) arg1)` → `boolean`
15
17
  * **`not`**: Logical NOT
@@ -52,14 +54,14 @@ Kumi provides a rich library of built-in functions for use within `value` and `t
52
54
  * **Usage**: `fn(:modulo, float arg1, float arg2)` → `float`
53
55
  * **`multiply`**: Multiply two numbers
54
56
  * **Usage**: `fn(:multiply, float arg1, float arg2)` → `float`
57
+ * **`piecewise_sum`**: Accumulate over tiered ranges; returns [sum, marginal_rate]
58
+ * **Usage**: `fn(:piecewise_sum, float arg1, array(float) arg2, array(float) arg3)` → `array(float)`
55
59
  * **`power`**: Raise first number to power of second
56
60
  * **Usage**: `fn(:power, float arg1, float arg2)` → `float`
57
61
  * **`round`**: Round number to specified precision
58
62
  * **Usage**: `fn(:round, float1, float2, ...)` → `float`
59
63
  * **`subtract`**: Subtract second number from first
60
64
  * **Usage**: `fn(:subtract, float arg1, float arg2)` → `float`
61
- * **`piecewise_sum`**: Accumulate over tiered ranges; returns [sum, marginal_rate]
62
- * **Usage**: `fn(:piecewise_sum, float arg1, array(float) arg2, array(float) arg3)` → `array(float)`
63
65
 
64
66
  ## String Functions
65
67
 
@@ -67,16 +69,22 @@ Kumi provides a rich library of built-in functions for use within `value` and `t
67
69
  * **Usage**: `fn(:capitalize, string arg1)` → `string`
68
70
  * **`concat`**: Concatenate multiple strings
69
71
  * **Usage**: `fn(:concat, string1, string2, ...)` → `string`
72
+ * **`contains?`**: Check if string contains substring
73
+ * **Usage**: `fn(:contains?, string arg1, string arg2)` → `boolean`
70
74
  * **`downcase`**: Convert string to lowercase
71
75
  * **Usage**: `fn(:downcase, string arg1)` → `string`
72
76
  * **`end_with?`**: Check if string ends with suffix
73
77
  * **Usage**: `fn(:end_with?, string arg1, string arg2)` → `boolean`
74
- * **`include?`**: Check if collection includes element
75
- * **Usage**: `fn(:include?, array(any) arg1, any arg2)` → `boolean`
76
- * **`length`**: Get collection length
77
- * **Usage**: `fn(:length, array(any) arg1)` → `integer`
78
+ * **`includes?`**: Check if string contains substring
79
+ * **Usage**: `fn(:includes?, string arg1, string arg2)` → `boolean`
80
+ * **`length`**: Get string length
81
+ * **Usage**: `fn(:length, string arg1)` → `integer`
78
82
  * **`start_with?`**: Check if string starts with prefix
79
83
  * **Usage**: `fn(:start_with?, string arg1, string arg2)` → `boolean`
84
+ * **`string_include?`**: Check if string contains substring
85
+ * **Usage**: `fn(:string_include?, string arg1, string arg2)` → `boolean`
86
+ * **`string_length`**: Get string length
87
+ * **Usage**: `fn(:string_length, string arg1)` → `integer`
80
88
  * **`strip`**: Remove leading and trailing whitespace
81
89
  * **Usage**: `fn(:strip, string arg1)` → `string`
82
90
  * **`upcase`**: Convert string to uppercase
@@ -84,20 +92,54 @@ Kumi provides a rich library of built-in functions for use within `value` and `t
84
92
 
85
93
  ## Collection Functions
86
94
 
95
+ * **`all_across`**: Check if all elements are truthy across all nested levels
96
+ * **Usage**: `fn(:all_across, array(any) arg1)` → `boolean`
97
+ * **`any_across`**: Check if any element is truthy across all nested levels
98
+ * **Usage**: `fn(:any_across, array(any) arg1)` → `boolean`
99
+ * **`avg_if`**: Average values where corresponding condition is true
100
+ * **Usage**: `fn(:avg_if, array(float) arg1, array(boolean) arg2)` → `float`
101
+ * **`build_array`**: Build array of given size with index values
102
+ * **Usage**: `fn(:build_array, integer arg1)` → `array(any)`
103
+ * **`count_across`**: Count total elements across all nested levels
104
+ * **Usage**: `fn(:count_across, array(any) arg1)` → `integer`
105
+ * **`count_if`**: Count number of true values in boolean array
106
+ * **Usage**: `fn(:count_if, array(boolean) arg1)` → `integer`
107
+ * **`each_slice`**: Group array elements into subarrays of given size
108
+ * **Usage**: `fn(:each_slice, array arg1, integer arg2)` → `array(array)`
87
109
  * **`empty?`**: Check if collection is empty
88
110
  * **Usage**: `fn(:empty?, array(any) arg1)` → `boolean`
89
111
  * **`first`**: Get first element of collection
90
112
  * **Usage**: `fn(:first, array(any) arg1)` → `any`
113
+ * **`flatten`**: Flatten nested arrays into a single array
114
+ * **Usage**: `fn(:flatten, array(any) arg1)` → `array(any)`
115
+ * **`flatten_deep`**: Recursively flatten all nested arrays (alias for flatten)
116
+ * **Usage**: `fn(:flatten_deep, array(any) arg1)` → `array(any)`
117
+ * **`flatten_one`**: Flatten nested arrays by one level only
118
+ * **Usage**: `fn(:flatten_one, array(any) arg1)` → `array(any)`
91
119
  * **`include?`**: Check if collection includes element
92
120
  * **Usage**: `fn(:include?, array(any) arg1, any arg2)` → `boolean`
121
+ * **`indices`**: Generate array of indices for the collection
122
+ * **Usage**: `fn(:indices, array(any) arg1)` → `array(integer)`
123
+ * **`join`**: Join array elements into string with separator
124
+ * **Usage**: `fn(:join, array arg1, string arg2)` → `string`
93
125
  * **`last`**: Get last element of collection
94
126
  * **Usage**: `fn(:last, array(any) arg1)` → `any`
95
- * **`length`**: Get collection length
96
- * **Usage**: `fn(:length, array(any) arg1)` → `integer`
127
+ * **`map_add`**: Add value to each element
128
+ * **Usage**: `fn(:map_add, array(float) arg1, float arg2)` → `array(float)`
129
+ * **`map_conditional`**: Transform elements based on condition: if element == condition_value then true_value else false_value
130
+ * **Usage**: `fn(:map_conditional, array arg1, any arg2, any arg3, any arg4)` → `array`
131
+ * **`map_join_rows`**: Join 2D array into string with row and column separators
132
+ * **Usage**: `fn(:map_join_rows, array(array) arg1, string arg2, string arg3)` → `string`
133
+ * **`map_multiply`**: Multiply each element by factor
134
+ * **Usage**: `fn(:map_multiply, array(float) arg1, float arg2)` → `array(float)`
135
+ * **`map_with_index`**: Map collection elements to [element, index] pairs
136
+ * **Usage**: `fn(:map_with_index, array(any) arg1)` → `array(any)`
97
137
  * **`max`**: Find maximum value in numeric collection
98
138
  * **Usage**: `fn(:max, array(float) arg1)` → `float`
99
139
  * **`min`**: Find minimum value in numeric collection
100
140
  * **Usage**: `fn(:min, array(float) arg1)` → `float`
141
+ * **`range`**: Generate range of integers from start to finish (exclusive)
142
+ * **Usage**: `fn(:range, integer arg1, integer arg2)` → `array(integer)`
101
143
  * **`reverse`**: Reverse collection order
102
144
  * **Usage**: `fn(:reverse, array(any) arg1)` → `array(any)`
103
145
  * **`size`**: Get collection size
@@ -106,6 +148,8 @@ Kumi provides a rich library of built-in functions for use within `value` and `t
106
148
  * **Usage**: `fn(:sort, array(any) arg1)` → `array(any)`
107
149
  * **`sum`**: Sum all numeric elements in collection
108
150
  * **Usage**: `fn(:sum, array(float) arg1)` → `float`
151
+ * **`sum_if`**: Sum values where corresponding condition is true
152
+ * **Usage**: `fn(:sum_if, array(float) arg1, array(boolean) arg2)` → `float`
109
153
  * **`unique`**: Remove duplicate elements from collection
110
154
  * **Usage**: `fn(:unique, array(any) arg1)` → `array(any)`
111
155