kumi 0.0.0 → 0.0.3

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 +257 -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 +20 -0
  41. data/lib/kumi/explain.rb +282 -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,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ module Passes
6
+ # RESPONSIBILITY: Validate function call arity and argument types against FunctionRegistry
7
+ # DEPENDENCIES: None (can run independently)
8
+ # PRODUCES: None (validation only)
9
+ # INTERFACE: new(schema, state).run(errors)
10
+ class TypeChecker < VisitorPass
11
+ def run(errors)
12
+ visit_nodes_of_type(Expressions::CallExpression, errors: errors) do |node, _decl, errs|
13
+ validate_function_call(node, errs)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def validate_function_call(node, errors)
20
+ signature = get_function_signature(node, errors)
21
+ return unless signature
22
+
23
+ validate_arity(node, signature, errors)
24
+ validate_argument_types(node, signature, errors)
25
+ end
26
+
27
+ def get_function_signature(node, errors)
28
+ FunctionRegistry.signature(node.fn_name)
29
+ rescue FunctionRegistry::UnknownFunction
30
+ # Use old format for backward compatibility, but node.loc provides better location
31
+ report_error(errors, "unsupported operator `#{node.fn_name}`", location: node.loc, type: :type)
32
+ nil
33
+ end
34
+
35
+ def validate_arity(node, signature, errors)
36
+ expected_arity = signature[:arity]
37
+ actual_arity = node.args.size
38
+
39
+ return if expected_arity.negative? || expected_arity == actual_arity
40
+
41
+ report_error(errors, "operator `#{node.fn_name}` expects #{expected_arity} args, got #{actual_arity}", location: node.loc,
42
+ type: :type)
43
+ end
44
+
45
+ def validate_argument_types(node, signature, errors)
46
+ types = signature[:param_types]
47
+ return if types.nil? || (signature[:arity].negative? && node.args.empty?)
48
+
49
+ node.args.each_with_index do |arg, i|
50
+ validate_argument_type(arg, i, types[i], node.fn_name, errors)
51
+ end
52
+ end
53
+
54
+ def validate_argument_type(arg, index, expected_type, fn_name, errors)
55
+ return if expected_type.nil? || expected_type == Kumi::Types::ANY
56
+
57
+ # Get the inferred type for this argument
58
+ actual_type = get_expression_type(arg)
59
+ return if Kumi::Types.compatible?(actual_type, expected_type)
60
+
61
+ # Generate descriptive error message
62
+ source_desc = describe_expression_type(arg, actual_type)
63
+ report_error(errors, "argument #{index + 1} of `fn(:#{fn_name})` expects #{expected_type}, " \
64
+ "got #{source_desc}", location: arg.loc, type: :type)
65
+ end
66
+
67
+ def get_expression_type(expr)
68
+ case expr
69
+ when TerminalExpressions::Literal
70
+ # Inferred type from literal value
71
+ Kumi::Types.infer_from_value(expr.value)
72
+
73
+ when TerminalExpressions::FieldRef
74
+ # Declared type from input block (user-specified)
75
+ get_declared_field_type(expr.name)
76
+
77
+ when TerminalExpressions::Binding
78
+ # Inferred type from type inference results
79
+ get_inferred_declaration_type(expr.name)
80
+
81
+ else
82
+ # For complex expressions, we should have type inference results
83
+ # This is a simplified approach - in reality we'd need to track types for all expressions
84
+ Kumi::Types::ANY
85
+ end
86
+ end
87
+
88
+ def get_declared_field_type(field_name)
89
+ # Get explicitly declared type from input metadata
90
+ input_meta = get_state(:input_meta, required: false) || {}
91
+ field_meta = input_meta[field_name]
92
+ field_meta&.dig(:type) || Kumi::Types::ANY
93
+ end
94
+
95
+ def get_inferred_declaration_type(decl_name)
96
+ # Get inferred type from type inference results
97
+ decl_types = get_state(:decl_types, required: true)
98
+ decl_types[decl_name] || Kumi::Types::ANY
99
+ end
100
+
101
+ def describe_expression_type(expr, type)
102
+ case expr
103
+ when TerminalExpressions::Literal
104
+ "`#{expr.value}` of type #{type} (literal value)"
105
+
106
+ when TerminalExpressions::FieldRef
107
+ input_meta = get_state(:input_meta, required: false) || {}
108
+ field_meta = input_meta[expr.name]
109
+
110
+ if field_meta&.dig(:type)
111
+ # Explicitly declared type
112
+ domain_desc = field_meta[:domain] ? " (domain: #{field_meta[:domain]})" : ""
113
+ "input field `#{expr.name}` of declared type #{type}#{domain_desc}"
114
+ else
115
+ # Undeclared field
116
+ "undeclared input field `#{expr.name}` (inferred as #{type})"
117
+ end
118
+
119
+ when TerminalExpressions::Binding
120
+ # This type was inferred from the declaration's expression
121
+ "reference to declaration `#{expr.name}` of inferred type #{type}"
122
+
123
+ when Expressions::CallExpression
124
+ "result of function `#{expr.fn_name}` returning #{type}"
125
+
126
+ when Expressions::ListExpression
127
+ "list expression of type #{type}"
128
+
129
+ when Expressions::CascadeExpression
130
+ "cascade expression of type #{type}"
131
+
132
+ else
133
+ "expression of type #{type}"
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ module Passes
6
+ # RESPONSIBILITY: Validate consistency between declared and inferred types
7
+ # DEPENDENCIES: :input_meta from InputCollector, :decl_types from TypeInferencer
8
+ # PRODUCES: None (validation only)
9
+ # INTERFACE: new(schema, state).run(errors)
10
+ class TypeConsistencyChecker < PassBase
11
+ def run(errors)
12
+ input_meta = get_state(:input_meta, required: false) || {}
13
+
14
+ # First, validate that all declared types are valid
15
+ validate_declared_types(input_meta, errors)
16
+
17
+ # Then check basic consistency (placeholder for now)
18
+ # In a full implementation, this would do sophisticated usage analysis
19
+ end
20
+
21
+ private
22
+
23
+ def validate_declared_types(input_meta, errors)
24
+ input_meta.each do |field_name, meta|
25
+ declared_type = meta[:type]
26
+ next unless declared_type # Skip fields without declared types
27
+ next if Kumi::Types.valid_type?(declared_type)
28
+
29
+ # Find the input field declaration for proper location information
30
+ field_decl = find_input_field_declaration(field_name)
31
+ location = field_decl&.loc
32
+
33
+ add_error(errors, location, "Invalid type declaration for field :#{field_name}: #{declared_type.inspect}")
34
+ end
35
+ end
36
+
37
+ def find_input_field_declaration(field_name)
38
+ return nil unless schema
39
+
40
+ schema.inputs.find { |input_decl| input_decl.name == field_name }
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ module Passes
6
+ # RESPONSIBILITY: Infer types for all declarations based on expression analysis
7
+ # DEPENDENCIES: Toposorter (needs topo_order), DefinitionValidator (needs definitions)
8
+ # PRODUCES: decl_types hash mapping declaration names to inferred types
9
+ # INTERFACE: new(schema, state).run(errors)
10
+ class TypeInferencer < PassBase
11
+ def run(errors)
12
+ return if state[:decl_types] # Already run
13
+
14
+ types = {}
15
+ topo_order = get_state(:topo_order)
16
+ definitions = get_state(:definitions)
17
+
18
+ # Process declarations in topological order to ensure dependencies are resolved
19
+ topo_order.each do |name|
20
+ decl = definitions[name]
21
+ next unless decl
22
+
23
+ begin
24
+ inferred_type = infer_expression_type(decl.expression, types)
25
+ types[name] = inferred_type
26
+ rescue StandardError => e
27
+ add_error(errors, decl&.loc, "Type inference failed: #{e.message}")
28
+ end
29
+ end
30
+
31
+ set_state(:decl_types, types)
32
+ end
33
+
34
+ private
35
+
36
+ def infer_expression_type(expr, type_context = {})
37
+ case expr
38
+ when Literal
39
+ Types.infer_from_value(expr.value)
40
+ when FieldRef
41
+ # Look up type from field metadata
42
+ input_meta = get_state(:input_meta, required: false) || {}
43
+ meta = input_meta[expr.name]
44
+ meta&.dig(:type) || :any
45
+ when Binding
46
+ type_context[expr.name] || :any
47
+ when CallExpression
48
+ infer_call_type(expr, type_context)
49
+ when ListExpression
50
+ infer_list_type(expr, type_context)
51
+ when CascadeExpression
52
+ infer_cascade_type(expr, type_context)
53
+ else
54
+ :any
55
+ end
56
+ end
57
+
58
+ def infer_call_type(call_expr, type_context)
59
+ fn_name = call_expr.fn_name
60
+ args = call_expr.args
61
+
62
+ # Check if function exists in registry
63
+ unless FunctionRegistry.supported?(fn_name)
64
+ # Don't push error here - let existing TypeChecker handle it
65
+ return :any
66
+ end
67
+
68
+ signature = FunctionRegistry.signature(fn_name)
69
+
70
+ # Validate arity if not variable
71
+ if signature[:arity] >= 0 && args.size != signature[:arity]
72
+ # Don't push error here - let existing TypeChecker handle it
73
+ return :any
74
+ end
75
+
76
+ # Infer argument types
77
+ arg_types = args.map { |arg| infer_expression_type(arg, type_context) }
78
+
79
+ # Validate parameter types (warn but don't fail)
80
+ param_types = signature[:param_types] || []
81
+ if signature[:arity] >= 0 && param_types.size.positive?
82
+ arg_types.each_with_index do |arg_type, i|
83
+ expected_type = param_types[i] || param_types.last
84
+ next if expected_type.nil?
85
+
86
+ unless Types.compatible?(arg_type, expected_type)
87
+ # Could add warning here in future, but for now just infer best type
88
+ end
89
+ end
90
+ end
91
+
92
+ signature[:return_type] || :any
93
+ end
94
+
95
+ def infer_list_type(list_expr, type_context)
96
+ return Types.array(:any) if list_expr.elements.empty?
97
+
98
+ element_types = list_expr.elements.map { |elem| infer_expression_type(elem, type_context) }
99
+
100
+ # Try to unify all element types
101
+ unified_type = element_types.reduce { |acc, type| Types.unify(acc, type) }
102
+ Types.array(unified_type)
103
+ rescue StandardError
104
+ # If unification fails, fall back to generic array
105
+ Types.array(:any)
106
+ end
107
+
108
+ def infer_cascade_type(cascade_expr, type_context)
109
+ return :any if cascade_expr.cases.empty?
110
+
111
+ result_types = cascade_expr.cases.map do |case_stmt|
112
+ infer_expression_type(case_stmt.result, type_context)
113
+ end
114
+
115
+ # Reduce all possible types into a single unified type
116
+ result_types.reduce { |unified, type| Types.unify(unified, type) } || :any
117
+ rescue StandardError
118
+ # Check if unification fails, fall back to base type
119
+ # TODO: understand if this right to fallback or we should raise
120
+ :any
121
+ end
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ module Passes
6
+ class UnsatDetector < VisitorPass
7
+ include Syntax
8
+
9
+ COMPARATORS = %i[> < >= <= == !=].freeze
10
+ Atom = Kumi::AtomUnsatSolver::Atom
11
+
12
+ def run(errors)
13
+ definitions = get_state(:definitions)
14
+ @evaluator = ConstantEvaluator.new(definitions)
15
+
16
+ each_decl do |decl|
17
+ if decl.expression.is_a?(CascadeExpression)
18
+ # Special handling for cascade expressions
19
+ check_cascade_expression(decl, definitions, errors)
20
+ else
21
+ # Normal handling for non-cascade expressions
22
+ atoms = gather_atoms(decl.expression, definitions, Set.new)
23
+ next if atoms.empty?
24
+
25
+ add_error(errors, decl.loc, "conjunction `#{decl.name}` is impossible") if Kumi::AtomUnsatSolver.unsat?(atoms)
26
+ end
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def gather_atoms(node, defs, visited, list = [])
33
+ return list unless node
34
+
35
+ # Use iterative approach with stack to avoid SystemStackError on deep graphs
36
+ stack = [node]
37
+
38
+ until stack.empty?
39
+ current = stack.pop
40
+ next unless current
41
+
42
+ if current.is_a?(CallExpression) && COMPARATORS.include?(current.fn_name)
43
+ lhs, rhs = current.args
44
+ list << Atom.new(current.fn_name, term(lhs, defs), term(rhs, defs))
45
+ elsif current.is_a?(Binding)
46
+ name = current.name
47
+ unless visited.include?(name)
48
+ visited << name
49
+ stack << defs[name].expression if defs.key?(name)
50
+ end
51
+ end
52
+
53
+ # Add children to stack for processing
54
+ current.children.each { |child| stack << child } if current.respond_to?(:children)
55
+ end
56
+
57
+ list
58
+ end
59
+
60
+ def check_cascade_expression(decl, definitions, errors)
61
+ # Analyze each cascade branch condition independently
62
+ # This is the correct behavior: each 'on' condition should be checked separately
63
+ # since only ONE will be evaluated at runtime (they're mutually exclusive by design)
64
+
65
+ decl.expression.cases.each_with_index do |when_case, _index|
66
+ # Skip the base case (it's typically a literal true condition)
67
+ next if when_case.condition.is_a?(Literal) && when_case.condition.value == true
68
+
69
+ # Skip single-trait 'on' branches: trait-level unsat detection covers these
70
+ if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
71
+ list = when_case.condition.args.first
72
+ next if list.is_a?(ListExpression) && list.elements.size < 2
73
+ end
74
+ # Gather atoms from this individual condition only
75
+ condition_atoms = gather_atoms(when_case.condition, definitions, Set.new, [])
76
+
77
+ # Only flag if this individual condition is impossible
78
+ next unless !condition_atoms.empty? && Kumi::AtomUnsatSolver.unsat?(condition_atoms)
79
+
80
+ # For multi-trait on-clauses, report the trait names rather than the value name
81
+ if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
82
+ list = when_case.condition.args.first
83
+ if list.is_a?(ListExpression) && list.elements.all?(Binding)
84
+ traits = list.elements.map(&:name).join(" AND ")
85
+ add_error(errors, decl.loc, "conjunction `#{traits}` is impossible")
86
+ next
87
+ end
88
+ end
89
+ add_error(errors, decl.loc, "conjunction `#{decl.name}` is impossible")
90
+ end
91
+ end
92
+
93
+ def term(node, _defs)
94
+ case node
95
+ when FieldRef, Binding
96
+ val = @evaluator.evaluate(node)
97
+ val == :unknown ? node.name : val
98
+ when Literal
99
+ node.value
100
+ else
101
+ :unknown
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ module Passes
6
+ # Base class for analyzer passes that need to traverse the AST using the visitor pattern
7
+ class VisitorPass < PassBase
8
+ # Visit a node and all its children using depth-first traversal
9
+ # @param node [Syntax::Node] The node to visit
10
+ # @yield [Syntax::Node] Each node in the traversal
11
+ def visit(node, &block)
12
+ return unless node
13
+
14
+ yield(node)
15
+ node.children.each { |child| visit(child, &block) }
16
+ end
17
+
18
+ protected
19
+
20
+ # Helper to visit each declaration's expression tree
21
+ # @param errors [Array] Error accumulator
22
+ # @yield [Syntax::Node, Syntax::Declarations::Base] Each node and its containing declaration
23
+ def visit_all_expressions(errors)
24
+ each_decl do |decl|
25
+ visit(decl.expression) { |node| yield(node, decl, errors) }
26
+ end
27
+ end
28
+
29
+ # Helper to visit only specific node types
30
+ # @param node_types [Array<Class>] Node types to match
31
+ # @param errors [Array] Error accumulator
32
+ # @yield [Syntax::Node, Syntax::Declarations::Base] Matching nodes and their declarations
33
+ def visit_nodes_of_type(*node_types, errors:)
34
+ visit_all_expressions(errors) do |node, decl, errs|
35
+ yield(node, decl, errs) if node_types.any? { |type| node.is_a?(type) }
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Analyzer
5
+ Result = Struct.new(:definitions, :dependency_graph, :leaf_map, :topo_order, :decl_types, :state, keyword_init: true)
6
+
7
+ module_function
8
+
9
+ DEFAULT_PASSES = [
10
+ Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
11
+ Passes::InputCollector, # 2. Collects field metadata from input declarations.
12
+ Passes::DefinitionValidator, # 3. Checks the basic structure of each rule.
13
+ Passes::DependencyResolver, # 4. Builds the dependency graph.
14
+ Passes::UnsatDetector, # 5. Detects unsatisfiable constraints in rules.
15
+ Passes::Toposorter, # 6. Creates the final evaluation order.
16
+ Passes::TypeInferencer, # 7. Infers types for all declarations (pure annotation).
17
+ Passes::TypeConsistencyChecker, # 8. Validates declared vs inferred type consistency.
18
+ Passes::TypeChecker # 9. Validates types using inferred information.
19
+ ].freeze
20
+
21
+ def analyze!(schema, passes: DEFAULT_PASSES, **opts)
22
+ analysis_state = { opts: opts }
23
+ errors = []
24
+
25
+ passes.each { |klass| klass.new(schema, analysis_state).run(errors) }
26
+
27
+ unless errors.empty?
28
+ # Check if we have type-specific errors to raise more specific exception
29
+ type_errors = errors.select { |e| e.type == :type }
30
+ first_error_location = errors.first.location
31
+
32
+ raise Errors::TypeError.new(format_errors(errors), first_error_location) if type_errors.any?
33
+
34
+ raise Errors::SemanticError.new(format_errors(errors), first_error_location)
35
+ end
36
+
37
+ Result.new(
38
+ definitions: analysis_state[:definitions].freeze,
39
+ dependency_graph: analysis_state[:dependency_graph].freeze,
40
+ leaf_map: analysis_state[:leaf_map].freeze,
41
+ topo_order: analysis_state[:topo_order].freeze,
42
+ decl_types: analysis_state[:decl_types].freeze,
43
+ state: analysis_state.freeze
44
+ )
45
+ end
46
+
47
+ # Handle both old and new error formats for backward compatibility
48
+ def format_errors(errors)
49
+ return "" if errors.empty?
50
+
51
+ errors.map(&:to_s).join("\n")
52
+ end
53
+ end
54
+ end