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,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "range_analyzer"
4
+ require_relative "enum_analyzer"
5
+ require_relative "violation_formatter"
6
+
7
+ module Kumi
8
+ module Domain
9
+ class Validator
10
+ def self.validate_field(_field_name, value, domain)
11
+ return true if domain.nil?
12
+
13
+ case domain
14
+ when Range
15
+ domain.cover?(value)
16
+ when Array
17
+ domain.include?(value)
18
+ when Proc
19
+ domain.call(value)
20
+ else
21
+ true
22
+ end
23
+ end
24
+
25
+ def self.validate_context(context, input_meta)
26
+ violations = []
27
+
28
+ context.each do |field, value|
29
+ meta = input_meta[field]
30
+ next unless meta&.dig(:domain)
31
+
32
+ violations << create_violation(field, value, meta[:domain]) unless validate_field(field, value, meta[:domain])
33
+ end
34
+
35
+ violations
36
+ end
37
+
38
+ def self.extract_domain_metadata(input_meta)
39
+ metadata = {}
40
+
41
+ input_meta.each do |field, meta|
42
+ domain = meta[:domain]
43
+ next unless domain
44
+
45
+ metadata[field] = analyze_domain(field, domain)
46
+ end
47
+
48
+ metadata
49
+ end
50
+
51
+ def self.create_violation(field, value, domain)
52
+ {
53
+ field: field,
54
+ value: value,
55
+ domain: domain,
56
+ message: ViolationFormatter.format_message(field, value, domain)
57
+ }
58
+ end
59
+
60
+ def self.analyze_domain(_field, domain)
61
+ case domain
62
+ when Range
63
+ RangeAnalyzer.analyze(domain)
64
+ when Array
65
+ EnumAnalyzer.analyze(domain)
66
+ when Proc
67
+ {
68
+ type: :custom,
69
+ description: "Custom constraint function",
70
+ sample_values: [],
71
+ invalid_samples: []
72
+ }
73
+ else
74
+ {
75
+ type: :unknown,
76
+ constraint: domain,
77
+ sample_values: [],
78
+ invalid_samples: []
79
+ }
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Domain
5
+ class ViolationFormatter
6
+ def self.format_message(field, value, domain)
7
+ case domain
8
+ when Range
9
+ format_range_violation(field, value, domain)
10
+ when Array
11
+ format_array_violation(field, value, domain)
12
+ when Proc
13
+ format_proc_violation(field, value)
14
+ else
15
+ format_default_violation(field, value, domain)
16
+ end
17
+ end
18
+
19
+ private_class_method def self.format_range_violation(field, value, range)
20
+ if range.exclude_end?
21
+ "Field :#{field} value #{value.inspect} is outside domain #{range.begin}...#{range.end} (exclusive)"
22
+ else
23
+ "Field :#{field} value #{value.inspect} is outside domain #{range.begin}..#{range.end}"
24
+ end
25
+ end
26
+
27
+ private_class_method def self.format_array_violation(field, value, array)
28
+ "Field :#{field} value #{value.inspect} is not in allowed values #{array.inspect}"
29
+ end
30
+
31
+ private_class_method def self.format_proc_violation(field, value)
32
+ "Field :#{field} value #{value.inspect} does not satisfy custom domain constraint"
33
+ end
34
+
35
+ private_class_method def self.format_default_violation(field, value, domain)
36
+ "Field :#{field} value #{value.inspect} violates domain constraint #{domain.inspect}"
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Domain
5
+ end
6
+ end
7
+
8
+ require_relative "domain/validator"
@@ -0,0 +1,164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ # Centralized error reporting interface for consistent location handling
5
+ # and error message formatting across the entire codebase.
6
+ #
7
+ # This module provides a unified way to:
8
+ # 1. Report errors with consistent location information
9
+ # 2. Format error messages uniformly
10
+ # 3. Handle missing location data gracefully
11
+ # 4. Support both immediate raising and error accumulation patterns
12
+ module ErrorReporter
13
+ # Standard error structure for internal use
14
+ ErrorEntry = Struct.new(:location, :message, :type, :context, keyword_init: true) do
15
+ def to_s
16
+ location_str = format_location(location)
17
+ "#{location_str}: #{message}"
18
+ end
19
+
20
+ def has_location?
21
+ location && !location.is_a?(Symbol)
22
+ end
23
+
24
+ private
25
+
26
+ def format_location(loc)
27
+ case loc
28
+ when nil
29
+ "at ?"
30
+ when Symbol
31
+ "at #{loc}"
32
+ when Syntax::Location
33
+ "at #{loc.file}:#{loc.line}:#{loc.column}"
34
+ else
35
+ "at #{loc}"
36
+ end
37
+ end
38
+ end
39
+
40
+ module_function
41
+
42
+ # Create a standardized error entry
43
+ #
44
+ # @param message [String] The error message
45
+ # @param location [Syntax::Location, Symbol, nil] Location information
46
+ # @param type [Symbol] Optional error category (:syntax, :semantic, :type, etc.)
47
+ # @param context [Hash] Optional additional context
48
+ # @return [ErrorEntry] Structured error entry
49
+ def create_error(message, location: nil, type: :semantic, context: {})
50
+ ErrorEntry.new(
51
+ location: location,
52
+ message: message,
53
+ type: type,
54
+ context: context
55
+ )
56
+ end
57
+
58
+ # Add an error to an accumulator array (for analyzer passes)
59
+ #
60
+ # @param errors [Array] Error accumulator array
61
+ # @param message [String] The error message
62
+ # @param location [Syntax::Location, Symbol, nil] Location information
63
+ # @param type [Symbol] Error category
64
+ # @param context [Hash] Additional context
65
+ def add_error(errors, message, location: nil, type: :semantic, context: {})
66
+ entry = create_error(message, location: location, type: type, context: context)
67
+ errors << entry
68
+ entry
69
+ end
70
+
71
+ # Immediately raise a localized error (for parser)
72
+ #
73
+ # @param message [String] The error message
74
+ # @param location [Syntax::Location, Symbol, nil] Location information
75
+ # @param error_class [Class] Exception class to raise
76
+ # @param type [Symbol] Error category
77
+ # @param context [Hash] Additional context
78
+ def raise_error(message, location: nil, error_class: Errors::SemanticError, type: :semantic, context: {})
79
+ entry = create_error(message, location: location, type: type, context: context)
80
+ # Pass both the formatted message and the original location to the error constructor
81
+ raise error_class.new(entry.to_s, location)
82
+ end
83
+
84
+ # Format multiple errors into a single message
85
+ #
86
+ # @param errors [Array<ErrorEntry>] Array of error entries
87
+ # @return [String] Formatted error message
88
+ def format_errors(errors)
89
+ errors.map(&:to_s).join("\n")
90
+ end
91
+
92
+ # Group errors by type for better organization
93
+ #
94
+ # @param errors [Array<ErrorEntry>] Array of error entries
95
+ # @return [Hash] Errors grouped by type
96
+ def group_errors_by_type(errors)
97
+ errors.group_by(&:type)
98
+ end
99
+
100
+ # Check if any errors lack location information
101
+ #
102
+ # @param errors [Array<ErrorEntry>] Array of error entries
103
+ # @return [Array<ErrorEntry>] Errors without location info
104
+ def missing_location_errors(errors)
105
+ errors.reject(&:has_location?)
106
+ end
107
+
108
+ # Enhanced error reporting with suggestions and context
109
+ #
110
+ # @param message [String] Base error message
111
+ # @param location [Syntax::Location, nil] Location information
112
+ # @param suggestions [Array<String>] Suggested fixes
113
+ # @param similar_names [Array<String>] Similar names for typo suggestions
114
+ # @param type [Symbol] Error category
115
+ # @return [ErrorEntry] Enhanced error entry
116
+ def create_enhanced_error(message, location: nil, suggestions: [], similar_names: [], type: :semantic)
117
+ enhanced_message = build_enhanced_message(message, suggestions, similar_names)
118
+ create_error(enhanced_message, location: location, type: type, context: {
119
+ suggestions: suggestions,
120
+ similar_names: similar_names
121
+ })
122
+ end
123
+
124
+ # Validate that location information is present where expected
125
+ #
126
+ # @param errors [Array<ErrorEntry>] Array of error entries
127
+ # @param expected_with_location [Array<Symbol>] Error types that should have locations
128
+ # @return [Hash] Validation report
129
+ def validate_error_locations(errors, expected_with_location: %i[syntax semantic type])
130
+ report = {
131
+ total_errors: errors.size,
132
+ errors_with_location: errors.count(&:has_location?),
133
+ errors_without_location: errors.reject(&:has_location?),
134
+ location_coverage: 0.0
135
+ }
136
+
137
+ report[:location_coverage] = (report[:errors_with_location].to_f / report[:total_errors]) * 100 if report[:total_errors].positive?
138
+
139
+ # Check specific types that should have locations
140
+ report[:problematic_errors] = errors.select do |error|
141
+ expected_with_location.include?(error.type) && !error.has_location?
142
+ end
143
+
144
+ report
145
+ end
146
+
147
+ private
148
+
149
+ def build_enhanced_message(base_message, suggestions, similar_names)
150
+ parts = [base_message]
151
+
152
+ parts << "Did you mean: #{similar_names.map { |name| "`#{name}`" }.join(', ')}?" unless similar_names.empty?
153
+
154
+ unless suggestions.empty?
155
+ parts << "Suggestions:"
156
+ suggestions.each { |suggestion| parts << " - #{suggestion}" }
157
+ end
158
+
159
+ parts.join("\n")
160
+ end
161
+
162
+ module_function :build_enhanced_message
163
+ end
164
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ # Mixin module providing error reporting capabilities to classes
5
+ # that need to report localized errors consistently.
6
+ #
7
+ # Usage:
8
+ # class MyAnalyzer
9
+ # include ErrorReporting
10
+ #
11
+ # def analyze(errors)
12
+ # report_error(errors, "Something went wrong", location: some_location)
13
+ # raise_localized_error("Critical error", location: some_location)
14
+ # end
15
+ # end
16
+ module ErrorReporting
17
+ # Report an error to an accumulator (analyzer pattern)
18
+ #
19
+ # @param errors [Array] Error accumulator array
20
+ # @param message [String] Error message
21
+ # @param location [Syntax::Location, Symbol, nil] Location info
22
+ # @param type [Symbol] Error category (:syntax, :semantic, :type, etc.)
23
+ # @param context [Hash] Additional context
24
+ # @return [ErrorReporter::ErrorEntry] The created error entry
25
+ def report_error(errors, message, location: nil, type: :semantic, context: {})
26
+ ErrorReporter.add_error(errors, message, location: location, type: type, context: context)
27
+ end
28
+
29
+ # Immediately raise a localized error (parser pattern)
30
+ #
31
+ # @param message [String] Error message
32
+ # @param location [Syntax::Location, Symbol, nil] Location info
33
+ # @param error_class [Class] Exception class to raise
34
+ # @param type [Symbol] Error category
35
+ # @param context [Hash] Additional context
36
+ def raise_localized_error(message, location: nil, error_class: Errors::SemanticError, type: :semantic, context: {})
37
+ ErrorReporter.raise_error(message, location: location, error_class: error_class, type: type, context: context)
38
+ end
39
+
40
+ # Report a syntax error to an accumulator
41
+ def report_syntax_error(errors, message, location: nil, context: {})
42
+ report_error(errors, message, location: location, type: :syntax, context: context)
43
+ end
44
+
45
+ # Report a type error to an accumulator
46
+ def report_type_error(errors, message, location: nil, context: {})
47
+ report_error(errors, message, location: location, type: :type, context: context)
48
+ end
49
+
50
+ # Report a semantic error to an accumulator
51
+ def report_semantic_error(errors, message, location: nil, context: {})
52
+ report_error(errors, message, location: location, type: :semantic, context: context)
53
+ end
54
+
55
+ # Immediately raise a syntax error
56
+ def raise_syntax_error(message, location: nil, context: {})
57
+ raise_localized_error(message, location: location, error_class: Errors::SyntaxError, type: :syntax, context: context)
58
+ end
59
+
60
+ # Immediately raise a type error
61
+ def raise_type_error(message, location: nil, context: {})
62
+ raise_localized_error(message, location: location, error_class: Errors::TypeError, type: :type, context: context)
63
+ end
64
+
65
+ # Create an enhanced error with suggestions
66
+ #
67
+ # @param errors [Array] Error accumulator array
68
+ # @param message [String] Base error message
69
+ # @param location [Syntax::Location, nil] Location info
70
+ # @param suggestions [Array<String>] Suggested fixes
71
+ # @param similar_names [Array<String>] Similar names for typos
72
+ # @param type [Symbol] Error category
73
+ def report_enhanced_error(errors, message, location: nil, suggestions: [], similar_names: [], type: :semantic)
74
+ entry = ErrorReporter.create_enhanced_error(
75
+ message,
76
+ location: location,
77
+ suggestions: suggestions,
78
+ similar_names: similar_names,
79
+ type: type
80
+ )
81
+ errors << entry
82
+ entry
83
+ end
84
+
85
+ # Get current location from caller stack (fallback method)
86
+ #
87
+ # @return [Syntax::Location] Location based on caller stack
88
+ def inferred_location
89
+ fallback = caller_locations.find(&:absolute_path)
90
+ return nil unless fallback
91
+
92
+ Syntax::Location.new(file: fallback.path, line: fallback.lineno, column: 0)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Errors
5
+ class Error < StandardError; end
6
+
7
+ class LocatedError < Error
8
+ attr_reader :location
9
+
10
+ def initialize(message, location = nil)
11
+ super(message)
12
+ @location = location
13
+ end
14
+
15
+ def to_s
16
+ if @location
17
+ "#{super} at #{@location.file}:#{@location.line}:#{@location.column}"
18
+ else
19
+ super
20
+ end
21
+ end
22
+ end
23
+
24
+ class SemanticError < LocatedError; end
25
+
26
+ class TypeError < SemanticError; end
27
+
28
+ class FieldMetadataError < SemanticError; end
29
+
30
+ class SyntaxError < LocatedError; end
31
+
32
+ class RuntimeError < Error; end
33
+
34
+ class DomainViolationError < Error
35
+ attr_reader :violations
36
+
37
+ def initialize(violations)
38
+ @violations = violations
39
+ super(format_message)
40
+ end
41
+
42
+ def single_violation?
43
+ violations.size == 1
44
+ end
45
+
46
+ def multiple_violations?
47
+ violations.size > 1
48
+ end
49
+
50
+ private
51
+
52
+ def format_message
53
+ if single_violation?
54
+ violations.first[:message]
55
+ else
56
+ "Multiple domain violations:\n#{violations.map { |v| " - #{v[:message]}" }.join("\n")}"
57
+ end
58
+ end
59
+ end
60
+
61
+ class InputValidationError < Error
62
+ attr_reader :violations
63
+
64
+ def initialize(violations)
65
+ @violations = violations
66
+ super(format_message)
67
+ end
68
+
69
+ def single_violation?
70
+ violations.size == 1
71
+ end
72
+
73
+ def multiple_violations?
74
+ violations.size > 1
75
+ end
76
+
77
+ def type_violations
78
+ violations.select { |v| v[:type] == :type_violation }
79
+ end
80
+
81
+ def domain_violations
82
+ violations.select { |v| v[:type] == :domain_violation }
83
+ end
84
+
85
+ def type_violations?
86
+ type_violations.any?
87
+ end
88
+
89
+ def domain_violations?
90
+ domain_violations.any?
91
+ end
92
+
93
+ private
94
+
95
+ def format_message
96
+ if single_violation?
97
+ violations.first[:message]
98
+ else
99
+ message_parts = []
100
+
101
+ if type_violations?
102
+ message_parts << "Type violations:"
103
+ type_violations.each { |v| message_parts << " - #{v[:message]}" }
104
+ end
105
+
106
+ if domain_violations?
107
+ message_parts << "Domain violations:"
108
+ domain_violations.each { |v| message_parts << " - #{v[:message]}" }
109
+ end
110
+
111
+ message_parts.join("\n")
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,20 @@
1
+ module Kumi
2
+ EvaluationWrapper = Struct.new(:ctx) do
3
+ def initialize(ctx)
4
+ @ctx = ctx
5
+ @__schema_cache__ = {} # memoization cache for bindings
6
+ end
7
+
8
+ def [](key)
9
+ @ctx[key]
10
+ end
11
+
12
+ def keys
13
+ @ctx.keys
14
+ end
15
+
16
+ def key?(key)
17
+ @ctx.key?(key)
18
+ end
19
+ end
20
+ end