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,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Input
5
+ class TypeMatcher
6
+ def self.matches?(value, declared_type)
7
+ case declared_type
8
+ when :integer
9
+ value.is_a?(Integer)
10
+ when :float
11
+ value.is_a?(Float) || value.is_a?(Integer) # Allow integer for float
12
+ when :string
13
+ value.is_a?(String)
14
+ when :boolean
15
+ value.is_a?(TrueClass) || value.is_a?(FalseClass)
16
+ when :symbol
17
+ value.is_a?(Symbol)
18
+ when :any
19
+ true
20
+ else
21
+ # Handle complex types (arrays, hashes)
22
+ handle_complex_type(value, declared_type)
23
+ end
24
+ end
25
+
26
+ def self.infer_type(value)
27
+ case value
28
+ when Integer then :integer
29
+ when Float then :float
30
+ when String then :string
31
+ when TrueClass, FalseClass then :boolean
32
+ when Symbol then :symbol
33
+ when Array then { array: :mixed }
34
+ when Hash then { hash: %i[mixed mixed] }
35
+ else :unknown
36
+ end
37
+ end
38
+
39
+ def self.format_type(type)
40
+ case type
41
+ when Symbol
42
+ type.to_s
43
+ when Hash
44
+ format_complex_type(type)
45
+ else
46
+ type.inspect
47
+ end
48
+ end
49
+
50
+ private_class_method def self.handle_complex_type(value, declared_type)
51
+ return false unless declared_type.is_a?(Hash)
52
+
53
+ if declared_type.key?(:array)
54
+ handle_array_type(value, declared_type[:array])
55
+ elsif declared_type.key?(:hash)
56
+ handle_hash_type(value, declared_type[:hash])
57
+ else
58
+ false
59
+ end
60
+ end
61
+
62
+ private_class_method def self.handle_array_type(value, element_type)
63
+ return false unless value.is_a?(Array)
64
+ return true if element_type == :any
65
+
66
+ value.all? { |elem| matches?(elem, element_type) }
67
+ end
68
+
69
+ private_class_method def self.handle_hash_type(value, hash_spec)
70
+ return false unless value.is_a?(Hash)
71
+
72
+ key_type, value_type = hash_spec
73
+ return true if key_type == :any && value_type == :any
74
+
75
+ value.all? do |k, v|
76
+ matches?(k, key_type) && matches?(v, value_type)
77
+ end
78
+ end
79
+
80
+ private_class_method def self.format_complex_type(type)
81
+ if type.key?(:array)
82
+ "array(#{format_type(type[:array])})"
83
+ elsif type.key?(:hash)
84
+ key_type, value_type = type[:hash]
85
+ "hash(#{format_type(key_type)}, #{format_type(value_type)})"
86
+ else
87
+ type.inspect
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "type_matcher"
4
+ require_relative "violation_creator"
5
+
6
+ module Kumi
7
+ module Input
8
+ class Validator
9
+ def self.validate_context(context, input_meta)
10
+ violations = []
11
+
12
+ context.each do |field, value|
13
+ meta = input_meta[field]
14
+ next unless meta
15
+
16
+ # Type validation first
17
+ if should_validate_type?(meta) && !TypeMatcher.matches?(value, meta[:type])
18
+ violations << ViolationCreator.create_type_violation(field, value, meta[:type])
19
+ next # Skip domain validation if type is wrong
20
+ end
21
+
22
+ # Domain validation second (only if type is correct)
23
+ if should_validate_domain?(meta) && !Domain::Validator.validate_field(field, value, meta[:domain])
24
+ violations << ViolationCreator.create_domain_violation(field, value, meta[:domain])
25
+ end
26
+ end
27
+
28
+ violations
29
+ end
30
+
31
+ def self.type_matches?(value, declared_type)
32
+ TypeMatcher.matches?(value, declared_type)
33
+ end
34
+
35
+ def self.infer_type(value)
36
+ TypeMatcher.infer_type(value)
37
+ end
38
+
39
+ def self.format_type(type)
40
+ TypeMatcher.format_type(type)
41
+ end
42
+
43
+ private_class_method def self.should_validate_type?(meta)
44
+ meta[:type] && meta[:type] != :any
45
+ end
46
+
47
+ private_class_method def self.should_validate_domain?(meta)
48
+ meta[:domain]
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Input
5
+ class ViolationCreator
6
+ def self.create_type_violation(field, value, expected_type)
7
+ {
8
+ type: :type_violation,
9
+ field: field,
10
+ value: value,
11
+ expected_type: expected_type,
12
+ actual_type: TypeMatcher.infer_type(value),
13
+ message: format_type_violation_message(field, value, expected_type)
14
+ }
15
+ end
16
+
17
+ def self.create_domain_violation(field, value, domain)
18
+ {
19
+ type: :domain_violation,
20
+ field: field,
21
+ value: value,
22
+ domain: domain,
23
+ message: Kumi::Domain::ViolationFormatter.format_message(field, value, domain)
24
+ }
25
+ end
26
+
27
+ def self.create_missing_field_violation(field, expected_type)
28
+ {
29
+ type: :missing_field_violation,
30
+ field: field,
31
+ expected_type: expected_type,
32
+ message: format_missing_field_message(field, expected_type)
33
+ }
34
+ end
35
+
36
+ private_class_method def self.format_type_violation_message(field, value, expected_type)
37
+ actual_type = TypeMatcher.infer_type(value)
38
+ expected_formatted = TypeMatcher.format_type(expected_type)
39
+ actual_formatted = TypeMatcher.format_type(actual_type)
40
+
41
+ "Field :#{field} expected #{expected_formatted}, got #{value.inspect} of type #{actual_formatted}"
42
+ end
43
+
44
+ private_class_method def self.format_missing_field_message(field, expected_type)
45
+ expected_formatted = TypeMatcher.format_type(expected_type)
46
+ "Missing required field :#{field} of type #{expected_formatted}"
47
+ end
48
+ end
49
+ end
50
+ end
data/lib/kumi/input.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Input
5
+ end
6
+ end
7
+
8
+ require_relative "input/validator"
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Parser
5
+ class BuildContext
6
+ attr_reader :inputs, :attributes, :traits
7
+ attr_accessor :current_location
8
+
9
+ def initialize
10
+ @inputs = []
11
+ @attributes = []
12
+ @traits = []
13
+ @input_block_defined = false
14
+ end
15
+
16
+ def input_block_defined?
17
+ @input_block_defined
18
+ end
19
+
20
+ def mark_input_block_defined!
21
+ @input_block_defined = true
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Parser
5
+ module Dsl
6
+ def self.build_syntax_tree(&rule_block)
7
+ parser = Parser.new
8
+ parser.parse(&rule_block)
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Parser
5
+ class DslCascadeBuilder
6
+ include Syntax
7
+
8
+ attr_reader :cases
9
+
10
+ def initialize(context, loc)
11
+ @context = context
12
+ @cases = []
13
+ @loc = loc
14
+ end
15
+
16
+ def on(*args)
17
+ on_loc = current_location
18
+ validate_on_args(args, "on", on_loc)
19
+
20
+ trait_names = args[0..-2]
21
+ expr = args.last
22
+
23
+ condition = create_function_call(:all?, trait_names, on_loc)
24
+ result = ensure_syntax(expr)
25
+ add_case(condition, result)
26
+ end
27
+
28
+ def on_any(*args)
29
+ on_loc = current_location
30
+ validate_on_args(args, "on_any", on_loc)
31
+
32
+ trait_names = args[0..-2]
33
+ expr = args.last
34
+
35
+ condition = create_function_call(:any?, trait_names, on_loc)
36
+ result = ensure_syntax(expr)
37
+ add_case(condition, result)
38
+ end
39
+
40
+ def on_none(*args)
41
+ on_loc = current_location
42
+ validate_on_args(args, "on_none", on_loc)
43
+
44
+ trait_names = args[0..-2]
45
+ expr = args.last
46
+
47
+ condition = create_function_call(:none?, trait_names, on_loc)
48
+ result = ensure_syntax(expr)
49
+ add_case(condition, result)
50
+ end
51
+
52
+ def base(expr)
53
+ result = ensure_syntax(expr)
54
+ add_case(create_literal(true), result)
55
+ end
56
+
57
+ def method_missing(method_name, *args, &block)
58
+ return super if !args.empty? || block_given?
59
+
60
+ # Allow direct trait references in cascade conditions
61
+ create_binding(method_name, @loc)
62
+ end
63
+
64
+ def respond_to_missing?(_method_name, _include_private = false)
65
+ true
66
+ end
67
+
68
+ private
69
+
70
+ def current_location
71
+ caller_info = caller_locations(1, 1).first
72
+ Location.new(file: caller_info.path, line: caller_info.lineno, column: 0)
73
+ end
74
+
75
+ def validate_on_args(args, method_name, location)
76
+ raise_error("cascade '#{method_name}' requires at least one trait name", location) if args.empty?
77
+
78
+ return unless args.size == 1
79
+
80
+ raise_error("cascade '#{method_name}' requires an expression as the last argument", location)
81
+ end
82
+
83
+ def create_function_call(function_name, trait_names, location)
84
+ trait_bindings = trait_names.map { |name| create_binding(name, location) }
85
+ create_fn(function_name, trait_bindings)
86
+ end
87
+
88
+ def add_case(condition, result)
89
+ @cases << WhenCaseExpression.new(condition, result)
90
+ end
91
+
92
+ def ref(name)
93
+ @context.ref(name)
94
+ end
95
+
96
+ def fn(name, *args)
97
+ @context.fn(name, *args)
98
+ end
99
+
100
+ def create_literal(value)
101
+ @context.literal(value)
102
+ end
103
+
104
+ def create_fn(name, args)
105
+ @context.fn(name, args)
106
+ end
107
+
108
+ def input
109
+ @context.input
110
+ end
111
+
112
+ def ensure_syntax(expr)
113
+ @context.ensure_syntax(expr)
114
+ end
115
+
116
+ def raise_error(message, location)
117
+ @context.raise_error(message, location)
118
+ end
119
+
120
+ def create_binding(name, location)
121
+ Binding.new(name, loc: location)
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Parser
5
+ class ExpressionConverter
6
+ include Syntax
7
+ include ErrorReporting
8
+
9
+ def initialize(context)
10
+ @context = context
11
+ end
12
+
13
+ def ensure_syntax(obj)
14
+ case obj
15
+ when Integer, String, TrueClass, FalseClass, Float, Regexp, Symbol
16
+ Literal.new(obj)
17
+ when Array
18
+ ListExpression.new(obj.map { |e| ensure_syntax(e) })
19
+ when Syntax::Node
20
+ obj
21
+ else
22
+ handle_complex_object(obj)
23
+ end
24
+ end
25
+
26
+ def ref(name)
27
+ Binding.new(name, loc: @context.current_location)
28
+ end
29
+
30
+ def literal(value)
31
+ Literal.new(value, loc: @context.current_location)
32
+ end
33
+
34
+ def fn(fn_name, *args)
35
+ expr_args = args.map { |a| ensure_syntax(a) }
36
+ CallExpression.new(fn_name, expr_args, loc: @context.current_location)
37
+ end
38
+
39
+ def input
40
+ InputProxy.new(@context)
41
+ end
42
+
43
+ def raise_error(message, location)
44
+ raise_syntax_error(message, location: location)
45
+ end
46
+
47
+ private
48
+
49
+ def handle_complex_object(obj)
50
+ if obj.class.instance_methods.include?(:to_ast_node)
51
+ obj.to_ast_node
52
+ else
53
+ raise_syntax_error("Invalid expression: #{obj.inspect}", location: @context.current_location)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Parser
5
+ module GuardRails
6
+ RESERVED = %i[input trait value fn lit ref].freeze
7
+
8
+ def self.included(base)
9
+ base.singleton_class.prepend(ClassMethods)
10
+ end
11
+
12
+ module ClassMethods
13
+ # prevent accidental addition of new DSL keywords
14
+ def method_added(name)
15
+ if GuardRails::RESERVED.include?(name)
16
+ # Check if this is a redefinition by looking at the call stack
17
+ # We want to allow the original definition but prevent redefinition
18
+ calling_location = caller_locations(1, 1).first
19
+
20
+ # Allow the original definition from schema_builder.rb
21
+ if calling_location&.path&.include?("schema_builder.rb")
22
+ super
23
+ return
24
+ end
25
+
26
+ # This is a redefinition attempt, prevent it
27
+ raise Kumi::Errors::SemanticError,
28
+ "DSL keyword `#{name}` is reserved; " \
29
+ "do not redefine it inside SchemaBuilder"
30
+ end
31
+ super
32
+ end
33
+ end
34
+
35
+ # catch any stray method call inside DSL block
36
+ def method_missing(name, *_args)
37
+ raise NoMethodError, "unknown DSL keyword `#{name}`"
38
+ end
39
+
40
+ def respond_to_missing?(*) = false
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Parser
5
+ class InputBuilder
6
+ include Syntax
7
+ include ErrorReporting
8
+
9
+ def initialize(context)
10
+ @context = context
11
+ end
12
+
13
+ def key(name, type: :any, domain: nil)
14
+ normalized_type = normalize_type(type, name)
15
+ @context.inputs << FieldDecl.new(name, domain, normalized_type, loc: @context.current_location)
16
+ end
17
+
18
+ %i[integer float string boolean any].each do |type_name|
19
+ define_method(type_name) do |name, domain: nil|
20
+ @context.inputs << FieldDecl.new(name, domain, type_name, loc: @context.current_location)
21
+ end
22
+ end
23
+
24
+ def array(name_or_elem_type, **kwargs)
25
+ if kwargs.any?
26
+ create_array_field(name_or_elem_type, kwargs)
27
+ else
28
+ Kumi::Types.array(name_or_elem_type)
29
+ end
30
+ end
31
+
32
+ def hash(name_or_key_type, val_type = nil, **kwargs)
33
+ return Kumi::Types.hash(name_or_key_type, val_type) unless val_type.nil?
34
+
35
+ create_hash_field(name_or_key_type, kwargs)
36
+ end
37
+
38
+ def method_missing(method_name, *_args)
39
+ allowed_methods = "'key', 'integer', 'float', 'string', 'boolean', 'any', 'array', and 'hash'"
40
+ raise_syntax_error("Unknown method '#{method_name}' in input block. Only #{allowed_methods} are allowed.",
41
+ location: @context.current_location)
42
+ end
43
+
44
+ def respond_to_missing?(_method_name, _include_private = false)
45
+ false
46
+ end
47
+
48
+ private
49
+
50
+ def normalize_type(type, name)
51
+ Kumi::Types.normalize(type)
52
+ rescue ArgumentError => e
53
+ raise_syntax_error("Invalid type for input `#{name}`: #{e.message}", location: @context.current_location)
54
+ end
55
+
56
+ def create_array_field(field_name, options)
57
+ elem_spec = options[:elem]
58
+ domain = options[:domain]
59
+ elem_type = elem_spec.is_a?(Hash) && elem_spec[:type] ? elem_spec[:type] : :any
60
+
61
+ array_type = create_array_type(field_name, elem_type)
62
+ @context.inputs << FieldDecl.new(field_name, domain, array_type, loc: @context.current_location)
63
+ end
64
+
65
+ def create_array_type(field_name, elem_type)
66
+ Kumi::Types.array(elem_type)
67
+ rescue ArgumentError => e
68
+ raise_syntax_error("Invalid element type for array `#{field_name}`: #{e.message}", location: @context.current_location)
69
+ end
70
+
71
+ def create_hash_field(field_name, options)
72
+ key_spec = options[:key]
73
+ val_spec = options[:val] || options[:value]
74
+ domain = options[:domain]
75
+
76
+ key_type = extract_type(key_spec)
77
+ val_type = extract_type(val_spec)
78
+
79
+ hash_type = create_hash_type(field_name, key_type, val_type)
80
+ @context.inputs << FieldDecl.new(field_name, domain, hash_type, loc: @context.current_location)
81
+ end
82
+
83
+ def extract_type(spec)
84
+ spec.is_a?(Hash) && spec[:type] ? spec[:type] : :any
85
+ end
86
+
87
+ def create_hash_type(field_name, key_type, val_type)
88
+ Kumi::Types.hash(key_type, val_type)
89
+ rescue ArgumentError => e
90
+ raise_syntax_error("Invalid types for hash `#{field_name}`: #{e.message}", location: @context.current_location)
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Parser
5
+ # Proxy object for input field references (input.field_name)
6
+ class InputProxy
7
+ include Syntax
8
+
9
+ def initialize(context)
10
+ @context = context
11
+ end
12
+
13
+ private
14
+
15
+ def method_missing(method_name, *_args)
16
+ # Create a FieldRef node for the given method name
17
+ FieldRef.new(method_name, loc: @context.current_location)
18
+ end
19
+
20
+ # This method is called when the user tries to access a field
21
+ # on the input object, e.g. `input.field_name`.
22
+ # It is used to create a FieldRef node in the AST.
23
+
24
+ def respond_to_missing?(_method_name, _include_private = false)
25
+ true # Allow any field name
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Parser
5
+ class Parser
6
+ include Syntax
7
+ include ErrorReporting
8
+
9
+ def initialize
10
+ @context = BuildContext.new
11
+ @interface = SchemaBuilder.new(@context)
12
+ end
13
+
14
+ def parse(&rule_block)
15
+ enable_refinements(rule_block)
16
+
17
+ before_consts = ::Object.constants
18
+ @interface.freeze # stop singleton hacks
19
+ @interface.instance_eval(&rule_block)
20
+ added = ::Object.constants - before_consts
21
+
22
+ unless added.empty?
23
+ raise Kumi::Errors::SemanticError,
24
+ "DSL cannot define global constants: #{added.join(', ')}"
25
+ end
26
+
27
+ build_syntax_tree
28
+ rescue ArgumentError => e
29
+ handle_parse_error(e)
30
+ raise
31
+ end
32
+
33
+ private
34
+
35
+ def enable_refinements(rule_block)
36
+ rule_block.binding.eval("using Kumi::Parser::Sugar::ExpressionRefinement")
37
+ rule_block.binding.eval("using Kumi::Parser::Sugar::NumericRefinement")
38
+ rule_block.binding.eval("using Kumi::Parser::Sugar::StringRefinement")
39
+ rescue RuntimeError, NoMethodError
40
+ # Refinements disabled in method scope - continue without them
41
+ end
42
+
43
+ def handle_parse_error(error)
44
+ return unless literal_comparison_error?(error)
45
+
46
+ warn <<~HINT
47
+ #{error.backtrace.first.split(':', 2).join(':')}: \
48
+ Literal‑left comparison failed because the schema block is \
49
+ defined inside a method (Ruby disallows refinements there).
50
+
51
+ • Move the `schema do … end` block to the top level of a class or module, OR
52
+ • Write the comparison as `input.age >= 80` (preferred), OR
53
+ • Wrap the literal: `lit(80) <= input.age`.
54
+ HINT
55
+ end
56
+
57
+ def literal_comparison_error?(error)
58
+ error.message =~ /comparison of Integer with Kumi::Syntax::/i
59
+ end
60
+
61
+ def build_syntax_tree
62
+ Root.new(@context.inputs, @context.attributes, @context.traits)
63
+ end
64
+ end
65
+ end
66
+ end