kumi 0.0.4 → 0.0.6

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 (89) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +160 -8
  3. data/README.md +278 -200
  4. data/{documents → docs}/AST.md +29 -29
  5. data/{documents → docs}/DSL.md +3 -3
  6. data/{documents → docs}/SYNTAX.md +107 -24
  7. data/docs/features/README.md +45 -0
  8. data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
  9. data/docs/features/analysis-type-inference.md +42 -0
  10. data/docs/features/analysis-unsat-detection.md +71 -0
  11. data/docs/features/array-broadcasting.md +170 -0
  12. data/docs/features/input-declaration-system.md +42 -0
  13. data/docs/features/performance.md +16 -0
  14. data/examples/federal_tax_calculator_2024.rb +43 -40
  15. data/examples/game_of_life.rb +97 -0
  16. data/examples/simple_rpg_game.rb +1000 -0
  17. data/examples/static_analysis_errors.rb +178 -0
  18. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
  19. data/lib/kumi/analyzer/analysis_state.rb +37 -0
  20. data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
  21. data/lib/kumi/analyzer/passes/broadcast_detector.rb +251 -0
  22. data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +8 -7
  23. data/lib/kumi/analyzer/passes/dependency_resolver.rb +106 -26
  24. data/lib/kumi/analyzer/passes/input_collector.rb +105 -23
  25. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  26. data/lib/kumi/analyzer/passes/pass_base.rb +11 -28
  27. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
  28. data/lib/kumi/analyzer/passes/toposorter.rb +45 -9
  29. data/lib/kumi/analyzer/passes/type_checker.rb +34 -11
  30. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
  31. data/lib/kumi/analyzer/passes/type_inferencer.rb +128 -21
  32. data/lib/kumi/analyzer/passes/unsat_detector.rb +312 -13
  33. data/lib/kumi/analyzer/passes/visitor_pass.rb +4 -3
  34. data/lib/kumi/analyzer.rb +41 -24
  35. data/lib/kumi/atom_unsat_solver.rb +45 -0
  36. data/lib/kumi/cli.rb +449 -0
  37. data/lib/kumi/compiler.rb +194 -16
  38. data/lib/kumi/constraint_relationship_solver.rb +638 -0
  39. data/lib/kumi/domain/validator.rb +0 -4
  40. data/lib/kumi/error_reporter.rb +6 -6
  41. data/lib/kumi/evaluation_wrapper.rb +20 -4
  42. data/lib/kumi/explain.rb +28 -28
  43. data/lib/kumi/export/node_registry.rb +26 -12
  44. data/lib/kumi/export/node_serializers.rb +1 -1
  45. data/lib/kumi/function_registry/collection_functions.rb +117 -9
  46. data/lib/kumi/function_registry/function_builder.rb +4 -3
  47. data/lib/kumi/function_registry.rb +8 -2
  48. data/lib/kumi/input/type_matcher.rb +3 -0
  49. data/lib/kumi/input/validator.rb +0 -3
  50. data/lib/kumi/parser/declaration_reference_proxy.rb +36 -0
  51. data/lib/kumi/parser/dsl_cascade_builder.rb +19 -8
  52. data/lib/kumi/parser/expression_converter.rb +80 -12
  53. data/lib/kumi/parser/input_builder.rb +40 -9
  54. data/lib/kumi/parser/input_field_proxy.rb +46 -0
  55. data/lib/kumi/parser/input_proxy.rb +3 -3
  56. data/lib/kumi/parser/nested_input.rb +15 -0
  57. data/lib/kumi/parser/parser.rb +2 -0
  58. data/lib/kumi/parser/schema_builder.rb +10 -9
  59. data/lib/kumi/parser/sugar.rb +171 -18
  60. data/lib/kumi/schema.rb +3 -1
  61. data/lib/kumi/schema_instance.rb +69 -3
  62. data/lib/kumi/syntax/array_expression.rb +15 -0
  63. data/lib/kumi/syntax/call_expression.rb +11 -0
  64. data/lib/kumi/syntax/cascade_expression.rb +11 -0
  65. data/lib/kumi/syntax/case_expression.rb +11 -0
  66. data/lib/kumi/syntax/declaration_reference.rb +11 -0
  67. data/lib/kumi/syntax/hash_expression.rb +11 -0
  68. data/lib/kumi/syntax/input_declaration.rb +12 -0
  69. data/lib/kumi/syntax/input_element_reference.rb +12 -0
  70. data/lib/kumi/syntax/input_reference.rb +12 -0
  71. data/lib/kumi/syntax/literal.rb +11 -0
  72. data/lib/kumi/syntax/root.rb +1 -0
  73. data/lib/kumi/syntax/trait_declaration.rb +11 -0
  74. data/lib/kumi/syntax/value_declaration.rb +11 -0
  75. data/lib/kumi/types/compatibility.rb +8 -0
  76. data/lib/kumi/types/validator.rb +1 -1
  77. data/lib/kumi/vectorization_metadata.rb +108 -0
  78. data/lib/kumi/version.rb +1 -1
  79. data/scripts/generate_function_docs.rb +22 -10
  80. metadata +38 -17
  81. data/CHANGELOG.md +0 -25
  82. data/lib/kumi/domain.rb +0 -8
  83. data/lib/kumi/input.rb +0 -8
  84. data/lib/kumi/syntax/declarations.rb +0 -23
  85. data/lib/kumi/syntax/expressions.rb +0 -30
  86. data/lib/kumi/syntax/terminal_expressions.rb +0 -27
  87. data/lib/kumi/syntax.rb +0 -9
  88. data/test_impossible_cascade.rb +0 -51
  89. /data/{documents → docs}/FUNCTIONS.md +0 -0
@@ -2,57 +2,125 @@
2
2
 
3
3
  module Kumi
4
4
  module Parser
5
+ # Converts Ruby objects and DSL expressions into AST nodes
6
+ # This is the bridge between Ruby's native types and Kumi's syntax tree
5
7
  class ExpressionConverter
6
8
  include Syntax
7
9
  include ErrorReporting
8
10
 
11
+ # Use the same literal types as Sugar module to avoid duplication
12
+ LITERAL_TYPES = Sugar::LITERAL_TYPES
13
+
9
14
  def initialize(context)
10
15
  @context = context
11
16
  end
12
17
 
18
+ # Convert any Ruby object into a syntax node
19
+ # @param obj [Object] The object to convert
20
+ # @return [Syntax::Node] The corresponding AST node
13
21
  def ensure_syntax(obj)
14
22
  case obj
15
- when Integer, String, TrueClass, FalseClass, Float, Regexp, Symbol
16
- Literal.new(obj)
23
+ when *LITERAL_TYPES
24
+ create_literal(obj)
17
25
  when Array
18
- ListExpression.new(obj.map { |e| ensure_syntax(e) })
26
+ create_list(obj)
19
27
  when Syntax::Node
20
28
  obj
21
29
  else
22
- handle_complex_object(obj)
30
+ handle_custom_object(obj)
23
31
  end
24
32
  end
25
33
 
34
+ # Create a reference to another declaration
35
+ # @param name [Symbol] The name to reference
36
+ # @return [Syntax::DeclarationReference] Reference node
26
37
  def ref(name)
27
- Binding.new(name, loc: @context.current_location)
38
+ validate_reference_name(name)
39
+ Kumi::Syntax::DeclarationReference.new(name, loc: current_location)
28
40
  end
29
41
 
42
+ # Create a literal value node
43
+ # @param value [Object] The literal value
44
+ # @return [Syntax::Literal] Literal node
30
45
  def literal(value)
31
- Literal.new(value, loc: @context.current_location)
46
+ Kumi::Syntax::Literal.new(value, loc: current_location)
32
47
  end
33
48
 
49
+ # Create a function call expression
50
+ # @param fn_name [Symbol] The function name
51
+ # @param args [Array] The function arguments
52
+ # @return [Syntax::CallExpression] Function call node
34
53
  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)
54
+ validate_function_name(fn_name)
55
+ expr_args = convert_arguments(args)
56
+ Kumi::Syntax::CallExpression.new(fn_name, expr_args, loc: current_location)
37
57
  end
38
58
 
59
+ # Access the input proxy for field references
60
+ # @return [InputProxy] Proxy for input field access
39
61
  def input
40
62
  InputProxy.new(@context)
41
63
  end
42
64
 
65
+ # Raise a syntax error with location information
66
+ # @param message [String] Error message
67
+ # @param location [Location] Error location
43
68
  def raise_error(message, location)
44
69
  raise_syntax_error(message, location: location)
45
70
  end
46
71
 
47
72
  private
48
73
 
49
- def handle_complex_object(obj)
50
- if obj.class.instance_methods.include?(:to_ast_node)
74
+ def create_literal(value)
75
+ Kumi::Syntax::Literal.new(value, loc: current_location)
76
+ end
77
+
78
+ def create_list(array)
79
+ elements = array.map { |element| ensure_syntax(element) }
80
+ Kumi::Syntax::ArrayExpression.new(elements, loc: current_location)
81
+ end
82
+
83
+ def handle_custom_object(obj)
84
+ if obj.respond_to?(:to_ast_node)
51
85
  obj.to_ast_node
52
86
  else
53
- raise_syntax_error("Invalid expression: #{obj.inspect}", location: @context.current_location)
87
+ raise_invalid_expression_error(obj)
54
88
  end
55
89
  end
90
+
91
+ def validate_reference_name(name)
92
+ unless name.is_a?(Symbol)
93
+ raise_syntax_error(
94
+ "Reference name must be a symbol, got #{name.class}",
95
+ location: current_location
96
+ )
97
+ end
98
+ end
99
+
100
+ def validate_function_name(fn_name)
101
+ unless fn_name.is_a?(Symbol)
102
+ raise_syntax_error(
103
+ "Function name must be a symbol, got #{fn_name.class}",
104
+ location: current_location
105
+ )
106
+ end
107
+ end
108
+
109
+ def convert_arguments(args)
110
+ args.map { |arg| ensure_syntax(arg) }
111
+ end
112
+
113
+ def raise_invalid_expression_error(obj)
114
+ raise_syntax_error(
115
+ "Cannot convert #{obj.class} to AST node. " \
116
+ "Value: #{obj.inspect}",
117
+ location: current_location
118
+ )
119
+ end
120
+
121
+ def current_location
122
+ @context.current_location
123
+ end
56
124
  end
57
125
  end
58
- end
126
+ end
@@ -12,17 +12,20 @@ module Kumi
12
12
 
13
13
  def key(name, type: :any, domain: nil)
14
14
  normalized_type = normalize_type(type, name)
15
- @context.inputs << FieldDecl.new(name, domain, normalized_type, loc: @context.current_location)
15
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, normalized_type, [], loc: @context.current_location)
16
16
  end
17
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)
18
+ %i[integer float string boolean any scalar].each do |type_name|
19
+ define_method(type_name) do |name, type: nil, domain: nil|
20
+ actual_type = type || (type_name == :scalar ? :any : type_name)
21
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, actual_type, [], loc: @context.current_location)
21
22
  end
22
23
  end
23
24
 
24
- def array(name_or_elem_type, **kwargs)
25
- if kwargs.any?
25
+ def array(name_or_elem_type, **kwargs, &block)
26
+ if block_given?
27
+ create_array_field_with_block(name_or_elem_type, kwargs, &block)
28
+ elsif kwargs.any?
26
29
  create_array_field(name_or_elem_type, kwargs)
27
30
  else
28
31
  Kumi::Types.array(name_or_elem_type)
@@ -36,7 +39,7 @@ module Kumi
36
39
  end
37
40
 
38
41
  def method_missing(method_name, *_args)
39
- allowed_methods = "'key', 'integer', 'float', 'string', 'boolean', 'any', 'array', and 'hash'"
42
+ allowed_methods = "'key', 'integer', 'float', 'string', 'boolean', 'any', 'scalar', 'array', and 'hash'"
40
43
  raise_syntax_error("Unknown method '#{method_name}' in input block. Only #{allowed_methods} are allowed.",
41
44
  location: @context.current_location)
42
45
  end
@@ -59,7 +62,7 @@ module Kumi
59
62
  elem_type = elem_spec.is_a?(Hash) && elem_spec[:type] ? elem_spec[:type] : :any
60
63
 
61
64
  array_type = create_array_type(field_name, elem_type)
62
- @context.inputs << FieldDecl.new(field_name, domain, array_type, loc: @context.current_location)
65
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, array_type, [], loc: @context.current_location)
63
66
  end
64
67
 
65
68
  def create_array_type(field_name, elem_type)
@@ -77,7 +80,7 @@ module Kumi
77
80
  val_type = extract_type(val_spec)
78
81
 
79
82
  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)
83
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, hash_type, [], loc: @context.current_location)
81
84
  end
82
85
 
83
86
  def extract_type(spec)
@@ -89,6 +92,34 @@ module Kumi
89
92
  rescue ArgumentError => e
90
93
  raise_syntax_error("Invalid types for hash `#{field_name}`: #{e.message}", location: @context.current_location)
91
94
  end
95
+
96
+ def create_array_field_with_block(field_name, options, &block)
97
+ domain = options[:domain]
98
+
99
+ # Collect children by creating a nested context
100
+ children = collect_array_children(&block)
101
+
102
+ # Create the InputDeclaration with children
103
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(
104
+ field_name,
105
+ domain,
106
+ :array,
107
+ children,
108
+ loc: @context.current_location
109
+ )
110
+ end
111
+
112
+ def collect_array_children(&block)
113
+ # Create a temporary nested context to collect children
114
+ nested_inputs = []
115
+ nested_context = NestedInput.new(nested_inputs, @context.current_location)
116
+ nested_builder = InputBuilder.new(nested_context)
117
+
118
+ # Execute the block in the nested context
119
+ nested_builder.instance_eval(&block)
120
+
121
+ nested_inputs
122
+ end
92
123
  end
93
124
  end
94
125
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Parser
5
+ # Proxy for input field access that can handle arbitrary depth nesting
6
+ # Handles input.field.subfield.subsubfield... syntax by building up path arrays
7
+ class InputFieldProxy
8
+ include Syntax
9
+
10
+ # Use shared operator methods instead of refinements
11
+ extend Sugar::ProxyRefinement
12
+
13
+ def initialize(path, context)
14
+ @path = Array(path) # Ensure it's always an array
15
+ @context = context
16
+ end
17
+
18
+ # Convert to appropriate AST node based on path length
19
+ def to_ast_node
20
+ if @path.length == 1
21
+ # Single field: input.field -> InputReference
22
+ Kumi::Syntax::InputReference.new(@path.first, loc: @context.current_location)
23
+ else
24
+ # Nested fields: input.field.subfield... -> InputElementReference
25
+ Kumi::Syntax::InputElementReference.new(@path, loc: @context.current_location)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def method_missing(method_name, *args, &block)
32
+ if args.empty? && block.nil?
33
+ # Extend the path: input.user.details -> InputFieldProxy([user, details])
34
+ InputFieldProxy.new(@path + [method_name], @context)
35
+ else
36
+ # Operators are now handled by ProxyRefinement methods
37
+ super
38
+ end
39
+ end
40
+
41
+ def respond_to_missing?(_method_name, _include_private = false)
42
+ true
43
+ end
44
+ end
45
+ end
46
+ end
@@ -13,13 +13,13 @@ module Kumi
13
13
  private
14
14
 
15
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)
16
+ # Create InputFieldProxy that can handle further field access
17
+ InputFieldProxy.new(method_name, @context)
18
18
  end
19
19
 
20
20
  # This method is called when the user tries to access a field
21
21
  # on the input object, e.g. `input.field_name`.
22
- # It is used to create a FieldRef node in the AST.
22
+ # It is used to create an InputReference node in the AST.
23
23
 
24
24
  def respond_to_missing?(_method_name, _include_private = false)
25
25
  true # Allow any field name
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Parser
5
+ # Simple context struct for nested input collection
6
+ class NestedInput
7
+ attr_reader :inputs, :current_location
8
+
9
+ def initialize(inputs_array, location)
10
+ @inputs = inputs_array
11
+ @current_location = location
12
+ end
13
+ end
14
+ end
15
+ end
@@ -36,6 +36,8 @@ module Kumi
36
36
  rule_block.binding.eval("using Kumi::Parser::Sugar::ExpressionRefinement")
37
37
  rule_block.binding.eval("using Kumi::Parser::Sugar::NumericRefinement")
38
38
  rule_block.binding.eval("using Kumi::Parser::Sugar::StringRefinement")
39
+ rule_block.binding.eval("using Kumi::Parser::Sugar::ArrayRefinement")
40
+ rule_block.binding.eval("using Kumi::Parser::Sugar::ModuleRefinement")
39
41
  rescue RuntimeError, NoMethodError
40
42
  # Refinements disabled in method scope - continue without them
41
43
  end
@@ -18,7 +18,7 @@ module Kumi
18
18
  validate_value_args(name, expr, blk)
19
19
 
20
20
  expression = blk ? build_cascade(&blk) : ensure_syntax(expr)
21
- @context.attributes << Attribute.new(name, expression, loc: @context.current_location)
21
+ @context.attributes << Kumi::Syntax::ValueDeclaration.new(name, expression, loc: @context.current_location)
22
22
  end
23
23
 
24
24
  def trait(*args, **kwargs)
@@ -40,24 +40,25 @@ module Kumi
40
40
 
41
41
  def ref(name)
42
42
  update_location
43
- Binding.new(name, loc: @context.current_location)
43
+ Kumi::Syntax::DeclarationReference.new(name, loc: @context.current_location)
44
44
  end
45
45
 
46
46
  def literal(value)
47
47
  update_location
48
- Literal.new(value, loc: @context.current_location)
48
+ Kumi::Syntax::Literal.new(value, loc: @context.current_location)
49
49
  end
50
50
 
51
51
  def fn(fn_name, *args)
52
52
  update_location
53
53
  expr_args = args.map { |a| ensure_syntax(a) }
54
- CallExpression.new(fn_name, expr_args, loc: @context.current_location)
54
+ Kumi::Syntax::CallExpression.new(fn_name, expr_args, loc: @context.current_location)
55
55
  end
56
56
 
57
57
  def method_missing(method_name, *args, &block)
58
58
  if args.empty? && !block_given?
59
59
  update_location
60
- Binding.new(method_name, loc: @context.current_location)
60
+ # Create proxy for declaration references (traits/values)
61
+ DeclarationReferenceProxy.new(method_name, @context)
61
62
  else
62
63
  super
63
64
  end
@@ -109,7 +110,7 @@ module Kumi
109
110
  name, expression = args
110
111
  validate_trait_name(name)
111
112
  expr = ensure_syntax(expression)
112
- @context.traits << Trait.new(name, expr, loc: @context.current_location)
113
+ @context.traits << Kumi::Syntax::TraitDeclaration.new(name, expr, loc: @context.current_location)
113
114
  else
114
115
  handle_deprecated_trait_syntax(args)
115
116
  end
@@ -137,8 +138,8 @@ module Kumi
137
138
  validate_operator(operator)
138
139
 
139
140
  rhs_exprs = rhs.map { |r| ensure_syntax(r) }
140
- expr = CallExpression.new(operator, [ensure_syntax(lhs)] + rhs_exprs, loc: @context.current_location)
141
- @context.traits << Trait.new(name, expr, loc: @context.current_location)
141
+ expr = Kumi::Syntax::CallExpression.new(operator, [ensure_syntax(lhs)] + rhs_exprs, loc: @context.current_location)
142
+ @context.traits << Kumi::Syntax::TraitDeclaration.new(name, expr, loc: @context.current_location)
142
143
  end
143
144
 
144
145
  def validate_trait_name(name)
@@ -161,7 +162,7 @@ module Kumi
161
162
  expression_converter = ExpressionConverter.new(@context)
162
163
  cascade_builder = DslCascadeBuilder.new(expression_converter, @context.current_location)
163
164
  cascade_builder.instance_eval(&blk)
164
- CascadeExpression.new(cascade_builder.cases, loc: @context.current_location)
165
+ Kumi::Syntax::CascadeExpression.new(cascade_builder.cases, loc: @context.current_location)
165
166
  end
166
167
 
167
168
  def ensure_syntax(obj)
@@ -5,49 +5,89 @@ module Kumi
5
5
  module Sugar
6
6
  include Syntax
7
7
 
8
- ARITHMETIC_OPS = { :+ => :add, :- => :subtract, :* => :multiply,
9
- :/ => :divide, :% => :modulo, :** => :power }.freeze
8
+ ARITHMETIC_OPS = {
9
+ :+ => :add, :- => :subtract, :* => :multiply,
10
+ :/ => :divide, :% => :modulo, :** => :power
11
+ }.freeze
12
+
10
13
  COMPARISON_OPS = %i[< <= > >= == !=].freeze
11
- LITERAL_TYPES = [Integer, String, Symbol, TrueClass, FalseClass, Float, Regexp].freeze
14
+
15
+ LITERAL_TYPES = [
16
+ Integer, String, Symbol, TrueClass, FalseClass, Float, Regexp, NilClass
17
+ ].freeze
18
+
19
+ # Collection methods that can be applied to arrays/syntax nodes
20
+ COLLECTION_METHODS = %i[
21
+ sum size length first last sort reverse unique min max empty? flatten
22
+ map_with_index indices
23
+ ].freeze
12
24
 
13
25
  def self.ensure_literal(obj)
14
- return Literal.new(obj) if LITERAL_TYPES.any? { |type| obj.is_a?(type) }
26
+ return Kumi::Syntax::Literal.new(obj) if LITERAL_TYPES.any? { |type| obj.is_a?(type) }
15
27
  return obj if obj.is_a?(Syntax::Node)
16
28
  return obj.to_ast_node if obj.respond_to?(:to_ast_node)
17
29
 
18
- Literal.new(obj)
30
+ Kumi::Syntax::Literal.new(obj)
19
31
  end
20
32
 
21
33
  def self.syntax_expression?(obj)
22
34
  obj.is_a?(Syntax::Node) || obj.respond_to?(:to_ast_node)
23
35
  end
24
36
 
37
+ # Create a call expression with consistent error handling
38
+ def self.create_call_expression(fn_name, args)
39
+ Kumi::Syntax::CallExpression.new(fn_name, args)
40
+ end
41
+
25
42
  module ExpressionRefinement
26
43
  refine Syntax::Node do
44
+ # Arithmetic operations
27
45
  ARITHMETIC_OPS.each do |op, op_name|
28
46
  define_method(op) do |other|
29
47
  other_node = Sugar.ensure_literal(other)
30
- Syntax::CallExpression.new(op_name, [self, other_node])
48
+ Sugar.create_call_expression(op_name, [self, other_node])
31
49
  end
32
50
  end
33
51
 
52
+ # Comparison operations
34
53
  COMPARISON_OPS.each do |op|
35
54
  define_method(op) do |other|
36
55
  other_node = Sugar.ensure_literal(other)
37
- Syntax::CallExpression.new(op, [self, other_node])
56
+ Sugar.create_call_expression(op, [self, other_node])
38
57
  end
39
58
  end
40
59
 
60
+ # Array access
41
61
  def [](index)
42
- Syntax::CallExpression.new(:at, [self, Sugar.ensure_literal(index)])
62
+ Sugar.create_call_expression(:at, [self, Sugar.ensure_literal(index)])
43
63
  end
44
64
 
65
+ # Unary minus
45
66
  def -@
46
- Syntax::CallExpression.new(:subtract, [Sugar.ensure_literal(0), self])
67
+ Sugar.create_call_expression(:subtract, [Sugar.ensure_literal(0), self])
47
68
  end
48
69
 
70
+ # Logical operations
49
71
  def &(other)
50
- Syntax::CallExpression.new(:and, [self, Sugar.ensure_literal(other)])
72
+ Sugar.create_call_expression(:and, [self, Sugar.ensure_literal(other)])
73
+ end
74
+
75
+ def |(other)
76
+ Sugar.create_call_expression(:or, [self, Sugar.ensure_literal(other)])
77
+ end
78
+
79
+ # Collection methods - single argument (self)
80
+ COLLECTION_METHODS.each do |method_name|
81
+ next if method_name == :include? # Special case with element argument
82
+
83
+ define_method(method_name) do
84
+ Sugar.create_call_expression(method_name, [self])
85
+ end
86
+ end
87
+
88
+ # Special case: include? takes an element argument
89
+ def include?(element)
90
+ Sugar.create_call_expression(:include?, [self, Sugar.ensure_literal(element)])
51
91
  end
52
92
  end
53
93
  end
@@ -55,22 +95,24 @@ module Kumi
55
95
  module NumericRefinement
56
96
  [Integer, Float].each do |klass|
57
97
  refine klass do
98
+ # Arithmetic operations with syntax expressions
58
99
  ARITHMETIC_OPS.each do |op, op_name|
59
100
  define_method(op) do |other|
60
101
  if Sugar.syntax_expression?(other)
61
- other_node = other.respond_to?(:to_ast_node) ? other.to_ast_node : other
62
- Syntax::CallExpression.new(op_name, [Syntax::Literal.new(self), other_node])
102
+ other_node = Sugar.ensure_literal(other)
103
+ Sugar.create_call_expression(op_name, [Kumi::Syntax::Literal.new(self), other_node])
63
104
  else
64
105
  super(other)
65
106
  end
66
107
  end
67
108
  end
68
109
 
110
+ # Comparison operations with syntax expressions
69
111
  COMPARISON_OPS.each do |op|
70
112
  define_method(op) do |other|
71
113
  if Sugar.syntax_expression?(other)
72
- other_node = other.respond_to?(:to_ast_node) ? other.to_ast_node : other
73
- Syntax::CallExpression.new(op, [Syntax::Literal.new(self), other_node])
114
+ other_node = Sugar.ensure_literal(other)
115
+ Sugar.create_call_expression(op, [Kumi::Syntax::Literal.new(self), other_node])
74
116
  else
75
117
  super(other)
76
118
  end
@@ -84,8 +126,8 @@ module Kumi
84
126
  refine String do
85
127
  def +(other)
86
128
  if Sugar.syntax_expression?(other)
87
- other_node = other.respond_to?(:to_ast_node) ? other.to_ast_node : other
88
- Syntax::CallExpression.new(:concat, [Syntax::Literal.new(self), other_node])
129
+ other_node = Sugar.ensure_literal(other)
130
+ Sugar.create_call_expression(:concat, [Kumi::Syntax::Literal.new(self), other_node])
89
131
  else
90
132
  super
91
133
  end
@@ -94,8 +136,8 @@ module Kumi
94
136
  %i[== !=].each do |op|
95
137
  define_method(op) do |other|
96
138
  if Sugar.syntax_expression?(other)
97
- other_node = other.respond_to?(:to_ast_node) ? other.to_ast_node : other
98
- Syntax::CallExpression.new(op, [Syntax::Literal.new(self), other_node])
139
+ other_node = Sugar.ensure_literal(other)
140
+ Sugar.create_call_expression(op, [Kumi::Syntax::Literal.new(self), other_node])
99
141
  else
100
142
  super(other)
101
143
  end
@@ -103,6 +145,117 @@ module Kumi
103
145
  end
104
146
  end
105
147
  end
148
+
149
+ module ArrayRefinement
150
+ refine Array do
151
+ # Helper method to check if array contains any syntax expressions
152
+ def any_syntax_expressions?
153
+ any? { |item| Sugar.syntax_expression?(item) }
154
+ end
155
+
156
+ # Convert array to syntax list expression with all elements as syntax nodes
157
+ def to_syntax_list
158
+ syntax_elements = map { |item| Sugar.ensure_literal(item) }
159
+ Kumi::Syntax::ArrayExpression.new(syntax_elements)
160
+ end
161
+
162
+ # Create array method that works with syntax expressions
163
+ def self.define_array_syntax_method(method_name, has_argument: false)
164
+ define_method(method_name) do |*args|
165
+ if any_syntax_expressions?
166
+ array_literal = to_syntax_list
167
+ call_args = [array_literal]
168
+ call_args.concat(args.map { |arg| Sugar.ensure_literal(arg) }) if has_argument
169
+ Sugar.create_call_expression(method_name, call_args)
170
+ else
171
+ super(*args)
172
+ end
173
+ end
174
+ end
175
+
176
+ # Define collection methods without arguments
177
+ %i[sum size length first last sort reverse unique min max empty? flatten].each do |method_name|
178
+ define_array_syntax_method(method_name)
179
+ end
180
+
181
+ # Define methods with arguments
182
+ define_array_syntax_method(:include?, has_argument: true)
183
+ end
184
+ end
185
+
186
+ module ModuleRefinement
187
+ refine Module do
188
+ # Allow modules to provide schema utilities and helpers
189
+ def with_schema_utilities
190
+ include Kumi::Schema if respond_to?(:include)
191
+ extend Kumi::Schema if respond_to?(:extend)
192
+ end
193
+
194
+ # Helper for defining schema constants that can be used in multiple schemas
195
+ def schema_const(name, value)
196
+ const_set(name, value.freeze)
197
+ end
198
+
199
+ # Enable easy schema composition
200
+ def compose_schema(*modules)
201
+ modules.each do |mod|
202
+ include mod if mod.is_a?(Module)
203
+ end
204
+ end
205
+ end
206
+ end
207
+
208
+ # Shared refinement for proxy objects that need to handle operators
209
+ # Both DeclarationReferenceProxy and InputFieldProxy can use this
210
+ module ProxyRefinement
211
+ def self.extended(proxy_class)
212
+ # Add operator methods directly to the proxy class
213
+ proxy_class.class_eval do
214
+ # Arithmetic operations
215
+ ARITHMETIC_OPS.each do |op, op_name|
216
+ define_method(op) do |other|
217
+ ast_node = to_ast_node
218
+ other_node = Sugar.ensure_literal(other)
219
+ Sugar.create_call_expression(op_name, [ast_node, other_node])
220
+ end
221
+ end
222
+
223
+ # Comparison operations (including == and != that don't work with refinements)
224
+ COMPARISON_OPS.each do |op|
225
+ define_method(op) do |other|
226
+ ast_node = to_ast_node
227
+ other_node = Sugar.ensure_literal(other)
228
+ Sugar.create_call_expression(op, [ast_node, other_node])
229
+ end
230
+ end
231
+
232
+ # Logical operations
233
+ define_method(:&) do |other|
234
+ ast_node = to_ast_node
235
+ other_node = Sugar.ensure_literal(other)
236
+ Sugar.create_call_expression(:and, [ast_node, other_node])
237
+ end
238
+
239
+ define_method(:|) do |other|
240
+ ast_node = to_ast_node
241
+ other_node = Sugar.ensure_literal(other)
242
+ Sugar.create_call_expression(:or, [ast_node, other_node])
243
+ end
244
+
245
+ # Array access
246
+ define_method(:[]) do |index|
247
+ ast_node = to_ast_node
248
+ Sugar.create_call_expression(:at, [ast_node, Sugar.ensure_literal(index)])
249
+ end
250
+
251
+ # Unary minus
252
+ define_method(:-@) do
253
+ ast_node = to_ast_node
254
+ Sugar.create_call_expression(:subtract, [Sugar.ensure_literal(0), ast_node])
255
+ end
256
+ end
257
+ end
258
+ end
106
259
  end
107
260
  end
108
261
  end
data/lib/kumi/schema.rb CHANGED
@@ -4,6 +4,8 @@ require "ostruct"
4
4
 
5
5
  module Kumi
6
6
  module Schema
7
+ attr_reader :__syntax_tree__, :__analyzer_result__, :__compiled_schema__
8
+
7
9
  Inspector = Struct.new(:syntax_tree, :analyzer_result, :compiled_schema) do
8
10
  def inspect
9
11
  "#<#{self.class} syntax_tree: #{syntax_tree.inspect}, analyzer_result: #{analyzer_result.inspect}, schema: #{schema.inspect}>"
@@ -19,7 +21,7 @@ module Kumi
19
21
 
20
22
  raise Errors::InputValidationError, violations unless violations.empty?
21
23
 
22
- SchemaInstance.new(@__compiled_schema__, @__analyzer_result__.definitions, context)
24
+ SchemaInstance.new(@__compiled_schema__, @__analyzer_result__, context)
23
25
  end
24
26
 
25
27
  def explain(context, *keys)