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.
- checksums.yaml +4 -4
- data/CLAUDE.md +160 -8
- data/README.md +278 -200
- data/{documents → docs}/AST.md +29 -29
- data/{documents → docs}/DSL.md +3 -3
- data/{documents → docs}/SYNTAX.md +107 -24
- data/docs/features/README.md +45 -0
- data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
- data/docs/features/analysis-type-inference.md +42 -0
- data/docs/features/analysis-unsat-detection.md +71 -0
- data/docs/features/array-broadcasting.md +170 -0
- data/docs/features/input-declaration-system.md +42 -0
- data/docs/features/performance.md +16 -0
- data/examples/federal_tax_calculator_2024.rb +43 -40
- data/examples/game_of_life.rb +97 -0
- data/examples/simple_rpg_game.rb +1000 -0
- data/examples/static_analysis_errors.rb +178 -0
- data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
- data/lib/kumi/analyzer/analysis_state.rb +37 -0
- data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
- data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
- data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +8 -7
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +106 -26
- data/lib/kumi/analyzer/passes/input_collector.rb +105 -23
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/pass_base.rb +11 -28
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
- data/lib/kumi/analyzer/passes/toposorter.rb +45 -9
- data/lib/kumi/analyzer/passes/type_checker.rb +34 -11
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
- data/lib/kumi/analyzer/passes/type_inferencer.rb +128 -21
- data/lib/kumi/analyzer/passes/unsat_detector.rb +312 -13
- data/lib/kumi/analyzer/passes/visitor_pass.rb +4 -3
- data/lib/kumi/analyzer.rb +41 -24
- data/lib/kumi/atom_unsat_solver.rb +45 -0
- data/lib/kumi/cli.rb +449 -0
- data/lib/kumi/compiler.rb +194 -16
- data/lib/kumi/constraint_relationship_solver.rb +638 -0
- data/lib/kumi/domain/validator.rb +0 -4
- data/lib/kumi/error_reporter.rb +6 -6
- data/lib/kumi/evaluation_wrapper.rb +20 -4
- data/lib/kumi/explain.rb +28 -28
- data/lib/kumi/export/node_registry.rb +26 -12
- data/lib/kumi/export/node_serializers.rb +1 -1
- data/lib/kumi/function_registry/collection_functions.rb +117 -9
- data/lib/kumi/function_registry/function_builder.rb +4 -3
- data/lib/kumi/function_registry.rb +8 -2
- data/lib/kumi/input/type_matcher.rb +3 -0
- data/lib/kumi/input/validator.rb +0 -3
- data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
- data/lib/kumi/parser/dsl_cascade_builder.rb +19 -8
- data/lib/kumi/parser/expression_converter.rb +80 -12
- data/lib/kumi/parser/input_builder.rb +40 -9
- data/lib/kumi/parser/input_field_proxy.rb +46 -0
- data/lib/kumi/parser/input_proxy.rb +3 -3
- data/lib/kumi/parser/nested_input.rb +15 -0
- data/lib/kumi/parser/parser.rb +2 -0
- data/lib/kumi/parser/schema_builder.rb +10 -9
- data/lib/kumi/parser/sugar.rb +171 -18
- data/lib/kumi/schema.rb +3 -1
- data/lib/kumi/schema_instance.rb +69 -3
- data/lib/kumi/syntax/array_expression.rb +15 -0
- data/lib/kumi/syntax/call_expression.rb +11 -0
- data/lib/kumi/syntax/cascade_expression.rb +11 -0
- data/lib/kumi/syntax/case_expression.rb +11 -0
- data/lib/kumi/syntax/declaration_reference.rb +11 -0
- data/lib/kumi/syntax/hash_expression.rb +11 -0
- data/lib/kumi/syntax/input_declaration.rb +12 -0
- data/lib/kumi/syntax/input_element_reference.rb +12 -0
- data/lib/kumi/syntax/input_reference.rb +12 -0
- data/lib/kumi/syntax/literal.rb +11 -0
- data/lib/kumi/syntax/root.rb +1 -0
- data/lib/kumi/syntax/trait_declaration.rb +11 -0
- data/lib/kumi/syntax/value_declaration.rb +11 -0
- data/lib/kumi/types/compatibility.rb +8 -0
- data/lib/kumi/types/validator.rb +1 -1
- data/lib/kumi/vectorization_metadata.rb +108 -0
- data/lib/kumi/version.rb +1 -1
- data/scripts/generate_function_docs.rb +22 -10
- metadata +38 -17
- data/CHANGELOG.md +0 -25
- data/lib/kumi/domain.rb +0 -8
- data/lib/kumi/input.rb +0 -8
- data/lib/kumi/syntax/declarations.rb +0 -23
- data/lib/kumi/syntax/expressions.rb +0 -30
- data/lib/kumi/syntax/terminal_expressions.rb +0 -27
- data/lib/kumi/syntax.rb +0 -9
- data/test_impossible_cascade.rb +0 -51
- /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
|
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
|
36
|
-
on
|
37
|
-
on
|
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
|
47
|
-
on
|
48
|
-
on
|
49
|
-
on
|
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,
|
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 /
|
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
|
70
|
-
on
|
71
|
-
on
|
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 /
|
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,
|
94
|
-
value :after_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
|
99
|
-
|
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: #{
|
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
|
-
|
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
|