kumi 0.0.3 → 0.0.5
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 +109 -2
- data/README.md +174 -205
- data/documents/DSL.md +3 -3
- data/documents/SYNTAX.md +17 -26
- data/examples/federal_tax_calculator_2024.rb +36 -38
- 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/definition_validator.rb +4 -3
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +50 -10
- data/lib/kumi/analyzer/passes/input_collector.rb +28 -7
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/pass_base.rb +10 -27
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
- data/lib/kumi/analyzer/passes/toposorter.rb +3 -3
- data/lib/kumi/analyzer/passes/type_checker.rb +2 -1
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
- data/lib/kumi/analyzer/passes/type_inferencer.rb +2 -4
- data/lib/kumi/analyzer/passes/unsat_detector.rb +233 -14
- data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -1
- data/lib/kumi/analyzer.rb +42 -24
- data/lib/kumi/atom_unsat_solver.rb +45 -0
- data/lib/kumi/cli.rb +449 -0
- data/lib/kumi/constraint_relationship_solver.rb +638 -0
- data/lib/kumi/error_reporter.rb +6 -6
- data/lib/kumi/evaluation_wrapper.rb +22 -4
- data/lib/kumi/explain.rb +9 -10
- data/lib/kumi/function_registry/collection_functions.rb +103 -0
- data/lib/kumi/function_registry/string_functions.rb +1 -1
- data/lib/kumi/parser/dsl_cascade_builder.rb +17 -6
- data/lib/kumi/parser/expression_converter.rb +80 -12
- data/lib/kumi/parser/guard_rails.rb +2 -2
- data/lib/kumi/parser/parser.rb +2 -0
- data/lib/kumi/parser/schema_builder.rb +1 -1
- data/lib/kumi/parser/sugar.rb +117 -16
- data/lib/kumi/schema.rb +3 -1
- data/lib/kumi/schema_instance.rb +69 -3
- data/lib/kumi/syntax/declarations.rb +3 -0
- data/lib/kumi/syntax/expressions.rb +4 -0
- data/lib/kumi/syntax/root.rb +1 -0
- data/lib/kumi/syntax/terminal_expressions.rb +3 -0
- data/lib/kumi/types/compatibility.rb +8 -0
- data/lib/kumi/types/validator.rb +1 -1
- data/lib/kumi/version.rb +1 -1
- data/scripts/generate_function_docs.rb +22 -10
- metadata +10 -6
- data/CHANGELOG.md +0 -25
- data/test_impossible_cascade.rb +0 -51
data/documents/SYNTAX.md
CHANGED
@@ -187,11 +187,13 @@ value :sorted_scores, fn(:sort, input.score_array)
|
|
187
187
|
|
188
188
|
### Built-in Functions Available
|
189
189
|
|
190
|
+
See [FUNCTIONS.md](documents/FUNCTIONS.md)
|
191
|
+
|
190
192
|
| Category | Sugar | Sugar-Free |
|
191
193
|
|----------|-------|------------|
|
192
194
|
| **Arithmetic** | `+`, `-`, `*`, `/`, `**` | `fn(:add, a, b)`, `fn(:subtract, a, b)`, etc. |
|
193
195
|
| **Comparison** | `>`, `<`, `>=`, `<=`, `==`, `!=` | `fn(:>, a, b)`, `fn(:<, a, b)`, etc. |
|
194
|
-
| **Logical** | `&`
|
196
|
+
| **Logical** | `&` `|` | `fn(:and, a, b)`, `fn(:or, a, b)`, `fn(:not, a)` |
|
195
197
|
| **Math** | `abs`, `round`, `ceil`, `floor` | `fn(:abs, x)`, `fn(:round, x)`, etc. |
|
196
198
|
| **String** | `.length`, `.upcase`, `.downcase` | `fn(:string_length, s)`, `fn(:upcase, s)`, etc. |
|
197
199
|
| **Collection** | `.sum`, `.size`, `.max`, `.min` | `fn(:sum, arr)`, `fn(:size, arr)`, etc. |
|
@@ -210,29 +212,17 @@ value :formatted_name, fn(:add, fn(:add, input.first_name, " "), input.last_name
|
|
210
212
|
|
211
213
|
## Cascade Logic
|
212
214
|
|
213
|
-
|
214
|
-
|
215
|
+
Cascades are similar to Ruby when case, where each case is one of more trait reference and finally the value if that branch is true.
|
215
216
|
```ruby
|
216
|
-
# With Sugar
|
217
|
-
value :grade_letter do
|
218
|
-
on :excellent_student, "A+"
|
219
|
-
on :high_scorer, "A"
|
220
|
-
on :above_average, "B"
|
221
|
-
on :needs_improvement, "C"
|
222
|
-
base "F"
|
223
|
-
end
|
224
|
-
|
225
|
-
# Sugar-Free
|
226
217
|
value :grade_letter do
|
227
|
-
on
|
228
|
-
on
|
229
|
-
on
|
230
|
-
on
|
218
|
+
on excellent_student, "A+"
|
219
|
+
on high_scorer, "A"
|
220
|
+
on above_average, "B"
|
221
|
+
on needs_improvement, "C"
|
231
222
|
base "F"
|
232
223
|
end
|
233
224
|
```
|
234
225
|
|
235
|
-
The difference is in how the traits referenced in cascade conditions are defined (see Trait Declarations above).
|
236
226
|
|
237
227
|
## References
|
238
228
|
|
@@ -287,9 +277,9 @@ module StudentEvaluation
|
|
287
277
|
|
288
278
|
# Cascade with sugar-defined traits
|
289
279
|
value :scholarship_amount do
|
290
|
-
on
|
291
|
-
on
|
292
|
-
on
|
280
|
+
on scholarship_candidate, 10000
|
281
|
+
on high_performer, 5000
|
282
|
+
on math_excellence, 2500
|
293
283
|
base 0
|
294
284
|
end
|
295
285
|
end
|
@@ -324,9 +314,9 @@ module StudentEvaluation
|
|
324
314
|
|
325
315
|
# Cascade with sugar-free defined traits
|
326
316
|
value :scholarship_amount do
|
327
|
-
on
|
328
|
-
on
|
329
|
-
on
|
317
|
+
on scholarship_candidate, 10000
|
318
|
+
on high_performer, 5000
|
319
|
+
on math_excellence, 2500
|
330
320
|
base 0
|
331
321
|
end
|
332
322
|
end
|
@@ -351,10 +341,11 @@ end
|
|
351
341
|
## Syntax Limitations
|
352
342
|
|
353
343
|
### Sugar Syntax Limitations:
|
354
|
-
-
|
355
|
-
-
|
344
|
+
- Supports `&` for logical AND (no `&&` due to Ruby precedence)
|
345
|
+
- Supports `|` for logical OR
|
356
346
|
- Limited operator precedence control
|
357
347
|
- Some Ruby methods not available as sugar
|
348
|
+
- **Refinement scope**: Array methods like `.max` on syntax expressions may not work in certain contexts (e.g., test helpers, Ruby < 3.0). Use `fn(:max, array)` instead.
|
358
349
|
|
359
350
|
### Sugar-Free Advantages:
|
360
351
|
- Full access to all registered functions
|
@@ -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: %(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 / f[input.income, 1.0].max
|
59
63
|
|
60
64
|
# ── FICA (employee share) ─────────────────────────────────────────────
|
61
65
|
value :ss_wage_base, 168_600.0
|
@@ -66,39 +70,33 @@ 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
|
-
# Create a runner for the schema
|
100
|
-
r = CompositeTax2024.from(income: income, filing_status: status)
|
101
|
-
# puts r.inspect
|
99
|
+
def calculate_tax(calculator, income: 1_000_000, status: "single")
|
102
100
|
puts "\n=== 2024 U.S. Income‑Tax Example ==="
|
103
101
|
printf "Income: $%0.2f\n", income
|
104
102
|
puts "Filing status: #{status}\n\n"
|
@@ -109,4 +107,4 @@ def example(income: 1_000_000, status: "single")
|
|
109
107
|
puts "After-tax income: $#{r[:after_tax].round(2)}"
|
110
108
|
end
|
111
109
|
|
112
|
-
|
110
|
+
calculate_tax(FederalTaxCalculator, income: 1_000_000, status: "single")
|
@@ -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
|