kumi 0.0.4 → 0.0.6

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +160 -8
  3. data/README.md +278 -200
  4. data/{documents → docs}/AST.md +29 -29
  5. data/{documents → docs}/DSL.md +3 -3
  6. data/{documents → docs}/SYNTAX.md +107 -24
  7. data/docs/features/README.md +45 -0
  8. data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
  9. data/docs/features/analysis-type-inference.md +42 -0
  10. data/docs/features/analysis-unsat-detection.md +71 -0
  11. data/docs/features/array-broadcasting.md +170 -0
  12. data/docs/features/input-declaration-system.md +42 -0
  13. data/docs/features/performance.md +16 -0
  14. data/examples/federal_tax_calculator_2024.rb +43 -40
  15. data/examples/game_of_life.rb +97 -0
  16. data/examples/simple_rpg_game.rb +1000 -0
  17. data/examples/static_analysis_errors.rb +178 -0
  18. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
  19. data/lib/kumi/analyzer/analysis_state.rb +37 -0
  20. data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
  21. data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
  22. data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +8 -7
  23. data/lib/kumi/analyzer/passes/dependency_resolver.rb +106 -26
  24. data/lib/kumi/analyzer/passes/input_collector.rb +105 -23
  25. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  26. data/lib/kumi/analyzer/passes/pass_base.rb +11 -28
  27. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
  28. data/lib/kumi/analyzer/passes/toposorter.rb +45 -9
  29. data/lib/kumi/analyzer/passes/type_checker.rb +34 -11
  30. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
  31. data/lib/kumi/analyzer/passes/type_inferencer.rb +128 -21
  32. data/lib/kumi/analyzer/passes/unsat_detector.rb +312 -13
  33. data/lib/kumi/analyzer/passes/visitor_pass.rb +4 -3
  34. data/lib/kumi/analyzer.rb +41 -24
  35. data/lib/kumi/atom_unsat_solver.rb +45 -0
  36. data/lib/kumi/cli.rb +449 -0
  37. data/lib/kumi/compiler.rb +194 -16
  38. data/lib/kumi/constraint_relationship_solver.rb +638 -0
  39. data/lib/kumi/domain/validator.rb +0 -4
  40. data/lib/kumi/error_reporter.rb +6 -6
  41. data/lib/kumi/evaluation_wrapper.rb +20 -4
  42. data/lib/kumi/explain.rb +28 -28
  43. data/lib/kumi/export/node_registry.rb +26 -12
  44. data/lib/kumi/export/node_serializers.rb +1 -1
  45. data/lib/kumi/function_registry/collection_functions.rb +117 -9
  46. data/lib/kumi/function_registry/function_builder.rb +4 -3
  47. data/lib/kumi/function_registry.rb +8 -2
  48. data/lib/kumi/input/type_matcher.rb +3 -0
  49. data/lib/kumi/input/validator.rb +0 -3
  50. data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
  51. data/lib/kumi/parser/dsl_cascade_builder.rb +19 -8
  52. data/lib/kumi/parser/expression_converter.rb +80 -12
  53. data/lib/kumi/parser/input_builder.rb +40 -9
  54. data/lib/kumi/parser/input_field_proxy.rb +46 -0
  55. data/lib/kumi/parser/input_proxy.rb +3 -3
  56. data/lib/kumi/parser/nested_input.rb +15 -0
  57. data/lib/kumi/parser/parser.rb +2 -0
  58. data/lib/kumi/parser/schema_builder.rb +10 -9
  59. data/lib/kumi/parser/sugar.rb +171 -18
  60. data/lib/kumi/schema.rb +3 -1
  61. data/lib/kumi/schema_instance.rb +69 -3
  62. data/lib/kumi/syntax/array_expression.rb +15 -0
  63. data/lib/kumi/syntax/call_expression.rb +11 -0
  64. data/lib/kumi/syntax/cascade_expression.rb +11 -0
  65. data/lib/kumi/syntax/case_expression.rb +11 -0
  66. data/lib/kumi/syntax/declaration_reference.rb +11 -0
  67. data/lib/kumi/syntax/hash_expression.rb +11 -0
  68. data/lib/kumi/syntax/input_declaration.rb +12 -0
  69. data/lib/kumi/syntax/input_element_reference.rb +12 -0
  70. data/lib/kumi/syntax/input_reference.rb +12 -0
  71. data/lib/kumi/syntax/literal.rb +11 -0
  72. data/lib/kumi/syntax/root.rb +1 -0
  73. data/lib/kumi/syntax/trait_declaration.rb +11 -0
  74. data/lib/kumi/syntax/value_declaration.rb +11 -0
  75. data/lib/kumi/types/compatibility.rb +8 -0
  76. data/lib/kumi/types/validator.rb +1 -1
  77. data/lib/kumi/vectorization_metadata.rb +108 -0
  78. data/lib/kumi/version.rb +1 -1
  79. data/scripts/generate_function_docs.rb +22 -10
  80. metadata +38 -17
  81. data/CHANGELOG.md +0 -25
  82. data/lib/kumi/domain.rb +0 -8
  83. data/lib/kumi/input.rb +0 -8
  84. data/lib/kumi/syntax/declarations.rb +0 -23
  85. data/lib/kumi/syntax/expressions.rb +0 -30
  86. data/lib/kumi/syntax/terminal_expressions.rb +0 -27
  87. data/lib/kumi/syntax.rb +0 -9
  88. data/test_impossible_cascade.rb +0 -51
  89. /data/{documents → docs}/FUNCTIONS.md +0 -0
@@ -0,0 +1,170 @@
1
+ # Array Broadcasting
2
+
3
+ Automatic vectorization of operations over array fields with element-wise computation and aggregation.
4
+
5
+ ## Overview
6
+
7
+ The array broadcasting system enables natural field access syntax on array inputs (`input.items.price`) that automatically applies operations element-wise across the array, with intelligent detection of map vs reduce operations.
8
+
9
+ ## Core Mechanism
10
+
11
+ The system uses a three-stage pipeline:
12
+
13
+ 1. **Parser** - Creates InputElementReference AST nodes for nested field access
14
+ 2. **BroadcastDetector** - Identifies which operations should be vectorized vs scalar
15
+ 3. **Compiler** - Generates appropriate map/reduce functions based on usage context
16
+
17
+ ## Basic Broadcasting
18
+
19
+ ```ruby
20
+ schema do
21
+ input do
22
+ array :line_items do
23
+ float :price
24
+ integer :quantity
25
+ string :category
26
+ end
27
+ scalar :tax_rate, type: :float
28
+ end
29
+
30
+ # Element-wise computation - broadcasts over each item
31
+ value :subtotals, input.line_items.price * input.line_items.quantity
32
+
33
+ # Element-wise traits - applied to each item
34
+ trait :is_taxable, (input.line_items.category != "digital")
35
+
36
+ # Conditional logic - element-wise evaluation
37
+ value :taxes, fn(:if, is_taxable, subtotals * input.tax_rate, 0.0)
38
+ end
39
+ ```
40
+
41
+ ## Aggregation Operations
42
+
43
+ Operations that consume arrays to produce scalars are automatically detected:
44
+
45
+ ```ruby
46
+ schema do
47
+ # These aggregate the vectorized results
48
+ value :total_subtotal, fn(:sum, subtotals)
49
+ value :total_tax, fn(:sum, taxes)
50
+ value :grand_total, total_subtotal + total_tax
51
+
52
+ # Statistics over arrays
53
+ value :avg_price, fn(:avg, input.line_items.price)
54
+ value :max_quantity, fn(:max, input.line_items.quantity)
55
+ end
56
+ ```
57
+
58
+ ## Field Access Nesting
59
+
60
+ Supports arbitrary depth field access with path building:
61
+
62
+ ```ruby
63
+ schema do
64
+ input do
65
+ array :orders do
66
+ array :items do
67
+ hash :product do
68
+ string :name
69
+ float :base_price
70
+ end
71
+ integer :quantity
72
+ end
73
+ end
74
+ end
75
+
76
+ # Deep field access - automatically broadcasts over nested arrays
77
+ value :all_product_names, input.orders.items.product.name
78
+ value :total_values, input.orders.items.product.base_price * input.orders.items.quantity
79
+ end
80
+ ```
81
+
82
+ ## Type Inference
83
+
84
+ The type system automatically infers appropriate types for broadcasted operations:
85
+
86
+ - `input.items.price` (float array) → inferred as `:float` per element
87
+ - `input.items.price * input.items.quantity` → element-wise `:float` result
88
+ - `fn(:sum, input.items.price)` → scalar `:float` result
89
+
90
+ ## Implementation Details
91
+
92
+ ### Parser Layer
93
+ - **InputFieldProxy** - Handles `input.field.subfield...` with path building
94
+ - **InputElementReference** - AST node representing array field access paths
95
+
96
+ ### Analysis Layer
97
+ - **BroadcastDetector** - Identifies vectorized vs scalar operations
98
+ - **TypeInferencer** - Infers types for array element access patterns
99
+
100
+ ### Compilation Layer
101
+ - **Automatic Dispatch** - Maps element-wise operations to array map functions
102
+ - **Reduction Detection** - Converts aggregation functions to array reduce operations
103
+
104
+ ## Usage Patterns
105
+
106
+ ### Element-wise Operations
107
+ ```ruby
108
+ # All of these broadcast element-wise
109
+ value :discounted_prices, input.items.price * 0.9
110
+ trait :expensive, (input.items.price > 100.0)
111
+ value :categories, input.items.category
112
+ ```
113
+
114
+ ### Aggregation Operations
115
+ ```ruby
116
+ # These consume arrays to produce scalars
117
+ value :item_count, fn(:size, input.items)
118
+ value :total_price, fn(:sum, input.items.price)
119
+ value :has_expensive, fn(:any?, expensive)
120
+ ```
121
+
122
+ ### Mixed Operations
123
+ ```ruby
124
+ # Element-wise computation followed by aggregation
125
+ value :line_totals, input.items.price * input.items.quantity
126
+ value :order_total, fn(:sum, line_totals)
127
+ value :avg_line_total, fn(:avg, line_totals)
128
+ ```
129
+
130
+ ## Error Handling
131
+
132
+ ### Dimension Mismatch Detection
133
+
134
+ Array broadcasting operations are only valid within the same array source. Attempting to broadcast across different arrays generates detailed error messages:
135
+
136
+ ```ruby
137
+ schema do
138
+ input do
139
+ array :items do
140
+ string :name
141
+ end
142
+ array :logs do
143
+ string :user_name
144
+ end
145
+ end
146
+
147
+ # This will generate a dimension mismatch error
148
+ trait :same_name, input.items.name == input.logs.user_name
149
+ end
150
+
151
+ # Error:
152
+ # Cannot broadcast operation across arrays from different sources: items, logs.
153
+ # Problem: Multiple operands are arrays from different sources:
154
+ # - Operand 1 resolves to array(string) from array 'items'
155
+ # - Operand 2 resolves to array(string) from array 'logs'
156
+ # Direct operations on arrays from different sources is ambiguous and not supported.
157
+ # Vectorized operations can only work on fields from the same array input.
158
+ ```
159
+
160
+ The error messages provide:
161
+ - **Quick Summary**: Identifies the conflicting array sources
162
+ - **Type Information**: Shows the resolved types of each operand
163
+ - **Clear Explanation**: Why the operation is ambiguous and not supported
164
+
165
+ ## Performance Characteristics
166
+
167
+ - **Single Pass** - Each array is traversed once per computation chain
168
+ - **Lazy Evaluation** - Operations are composed into efficient pipelines
169
+ - **Memory Efficient** - No intermediate array allocations for simple operations
170
+ - **Type Safe** - Full compile-time type checking for array element operations
@@ -0,0 +1,42 @@
1
+ # Input Declarations
2
+
3
+ Declares expected inputs with types and domain constraints, separating input metadata from business logic.
4
+
5
+ ## Declaration Syntax
6
+
7
+ ```ruby
8
+ schema do
9
+ input do
10
+ string :customer_name
11
+ integer :age, domain: 18..120
12
+ float :balance, domain: 0.0..Float::INFINITY
13
+ boolean :verified
14
+ array :tags, elem: { type: :string }
15
+ hash :metadata, key: { type: :string }, val: { type: :any }
16
+ any :flexible
17
+ end
18
+
19
+ trait :adult, (input.age >= 18)
20
+ value :status, input.verified ? "verified" : "pending"
21
+ end
22
+ ```
23
+
24
+ ## Domain Constraints
25
+
26
+ **Validation occurs at runtime:**
27
+ ```ruby
28
+ schema.from(credit_score: 900) # Domain: 300..850
29
+ # => InputValidationError: Field :credit_score value 900 is outside domain 300..850
30
+ ```
31
+
32
+ **Constraint types:**
33
+ - Range domains: `domain: 18..120`
34
+ - Array domains: `domain: %w[active inactive]`
35
+ - Regex domains: `domain: /^[a-zA-Z]+$/`
36
+
37
+ ## Validation Process
38
+
39
+ - Input data validated against declared field metadata
40
+ - Type validation checks value matches declared type
41
+ - Domain validation checks value satisfies constraints
42
+ - Detailed error messages for violations
@@ -0,0 +1,16 @@
1
+ # Performance
2
+
3
+ TODO: Add benchmark data
4
+
5
+ Processes large schemas with optimized algorithms for analysis, compilation, and execution.
6
+
7
+ ## Execution Model
8
+
9
+ **Compilation:**
10
+ - Each expression compiled to executable lambda
11
+ - Direct function calls for operations
12
+
13
+ **Runtime:**
14
+ - Result caching to avoid recomputation
15
+ - Selective evaluation: only requested keys computed
16
+ - Direct lambda invocation
@@ -1,28 +1,34 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # U.S. federal income‑tax plus FICA
2
4
 
3
5
  require_relative "../lib/kumi"
4
6
 
5
- module CompositeTax2024
6
- extend Kumi::Schema
7
+ module Tax2024
7
8
  FED_BREAKS_SINGLE = [11_600, 47_150, 100_525, 191_950,
8
- 243_725, 609_350, Float::INFINITY]
9
+ 243_725, 609_350, Float::INFINITY].freeze
9
10
 
10
11
  FED_BREAKS_MARRIED = [23_200, 94_300, 201_050, 383_900,
11
- 487_450, 731_200, Float::INFINITY]
12
+ 487_450, 731_200, Float::INFINITY].freeze
12
13
 
13
14
  FED_BREAKS_SEPARATE = [11_600, 47_150, 100_525, 191_950,
14
- 243_725, 365_600, Float::INFINITY]
15
+ 243_725, 365_600, Float::INFINITY].freeze
15
16
 
16
17
  FED_BREAKS_HOH = [16_550, 63_100, 100_500, 191_950,
17
- 243_700, 609_350, Float::INFINITY]
18
-
18
+ 243_700, 609_350, Float::INFINITY].freeze
19
+
19
20
  FED_RATES = [0.10, 0.12, 0.22, 0.24,
20
- 0.32, 0.35, 0.37]
21
+ 0.32, 0.35, 0.37].freeze
22
+ end
23
+
24
+ module FederalTaxCalculator
25
+ extend Kumi::Schema
26
+ include Tax2024
21
27
 
22
28
  schema do
23
29
  input do
24
30
  float :income
25
- string :filing_status
31
+ string :filing_status, domain: %w[single married_joint married_separate head_of_household]
26
32
  end
27
33
 
28
34
  # ── standard deduction table ───────────────────────────────────────
@@ -32,30 +38,28 @@ module CompositeTax2024
32
38
  trait :hoh, input.filing_status == "head_of_household"
33
39
 
34
40
  value :std_deduction do
35
- on :single, 14_600
36
- on :married, 29_200
37
- on :separate, 14_600
41
+ on single, 14_600
42
+ on married, 29_200
43
+ on separate, 14_600
38
44
  base 21_900 # HOH default
39
45
  end
40
46
 
41
- value :taxable_income,
42
- fn(:max, [input.income - std_deduction, 0])
47
+ value :taxable_income, [input.income - std_deduction, 0].max
43
48
 
44
- # ── FEDERAL brackets (single shown; others similar if needed) ──────
45
49
  value :fed_breaks do
46
- on :single, FED_BREAKS_SINGLE
47
- on :married, FED_BREAKS_MARRIED
48
- on :separate, FED_BREAKS_SEPARATE
49
- on :hoh, FED_BREAKS_HOH
50
+ on single, FED_BREAKS_SINGLE
51
+ on married, FED_BREAKS_MARRIED
52
+ on separate, FED_BREAKS_SEPARATE
53
+ on hoh, FED_BREAKS_HOH
50
54
  end
51
55
 
52
- value :fed_rates, FED_RATES
56
+ value :fed_rates, FED_RATES
53
57
  value :fed_calc,
54
58
  fn(:piecewise_sum, taxable_income, fed_breaks, fed_rates)
55
59
 
56
60
  value :fed_tax, fed_calc[0]
57
61
  value :fed_marginal, fed_calc[1]
58
- value :fed_eff, fed_tax / fn(:max, [input.income, 1.0])
62
+ value :fed_eff, fed_tax / [input.income, 1.0].max
59
63
 
60
64
  # ── FICA (employee share) ─────────────────────────────────────────────
61
65
  value :ss_wage_base, 168_600.0
@@ -66,42 +70,37 @@ module CompositeTax2024
66
70
 
67
71
  # additional‑Medicare threshold depends on filing status
68
72
  value :addl_threshold do
69
- on :single, 200_000
70
- on :married, 250_000
71
- on :separate, 125_000
73
+ on single, 200_000
74
+ on married, 250_000
75
+ on separate, 125_000
72
76
  base 200_000 # HOH same as single
73
77
  end
74
78
 
75
79
  # social‑security portion (capped)
76
- value :ss_tax,
77
- fn(:min, [input.income, ss_wage_base]) * ss_rate
80
+ value :ss_tax, [input.income, ss_wage_base].min * ss_rate
78
81
 
79
82
  # medicare (1.45 % on everything)
80
83
  value :med_tax, input.income * med_base_rate
81
84
 
82
85
  # additional medicare on income above threshold
83
- value :addl_med_tax,
84
- fn(:max, [input.income - addl_threshold, 0]) * addl_med_rate
86
+ value :addl_med_tax, [input.income - addl_threshold, 0].max * addl_med_rate
85
87
 
86
88
  value :fica_tax, ss_tax + med_tax + addl_med_tax
87
- value :fica_eff, fica_tax / fn(:max, [input.income, 1.0])
89
+ value :fica_eff, fica_tax / [input.income, 1.0].max
88
90
 
89
91
  # ── Totals ─────────────────────────────────────────────────────────
90
- value :total_tax,
91
- fed_tax + fica_tax
92
+ value :total_tax, fed_tax + fica_tax
92
93
 
93
- value :total_eff, total_tax / fn(:max, [input.income, 1.0])
94
- value :after_tax, input.income - total_tax
94
+ value :total_eff, total_tax / fn(:max, [input.income, 1.0])
95
+ value :after_tax, input.income - total_tax
95
96
  end
96
97
  end
97
98
 
98
- def example(income: 1_000_000, status: "single")
99
- # Create a runner for the schema
100
- r = CompositeTax2024.from(income: income, filing_status: status)
101
- # puts r.inspect
99
+ def print_tax_summary(args)
100
+ r = FederalTaxCalculator.from(args)
102
101
  puts "\n=== 2024 U.S. Income‑Tax Example ==="
103
- printf "Income: $%0.2f\n", income
104
- puts "Filing status: #{status}\n\n"
102
+ printf "Income: $%0.2f\n", args[:income]
103
+ puts "Filing status: #{args[:filing_status]}\n\n"
105
104
 
106
105
  puts "Federal tax: $#{r[:fed_tax].round(2)} (#{(r[:fed_eff] * 100).round(2)}% effective)"
107
106
  puts "FICA tax: $#{r[:fica_tax].round(2)} (#{(r[:fica_eff] * 100).round(2)}% effective)"
@@ -109,4 +108,8 @@ def example(income: 1_000_000, status: "single")
109
108
  puts "After-tax income: $#{r[:after_tax].round(2)}"
110
109
  end
111
110
 
112
- example
111
+
112
+ input = { income: 1_000_000,
113
+ filing_status: "single"
114
+ }
115
+ print_tax_summary(input)
@@ -0,0 +1,97 @@
1
+ $LOAD_PATH.unshift(File.join(__dir__, "..", "lib"))
2
+ require "kumi"
3
+
4
+ NEIGHBOR_DELTAS = [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]
5
+ begin
6
+ # in a block so we dont define this globally
7
+ def neighbor_cells_sum_method(cells, row, col, height, width)
8
+ # Calculate neighbor indices with wraparound
9
+ NEIGHBOR_DELTAS.map do |dr, dc|
10
+ neighbor_row = (row + dr) % height
11
+ neighbor_col = (col + dc) % width
12
+ neighbor_index = (neighbor_row * width) + neighbor_col
13
+ cells[neighbor_index]
14
+ end.sum
15
+ end
16
+ Kumi::FunctionRegistry.register_with_metadata(:neighbor_cells_sum, method(:neighbor_cells_sum_method),
17
+ return_type: :integer, arity: 5,
18
+ param_types: %i[array integer integer integer integer],
19
+ description: "Get neighbor cells for Conway's Game of Life")
20
+ end
21
+
22
+ module GameOfLife
23
+ extend Kumi::Schema
24
+ WIDTH = 50
25
+ HEIGHT = 30
26
+
27
+ schema do
28
+ # Complete Game of Life engine - computes entire next generation
29
+ input do
30
+ array :cells, elem: { type: :integer }
31
+ end
32
+
33
+ # Generate next state for every cell in the grid
34
+ next_cell_values = []
35
+
36
+ (0...HEIGHT).each do |row|
37
+ (0...WIDTH).each do |col|
38
+ # Neighbor count and current state for this cell
39
+ cell_index = (row * WIDTH) + col
40
+ value :"neighbor_sum_#{cell_index}", fn(:neighbor_cells_sum, input.cells, row, col, HEIGHT, WIDTH)
41
+
42
+ # Game of Life rules: (alive && neighbors == 2) || (neighbors == 3)
43
+ trait :"cell_#{cell_index}_alive",
44
+ ((input.cells[cell_index] == 1) & (ref(:"neighbor_sum_#{cell_index}") == 2)) |
45
+ (ref(:"neighbor_sum_#{cell_index}") == 3)
46
+
47
+ # Next state for this cell
48
+ value :"cell_#{cell_index}_next" do
49
+ on :"cell_#{cell_index}_alive", 1
50
+ base 0
51
+ end
52
+
53
+ next_cell_values << ref(:"cell_#{cell_index}_next")
54
+ end
55
+ end
56
+
57
+ # Complete next generation as array
58
+ value :next_cells, next_cell_values
59
+
60
+ # Render current state as visual string
61
+ value :cell_symbols, fn(:map_conditional, input.cells, 1, "█", " ")
62
+ value :grid_rows, fn(:each_slice, cell_symbols, WIDTH)
63
+ value :rendered_grid, fn(:map_join_rows, grid_rows, "", "\n")
64
+ end
65
+ end
66
+
67
+ # # Helper to pretty‑print the grid
68
+ def render(cells, width)
69
+ cells.each_slice(width) do |row|
70
+ puts row.map { |v| v == 1 ? "█" : " " }.join
71
+ end
72
+ end
73
+
74
+ # # Bootstrap a simple glider on a 10×10 grid
75
+ width = GameOfLife::WIDTH
76
+ height = GameOfLife::HEIGHT
77
+ cells = Array.new(width * height, 0)
78
+ # Glider pattern
79
+ [[1, 1], [2, 3], [3, 1], [3, 2], [3, 3]].each { |r, c| cells[(r * width) + c] = 1 }
80
+
81
+ 10_000.times do |gen|
82
+ system("clear") || system("cls")
83
+ puts "Conway's Game of Life - Generation #{gen}"
84
+ puts ""
85
+
86
+ # Create schema instance for this generation
87
+ runner = GameOfLife.from(cells: cells)
88
+
89
+ # Render using Kumi instead of Ruby function!
90
+ rendered_output = runner[:rendered_grid]
91
+ puts rendered_output
92
+
93
+ # Calculate next generation with single schema call!
94
+ cells = runner[:next_cells]
95
+
96
+ sleep 0.1
97
+ end