kumi 0.0.6 → 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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +34 -177
  3. data/README.md +41 -7
  4. data/docs/SYNTAX.md +2 -7
  5. data/docs/features/array-broadcasting.md +1 -1
  6. data/docs/schema_metadata/broadcasts.md +53 -0
  7. data/docs/schema_metadata/cascades.md +45 -0
  8. data/docs/schema_metadata/declarations.md +54 -0
  9. data/docs/schema_metadata/dependencies.md +57 -0
  10. data/docs/schema_metadata/evaluation_order.md +29 -0
  11. data/docs/schema_metadata/examples.md +95 -0
  12. data/docs/schema_metadata/inferred_types.md +46 -0
  13. data/docs/schema_metadata/inputs.md +86 -0
  14. data/docs/schema_metadata.md +108 -0
  15. data/examples/game_of_life.rb +1 -1
  16. data/examples/static_analysis_errors.rb +7 -7
  17. data/lib/kumi/analyzer.rb +20 -20
  18. data/lib/kumi/compiler.rb +44 -50
  19. data/lib/kumi/core/analyzer/analysis_state.rb +39 -0
  20. data/lib/kumi/core/analyzer/constant_evaluator.rb +59 -0
  21. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +248 -0
  22. data/lib/kumi/core/analyzer/passes/declaration_validator.rb +45 -0
  23. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +153 -0
  24. data/lib/kumi/core/analyzer/passes/input_collector.rb +139 -0
  25. data/lib/kumi/core/analyzer/passes/name_indexer.rb +26 -0
  26. data/lib/kumi/core/analyzer/passes/pass_base.rb +52 -0
  27. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +111 -0
  28. data/lib/kumi/core/analyzer/passes/toposorter.rb +110 -0
  29. data/lib/kumi/core/analyzer/passes/type_checker.rb +162 -0
  30. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +48 -0
  31. data/lib/kumi/core/analyzer/passes/type_inferencer.rb +236 -0
  32. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +406 -0
  33. data/lib/kumi/core/analyzer/passes/visitor_pass.rb +44 -0
  34. data/lib/kumi/core/atom_unsat_solver.rb +396 -0
  35. data/lib/kumi/core/compiled_schema.rb +43 -0
  36. data/lib/kumi/core/constraint_relationship_solver.rb +641 -0
  37. data/lib/kumi/core/domain/enum_analyzer.rb +55 -0
  38. data/lib/kumi/core/domain/range_analyzer.rb +85 -0
  39. data/lib/kumi/core/domain/validator.rb +82 -0
  40. data/lib/kumi/core/domain/violation_formatter.rb +42 -0
  41. data/lib/kumi/core/error_reporter.rb +166 -0
  42. data/lib/kumi/core/error_reporting.rb +97 -0
  43. data/lib/kumi/core/errors.rb +120 -0
  44. data/lib/kumi/core/evaluation_wrapper.rb +40 -0
  45. data/lib/kumi/core/explain.rb +295 -0
  46. data/lib/kumi/core/export/deserializer.rb +41 -0
  47. data/lib/kumi/core/export/errors.rb +14 -0
  48. data/lib/kumi/core/export/node_builders.rb +142 -0
  49. data/lib/kumi/core/export/node_registry.rb +54 -0
  50. data/lib/kumi/core/export/node_serializers.rb +158 -0
  51. data/lib/kumi/core/export/serializer.rb +25 -0
  52. data/lib/kumi/core/export.rb +35 -0
  53. data/lib/kumi/core/function_registry/collection_functions.rb +202 -0
  54. data/lib/kumi/core/function_registry/comparison_functions.rb +33 -0
  55. data/lib/kumi/core/function_registry/conditional_functions.rb +38 -0
  56. data/lib/kumi/core/function_registry/function_builder.rb +95 -0
  57. data/lib/kumi/core/function_registry/logical_functions.rb +44 -0
  58. data/lib/kumi/core/function_registry/math_functions.rb +74 -0
  59. data/lib/kumi/core/function_registry/string_functions.rb +57 -0
  60. data/lib/kumi/core/function_registry/type_functions.rb +53 -0
  61. data/lib/kumi/{function_registry.rb → core/function_registry.rb} +28 -36
  62. data/lib/kumi/core/input/type_matcher.rb +97 -0
  63. data/lib/kumi/core/input/validator.rb +51 -0
  64. data/lib/kumi/core/input/violation_creator.rb +52 -0
  65. data/lib/kumi/core/json_schema/generator.rb +65 -0
  66. data/lib/kumi/core/json_schema/validator.rb +27 -0
  67. data/lib/kumi/core/json_schema.rb +16 -0
  68. data/lib/kumi/core/ruby_parser/build_context.rb +27 -0
  69. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +38 -0
  70. data/lib/kumi/core/ruby_parser/dsl.rb +14 -0
  71. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +138 -0
  72. data/lib/kumi/core/ruby_parser/expression_converter.rb +128 -0
  73. data/lib/kumi/core/ruby_parser/guard_rails.rb +45 -0
  74. data/lib/kumi/core/ruby_parser/input_builder.rb +127 -0
  75. data/lib/kumi/core/ruby_parser/input_field_proxy.rb +48 -0
  76. data/lib/kumi/core/ruby_parser/input_proxy.rb +31 -0
  77. data/lib/kumi/core/ruby_parser/nested_input.rb +17 -0
  78. data/lib/kumi/core/ruby_parser/parser.rb +71 -0
  79. data/lib/kumi/core/ruby_parser/schema_builder.rb +175 -0
  80. data/lib/kumi/core/ruby_parser/sugar.rb +263 -0
  81. data/lib/kumi/core/ruby_parser.rb +12 -0
  82. data/lib/kumi/core/schema_instance.rb +111 -0
  83. data/lib/kumi/core/types/builder.rb +23 -0
  84. data/lib/kumi/core/types/compatibility.rb +96 -0
  85. data/lib/kumi/core/types/formatter.rb +26 -0
  86. data/lib/kumi/core/types/inference.rb +42 -0
  87. data/lib/kumi/core/types/normalizer.rb +72 -0
  88. data/lib/kumi/core/types/validator.rb +37 -0
  89. data/lib/kumi/core/types.rb +66 -0
  90. data/lib/kumi/core/vectorization_metadata.rb +110 -0
  91. data/lib/kumi/errors.rb +1 -112
  92. data/lib/kumi/registry.rb +37 -0
  93. data/lib/kumi/schema.rb +13 -7
  94. data/lib/kumi/schema_metadata.rb +524 -0
  95. data/lib/kumi/syntax/array_expression.rb +6 -6
  96. data/lib/kumi/syntax/call_expression.rb +4 -4
  97. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  98. data/lib/kumi/syntax/case_expression.rb +4 -4
  99. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  100. data/lib/kumi/syntax/hash_expression.rb +4 -4
  101. data/lib/kumi/syntax/input_declaration.rb +5 -5
  102. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  103. data/lib/kumi/syntax/input_reference.rb +5 -5
  104. data/lib/kumi/syntax/literal.rb +4 -4
  105. data/lib/kumi/syntax/node.rb +34 -34
  106. data/lib/kumi/syntax/root.rb +6 -6
  107. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  108. data/lib/kumi/syntax/value_declaration.rb +4 -4
  109. data/lib/kumi/version.rb +1 -1
  110. data/lib/kumi.rb +14 -0
  111. data/migrate_to_core_iterative.rb +938 -0
  112. data/scripts/generate_function_docs.rb +9 -9
  113. metadata +85 -69
  114. data/lib/generators/trait_engine/templates/schema_spec.rb.erb +0 -27
  115. data/lib/kumi/analyzer/analysis_state.rb +0 -37
  116. data/lib/kumi/analyzer/constant_evaluator.rb +0 -57
  117. data/lib/kumi/analyzer/passes/broadcast_detector.rb +0 -251
  118. data/lib/kumi/analyzer/passes/declaration_validator.rb +0 -43
  119. data/lib/kumi/analyzer/passes/dependency_resolver.rb +0 -151
  120. data/lib/kumi/analyzer/passes/input_collector.rb +0 -137
  121. data/lib/kumi/analyzer/passes/name_indexer.rb +0 -24
  122. data/lib/kumi/analyzer/passes/pass_base.rb +0 -50
  123. data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +0 -110
  124. data/lib/kumi/analyzer/passes/toposorter.rb +0 -108
  125. data/lib/kumi/analyzer/passes/type_checker.rb +0 -162
  126. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +0 -46
  127. data/lib/kumi/analyzer/passes/type_inferencer.rb +0 -232
  128. data/lib/kumi/analyzer/passes/unsat_detector.rb +0 -406
  129. data/lib/kumi/analyzer/passes/visitor_pass.rb +0 -42
  130. data/lib/kumi/atom_unsat_solver.rb +0 -394
  131. data/lib/kumi/compiled_schema.rb +0 -41
  132. data/lib/kumi/constraint_relationship_solver.rb +0 -638
  133. data/lib/kumi/domain/enum_analyzer.rb +0 -53
  134. data/lib/kumi/domain/range_analyzer.rb +0 -83
  135. data/lib/kumi/domain/validator.rb +0 -80
  136. data/lib/kumi/domain/violation_formatter.rb +0 -40
  137. data/lib/kumi/error_reporter.rb +0 -164
  138. data/lib/kumi/error_reporting.rb +0 -95
  139. data/lib/kumi/evaluation_wrapper.rb +0 -38
  140. data/lib/kumi/explain.rb +0 -281
  141. data/lib/kumi/export/deserializer.rb +0 -39
  142. data/lib/kumi/export/errors.rb +0 -12
  143. data/lib/kumi/export/node_builders.rb +0 -140
  144. data/lib/kumi/export/node_registry.rb +0 -52
  145. data/lib/kumi/export/node_serializers.rb +0 -156
  146. data/lib/kumi/export/serializer.rb +0 -23
  147. data/lib/kumi/export.rb +0 -33
  148. data/lib/kumi/function_registry/collection_functions.rb +0 -200
  149. data/lib/kumi/function_registry/comparison_functions.rb +0 -31
  150. data/lib/kumi/function_registry/conditional_functions.rb +0 -36
  151. data/lib/kumi/function_registry/function_builder.rb +0 -93
  152. data/lib/kumi/function_registry/logical_functions.rb +0 -42
  153. data/lib/kumi/function_registry/math_functions.rb +0 -72
  154. data/lib/kumi/function_registry/string_functions.rb +0 -54
  155. data/lib/kumi/function_registry/type_functions.rb +0 -51
  156. data/lib/kumi/input/type_matcher.rb +0 -95
  157. data/lib/kumi/input/validator.rb +0 -49
  158. data/lib/kumi/input/violation_creator.rb +0 -50
  159. data/lib/kumi/parser/build_context.rb +0 -25
  160. data/lib/kumi/parser/declaration_reference_proxy.rb +0 -36
  161. data/lib/kumi/parser/dsl.rb +0 -12
  162. data/lib/kumi/parser/dsl_cascade_builder.rb +0 -136
  163. data/lib/kumi/parser/expression_converter.rb +0 -126
  164. data/lib/kumi/parser/guard_rails.rb +0 -43
  165. data/lib/kumi/parser/input_builder.rb +0 -125
  166. data/lib/kumi/parser/input_field_proxy.rb +0 -46
  167. data/lib/kumi/parser/input_proxy.rb +0 -29
  168. data/lib/kumi/parser/nested_input.rb +0 -15
  169. data/lib/kumi/parser/parser.rb +0 -68
  170. data/lib/kumi/parser/schema_builder.rb +0 -173
  171. data/lib/kumi/parser/sugar.rb +0 -261
  172. data/lib/kumi/schema_instance.rb +0 -109
  173. data/lib/kumi/types/builder.rb +0 -21
  174. data/lib/kumi/types/compatibility.rb +0 -94
  175. data/lib/kumi/types/formatter.rb +0 -24
  176. data/lib/kumi/types/inference.rb +0 -40
  177. data/lib/kumi/types/normalizer.rb +0 -70
  178. data/lib/kumi/types/validator.rb +0 -35
  179. data/lib/kumi/types.rb +0 -64
  180. data/lib/kumi/vectorization_metadata.rb +0 -108
@@ -1,46 +0,0 @@
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
@@ -1,29 +0,0 @@
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 InputFieldProxy that can handle further field access
17
- InputFieldProxy.new(method_name, @context)
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 an InputReference 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
@@ -1,15 +0,0 @@
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
@@ -1,68 +0,0 @@
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
- rule_block.binding.eval("using Kumi::Parser::Sugar::ArrayRefinement")
40
- rule_block.binding.eval("using Kumi::Parser::Sugar::ModuleRefinement")
41
- rescue RuntimeError, NoMethodError
42
- # Refinements disabled in method scope - continue without them
43
- end
44
-
45
- def handle_parse_error(error)
46
- return unless literal_comparison_error?(error)
47
-
48
- warn <<~HINT
49
- #{error.backtrace.first.split(':', 2).join(':')}: \
50
- Literal‑left comparison failed because the schema block is \
51
- defined inside a method (Ruby disallows refinements there).
52
-
53
- • Move the `schema do … end` block to the top level of a class or module, OR
54
- • Write the comparison as `input.age >= 80` (preferred), OR
55
- • Wrap the literal: `lit(80) <= input.age`.
56
- HINT
57
- end
58
-
59
- def literal_comparison_error?(error)
60
- error.message =~ /comparison of Integer with Kumi::Syntax::/i
61
- end
62
-
63
- def build_syntax_tree
64
- Root.new(@context.inputs, @context.attributes, @context.traits)
65
- end
66
- end
67
- end
68
- end
@@ -1,173 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kumi
4
- module Parser
5
- class SchemaBuilder
6
- include GuardRails
7
- include Syntax
8
- include ErrorReporting
9
-
10
- DSL_METHODS = %i[value trait input ref literal fn].freeze
11
-
12
- def initialize(context)
13
- @context = context
14
- end
15
-
16
- def value(name = nil, expr = nil, &blk)
17
- update_location
18
- validate_value_args(name, expr, blk)
19
-
20
- expression = blk ? build_cascade(&blk) : ensure_syntax(expr)
21
- @context.attributes << Kumi::Syntax::ValueDeclaration.new(name, expression, loc: @context.current_location)
22
- end
23
-
24
- def trait(*args, **kwargs)
25
- update_location
26
- raise_syntax_error("keyword trait syntax not supported", location: @context.current_location) unless kwargs.empty?
27
- build_positional_trait(args)
28
- end
29
-
30
- def input(&blk)
31
- return InputProxy.new(@context) unless block_given?
32
-
33
- raise_syntax_error("input block already defined", location: @context.current_location) if @context.input_block_defined?
34
- @context.mark_input_block_defined!
35
-
36
- update_location
37
- input_builder = InputBuilder.new(@context)
38
- input_builder.instance_eval(&blk)
39
- end
40
-
41
- def ref(name)
42
- update_location
43
- Kumi::Syntax::DeclarationReference.new(name, loc: @context.current_location)
44
- end
45
-
46
- def literal(value)
47
- update_location
48
- Kumi::Syntax::Literal.new(value, loc: @context.current_location)
49
- end
50
-
51
- def fn(fn_name, *args)
52
- update_location
53
- expr_args = args.map { |a| ensure_syntax(a) }
54
- Kumi::Syntax::CallExpression.new(fn_name, expr_args, loc: @context.current_location)
55
- end
56
-
57
- def method_missing(method_name, *args, &block)
58
- if args.empty? && !block_given?
59
- update_location
60
- # Create proxy for declaration references (traits/values)
61
- DeclarationReferenceProxy.new(method_name, @context)
62
- else
63
- super
64
- end
65
- end
66
-
67
- def respond_to_missing?(_method_name, _include_private = false)
68
- true
69
- end
70
-
71
- private
72
-
73
- def update_location
74
- # Use caller_locations(2, 1) to skip the DSL method and get the actual user code location
75
- # Stack: [0] update_location, [1] DSL method (value/trait/etc), [2] user's DSL code
76
- caller_location = caller_locations(2, 1).first
77
-
78
- @context.current_location = Location.new(
79
- file: caller_location.path,
80
- line: caller_location.lineno,
81
- column: 0
82
- )
83
- end
84
-
85
- def validate_value_args(name, expr, blk)
86
- raise_syntax_error("value requires a name as first argument", location: @context.current_location) if name.nil?
87
- unless name.is_a?(Symbol)
88
- raise_syntax_error("The name for 'value' must be a Symbol, got #{name.class}",
89
- location: @context.current_location)
90
- end
91
-
92
- has_expr = !expr.nil?
93
- has_block = blk
94
-
95
- if has_expr && has_block
96
- raise_syntax_error("value '#{name}' cannot be called with both an expression and a block", location: @context.current_location)
97
- elsif !has_expr && !has_block
98
- raise_syntax_error("value '#{name}' requires an expression or a block", location: @context.current_location)
99
- end
100
- end
101
-
102
- def build_positional_trait(args)
103
- case args.size
104
- when 0
105
- raise_syntax_error("trait requires a name and expression", location: @context.current_location)
106
- when 1
107
- name = args.first
108
- raise_syntax_error("trait '#{name}' requires an expression", location: @context.current_location)
109
- when 2
110
- name, expression = args
111
- validate_trait_name(name)
112
- expr = ensure_syntax(expression)
113
- @context.traits << Kumi::Syntax::TraitDeclaration.new(name, expr, loc: @context.current_location)
114
- else
115
- handle_deprecated_trait_syntax(args)
116
- end
117
- end
118
-
119
- def handle_deprecated_trait_syntax(args)
120
- if args.size == 3
121
- name, = args
122
- raise_syntax_error("trait '#{name}' requires exactly 3 arguments: lhs, operator, and rhs", location: @context.current_location)
123
- end
124
-
125
- # warn "DEPRECATION: trait(:name, lhs, operator, rhs) syntax is deprecated. Use: trait :name, (lhs operator rhs)"
126
-
127
- if args.size == 4
128
- name, lhs, operator, rhs = args
129
- build_deprecated_trait(name, lhs, operator, [rhs])
130
- else
131
- name, lhs, operator, *rhs = args
132
- build_deprecated_trait(name, lhs, operator, rhs)
133
- end
134
- end
135
-
136
- def build_deprecated_trait(name, lhs, operator, rhs)
137
- validate_trait_name(name)
138
- validate_operator(operator)
139
-
140
- rhs_exprs = rhs.map { |r| ensure_syntax(r) }
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)
143
- end
144
-
145
- def validate_trait_name(name)
146
- return if name.is_a?(Symbol)
147
-
148
- raise_syntax_error("The name for 'trait' must be a Symbol, got #{name.class}", location: @context.current_location)
149
- end
150
-
151
- def validate_operator(operator)
152
- unless operator.is_a?(Symbol)
153
- raise_syntax_error("expects a symbol for an operator, got #{operator.class}", location: @context.current_location)
154
- end
155
-
156
- return if FunctionRegistry.operator?(operator)
157
-
158
- raise_syntax_error("unsupported operator `#{operator}`", location: @context.current_location)
159
- end
160
-
161
- def build_cascade(&blk)
162
- expression_converter = ExpressionConverter.new(@context)
163
- cascade_builder = DslCascadeBuilder.new(expression_converter, @context.current_location)
164
- cascade_builder.instance_eval(&blk)
165
- Kumi::Syntax::CascadeExpression.new(cascade_builder.cases, loc: @context.current_location)
166
- end
167
-
168
- def ensure_syntax(obj)
169
- ExpressionConverter.new(@context).ensure_syntax(obj)
170
- end
171
- end
172
- end
173
- end
@@ -1,261 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kumi
4
- module Parser
5
- module Sugar
6
- include Syntax
7
-
8
- ARITHMETIC_OPS = {
9
- :+ => :add, :- => :subtract, :* => :multiply,
10
- :/ => :divide, :% => :modulo, :** => :power
11
- }.freeze
12
-
13
- COMPARISON_OPS = %i[< <= > >= == !=].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
24
-
25
- def self.ensure_literal(obj)
26
- return Kumi::Syntax::Literal.new(obj) if LITERAL_TYPES.any? { |type| obj.is_a?(type) }
27
- return obj if obj.is_a?(Syntax::Node)
28
- return obj.to_ast_node if obj.respond_to?(:to_ast_node)
29
-
30
- Kumi::Syntax::Literal.new(obj)
31
- end
32
-
33
- def self.syntax_expression?(obj)
34
- obj.is_a?(Syntax::Node) || obj.respond_to?(:to_ast_node)
35
- end
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
-
42
- module ExpressionRefinement
43
- refine Syntax::Node do
44
- # Arithmetic operations
45
- ARITHMETIC_OPS.each do |op, op_name|
46
- define_method(op) do |other|
47
- other_node = Sugar.ensure_literal(other)
48
- Sugar.create_call_expression(op_name, [self, other_node])
49
- end
50
- end
51
-
52
- # Comparison operations
53
- COMPARISON_OPS.each do |op|
54
- define_method(op) do |other|
55
- other_node = Sugar.ensure_literal(other)
56
- Sugar.create_call_expression(op, [self, other_node])
57
- end
58
- end
59
-
60
- # Array access
61
- def [](index)
62
- Sugar.create_call_expression(:at, [self, Sugar.ensure_literal(index)])
63
- end
64
-
65
- # Unary minus
66
- def -@
67
- Sugar.create_call_expression(:subtract, [Sugar.ensure_literal(0), self])
68
- end
69
-
70
- # Logical operations
71
- def &(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)])
91
- end
92
- end
93
- end
94
-
95
- module NumericRefinement
96
- [Integer, Float].each do |klass|
97
- refine klass do
98
- # Arithmetic operations with syntax expressions
99
- ARITHMETIC_OPS.each do |op, op_name|
100
- define_method(op) do |other|
101
- if Sugar.syntax_expression?(other)
102
- other_node = Sugar.ensure_literal(other)
103
- Sugar.create_call_expression(op_name, [Kumi::Syntax::Literal.new(self), other_node])
104
- else
105
- super(other)
106
- end
107
- end
108
- end
109
-
110
- # Comparison operations with syntax expressions
111
- COMPARISON_OPS.each do |op|
112
- define_method(op) do |other|
113
- if Sugar.syntax_expression?(other)
114
- other_node = Sugar.ensure_literal(other)
115
- Sugar.create_call_expression(op, [Kumi::Syntax::Literal.new(self), other_node])
116
- else
117
- super(other)
118
- end
119
- end
120
- end
121
- end
122
- end
123
- end
124
-
125
- module StringRefinement
126
- refine String do
127
- def +(other)
128
- if Sugar.syntax_expression?(other)
129
- other_node = Sugar.ensure_literal(other)
130
- Sugar.create_call_expression(:concat, [Kumi::Syntax::Literal.new(self), other_node])
131
- else
132
- super
133
- end
134
- end
135
-
136
- %i[== !=].each do |op|
137
- define_method(op) do |other|
138
- if Sugar.syntax_expression?(other)
139
- other_node = Sugar.ensure_literal(other)
140
- Sugar.create_call_expression(op, [Kumi::Syntax::Literal.new(self), other_node])
141
- else
142
- super(other)
143
- end
144
- end
145
- end
146
- end
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
259
- end
260
- end
261
- end
@@ -1,109 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Kumi
4
- # A bound pair of <compiled schema + context>. Immutable.
5
- #
6
- # Public API ----------------------------------------------------------
7
- # instance.evaluate # => full Hash of all bindings
8
- # instance.evaluate(:tax_due, :rate)
9
- # instance.slice(:tax_due) # alias for evaluate(*keys)
10
- # instance.explain(:tax_due) # pretty trace string
11
- # instance.input # original context (read‑only)
12
-
13
- class SchemaInstance
14
- attr_reader :compiled_schema, :analysis, :context
15
-
16
- def initialize(compiled_schema, analysis, context)
17
- @compiled_schema = compiled_schema # Kumi::CompiledSchema
18
- @analysis = analysis # Analyzer result (for deps)
19
- @context = context.is_a?(EvaluationWrapper) ? context : EvaluationWrapper.new(context)
20
- end
21
-
22
- # Hash‑like read of one or many bindings
23
- def evaluate(*key_names)
24
- if key_names.empty?
25
- @compiled_schema.evaluate(@context)
26
- else
27
- @compiled_schema.evaluate(@context, *key_names)
28
- end
29
- end
30
-
31
- def slice(*key_names)
32
- return {} if key_names.empty?
33
-
34
- evaluate(*key_names)
35
- end
36
-
37
- def [](key_name)
38
- evaluate(key_name)[key_name]
39
- end
40
-
41
- # Update input values and clear affected cached computations
42
- def update(**changes)
43
- changes.each do |field, value|
44
- # Validate field exists
45
- raise ArgumentError, "unknown input field: #{field}" unless input_field_exists?(field)
46
-
47
- # Validate domain constraints
48
- validate_domain_constraint(field, value)
49
-
50
- # Update the input data
51
- @context[field] = value
52
-
53
- # Clear affected cached values using transitive closure by default
54
- if ENV["KUMI_SIMPLE_CACHE"] == "true"
55
- # Simple fallback: clear all cached values
56
- @context.clear_cache
57
- else
58
- # Default: selective cache clearing using precomputed transitive closure
59
- affected_keys = find_dependent_declarations_optimized(field)
60
- affected_keys.each { |key| @context.clear_cache(key) }
61
- end
62
- end
63
-
64
- self # Return self for chaining
65
- end
66
-
67
- private
68
-
69
- def input_field_exists?(field)
70
- # Check if field is declared in input block
71
- input_meta = @analysis&.state&.dig(:input_meta) || {}
72
- input_meta.key?(field) || @context.key?(field)
73
- end
74
-
75
- def validate_domain_constraint(field, value)
76
- input_meta = @analysis&.state&.dig(:input_meta) || {}
77
- field_meta = input_meta[field]
78
- return unless field_meta&.dig(:domain)
79
-
80
- domain = field_meta[:domain]
81
- return unless violates_domain?(value, domain)
82
-
83
- raise ArgumentError, "value #{value} is not in domain #{domain}"
84
- end
85
-
86
- def violates_domain?(value, domain)
87
- case domain
88
- when Range
89
- !domain.include?(value)
90
- when Array
91
- !domain.include?(value)
92
- when Proc
93
- # For Proc domains, we can't statically analyze
94
- false
95
- else
96
- false
97
- end
98
- end
99
-
100
- def find_dependent_declarations_optimized(field)
101
- # Use precomputed transitive closure for true O(1) lookup!
102
- transitive_dependents = @analysis&.state&.dig(:transitive_dependents)
103
- return [] unless transitive_dependents
104
-
105
- # This is truly O(1) - just array lookup, no traversal needed
106
- transitive_dependents[field] || []
107
- end
108
- end
109
- end