kumi 0.0.3 → 0.0.5

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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +109 -2
  3. data/README.md +174 -205
  4. data/documents/DSL.md +3 -3
  5. data/documents/SYNTAX.md +17 -26
  6. data/examples/federal_tax_calculator_2024.rb +36 -38
  7. data/examples/game_of_life.rb +97 -0
  8. data/examples/simple_rpg_game.rb +1000 -0
  9. data/examples/static_analysis_errors.rb +178 -0
  10. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +1 -1
  11. data/lib/kumi/analyzer/analysis_state.rb +37 -0
  12. data/lib/kumi/analyzer/constant_evaluator.rb +22 -16
  13. data/lib/kumi/analyzer/passes/definition_validator.rb +4 -3
  14. data/lib/kumi/analyzer/passes/dependency_resolver.rb +50 -10
  15. data/lib/kumi/analyzer/passes/input_collector.rb +28 -7
  16. data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
  17. data/lib/kumi/analyzer/passes/pass_base.rb +10 -27
  18. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +110 -0
  19. data/lib/kumi/analyzer/passes/toposorter.rb +3 -3
  20. data/lib/kumi/analyzer/passes/type_checker.rb +2 -1
  21. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -1
  22. data/lib/kumi/analyzer/passes/type_inferencer.rb +2 -4
  23. data/lib/kumi/analyzer/passes/unsat_detector.rb +233 -14
  24. data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -1
  25. data/lib/kumi/analyzer.rb +42 -24
  26. data/lib/kumi/atom_unsat_solver.rb +45 -0
  27. data/lib/kumi/cli.rb +449 -0
  28. data/lib/kumi/constraint_relationship_solver.rb +638 -0
  29. data/lib/kumi/error_reporter.rb +6 -6
  30. data/lib/kumi/evaluation_wrapper.rb +22 -4
  31. data/lib/kumi/explain.rb +9 -10
  32. data/lib/kumi/function_registry/collection_functions.rb +103 -0
  33. data/lib/kumi/function_registry/string_functions.rb +1 -1
  34. data/lib/kumi/parser/dsl_cascade_builder.rb +17 -6
  35. data/lib/kumi/parser/expression_converter.rb +80 -12
  36. data/lib/kumi/parser/guard_rails.rb +2 -2
  37. data/lib/kumi/parser/parser.rb +2 -0
  38. data/lib/kumi/parser/schema_builder.rb +1 -1
  39. data/lib/kumi/parser/sugar.rb +117 -16
  40. data/lib/kumi/schema.rb +3 -1
  41. data/lib/kumi/schema_instance.rb +69 -3
  42. data/lib/kumi/syntax/declarations.rb +3 -0
  43. data/lib/kumi/syntax/expressions.rb +4 -0
  44. data/lib/kumi/syntax/root.rb +1 -0
  45. data/lib/kumi/syntax/terminal_expressions.rb +3 -0
  46. data/lib/kumi/types/compatibility.rb +8 -0
  47. data/lib/kumi/types/validator.rb +1 -1
  48. data/lib/kumi/version.rb +1 -1
  49. data/scripts/generate_function_docs.rb +22 -10
  50. metadata +10 -6
  51. data/CHANGELOG.md +0 -25
  52. data/test_impossible_cascade.rb +0 -51
@@ -1,20 +1,38 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kumi
2
4
  EvaluationWrapper = Struct.new(:ctx) do
3
5
  def initialize(ctx)
4
- @ctx = ctx
6
+ super
5
7
  @__schema_cache__ = {} # memoization cache for bindings
6
8
  end
7
9
 
8
10
  def [](key)
9
- @ctx[key]
11
+ ctx[key]
12
+ end
13
+
14
+ def []=(key, value)
15
+ ctx[key] = value
10
16
  end
11
17
 
12
18
  def keys
13
- @ctx.keys
19
+ ctx.keys
14
20
  end
15
21
 
16
22
  def key?(key)
17
- @ctx.key?(key)
23
+ ctx.key?(key)
24
+ end
25
+
26
+ def clear
27
+ @__schema_cache__.clear
28
+ end
29
+
30
+ def clear_cache(*keys)
31
+ if keys.empty?
32
+ @__schema_cache__.clear
33
+ else
34
+ keys.each { |key| @__schema_cache__.delete(key) }
35
+ end
18
36
  end
19
37
  end
20
38
  end
data/lib/kumi/explain.rb CHANGED
@@ -56,16 +56,16 @@ module Kumi
56
56
 
57
57
  def format_call_expression(expr, indent_context: 0, nested: false)
58
58
  if pretty_printable?(expr.fn_name)
59
- format_pretty_function(expr, expr.fn_name, indent_context, nested)
59
+ format_pretty_function(expr, expr.fn_name, indent_context, nested: nested)
60
60
  else
61
61
  format_generic_function(expr, indent_context)
62
62
  end
63
63
  end
64
64
 
65
- def format_pretty_function(expr, fn_name, _indent_context, nested = false)
65
+ def format_pretty_function(expr, fn_name, _indent_context, nested: false)
66
66
  if needs_evaluation?(expr.args) && !nested
67
67
  # For top-level expressions, show the flattened symbolic form and evaluation
68
- if is_chain_of_same_operator?(expr, fn_name)
68
+ if chain_of_same_operator?(expr, fn_name)
69
69
  # For chains like a + b + c, flatten to show all operands
70
70
  all_operands = flatten_operator_chain(expr, fn_name)
71
71
  symbolic_operands = all_operands.map { |op| format_expression(op, indent_context: 0, nested: true) }
@@ -85,11 +85,10 @@ module Kumi
85
85
  end
86
86
  evaluated_format = evaluated_operands.join(" #{get_operator_symbol(fn_name)} ")
87
87
 
88
- "#{symbolic_format} = #{evaluated_format}"
89
88
  else
90
89
  # Regular pretty formatting for non-chain expressions
91
90
  symbolic_args = expr.args.map { |arg| format_expression(arg, indent_context: 0, nested: true) }
92
- symbolic_format = get_display_format(fn_name, symbolic_args)
91
+ symbolic_format = display_format(fn_name, symbolic_args)
93
92
 
94
93
  evaluated_args = expr.args.map do |arg|
95
94
  if arg.is_a?(Syntax::TerminalExpressions::Literal)
@@ -104,18 +103,18 @@ module Kumi
104
103
  end
105
104
  end
106
105
  end
107
- evaluated_format = get_display_format(fn_name, evaluated_args)
106
+ evaluated_format = display_format(fn_name, evaluated_args)
108
107
 
109
- "#{symbolic_format} = #{evaluated_format}"
110
108
  end
109
+ "#{symbolic_format} = #{evaluated_format}"
111
110
  else
112
111
  # For nested expressions, just show the symbolic form without evaluation details
113
112
  args = expr.args.map { |arg| format_expression(arg, indent_context: 0, nested: true) }
114
- get_display_format(fn_name, args)
113
+ display_format(fn_name, args)
115
114
  end
116
115
  end
117
116
 
118
- def is_chain_of_same_operator?(expr, fn_name)
117
+ def chain_of_same_operator?(expr, fn_name)
119
118
  return false unless %i[add subtract multiply divide].include?(fn_name)
120
119
 
121
120
  # Check if any argument is the same operator
@@ -153,7 +152,7 @@ module Kumi
153
152
  %i[add subtract multiply divide == != > < >= <= and or not].include?(fn_name)
154
153
  end
155
154
 
156
- def get_display_format(fn_name, args)
155
+ def display_format(fn_name, args)
157
156
  case fn_name
158
157
  when :add then args.join(" + ")
159
158
  when :subtract then args.join(" - ")
@@ -84,6 +84,109 @@ module Kumi
84
84
  param_types: [Kumi::Types.array(:any)],
85
85
  return_type: Kumi::Types.array(:any),
86
86
  description: "Remove duplicate elements from collection"
87
+ ),
88
+
89
+ # Array transformation functions
90
+ flatten: FunctionBuilder::Entry.new(
91
+ fn: lambda(&:flatten),
92
+ arity: 1,
93
+ param_types: [Kumi::Types.array(:any)],
94
+ return_type: Kumi::Types.array(:any),
95
+ description: "Flatten nested arrays into a single array"
96
+ ),
97
+
98
+ # Mathematical transformation functions
99
+ map_multiply: FunctionBuilder::Entry.new(
100
+ fn: ->(collection, factor) { collection.map { |x| x * factor } },
101
+ arity: 2,
102
+ param_types: [Kumi::Types.array(:float), :float],
103
+ return_type: Kumi::Types.array(:float),
104
+ description: "Multiply each element by factor"
105
+ ),
106
+
107
+ map_add: FunctionBuilder::Entry.new(
108
+ fn: ->(collection, value) { collection.map { |x| x + value } },
109
+ arity: 2,
110
+ param_types: [Kumi::Types.array(:float), :float],
111
+ return_type: Kumi::Types.array(:float),
112
+ description: "Add value to each element"
113
+ ),
114
+
115
+ # Conditional transformation functions
116
+ map_conditional: FunctionBuilder::Entry.new(
117
+ fn: lambda { |collection, condition_value, true_value, false_value|
118
+ collection.map { |x| x == condition_value ? true_value : false_value }
119
+ },
120
+ arity: 4,
121
+ param_types: %i[array any any any],
122
+ return_type: :array,
123
+ description: "Transform elements based on condition: if element == condition_value then true_value else false_value"
124
+ ),
125
+
126
+ # Range/index functions for grid operations
127
+ build_array: FunctionBuilder::Entry.new(
128
+ fn: lambda { |size, &generator|
129
+ (0...size).map { |i| generator ? generator.call(i) : i }
130
+ },
131
+ arity: 1,
132
+ param_types: [:integer],
133
+ return_type: Kumi::Types.array(:any),
134
+ description: "Build array of given size with index values"
135
+ ),
136
+
137
+ range: FunctionBuilder::Entry.new(
138
+ fn: ->(start, finish) { (start...finish).to_a },
139
+ arity: 2,
140
+ param_types: %i[integer integer],
141
+ return_type: Kumi::Types.array(:integer),
142
+ description: "Generate range of integers from start to finish (exclusive)"
143
+ ),
144
+
145
+ # Array slicing and grouping for rendering
146
+ each_slice: FunctionBuilder::Entry.new(
147
+ fn: ->(array, size) { array.each_slice(size).to_a },
148
+ arity: 2,
149
+ param_types: %i[array integer],
150
+ return_type: Kumi::Types.array(:array),
151
+ description: "Group array elements into subarrays of given size"
152
+ ),
153
+
154
+ join: FunctionBuilder::Entry.new(
155
+ fn: lambda { |array, separator = ""|
156
+ array.map(&:to_s).join(separator.to_s)
157
+ },
158
+ arity: 2,
159
+ param_types: %i[array string],
160
+ return_type: :string,
161
+ description: "Join array elements into string with separator"
162
+ ),
163
+
164
+ # Transform each subarray to string and join the results
165
+ map_join_rows: FunctionBuilder::Entry.new(
166
+ fn: lambda { |array_of_arrays, row_separator = "", column_separator = "\n"|
167
+ array_of_arrays.map { |row| row.join(row_separator.to_s) }.join(column_separator.to_s)
168
+ },
169
+ arity: 3,
170
+ param_types: [Kumi::Types.array(:array), :string, :string],
171
+ return_type: :string,
172
+ description: "Join 2D array into string with row and column separators"
173
+ ),
174
+
175
+ # Higher-order collection functions (limited to common patterns)
176
+ map_with_index: FunctionBuilder::Entry.new(
177
+ fn: ->(collection) { collection.map.with_index.to_a },
178
+ arity: 1,
179
+ param_types: [Kumi::Types.array(:any)],
180
+ return_type: Kumi::Types.array(:any),
181
+ description: "Map collection elements to [element, index] pairs"
182
+ ),
183
+
184
+ indices: FunctionBuilder::Entry.new(
185
+ fn: ->(collection) { (0...collection.size).to_a },
186
+ arity: 1,
187
+ param_types: [Kumi::Types.array(:any)],
188
+ return_type: Kumi::Types.array(:integer),
189
+ description: "Generate array of indices for the collection"
87
190
  )
88
191
  }
89
192
  end
@@ -34,7 +34,7 @@ module Kumi
34
34
  string_include?: FunctionBuilder.string_binary(:include?, "Check if string contains substring", :include?, return_type: :boolean),
35
35
  includes?: FunctionBuilder.string_binary(:include?, "Check if string contains substring", :include?, return_type: :boolean),
36
36
  contains?: FunctionBuilder.string_binary(:include?, "Check if string contains substring", :include?, return_type: :boolean),
37
-
37
+
38
38
  start_with?: FunctionBuilder.string_binary(:start_with?, "Check if string starts with prefix", :start_with?,
39
39
  return_type: :boolean),
40
40
  end_with?: FunctionBuilder.string_binary(:end_with?, "Check if string ends with suffix", :end_with?, return_type: :boolean),
@@ -20,7 +20,8 @@ module Kumi
20
20
  trait_names = args[0..-2]
21
21
  expr = args.last
22
22
 
23
- condition = create_function_call(:all?, trait_names, on_loc)
23
+ trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
24
+ condition = create_fn(:all?, trait_bindings)
24
25
  result = ensure_syntax(expr)
25
26
  add_case(condition, result)
26
27
  end
@@ -32,7 +33,8 @@ module Kumi
32
33
  trait_names = args[0..-2]
33
34
  expr = args.last
34
35
 
35
- condition = create_function_call(:any?, trait_names, on_loc)
36
+ trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
37
+ condition = create_fn(:any?, trait_bindings)
36
38
  result = ensure_syntax(expr)
37
39
  add_case(condition, result)
38
40
  end
@@ -44,7 +46,8 @@ module Kumi
44
46
  trait_names = args[0..-2]
45
47
  expr = args.last
46
48
 
47
- condition = create_function_call(:none?, trait_names, on_loc)
49
+ trait_bindings = convert_trait_names_to_bindings(trait_names, on_loc)
50
+ condition = create_fn(:none?, trait_bindings)
48
51
  result = ensure_syntax(expr)
49
52
  add_case(condition, result)
50
53
  end
@@ -80,9 +83,17 @@ module Kumi
80
83
  raise_error("cascade '#{method_name}' requires an expression as the last argument", location)
81
84
  end
82
85
 
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
+ def convert_trait_names_to_bindings(trait_names, location)
87
+ trait_names.map do |name|
88
+ case name
89
+ when Symbol
90
+ create_binding(name, location)
91
+ when Binding
92
+ name # Already a binding from method_missing
93
+ else
94
+ raise_error("trait reference must be a symbol or bare identifier, got #{name.class}", location)
95
+ end
96
+ end
86
97
  end
87
98
 
88
99
  def add_case(condition, result)
@@ -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::Binding] Reference node
26
37
  def ref(name)
27
- Binding.new(name, loc: @context.current_location)
38
+ validate_reference_name(name)
39
+ Binding.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
+ 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
+ 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
+ Literal.new(value, loc: current_location)
76
+ end
77
+
78
+ def create_list(array)
79
+ elements = array.map { |element| ensure_syntax(element) }
80
+ ListExpression.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
@@ -16,13 +16,13 @@ module Kumi
16
16
  # Check if this is a redefinition by looking at the call stack
17
17
  # We want to allow the original definition but prevent redefinition
18
18
  calling_location = caller_locations(1, 1).first
19
-
19
+
20
20
  # Allow the original definition from schema_builder.rb
21
21
  if calling_location&.path&.include?("schema_builder.rb")
22
22
  super
23
23
  return
24
24
  end
25
-
25
+
26
26
  # This is a redefinition attempt, prevent it
27
27
  raise Kumi::Errors::SemanticError,
28
28
  "DSL keyword `#{name}` is reserved; " \
@@ -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
@@ -73,7 +73,7 @@ module Kumi
73
73
  # Use caller_locations(2, 1) to skip the DSL method and get the actual user code location
74
74
  # Stack: [0] update_location, [1] DSL method (value/trait/etc), [2] user's DSL code
75
75
  caller_location = caller_locations(2, 1).first
76
-
76
+
77
77
  @context.current_location = Location.new(
78
78
  file: caller_location.path,
79
79
  line: caller_location.lineno,
@@ -5,10 +5,22 @@ 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
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
26
  return Literal.new(obj) if LITERAL_TYPES.any? { |type| obj.is_a?(type) }
@@ -22,32 +34,60 @@ module Kumi
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
+ 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, [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, [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, [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, [Syntax::Literal.new(self), other_node])
99
141
  else
100
142
  super(other)
101
143
  end
@@ -103,6 +145,65 @@ 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
+ Syntax::ListExpression.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
106
207
  end
107
208
  end
108
209
  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)