kumi 0.0.0 → 0.0.4

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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +113 -3
  3. data/CHANGELOG.md +21 -1
  4. data/CLAUDE.md +387 -0
  5. data/README.md +270 -20
  6. data/docs/development/README.md +120 -0
  7. data/docs/development/error-reporting.md +361 -0
  8. data/documents/AST.md +126 -0
  9. data/documents/DSL.md +154 -0
  10. data/documents/FUNCTIONS.md +132 -0
  11. data/documents/SYNTAX.md +367 -0
  12. data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +106 -0
  13. data/examples/federal_tax_calculator_2024.rb +112 -0
  14. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +80 -0
  15. data/lib/generators/trait_engine/templates/schema_spec.rb.erb +27 -0
  16. data/lib/kumi/analyzer/constant_evaluator.rb +51 -0
  17. data/lib/kumi/analyzer/passes/definition_validator.rb +42 -0
  18. data/lib/kumi/analyzer/passes/dependency_resolver.rb +71 -0
  19. data/lib/kumi/analyzer/passes/input_collector.rb +55 -0
  20. data/lib/kumi/analyzer/passes/name_indexer.rb +24 -0
  21. data/lib/kumi/analyzer/passes/pass_base.rb +67 -0
  22. data/lib/kumi/analyzer/passes/toposorter.rb +72 -0
  23. data/lib/kumi/analyzer/passes/type_checker.rb +139 -0
  24. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +45 -0
  25. data/lib/kumi/analyzer/passes/type_inferencer.rb +125 -0
  26. data/lib/kumi/analyzer/passes/unsat_detector.rb +107 -0
  27. data/lib/kumi/analyzer/passes/visitor_pass.rb +41 -0
  28. data/lib/kumi/analyzer.rb +54 -0
  29. data/lib/kumi/atom_unsat_solver.rb +349 -0
  30. data/lib/kumi/compiled_schema.rb +41 -0
  31. data/lib/kumi/compiler.rb +127 -0
  32. data/lib/kumi/domain/enum_analyzer.rb +53 -0
  33. data/lib/kumi/domain/range_analyzer.rb +83 -0
  34. data/lib/kumi/domain/validator.rb +84 -0
  35. data/lib/kumi/domain/violation_formatter.rb +40 -0
  36. data/lib/kumi/domain.rb +8 -0
  37. data/lib/kumi/error_reporter.rb +164 -0
  38. data/lib/kumi/error_reporting.rb +95 -0
  39. data/lib/kumi/errors.rb +116 -0
  40. data/lib/kumi/evaluation_wrapper.rb +22 -0
  41. data/lib/kumi/explain.rb +281 -0
  42. data/lib/kumi/export/deserializer.rb +39 -0
  43. data/lib/kumi/export/errors.rb +12 -0
  44. data/lib/kumi/export/node_builders.rb +140 -0
  45. data/lib/kumi/export/node_registry.rb +38 -0
  46. data/lib/kumi/export/node_serializers.rb +156 -0
  47. data/lib/kumi/export/serializer.rb +23 -0
  48. data/lib/kumi/export.rb +33 -0
  49. data/lib/kumi/function_registry/collection_functions.rb +92 -0
  50. data/lib/kumi/function_registry/comparison_functions.rb +31 -0
  51. data/lib/kumi/function_registry/conditional_functions.rb +36 -0
  52. data/lib/kumi/function_registry/function_builder.rb +92 -0
  53. data/lib/kumi/function_registry/logical_functions.rb +42 -0
  54. data/lib/kumi/function_registry/math_functions.rb +72 -0
  55. data/lib/kumi/function_registry/string_functions.rb +54 -0
  56. data/lib/kumi/function_registry/type_functions.rb +51 -0
  57. data/lib/kumi/function_registry.rb +138 -0
  58. data/lib/kumi/input/type_matcher.rb +92 -0
  59. data/lib/kumi/input/validator.rb +52 -0
  60. data/lib/kumi/input/violation_creator.rb +50 -0
  61. data/lib/kumi/input.rb +8 -0
  62. data/lib/kumi/parser/build_context.rb +25 -0
  63. data/lib/kumi/parser/dsl.rb +12 -0
  64. data/lib/kumi/parser/dsl_cascade_builder.rb +125 -0
  65. data/lib/kumi/parser/expression_converter.rb +58 -0
  66. data/lib/kumi/parser/guard_rails.rb +43 -0
  67. data/lib/kumi/parser/input_builder.rb +94 -0
  68. data/lib/kumi/parser/input_proxy.rb +29 -0
  69. data/lib/kumi/parser/parser.rb +66 -0
  70. data/lib/kumi/parser/schema_builder.rb +172 -0
  71. data/lib/kumi/parser/sugar.rb +108 -0
  72. data/lib/kumi/schema.rb +49 -0
  73. data/lib/kumi/schema_instance.rb +43 -0
  74. data/lib/kumi/syntax/declarations.rb +23 -0
  75. data/lib/kumi/syntax/expressions.rb +30 -0
  76. data/lib/kumi/syntax/node.rb +46 -0
  77. data/lib/kumi/syntax/root.rb +12 -0
  78. data/lib/kumi/syntax/terminal_expressions.rb +27 -0
  79. data/lib/kumi/syntax.rb +9 -0
  80. data/lib/kumi/types/builder.rb +21 -0
  81. data/lib/kumi/types/compatibility.rb +86 -0
  82. data/lib/kumi/types/formatter.rb +24 -0
  83. data/lib/kumi/types/inference.rb +40 -0
  84. data/lib/kumi/types/normalizer.rb +70 -0
  85. data/lib/kumi/types/validator.rb +35 -0
  86. data/lib/kumi/types.rb +64 -0
  87. data/lib/kumi/version.rb +1 -1
  88. data/lib/kumi.rb +7 -3
  89. data/scripts/generate_function_docs.rb +59 -0
  90. data/test_impossible_cascade.rb +51 -0
  91. metadata +93 -10
  92. data/sig/kumi.rbs +0 -4
@@ -0,0 +1,112 @@
1
+ # U.S. federal income‑tax plus FICA
2
+
3
+ require_relative "../lib/kumi"
4
+
5
+ module CompositeTax2024
6
+ extend Kumi::Schema
7
+ FED_BREAKS_SINGLE = [11_600, 47_150, 100_525, 191_950,
8
+ 243_725, 609_350, Float::INFINITY]
9
+
10
+ FED_BREAKS_MARRIED = [23_200, 94_300, 201_050, 383_900,
11
+ 487_450, 731_200, Float::INFINITY]
12
+
13
+ FED_BREAKS_SEPARATE = [11_600, 47_150, 100_525, 191_950,
14
+ 243_725, 365_600, Float::INFINITY]
15
+
16
+ FED_BREAKS_HOH = [16_550, 63_100, 100_500, 191_950,
17
+ 243_700, 609_350, Float::INFINITY]
18
+
19
+ FED_RATES = [0.10, 0.12, 0.22, 0.24,
20
+ 0.32, 0.35, 0.37]
21
+
22
+ schema do
23
+ input do
24
+ float :income
25
+ string :filing_status
26
+ end
27
+
28
+ # ── standard deduction table ───────────────────────────────────────
29
+ trait :single, input.filing_status == "single"
30
+ trait :married, input.filing_status == "married_joint"
31
+ trait :separate, input.filing_status == "married_separate"
32
+ trait :hoh, input.filing_status == "head_of_household"
33
+
34
+ value :std_deduction do
35
+ on :single, 14_600
36
+ on :married, 29_200
37
+ on :separate, 14_600
38
+ base 21_900 # HOH default
39
+ end
40
+
41
+ value :taxable_income,
42
+ fn(:max, [input.income - std_deduction, 0])
43
+
44
+ # ── FEDERAL brackets (single shown; others similar if needed) ──────
45
+ 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
+ end
51
+
52
+ value :fed_rates, FED_RATES
53
+ value :fed_calc,
54
+ fn(:piecewise_sum, taxable_income, fed_breaks, fed_rates)
55
+
56
+ value :fed_tax, fed_calc[0]
57
+ value :fed_marginal, fed_calc[1]
58
+ value :fed_eff, fed_tax / fn(:max, [input.income, 1.0])
59
+
60
+ # ── FICA (employee share) ─────────────────────────────────────────────
61
+ value :ss_wage_base, 168_600.0
62
+ value :ss_rate, 0.062
63
+
64
+ value :med_base_rate, 0.0145
65
+ value :addl_med_rate, 0.009
66
+
67
+ # additional‑Medicare threshold depends on filing status
68
+ value :addl_threshold do
69
+ on :single, 200_000
70
+ on :married, 250_000
71
+ on :separate, 125_000
72
+ base 200_000 # HOH same as single
73
+ end
74
+
75
+ # social‑security portion (capped)
76
+ value :ss_tax,
77
+ fn(:min, [input.income, ss_wage_base]) * ss_rate
78
+
79
+ # medicare (1.45 % on everything)
80
+ value :med_tax, input.income * med_base_rate
81
+
82
+ # additional medicare on income above threshold
83
+ value :addl_med_tax,
84
+ fn(:max, [input.income - addl_threshold, 0]) * addl_med_rate
85
+
86
+ value :fica_tax, ss_tax + med_tax + addl_med_tax
87
+ value :fica_eff, fica_tax / fn(:max, [input.income, 1.0])
88
+
89
+ # ── Totals ─────────────────────────────────────────────────────────
90
+ value :total_tax,
91
+ fed_tax + fica_tax
92
+
93
+ value :total_eff, total_tax / fn(:max, [input.income, 1.0])
94
+ value :after_tax, input.income - total_tax
95
+ end
96
+ end
97
+
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
102
+ puts "\n=== 2024 U.S. Income‑Tax Example ==="
103
+ printf "Income: $%0.2f\n", income
104
+ puts "Filing status: #{status}\n\n"
105
+
106
+ puts "Federal tax: $#{r[:fed_tax].round(2)} (#{(r[:fed_eff] * 100).round(2)}% effective)"
107
+ puts "FICA tax: $#{r[:fica_tax].round(2)} (#{(r[:fica_eff] * 100).round(2)}% effective)"
108
+ puts "Total tax: $#{r[:total_tax].round(2)} (#{(r[:total_eff] * 100).round(2)}% effective)"
109
+ puts "After-tax income: $#{r[:after_tax].round(2)}"
110
+ end
111
+
112
+ example
@@ -0,0 +1,80 @@
1
+ # Wide Schema Compilation and Evaluation Benchmark
2
+ #
3
+ # This benchmark measures Kumi's performance with increasingly wide schemas
4
+ # to understand how compilation and evaluation times scale with schema complexity.
5
+ #
6
+ # What it tests:
7
+ # - Compilation time for schemas with 1k, 5k, and 10k value declarations
8
+ # - Evaluation performance for computing aggregated results from many values
9
+ # - Memory efficiency through memoized schema compilation
10
+ #
11
+ # Schema structure:
12
+ # - input: single integer seed
13
+ # - values: v1 = seed + 1, v2 = seed + 2, ..., v_n = seed + n
14
+ # - aggregations: sum_all, avg_all
15
+ # - trait: large_total (conditional logic)
16
+ # - cascade: final_total (depends on trait evaluation)
17
+ #
18
+ # Usage: bundle exec ruby examples/wide_schema_compilation_and_evaluation_benchmark.rb
19
+ require "benchmark"
20
+ require "benchmark/ips"
21
+ require_relative "../lib/kumi"
22
+
23
+ # ------------------------------------------------------------------
24
+ # 1. Helper that builds a *sugar‑free* wide‑but‑shallow schema
25
+ # ------------------------------------------------------------------
26
+ def build_wide_schema(width)
27
+ Class.new do
28
+ extend Kumi::Schema
29
+
30
+ schema do
31
+ input { integer :seed }
32
+
33
+ # width independent leaf nodes: v_i = seed + i
34
+ 1.upto(width) { |i| value :"v#{i}", fn(:add, input.seed, i) }
35
+
36
+ # Aggregations
37
+ value :sum_all, fn(:sum, (1..width).map { |i| ref(:"v#{i}") })
38
+ value :avg_all, fn(:divide, ref(:sum_all), width)
39
+
40
+ trait :large_total,
41
+ ref(:sum_all), :>, (width * (width + 1) / 2)
42
+
43
+ value :final_total do
44
+ on :large_total, fn(:add, ref(:sum_all), ref(:avg_all))
45
+ base ref(:sum_all)
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ WIDTHS = [1_000, 5_000, 10_000]
52
+
53
+ # ------------------------------------------------------------------
54
+ # 2. Measure compilation once per width
55
+ # ------------------------------------------------------------------
56
+ compile_times = {}
57
+ schemas = {}
58
+
59
+ WIDTHS.each do |w|
60
+ t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
61
+ schemas[w] = build_wide_schema(w)
62
+ compile_times[w] = Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0
63
+ end
64
+
65
+ puts "=== compilation times ==="
66
+ compile_times.each do |w, t|
67
+ puts format("compile %5d‑wide: %6.1f ms", w, t * 1_000)
68
+ end
69
+ puts
70
+
71
+ # ------------------------------------------------------------------
72
+ # 3. Pure evaluation benchmark – no compilation inside the loop
73
+ # ------------------------------------------------------------------
74
+ Benchmark.ips do |x|
75
+ schemas.each do |w, schema|
76
+ runner = schema.from(seed: 0) # memoised runner
77
+ x.report("eval #{w}-wide") { runner[:final_total] }
78
+ end
79
+ x.compare!
80
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ require "rails_helper"
3
+
4
+ RSpec.describe <%= schema_constant %> do
5
+ # Single shared instance for every example
6
+ let(:schema) { described_class }
7
+
8
+ # Minimal dummy context so every binding can run
9
+ let(:ctx) do
10
+ {
11
+ <% leaf_keys.each do |k| -%>
12
+ <%= "#{k}:" %> nil,
13
+ <% end -%>
14
+ }
15
+ end
16
+
17
+ <% expose_names.each do |name| -%>
18
+ describe "<%= name %>" do
19
+ it "evaluates without raising" do
20
+ expect {
21
+ schema.evaluate_binding(:<%= name %>, ctx)
22
+ }.not_to raise_error
23
+ end
24
+ end
25
+
26
+ <% end -%>
27
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ class ConstantEvaluator
6
+ include Syntax
7
+
8
+ def initialize(definitions)
9
+ @definitions = definitions
10
+ @memo = {}
11
+ end
12
+
13
+ OPERATORS = {
14
+ add: :+,
15
+ subtract: :-,
16
+ multiply: :*,
17
+ divide: :/
18
+ }.freeze
19
+
20
+ def evaluate(node, visited = Set.new)
21
+ return :unknown unless node
22
+ return @memo[node] if @memo.key?(node)
23
+ return node.value if node.is_a?(Literal)
24
+
25
+ if node.is_a?(Binding)
26
+ return :unknown if visited.include?(node.name)
27
+
28
+ visited << node.name
29
+
30
+ definition = @definitions[node.name]
31
+ return :unknown unless definition
32
+
33
+ @memo[node] = evaluate(definition.expression, visited)
34
+ return @memo[node]
35
+ end
36
+
37
+ if node.is_a?(CallExpression)
38
+ return :unknown unless OPERATORS.key?(node.fn_name)
39
+
40
+ args = node.args.map { |arg| evaluate(arg, visited) }
41
+ return :unknown if args.any?(:unknown)
42
+
43
+ @memo[node] = args.reduce(OPERATORS[node.fn_name])
44
+ return @memo[node]
45
+ end
46
+
47
+ :unknown
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ module Passes
6
+ # RESPONSIBILITY: Perform local structural validation on each declaration
7
+ # DEPENDENCIES: None (can run independently)
8
+ # PRODUCES: None (validation only)
9
+ # INTERFACE: new(schema, state).run(errors)
10
+ class DefinitionValidator < VisitorPass
11
+ def run(errors)
12
+ each_decl do |decl|
13
+ visit(decl) { |node| validate_node(node, errors) }
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def validate_node(node, errors)
20
+ case node
21
+ when Declarations::Attribute
22
+ validate_attribute(node, errors)
23
+ when Declarations::Trait
24
+ validate_trait(node, errors)
25
+ end
26
+ end
27
+
28
+ def validate_attribute(node, errors)
29
+ return unless node.expression.nil?
30
+
31
+ add_error(errors, node.loc, "attribute `#{node.name}` requires an expression")
32
+ end
33
+
34
+ def validate_trait(node, errors)
35
+ return if node.expression.is_a?(Expressions::CallExpression)
36
+
37
+ add_error(errors, node.loc, "trait `#{node.name}` must wrap a CallExpression")
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ module Passes
6
+ # RESPONSIBILITY: Build dependency graph and leaf map, validate references
7
+ # DEPENDENCIES: :definitions from NameIndexer
8
+ # PRODUCES: :dependency_graph - Hash of name → [DependencyEdge], :leaf_map - Hash of name → Set[nodes]
9
+ # INTERFACE: new(schema, state).run(errors)
10
+ class DependencyResolver < PassBase
11
+ # A Struct to hold rich dependency information
12
+ DependencyEdge = Struct.new(:to, :type, :via, keyword_init: true)
13
+ include Syntax
14
+
15
+ def run(errors)
16
+ definitions = get_state(:definitions)
17
+ input_meta = get_state(:input_meta)
18
+
19
+ dependency_graph = Hash.new { |h, k| h[k] = [] }
20
+ leaf_map = Hash.new { |h, k| h[k] = Set.new }
21
+
22
+ each_decl do |decl|
23
+ # Traverse the expression for each declaration, passing context down
24
+ visit_with_context(decl.expression) do |node, context|
25
+ process_node(node, decl, dependency_graph, leaf_map, definitions, input_meta, errors, context)
26
+ end
27
+ end
28
+
29
+ set_state(:dependency_graph, dependency_graph.transform_values(&:freeze).freeze)
30
+ set_state(:leaf_map, leaf_map.transform_values(&:freeze).freeze)
31
+ end
32
+
33
+ private
34
+
35
+ def process_node(node, decl, graph, leaves, definitions, input_meta, errors, context)
36
+ case node
37
+ when Binding
38
+ add_error(errors, node.loc, "undefined reference to `#{node.name}`") unless definitions.key?(node.name)
39
+ add_dependency_edge(graph, decl.name, node.name, :ref, context[:via])
40
+ when FieldRef
41
+ add_error(errors, node.loc, "undeclared input `#{node.name}`") unless input_meta.key?(node.name)
42
+ add_dependency_edge(graph, decl.name, node.name, :key, context[:via])
43
+ leaves[decl.name] << node # put it back
44
+ when Literal
45
+ leaves[decl.name] << node
46
+ end
47
+ end
48
+
49
+ def add_dependency_edge(graph, from, to, type, via)
50
+ edge = DependencyEdge.new(to: to, type: type, via: via)
51
+ graph[from] << edge
52
+ end
53
+
54
+ # Custom visitor that passes context (like function name) down the tree
55
+ def visit_with_context(node, context = {}, &block)
56
+ return unless node
57
+
58
+ yield(node, context)
59
+
60
+ new_context = if node.is_a?(Expressions::CallExpression)
61
+ { via: node.fn_name }
62
+ else
63
+ context
64
+ end
65
+
66
+ node.children.each { |child| visit_with_context(child, new_context, &block) }
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ module Passes
6
+ # RESPONSIBILITY: Collect field metadata from input declarations and validate consistency
7
+ # DEPENDENCIES: None
8
+ # PRODUCES: :input_meta - Hash mapping field names to {type:, domain:} metadata
9
+ # INTERFACE: new(schema, state).run(errors)
10
+ class InputCollector < PassBase
11
+ include Syntax::TerminalExpressions
12
+
13
+ def run(errors)
14
+ input_meta = {}
15
+
16
+ schema.inputs.each do |field_decl|
17
+ unless field_decl.is_a?(FieldDecl)
18
+ add_error(errors, field_decl.loc, "Expected FieldDecl node, got #{field_decl.class}")
19
+ next
20
+ end
21
+
22
+ name = field_decl.name
23
+ existing = input_meta[name]
24
+
25
+ if existing
26
+ # Check for compatibility
27
+ if existing[:type] != field_decl.type && field_decl.type && existing[:type]
28
+ add_error(errors, field_decl.loc,
29
+ "Field :#{name} declared with conflicting types: #{existing[:type]} vs #{field_decl.type}")
30
+ end
31
+
32
+ if existing[:domain] != field_decl.domain && field_decl.domain && existing[:domain]
33
+ add_error(errors, field_decl.loc,
34
+ "Field :#{name} declared with conflicting domains: #{existing[:domain].inspect} vs #{field_decl.domain.inspect}")
35
+ end
36
+
37
+ # Merge metadata (later declarations override nil values)
38
+ input_meta[name] = {
39
+ type: field_decl.type || existing[:type],
40
+ domain: field_decl.domain || existing[:domain]
41
+ }
42
+ else
43
+ input_meta[name] = {
44
+ type: field_decl.type,
45
+ domain: field_decl.domain
46
+ }
47
+ end
48
+ end
49
+
50
+ set_state(:input_meta, input_meta.freeze)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ module Passes
6
+ # RESPONSIBILITY: Build definitions index and detect duplicate names
7
+ # DEPENDENCIES: None (first pass in pipeline)
8
+ # PRODUCES: :definitions - Hash mapping names to declaration nodes
9
+ # INTERFACE: new(schema, state).run(errors)
10
+ class NameIndexer < PassBase
11
+ def run(errors)
12
+ definitions = {}
13
+
14
+ each_decl do |decl|
15
+ add_error(errors, decl.loc, "duplicated definition `#{decl.name}`") if definitions.key?(decl.name)
16
+ definitions[decl.name] = decl
17
+ end
18
+
19
+ set_state(:definitions, definitions.freeze)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ module Passes
6
+ # Base class for all analyzer passes providing common functionality
7
+ # and enforcing consistent interface patterns.
8
+ class PassBase
9
+ include Kumi::Syntax
10
+ include Kumi::ErrorReporting
11
+
12
+ # @param schema [Syntax::Root] The schema to analyze
13
+ # @param state [Hash] Shared analysis state accumulator
14
+ def initialize(schema, state)
15
+ @schema = schema
16
+ @state = state
17
+ end
18
+
19
+ # Main entry point for pass execution
20
+ # @param errors [Array] Error accumulator array
21
+ # @abstract Subclasses must implement this method
22
+ def run(errors)
23
+ raise NotImplementedError, "#{self.class.name} must implement #run"
24
+ end
25
+
26
+ protected
27
+
28
+ attr_reader :schema, :state
29
+
30
+ # Iterate over all declarations (attributes and traits) in the schema
31
+ # @yield [Syntax::Declarations::Attribute|Syntax::Declarations::Trait] Each declaration
32
+ def each_decl(&block)
33
+ schema.attributes.each(&block)
34
+ schema.traits.each(&block)
35
+ end
36
+
37
+ # DEPRECATED: Use report_error instead for consistent error handling
38
+ # Helper to add standardized error messages
39
+ # @param errors [Array] Error accumulator
40
+ # @param location [Syntax::Location] Error location
41
+ # @param message [String] Error message
42
+ def add_error(errors, location, message)
43
+ errors << ErrorReporter.create_error(message, location: location, type: :semantic)
44
+ end
45
+
46
+ # Helper to get required state from previous passes
47
+ # @param key [Symbol] State key to retrieve
48
+ # @param required [Boolean] Whether this state is required
49
+ # @return [Object] The state value
50
+ # @raise [StandardError] If required state is missing
51
+ def get_state(key, required: true)
52
+ value = state[key]
53
+ raise "Pass #{self.class.name} requires #{key} from previous passes, but it was not found" if required && value.nil?
54
+
55
+ value
56
+ end
57
+
58
+ # Helper to set state for subsequent passes
59
+ # @param key [Symbol] State key to set
60
+ # @param value [Object] Value to store
61
+ def set_state(key, value)
62
+ state[key] = value
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ module Passes
6
+ # RESPONSIBILITY: Compute topological ordering of declarations from dependency graph
7
+ # DEPENDENCIES: :dependency_graph from DependencyResolver, :definitions from NameIndexer
8
+ # PRODUCES: :topo_order - Array of declaration names in evaluation order
9
+ # INTERFACE: new(schema, state).run(errors)
10
+ class Toposorter < PassBase
11
+ def run(errors)
12
+ dependency_graph = get_state(:dependency_graph, required: false) || {}
13
+ definitions = get_state(:definitions, required: false) || {}
14
+
15
+ order = compute_topological_order(dependency_graph, definitions, errors)
16
+ set_state(:topo_order, order)
17
+ end
18
+
19
+ private
20
+
21
+ def compute_topological_order(graph, definitions, errors)
22
+ temp_marks = Set.new
23
+ perm_marks = Set.new
24
+ order = []
25
+
26
+ visit_node = lambda do |node|
27
+ return if perm_marks.include?(node)
28
+
29
+ if temp_marks.include?(node)
30
+ report_unexpected_cycle(temp_marks, node, errors)
31
+ return
32
+ end
33
+
34
+ temp_marks << node
35
+ Array(graph[node]).each { |edge| visit_node.call(edge.to) }
36
+ temp_marks.delete(node)
37
+ perm_marks << node
38
+
39
+ # Only include declaration nodes in the final order
40
+ order << node if definitions.key?(node)
41
+ end
42
+
43
+ # Visit all nodes in the graph
44
+ graph.each_key { |node| visit_node.call(node) }
45
+
46
+ # Also visit any definitions that aren't in the dependency graph
47
+ # (i.e., declarations with no dependencies)
48
+ definitions.each_key { |node| visit_node.call(node) }
49
+
50
+ order
51
+ end
52
+
53
+ def report_unexpected_cycle(temp_marks, current_node, errors)
54
+ cycle_path = temp_marks.to_a.join(" → ") + " → #{current_node}"
55
+
56
+ # Try to find the first declaration in the cycle for location info
57
+ first_decl = find_declaration_by_name(temp_marks.first || current_node)
58
+ location = first_decl&.loc
59
+
60
+ add_error(errors, location, "cycle detected: #{cycle_path}")
61
+ end
62
+
63
+ def find_declaration_by_name(name)
64
+ return nil unless schema
65
+
66
+ schema.attributes.find { |attr| attr.name == name } ||
67
+ schema.traits.find { |trait| trait.name == name }
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end