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.
Files changed (52) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +109 -2
  3. data/README.md +174 -205
  4. data/documents/DSL.md +3 -3
  5. data/documents/SYNTAX.md +17 -26
  6. data/examples/federal_tax_calculator_2024.rb +36 -38
  7. data/examples/game_of_life.rb +97 -0
  8. data/examples/simple_rpg_game.rb +1000 -0
  9. data/examples/static_analysis_errors.rb +178 -0
  10. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
  11. data/lib/kumi/analyzer/analysis_state.rb +37 -0
  12. data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
  13. data/lib/kumi/analyzer/passes/definition_validator.rb +4 -3
  14. data/lib/kumi/analyzer/passes/dependency_resolver.rb +50 -10
  15. data/lib/kumi/analyzer/passes/input_collector.rb +28 -7
  16. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  17. data/lib/kumi/analyzer/passes/pass_base.rb +10 -27
  18. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
  19. data/lib/kumi/analyzer/passes/toposorter.rb +3 -3
  20. data/lib/kumi/analyzer/passes/type_checker.rb +2 -1
  21. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
  22. data/lib/kumi/analyzer/passes/type_inferencer.rb +2 -4
  23. data/lib/kumi/analyzer/passes/unsat_detector.rb +233 -14
  24. data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -1
  25. data/lib/kumi/analyzer.rb +42 -24
  26. data/lib/kumi/atom_unsat_solver.rb +45 -0
  27. data/lib/kumi/cli.rb +449 -0
  28. data/lib/kumi/constraint_relationship_solver.rb +638 -0
  29. data/lib/kumi/error_reporter.rb +6 -6
  30. data/lib/kumi/evaluation_wrapper.rb +22 -4
  31. data/lib/kumi/explain.rb +9 -10
  32. data/lib/kumi/function_registry/collection_functions.rb +103 -0
  33. data/lib/kumi/function_registry/string_functions.rb +1 -1
  34. data/lib/kumi/parser/dsl_cascade_builder.rb +17 -6
  35. data/lib/kumi/parser/expression_converter.rb +80 -12
  36. data/lib/kumi/parser/guard_rails.rb +2 -2
  37. data/lib/kumi/parser/parser.rb +2 -0
  38. data/lib/kumi/parser/schema_builder.rb +1 -1
  39. data/lib/kumi/parser/sugar.rb +117 -16
  40. data/lib/kumi/schema.rb +3 -1
  41. data/lib/kumi/schema_instance.rb +69 -3
  42. data/lib/kumi/syntax/declarations.rb +3 -0
  43. data/lib/kumi/syntax/expressions.rb +4 -0
  44. data/lib/kumi/syntax/root.rb +1 -0
  45. data/lib/kumi/syntax/terminal_expressions.rb +3 -0
  46. data/lib/kumi/types/compatibility.rb +8 -0
  47. data/lib/kumi/types/validator.rb +1 -1
  48. data/lib/kumi/version.rb +1 -1
  49. data/scripts/generate_function_docs.rb +22 -10
  50. metadata +10 -6
  51. data/CHANGELOG.md +0 -25
  52. 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** | `&` (AND only) | `fn(:and, a, b)`, `fn(:or, a, b)`, `fn(:not, a)` |
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
- Cascade syntax is the same in both approaches, but conditions use different syntax:
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 :excellent_student, "A+"
228
- on :high_scorer, "A"
229
- on :above_average, "B"
230
- on :needs_improvement, "C"
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 :scholarship_candidate, 10000
291
- on :high_performer, 5000
292
- on :math_excellence, 2500
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 :scholarship_candidate, 10000
328
- on :high_performer, 5000
329
- on :math_excellence, 2500
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
- - Only supports `&` for logical AND (no `&&` due to Ruby precedence)
355
- - No logical OR sugar syntax (must use `fn(:or, a, b)`)
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 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: %(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 / 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 :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 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
- example
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