kumi 0.0.7 → 0.0.8

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 (171) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +1 -1
  3. data/README.md +8 -5
  4. data/examples/game_of_life.rb +1 -1
  5. data/examples/static_analysis_errors.rb +7 -7
  6. data/lib/kumi/analyzer.rb +15 -15
  7. data/lib/kumi/compiler.rb +6 -6
  8. data/lib/kumi/core/analyzer/analysis_state.rb +39 -0
  9. data/lib/kumi/core/analyzer/constant_evaluator.rb +59 -0
  10. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +248 -0
  11. data/lib/kumi/core/analyzer/passes/declaration_validator.rb +45 -0
  12. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +153 -0
  13. data/lib/kumi/core/analyzer/passes/input_collector.rb +139 -0
  14. data/lib/kumi/core/analyzer/passes/name_indexer.rb +26 -0
  15. data/lib/kumi/core/analyzer/passes/pass_base.rb +52 -0
  16. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +111 -0
  17. data/lib/kumi/core/analyzer/passes/toposorter.rb +110 -0
  18. data/lib/kumi/core/analyzer/passes/type_checker.rb +162 -0
  19. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +48 -0
  20. data/lib/kumi/core/analyzer/passes/type_inferencer.rb +236 -0
  21. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +406 -0
  22. data/lib/kumi/core/analyzer/passes/visitor_pass.rb +44 -0
  23. data/lib/kumi/core/atom_unsat_solver.rb +396 -0
  24. data/lib/kumi/core/compiled_schema.rb +43 -0
  25. data/lib/kumi/core/constraint_relationship_solver.rb +641 -0
  26. data/lib/kumi/core/domain/enum_analyzer.rb +55 -0
  27. data/lib/kumi/core/domain/range_analyzer.rb +85 -0
  28. data/lib/kumi/core/domain/validator.rb +82 -0
  29. data/lib/kumi/core/domain/violation_formatter.rb +42 -0
  30. data/lib/kumi/core/error_reporter.rb +166 -0
  31. data/lib/kumi/core/error_reporting.rb +97 -0
  32. data/lib/kumi/core/errors.rb +120 -0
  33. data/lib/kumi/core/evaluation_wrapper.rb +40 -0
  34. data/lib/kumi/core/explain.rb +295 -0
  35. data/lib/kumi/core/export/deserializer.rb +41 -0
  36. data/lib/kumi/core/export/errors.rb +14 -0
  37. data/lib/kumi/core/export/node_builders.rb +142 -0
  38. data/lib/kumi/core/export/node_registry.rb +54 -0
  39. data/lib/kumi/core/export/node_serializers.rb +158 -0
  40. data/lib/kumi/core/export/serializer.rb +25 -0
  41. data/lib/kumi/core/export.rb +35 -0
  42. data/lib/kumi/core/function_registry/collection_functions.rb +202 -0
  43. data/lib/kumi/core/function_registry/comparison_functions.rb +33 -0
  44. data/lib/kumi/core/function_registry/conditional_functions.rb +38 -0
  45. data/lib/kumi/core/function_registry/function_builder.rb +95 -0
  46. data/lib/kumi/core/function_registry/logical_functions.rb +44 -0
  47. data/lib/kumi/core/function_registry/math_functions.rb +74 -0
  48. data/lib/kumi/core/function_registry/string_functions.rb +57 -0
  49. data/lib/kumi/core/function_registry/type_functions.rb +53 -0
  50. data/lib/kumi/{function_registry.rb → core/function_registry.rb} +28 -36
  51. data/lib/kumi/core/input/type_matcher.rb +97 -0
  52. data/lib/kumi/core/input/validator.rb +51 -0
  53. data/lib/kumi/core/input/violation_creator.rb +52 -0
  54. data/lib/kumi/core/json_schema/generator.rb +65 -0
  55. data/lib/kumi/core/json_schema/validator.rb +27 -0
  56. data/lib/kumi/core/json_schema.rb +16 -0
  57. data/lib/kumi/core/ruby_parser/build_context.rb +27 -0
  58. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +38 -0
  59. data/lib/kumi/core/ruby_parser/dsl.rb +14 -0
  60. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +138 -0
  61. data/lib/kumi/core/ruby_parser/expression_converter.rb +128 -0
  62. data/lib/kumi/core/ruby_parser/guard_rails.rb +45 -0
  63. data/lib/kumi/core/ruby_parser/input_builder.rb +127 -0
  64. data/lib/kumi/core/ruby_parser/input_field_proxy.rb +48 -0
  65. data/lib/kumi/core/ruby_parser/input_proxy.rb +31 -0
  66. data/lib/kumi/core/ruby_parser/nested_input.rb +17 -0
  67. data/lib/kumi/core/ruby_parser/parser.rb +71 -0
  68. data/lib/kumi/core/ruby_parser/schema_builder.rb +175 -0
  69. data/lib/kumi/core/ruby_parser/sugar.rb +263 -0
  70. data/lib/kumi/core/ruby_parser.rb +12 -0
  71. data/lib/kumi/core/schema_instance.rb +111 -0
  72. data/lib/kumi/core/types/builder.rb +23 -0
  73. data/lib/kumi/core/types/compatibility.rb +96 -0
  74. data/lib/kumi/core/types/formatter.rb +26 -0
  75. data/lib/kumi/core/types/inference.rb +42 -0
  76. data/lib/kumi/core/types/normalizer.rb +72 -0
  77. data/lib/kumi/core/types/validator.rb +37 -0
  78. data/lib/kumi/core/types.rb +66 -0
  79. data/lib/kumi/core/vectorization_metadata.rb +110 -0
  80. data/lib/kumi/errors.rb +1 -112
  81. data/lib/kumi/registry.rb +37 -0
  82. data/lib/kumi/schema.rb +5 -5
  83. data/lib/kumi/schema_metadata.rb +3 -3
  84. data/lib/kumi/syntax/array_expression.rb +6 -6
  85. data/lib/kumi/syntax/call_expression.rb +4 -4
  86. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  87. data/lib/kumi/syntax/case_expression.rb +4 -4
  88. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  89. data/lib/kumi/syntax/hash_expression.rb +4 -4
  90. data/lib/kumi/syntax/input_declaration.rb +5 -5
  91. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  92. data/lib/kumi/syntax/input_reference.rb +5 -5
  93. data/lib/kumi/syntax/literal.rb +4 -4
  94. data/lib/kumi/syntax/node.rb +34 -34
  95. data/lib/kumi/syntax/root.rb +6 -6
  96. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  97. data/lib/kumi/syntax/value_declaration.rb +4 -4
  98. data/lib/kumi/version.rb +1 -1
  99. data/migrate_to_core_iterative.rb +938 -0
  100. data/scripts/generate_function_docs.rb +9 -9
  101. metadata +75 -72
  102. data/lib/kumi/analyzer/analysis_state.rb +0 -37
  103. data/lib/kumi/analyzer/constant_evaluator.rb +0 -57
  104. data/lib/kumi/analyzer/passes/broadcast_detector.rb +0 -246
  105. data/lib/kumi/analyzer/passes/declaration_validator.rb +0 -43
  106. data/lib/kumi/analyzer/passes/dependency_resolver.rb +0 -151
  107. data/lib/kumi/analyzer/passes/input_collector.rb +0 -137
  108. data/lib/kumi/analyzer/passes/name_indexer.rb +0 -24
  109. data/lib/kumi/analyzer/passes/pass_base.rb +0 -50
  110. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +0 -109
  111. data/lib/kumi/analyzer/passes/toposorter.rb +0 -108
  112. data/lib/kumi/analyzer/passes/type_checker.rb +0 -160
  113. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +0 -46
  114. data/lib/kumi/analyzer/passes/type_inferencer.rb +0 -232
  115. data/lib/kumi/analyzer/passes/unsat_detector.rb +0 -404
  116. data/lib/kumi/analyzer/passes/visitor_pass.rb +0 -42
  117. data/lib/kumi/atom_unsat_solver.rb +0 -394
  118. data/lib/kumi/compiled_schema.rb +0 -41
  119. data/lib/kumi/constraint_relationship_solver.rb +0 -638
  120. data/lib/kumi/domain/enum_analyzer.rb +0 -53
  121. data/lib/kumi/domain/range_analyzer.rb +0 -83
  122. data/lib/kumi/domain/validator.rb +0 -80
  123. data/lib/kumi/domain/violation_formatter.rb +0 -40
  124. data/lib/kumi/error_reporter.rb +0 -164
  125. data/lib/kumi/error_reporting.rb +0 -95
  126. data/lib/kumi/evaluation_wrapper.rb +0 -38
  127. data/lib/kumi/explain.rb +0 -293
  128. data/lib/kumi/export/deserializer.rb +0 -39
  129. data/lib/kumi/export/errors.rb +0 -12
  130. data/lib/kumi/export/node_builders.rb +0 -140
  131. data/lib/kumi/export/node_registry.rb +0 -52
  132. data/lib/kumi/export/node_serializers.rb +0 -156
  133. data/lib/kumi/export/serializer.rb +0 -23
  134. data/lib/kumi/export.rb +0 -33
  135. data/lib/kumi/function_registry/collection_functions.rb +0 -200
  136. data/lib/kumi/function_registry/comparison_functions.rb +0 -31
  137. data/lib/kumi/function_registry/conditional_functions.rb +0 -36
  138. data/lib/kumi/function_registry/function_builder.rb +0 -93
  139. data/lib/kumi/function_registry/logical_functions.rb +0 -42
  140. data/lib/kumi/function_registry/math_functions.rb +0 -72
  141. data/lib/kumi/function_registry/string_functions.rb +0 -54
  142. data/lib/kumi/function_registry/type_functions.rb +0 -51
  143. data/lib/kumi/input/type_matcher.rb +0 -95
  144. data/lib/kumi/input/validator.rb +0 -49
  145. data/lib/kumi/input/violation_creator.rb +0 -50
  146. data/lib/kumi/json_schema/generator.rb +0 -63
  147. data/lib/kumi/json_schema/validator.rb +0 -25
  148. data/lib/kumi/json_schema.rb +0 -14
  149. data/lib/kumi/ruby_parser/build_context.rb +0 -25
  150. data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +0 -36
  151. data/lib/kumi/ruby_parser/dsl.rb +0 -12
  152. data/lib/kumi/ruby_parser/dsl_cascade_builder.rb +0 -136
  153. data/lib/kumi/ruby_parser/expression_converter.rb +0 -126
  154. data/lib/kumi/ruby_parser/guard_rails.rb +0 -43
  155. data/lib/kumi/ruby_parser/input_builder.rb +0 -125
  156. data/lib/kumi/ruby_parser/input_field_proxy.rb +0 -46
  157. data/lib/kumi/ruby_parser/input_proxy.rb +0 -29
  158. data/lib/kumi/ruby_parser/nested_input.rb +0 -15
  159. data/lib/kumi/ruby_parser/parser.rb +0 -69
  160. data/lib/kumi/ruby_parser/schema_builder.rb +0 -173
  161. data/lib/kumi/ruby_parser/sugar.rb +0 -261
  162. data/lib/kumi/ruby_parser.rb +0 -10
  163. data/lib/kumi/schema_instance.rb +0 -109
  164. data/lib/kumi/types/builder.rb +0 -21
  165. data/lib/kumi/types/compatibility.rb +0 -94
  166. data/lib/kumi/types/formatter.rb +0 -24
  167. data/lib/kumi/types/inference.rb +0 -40
  168. data/lib/kumi/types/normalizer.rb +0 -70
  169. data/lib/kumi/types/validator.rb +0 -35
  170. data/lib/kumi/types.rb +0 -64
  171. data/lib/kumi/vectorization_metadata.rb +0 -108
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module RubyParser
6
+ class DslCascadeBuilder
7
+ include Syntax
8
+
9
+ attr_reader :cases
10
+
11
+ def initialize(context, loc)
12
+ @context = context
13
+ @cases = []
14
+ @loc = loc
15
+ end
16
+
17
+ def on(*args)
18
+ on_loc = current_location
19
+ validate_on_args(args, "on", on_loc)
20
+
21
+ trait_names = args[0..-2]
22
+ expr = args.last
23
+
24
+ trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
25
+ condition = create_fn(:all?, trait_bindings)
26
+ result = ensure_syntax(expr)
27
+ add_case(condition, result)
28
+ end
29
+
30
+ def on_any(*args)
31
+ on_loc = current_location
32
+ validate_on_args(args, "on_any", on_loc)
33
+
34
+ trait_names = args[0..-2]
35
+ expr = args.last
36
+
37
+ trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
38
+ condition = create_fn(:any?, trait_bindings)
39
+ result = ensure_syntax(expr)
40
+ add_case(condition, result)
41
+ end
42
+
43
+ def on_none(*args)
44
+ on_loc = current_location
45
+ validate_on_args(args, "on_none", on_loc)
46
+
47
+ trait_names = args[0..-2]
48
+ expr = args.last
49
+
50
+ trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
51
+ condition = create_fn(:none?, trait_bindings)
52
+ result = ensure_syntax(expr)
53
+ add_case(condition, result)
54
+ end
55
+
56
+ def base(expr)
57
+ result = ensure_syntax(expr)
58
+ add_case(create_literal(true), result)
59
+ end
60
+
61
+ def method_missing(method_name, *args, &block)
62
+ return super if !args.empty? || block_given?
63
+
64
+ # Allow direct trait references in cascade conditions
65
+ create_binding(method_name, @loc)
66
+ end
67
+
68
+ def respond_to_missing?(_method_name, _include_private = false)
69
+ true
70
+ end
71
+
72
+ private
73
+
74
+ def current_location
75
+ caller_info = caller_locations(1, 1).first
76
+ Location.new(file: caller_info.path, line: caller_info.lineno, column: 0)
77
+ end
78
+
79
+ def validate_on_args(args, method_name, location)
80
+ raise_error("cascade '#{method_name}' requires at least one trait name", location) if args.empty?
81
+
82
+ return unless args.size == 1
83
+
84
+ raise_error("cascade '#{method_name}' requires an expression as the last argument", location)
85
+ end
86
+
87
+ def convert_trait_names_to_bindings(trait_names, location)
88
+ trait_names.map do |name|
89
+ case name
90
+ when Symbol
91
+ create_binding(name, location)
92
+ when DeclarationReference
93
+ name # Already a binding from method_missing
94
+ else
95
+ raise_error("trait reference must be a symbol or bare identifier, got #{name.class}", location)
96
+ end
97
+ end
98
+ end
99
+
100
+ def add_case(condition, result)
101
+ @cases << Kumi::Syntax::CaseExpression.new(condition, result)
102
+ end
103
+
104
+ def ref(name)
105
+ @context.ref(name)
106
+ end
107
+
108
+ def fn(name, *args)
109
+ @context.fn(name, *args)
110
+ end
111
+
112
+ def create_literal(value)
113
+ @context.literal(value)
114
+ end
115
+
116
+ def create_fn(name, args)
117
+ @context.fn(name, args)
118
+ end
119
+
120
+ def input
121
+ @context.input
122
+ end
123
+
124
+ def ensure_syntax(expr)
125
+ @context.ensure_syntax(expr)
126
+ end
127
+
128
+ def raise_error(message, location)
129
+ @context.raise_error(message, location)
130
+ end
131
+
132
+ def create_binding(name, location)
133
+ Kumi::Syntax::DeclarationReference.new(name, loc: location)
134
+ end
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module RubyParser
6
+ # Converts Ruby objects and DSL expressions into AST nodes
7
+ # This is the bridge between Ruby's native types and Kumi's syntax tree
8
+ class ExpressionConverter
9
+ include Syntax
10
+ include ErrorReporting
11
+
12
+ # Use the same literal types as Sugar module to avoid duplication
13
+ LITERAL_TYPES = Sugar::LITERAL_TYPES
14
+
15
+ def initialize(context)
16
+ @context = context
17
+ end
18
+
19
+ # Convert any Ruby object into a syntax node
20
+ # @param obj [Object] The object to convert
21
+ # @return [Syntax::Node] The corresponding AST node
22
+ def ensure_syntax(obj)
23
+ case obj
24
+ when *LITERAL_TYPES
25
+ create_literal(obj)
26
+ when Array
27
+ create_list(obj)
28
+ when Syntax::Node
29
+ obj
30
+ else
31
+ handle_custom_object(obj)
32
+ end
33
+ end
34
+
35
+ # Create a reference to another declaration
36
+ # @param name [Symbol] The name to reference
37
+ # @return [Syntax::DeclarationReference] Reference node
38
+ def ref(name)
39
+ validate_reference_name(name)
40
+ Kumi::Syntax::DeclarationReference.new(name, loc: current_location)
41
+ end
42
+
43
+ # Create a literal value node
44
+ # @param value [Object] The literal value
45
+ # @return [Syntax::Literal] Literal node
46
+ def literal(value)
47
+ Kumi::Syntax::Literal.new(value, loc: current_location)
48
+ end
49
+
50
+ # Create a function call expression
51
+ # @param fn_name [Symbol] The function name
52
+ # @param args [Array] The function arguments
53
+ # @return [Syntax::CallExpression] Function call node
54
+ def fn(fn_name, *args)
55
+ validate_function_name(fn_name)
56
+ expr_args = convert_arguments(args)
57
+ Kumi::Syntax::CallExpression.new(fn_name, expr_args, loc: current_location)
58
+ end
59
+
60
+ # Access the input proxy for field references
61
+ # @return [InputProxy] Proxy for input field access
62
+ def input
63
+ InputProxy.new(@context)
64
+ end
65
+
66
+ # Raise a syntax error with location information
67
+ # @param message [String] Error message
68
+ # @param location [Location] Error location
69
+ def raise_error(message, location)
70
+ raise_syntax_error(message, location: location)
71
+ end
72
+
73
+ private
74
+
75
+ def create_literal(value)
76
+ Kumi::Syntax::Literal.new(value, loc: current_location)
77
+ end
78
+
79
+ def create_list(array)
80
+ elements = array.map { |element| ensure_syntax(element) }
81
+ Kumi::Syntax::ArrayExpression.new(elements, loc: current_location)
82
+ end
83
+
84
+ def handle_custom_object(obj)
85
+ if obj.respond_to?(:to_ast_node)
86
+ obj.to_ast_node
87
+ else
88
+ raise_invalid_expression_error(obj)
89
+ end
90
+ end
91
+
92
+ def validate_reference_name(name)
93
+ return if name.is_a?(Symbol)
94
+
95
+ raise_syntax_error(
96
+ "Reference name must be a symbol, got #{name.class}",
97
+ location: current_location
98
+ )
99
+ end
100
+
101
+ def validate_function_name(fn_name)
102
+ return if fn_name.is_a?(Symbol)
103
+
104
+ raise_syntax_error(
105
+ "Function name must be a symbol, got #{fn_name.class}",
106
+ location: current_location
107
+ )
108
+ end
109
+
110
+ def convert_arguments(args)
111
+ args.map { |arg| ensure_syntax(arg) }
112
+ end
113
+
114
+ def raise_invalid_expression_error(obj)
115
+ raise_syntax_error(
116
+ "Cannot convert #{obj.class} to AST node. " \
117
+ "Value: #{obj.inspect}",
118
+ location: current_location
119
+ )
120
+ end
121
+
122
+ def current_location
123
+ @context.current_location
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module RubyParser
6
+ module GuardRails
7
+ RESERVED = %i[input trait value fn lit ref].freeze
8
+
9
+ def self.included(base)
10
+ base.singleton_class.prepend(ClassMethods)
11
+ end
12
+
13
+ module ClassMethods
14
+ # prevent accidental addition of new DSL keywords
15
+ def method_added(name)
16
+ if GuardRails::RESERVED.include?(name)
17
+ # Check if this is a redefinition by looking at the call stack
18
+ # We want to allow the original definition but prevent redefinition
19
+ calling_location = caller_locations(1, 1).first
20
+
21
+ # Allow the original definition from schema_builder.rb
22
+ if calling_location&.path&.include?("schema_builder.rb")
23
+ super
24
+ return
25
+ end
26
+
27
+ # This is a redefinition attempt, prevent it
28
+ raise Kumi::Core::Errors::SemanticError,
29
+ "DSL keyword `#{name}` is reserved; " \
30
+ "do not redefine it inside SchemaBuilder"
31
+ end
32
+ super
33
+ end
34
+ end
35
+
36
+ # catch any stray method call inside DSL block
37
+ def method_missing(name, *_args)
38
+ raise NoMethodError, "unknown DSL keyword `#{name}`"
39
+ end
40
+
41
+ def respond_to_missing?(*) = false
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module RubyParser
6
+ class InputBuilder
7
+ include Syntax
8
+ include ErrorReporting
9
+
10
+ def initialize(context)
11
+ @context = context
12
+ end
13
+
14
+ def key(name, type: :any, domain: nil)
15
+ normalized_type = normalize_type(type, name)
16
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, normalized_type, [], loc: @context.current_location)
17
+ end
18
+
19
+ %i[integer float string boolean any scalar].each do |type_name|
20
+ define_method(type_name) do |name, type: nil, domain: nil|
21
+ actual_type = type || (type_name == :scalar ? :any : type_name)
22
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, actual_type, [], loc: @context.current_location)
23
+ end
24
+ end
25
+
26
+ def array(name_or_elem_type, **kwargs, &block)
27
+ if block_given?
28
+ create_array_field_with_block(name_or_elem_type, kwargs, &block)
29
+ elsif kwargs.any?
30
+ create_array_field(name_or_elem_type, kwargs)
31
+ else
32
+ Kumi::Core::Types.array(name_or_elem_type)
33
+ end
34
+ end
35
+
36
+ def hash(name_or_key_type, val_type = nil, **kwargs)
37
+ return Kumi::Core::Types.hash(name_or_key_type, val_type) unless val_type.nil?
38
+
39
+ create_hash_field(name_or_key_type, kwargs)
40
+ end
41
+
42
+ def method_missing(method_name, *_args)
43
+ allowed_methods = "'key', 'integer', 'float', 'string', 'boolean', 'any', 'scalar', 'array', and 'hash'"
44
+ raise_syntax_error("Unknown method '#{method_name}' in input block. Only #{allowed_methods} are allowed.",
45
+ location: @context.current_location)
46
+ end
47
+
48
+ def respond_to_missing?(_method_name, _include_private = false)
49
+ false
50
+ end
51
+
52
+ private
53
+
54
+ def normalize_type(type, name)
55
+ Kumi::Core::Types.normalize(type)
56
+ rescue ArgumentError => e
57
+ raise_syntax_error("Invalid type for input `#{name}`: #{e.message}", location: @context.current_location)
58
+ end
59
+
60
+ def create_array_field(field_name, options)
61
+ elem_spec = options[:elem]
62
+ domain = options[:domain]
63
+ elem_type = elem_spec.is_a?(Hash) && elem_spec[:type] ? elem_spec[:type] : :any
64
+
65
+ array_type = create_array_type(field_name, elem_type)
66
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, array_type, [], loc: @context.current_location)
67
+ end
68
+
69
+ def create_array_type(field_name, elem_type)
70
+ Kumi::Core::Types.array(elem_type)
71
+ rescue ArgumentError => e
72
+ raise_syntax_error("Invalid element type for array `#{field_name}`: #{e.message}", location: @context.current_location)
73
+ end
74
+
75
+ def create_hash_field(field_name, options)
76
+ key_spec = options[:key]
77
+ val_spec = options[:val] || options[:value]
78
+ domain = options[:domain]
79
+
80
+ key_type = extract_type(key_spec)
81
+ val_type = extract_type(val_spec)
82
+
83
+ hash_type = create_hash_type(field_name, key_type, val_type)
84
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, hash_type, [], loc: @context.current_location)
85
+ end
86
+
87
+ def extract_type(spec)
88
+ spec.is_a?(Hash) && spec[:type] ? spec[:type] : :any
89
+ end
90
+
91
+ def create_hash_type(field_name, key_type, val_type)
92
+ Kumi::Core::Types.hash(key_type, val_type)
93
+ rescue ArgumentError => e
94
+ raise_syntax_error("Invalid types for hash `#{field_name}`: #{e.message}", location: @context.current_location)
95
+ end
96
+
97
+ def create_array_field_with_block(field_name, options, &block)
98
+ domain = options[:domain]
99
+
100
+ # Collect children by creating a nested context
101
+ children = collect_array_children(&block)
102
+
103
+ # Create the InputDeclaration with children
104
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(
105
+ field_name,
106
+ domain,
107
+ :array,
108
+ children,
109
+ loc: @context.current_location
110
+ )
111
+ end
112
+
113
+ def collect_array_children(&block)
114
+ # Create a temporary nested context to collect children
115
+ nested_inputs = []
116
+ nested_context = NestedInput.new(nested_inputs, @context.current_location)
117
+ nested_builder = InputBuilder.new(nested_context)
118
+
119
+ # Execute the block in the nested context
120
+ nested_builder.instance_eval(&block)
121
+
122
+ nested_inputs
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module RubyParser
6
+ # Proxy for input field access that can handle arbitrary depth nesting
7
+ # Handles input.field.subfield.subsubfield... syntax by building up path arrays
8
+ class InputFieldProxy
9
+ include Syntax
10
+
11
+ # Use shared operator methods instead of refinements
12
+ extend Sugar::ProxyRefinement
13
+
14
+ def initialize(path, context)
15
+ @path = Array(path) # Ensure it's always an array
16
+ @context = context
17
+ end
18
+
19
+ # Convert to appropriate AST node based on path length
20
+ def to_ast_node
21
+ if @path.length == 1
22
+ # Single field: input.field -> InputReference
23
+ Kumi::Syntax::InputReference.new(@path.first, loc: @context.current_location)
24
+ else
25
+ # Nested fields: input.field.subfield... -> InputElementReference
26
+ Kumi::Syntax::InputElementReference.new(@path, loc: @context.current_location)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def method_missing(method_name, *args, &block)
33
+ if args.empty? && block.nil?
34
+ # Extend the path: input.user.details -> InputFieldProxy([user, details])
35
+ InputFieldProxy.new(@path + [method_name], @context)
36
+ else
37
+ # Operators are now handled by ProxyRefinement methods
38
+ super
39
+ end
40
+ end
41
+
42
+ def respond_to_missing?(_method_name, _include_private = false)
43
+ true
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module RubyParser
6
+ # Proxy object for input field references (input.field_name)
7
+ class InputProxy
8
+ include Syntax
9
+
10
+ def initialize(context)
11
+ @context = context
12
+ end
13
+
14
+ private
15
+
16
+ def method_missing(method_name, *_args)
17
+ # Create InputFieldProxy that can handle further field access
18
+ InputFieldProxy.new(method_name, @context)
19
+ end
20
+
21
+ # This method is called when the user tries to access a field
22
+ # on the input object, e.g. `input.field_name`.
23
+ # It is used to create an InputReference node in the AST.
24
+
25
+ def respond_to_missing?(_method_name, _include_private = false)
26
+ true # Allow any field name
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module RubyParser
6
+ # Simple context struct for nested input collection
7
+ class NestedInput
8
+ attr_reader :inputs, :current_location
9
+
10
+ def initialize(inputs_array, location)
11
+ @inputs = inputs_array
12
+ @current_location = location
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Core
5
+ module RubyParser
6
+ # Main parser class for Ruby DSL
7
+ class Parser
8
+ include Syntax
9
+ include ErrorReporting
10
+
11
+ def initialize
12
+ @context = BuildContext.new
13
+ @interface = SchemaBuilder.new(@context)
14
+ end
15
+
16
+ def parse(&rule_block)
17
+ enable_refinements(rule_block)
18
+
19
+ before_consts = ::Object.constants
20
+ @interface.freeze # stop singleton hacks
21
+ @interface.instance_eval(&rule_block)
22
+ added = ::Object.constants - before_consts
23
+
24
+ unless added.empty?
25
+ raise Kumi::Core::Errors::SemanticError,
26
+ "DSL cannot define global constants: #{added.join(', ')}"
27
+ end
28
+
29
+ build_syntax_tree
30
+ rescue ArgumentError => e
31
+ handle_parse_error(e)
32
+ raise
33
+ end
34
+
35
+ private
36
+
37
+ def enable_refinements(rule_block)
38
+ rule_block.binding.eval("using Kumi::Core::RubyParser::Sugar::ExpressionRefinement")
39
+ rule_block.binding.eval("using Kumi::Core::RubyParser::Sugar::NumericRefinement")
40
+ rule_block.binding.eval("using Kumi::Core::RubyParser::Sugar::StringRefinement")
41
+ rule_block.binding.eval("using Kumi::Core::RubyParser::Sugar::ArrayRefinement")
42
+ rule_block.binding.eval("using Kumi::Core::RubyParser::Sugar::ModuleRefinement")
43
+ rescue RuntimeError, NoMethodError
44
+ # Refinements disabled in method scope - continue without them
45
+ end
46
+
47
+ def build_syntax_tree
48
+ Root.new(@context.inputs, @context.attributes, @context.traits)
49
+ end
50
+
51
+ def handle_parse_error(error)
52
+ return unless literal_comparison_error?(error)
53
+
54
+ warn <<~HINT
55
+ #{error.backtrace.first.split(':', 2).join(':')}: \
56
+ Literal‑left comparison failed because the schema block is \
57
+ defined inside a method (Ruby disallows refinements there).
58
+
59
+ • Move the `schema do … end` block to the top level of a class or module, OR
60
+ • Write the comparison as `input.age >= 80` (preferred), OR
61
+ • Wrap the literal: `lit(80) <= input.age`.
62
+ HINT
63
+ end
64
+
65
+ def literal_comparison_error?(error)
66
+ error.message =~ /comparison of Integer with Kumi::Syntax::/i
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end