kumi 0.0.0 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +113 -3
  3. data/CHANGELOG.md +21 -1
  4. data/CLAUDE.md +387 -0
  5. data/README.md +257 -20
  6. data/docs/development/README.md +120 -0
  7. data/docs/development/error-reporting.md +361 -0
  8. data/documents/AST.md +126 -0
  9. data/documents/DSL.md +154 -0
  10. data/documents/FUNCTIONS.md +132 -0
  11. data/documents/SYNTAX.md +367 -0
  12. data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +106 -0
  13. data/examples/federal_tax_calculator_2024.rb +112 -0
  14. data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +80 -0
  15. data/lib/generators/trait_engine/templates/schema_spec.rb.erb +27 -0
  16. data/lib/kumi/analyzer/constant_evaluator.rb +51 -0
  17. data/lib/kumi/analyzer/passes/definition_validator.rb +42 -0
  18. data/lib/kumi/analyzer/passes/dependency_resolver.rb +71 -0
  19. data/lib/kumi/analyzer/passes/input_collector.rb +55 -0
  20. data/lib/kumi/analyzer/passes/name_indexer.rb +24 -0
  21. data/lib/kumi/analyzer/passes/pass_base.rb +67 -0
  22. data/lib/kumi/analyzer/passes/toposorter.rb +72 -0
  23. data/lib/kumi/analyzer/passes/type_checker.rb +139 -0
  24. data/lib/kumi/analyzer/passes/type_consistency_checker.rb +45 -0
  25. data/lib/kumi/analyzer/passes/type_inferencer.rb +125 -0
  26. data/lib/kumi/analyzer/passes/unsat_detector.rb +107 -0
  27. data/lib/kumi/analyzer/passes/visitor_pass.rb +41 -0
  28. data/lib/kumi/analyzer.rb +54 -0
  29. data/lib/kumi/atom_unsat_solver.rb +349 -0
  30. data/lib/kumi/compiled_schema.rb +41 -0
  31. data/lib/kumi/compiler.rb +127 -0
  32. data/lib/kumi/domain/enum_analyzer.rb +53 -0
  33. data/lib/kumi/domain/range_analyzer.rb +83 -0
  34. data/lib/kumi/domain/validator.rb +84 -0
  35. data/lib/kumi/domain/violation_formatter.rb +40 -0
  36. data/lib/kumi/domain.rb +8 -0
  37. data/lib/kumi/error_reporter.rb +164 -0
  38. data/lib/kumi/error_reporting.rb +95 -0
  39. data/lib/kumi/errors.rb +116 -0
  40. data/lib/kumi/evaluation_wrapper.rb +20 -0
  41. data/lib/kumi/explain.rb +282 -0
  42. data/lib/kumi/export/deserializer.rb +39 -0
  43. data/lib/kumi/export/errors.rb +12 -0
  44. data/lib/kumi/export/node_builders.rb +140 -0
  45. data/lib/kumi/export/node_registry.rb +38 -0
  46. data/lib/kumi/export/node_serializers.rb +156 -0
  47. data/lib/kumi/export/serializer.rb +23 -0
  48. data/lib/kumi/export.rb +33 -0
  49. data/lib/kumi/function_registry/collection_functions.rb +92 -0
  50. data/lib/kumi/function_registry/comparison_functions.rb +31 -0
  51. data/lib/kumi/function_registry/conditional_functions.rb +36 -0
  52. data/lib/kumi/function_registry/function_builder.rb +92 -0
  53. data/lib/kumi/function_registry/logical_functions.rb +42 -0
  54. data/lib/kumi/function_registry/math_functions.rb +72 -0
  55. data/lib/kumi/function_registry/string_functions.rb +54 -0
  56. data/lib/kumi/function_registry/type_functions.rb +51 -0
  57. data/lib/kumi/function_registry.rb +138 -0
  58. data/lib/kumi/input/type_matcher.rb +92 -0
  59. data/lib/kumi/input/validator.rb +52 -0
  60. data/lib/kumi/input/violation_creator.rb +50 -0
  61. data/lib/kumi/input.rb +8 -0
  62. data/lib/kumi/parser/build_context.rb +25 -0
  63. data/lib/kumi/parser/dsl.rb +12 -0
  64. data/lib/kumi/parser/dsl_cascade_builder.rb +125 -0
  65. data/lib/kumi/parser/expression_converter.rb +58 -0
  66. data/lib/kumi/parser/guard_rails.rb +43 -0
  67. data/lib/kumi/parser/input_builder.rb +94 -0
  68. data/lib/kumi/parser/input_proxy.rb +29 -0
  69. data/lib/kumi/parser/parser.rb +66 -0
  70. data/lib/kumi/parser/schema_builder.rb +172 -0
  71. data/lib/kumi/parser/sugar.rb +108 -0
  72. data/lib/kumi/schema.rb +49 -0
  73. data/lib/kumi/schema_instance.rb +43 -0
  74. data/lib/kumi/syntax/declarations.rb +23 -0
  75. data/lib/kumi/syntax/expressions.rb +30 -0
  76. data/lib/kumi/syntax/node.rb +46 -0
  77. data/lib/kumi/syntax/root.rb +12 -0
  78. data/lib/kumi/syntax/terminal_expressions.rb +27 -0
  79. data/lib/kumi/syntax.rb +9 -0
  80. data/lib/kumi/types/builder.rb +21 -0
  81. data/lib/kumi/types/compatibility.rb +86 -0
  82. data/lib/kumi/types/formatter.rb +24 -0
  83. data/lib/kumi/types/inference.rb +40 -0
  84. data/lib/kumi/types/normalizer.rb +70 -0
  85. data/lib/kumi/types/validator.rb +35 -0
  86. data/lib/kumi/types.rb +64 -0
  87. data/lib/kumi/version.rb +1 -1
  88. data/lib/kumi.rb +7 -3
  89. data/scripts/generate_function_docs.rb +59 -0
  90. data/test_impossible_cascade.rb +51 -0
  91. metadata +93 -10
  92. data/sig/kumi.rbs +0 -4
@@ -0,0 +1,172 @@
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 << Attribute.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
+ Binding.new(name, loc: @context.current_location)
44
+ end
45
+
46
+ def literal(value)
47
+ update_location
48
+ 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
+ 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
+ Binding.new(method_name, loc: @context.current_location)
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+ def respond_to_missing?(_method_name, _include_private = false)
67
+ true
68
+ end
69
+
70
+ private
71
+
72
+ def update_location
73
+ # Use caller_locations(2, 1) to skip the DSL method and get the actual user code location
74
+ # Stack: [0] update_location, [1] DSL method (value/trait/etc), [2] user's DSL code
75
+ caller_location = caller_locations(2, 1).first
76
+
77
+ @context.current_location = Location.new(
78
+ file: caller_location.path,
79
+ line: caller_location.lineno,
80
+ column: 0
81
+ )
82
+ end
83
+
84
+ def validate_value_args(name, expr, blk)
85
+ raise_syntax_error("value requires a name as first argument", location: @context.current_location) if name.nil?
86
+ unless name.is_a?(Symbol)
87
+ raise_syntax_error("The name for 'value' must be a Symbol, got #{name.class}",
88
+ location: @context.current_location)
89
+ end
90
+
91
+ has_expr = !expr.nil?
92
+ has_block = blk
93
+
94
+ if has_expr && has_block
95
+ raise_syntax_error("value '#{name}' cannot be called with both an expression and a block", location: @context.current_location)
96
+ elsif !has_expr && !has_block
97
+ raise_syntax_error("value '#{name}' requires an expression or a block", location: @context.current_location)
98
+ end
99
+ end
100
+
101
+ def build_positional_trait(args)
102
+ case args.size
103
+ when 0
104
+ raise_syntax_error("trait requires a name and expression", location: @context.current_location)
105
+ when 1
106
+ name = args.first
107
+ raise_syntax_error("trait '#{name}' requires an expression", location: @context.current_location)
108
+ when 2
109
+ name, expression = args
110
+ validate_trait_name(name)
111
+ expr = ensure_syntax(expression)
112
+ @context.traits << Trait.new(name, expr, loc: @context.current_location)
113
+ else
114
+ handle_deprecated_trait_syntax(args)
115
+ end
116
+ end
117
+
118
+ def handle_deprecated_trait_syntax(args)
119
+ if args.size == 3
120
+ name, = args
121
+ raise_syntax_error("trait '#{name}' requires exactly 3 arguments: lhs, operator, and rhs", location: @context.current_location)
122
+ end
123
+
124
+ # warn "DEPRECATION: trait(:name, lhs, operator, rhs) syntax is deprecated. Use: trait :name, (lhs operator rhs)"
125
+
126
+ if args.size == 4
127
+ name, lhs, operator, rhs = args
128
+ build_deprecated_trait(name, lhs, operator, [rhs])
129
+ else
130
+ name, lhs, operator, *rhs = args
131
+ build_deprecated_trait(name, lhs, operator, rhs)
132
+ end
133
+ end
134
+
135
+ def build_deprecated_trait(name, lhs, operator, rhs)
136
+ validate_trait_name(name)
137
+ validate_operator(operator)
138
+
139
+ 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)
142
+ end
143
+
144
+ def validate_trait_name(name)
145
+ return if name.is_a?(Symbol)
146
+
147
+ raise_syntax_error("The name for 'trait' must be a Symbol, got #{name.class}", location: @context.current_location)
148
+ end
149
+
150
+ def validate_operator(operator)
151
+ unless operator.is_a?(Symbol)
152
+ raise_syntax_error("expects a symbol for an operator, got #{operator.class}", location: @context.current_location)
153
+ end
154
+
155
+ return if FunctionRegistry.operator?(operator)
156
+
157
+ raise_syntax_error("unsupported operator `#{operator}`", location: @context.current_location)
158
+ end
159
+
160
+ def build_cascade(&blk)
161
+ expression_converter = ExpressionConverter.new(@context)
162
+ cascade_builder = DslCascadeBuilder.new(expression_converter, @context.current_location)
163
+ cascade_builder.instance_eval(&blk)
164
+ CascadeExpression.new(cascade_builder.cases, loc: @context.current_location)
165
+ end
166
+
167
+ def ensure_syntax(obj)
168
+ ExpressionConverter.new(@context).ensure_syntax(obj)
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Parser
5
+ module Sugar
6
+ include Syntax
7
+
8
+ ARITHMETIC_OPS = { :+ => :add, :- => :subtract, :* => :multiply,
9
+ :/ => :divide, :% => :modulo, :** => :power }.freeze
10
+ COMPARISON_OPS = %i[< <= > >= == !=].freeze
11
+ LITERAL_TYPES = [Integer, String, Symbol, TrueClass, FalseClass, Float, Regexp].freeze
12
+
13
+ def self.ensure_literal(obj)
14
+ return Literal.new(obj) if LITERAL_TYPES.any? { |type| obj.is_a?(type) }
15
+ return obj if obj.is_a?(Syntax::Node)
16
+ return obj.to_ast_node if obj.respond_to?(:to_ast_node)
17
+
18
+ Literal.new(obj)
19
+ end
20
+
21
+ def self.syntax_expression?(obj)
22
+ obj.is_a?(Syntax::Node) || obj.respond_to?(:to_ast_node)
23
+ end
24
+
25
+ module ExpressionRefinement
26
+ refine Syntax::Node do
27
+ ARITHMETIC_OPS.each do |op, op_name|
28
+ define_method(op) do |other|
29
+ other_node = Sugar.ensure_literal(other)
30
+ Syntax::CallExpression.new(op_name, [self, other_node])
31
+ end
32
+ end
33
+
34
+ COMPARISON_OPS.each do |op|
35
+ define_method(op) do |other|
36
+ other_node = Sugar.ensure_literal(other)
37
+ Syntax::CallExpression.new(op, [self, other_node])
38
+ end
39
+ end
40
+
41
+ def [](index)
42
+ Syntax::CallExpression.new(:at, [self, Sugar.ensure_literal(index)])
43
+ end
44
+
45
+ def -@
46
+ Syntax::CallExpression.new(:subtract, [Sugar.ensure_literal(0), self])
47
+ end
48
+
49
+ def &(other)
50
+ Syntax::CallExpression.new(:and, [self, Sugar.ensure_literal(other)])
51
+ end
52
+ end
53
+ end
54
+
55
+ module NumericRefinement
56
+ [Integer, Float].each do |klass|
57
+ refine klass do
58
+ ARITHMETIC_OPS.each do |op, op_name|
59
+ define_method(op) do |other|
60
+ 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])
63
+ else
64
+ super(other)
65
+ end
66
+ end
67
+ end
68
+
69
+ COMPARISON_OPS.each do |op|
70
+ define_method(op) do |other|
71
+ 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])
74
+ else
75
+ super(other)
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ module StringRefinement
84
+ refine String do
85
+ def +(other)
86
+ 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])
89
+ else
90
+ super
91
+ end
92
+ end
93
+
94
+ %i[== !=].each do |op|
95
+ define_method(op) do |other|
96
+ 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])
99
+ else
100
+ super(other)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ module Kumi
6
+ module Schema
7
+ Inspector = Struct.new(:syntax_tree, :analyzer_result, :compiled_schema) do
8
+ def inspect
9
+ "#<#{self.class} syntax_tree: #{syntax_tree.inspect}, analyzer_result: #{analyzer_result.inspect}, schema: #{schema.inspect}>"
10
+ end
11
+ end
12
+
13
+ def from(context)
14
+ raise("No schema defined") unless @__compiled_schema__
15
+
16
+ # Validate input types and domain constraints
17
+ input_meta = @__analyzer_result__.state[:input_meta] || {}
18
+ violations = Input::Validator.validate_context(context, input_meta)
19
+
20
+ raise Errors::InputValidationError, violations unless violations.empty?
21
+
22
+ SchemaInstance.new(@__compiled_schema__, @__analyzer_result__.definitions, context)
23
+ end
24
+
25
+ def explain(context, *keys)
26
+ raise("No schema defined") unless @__compiled_schema__
27
+
28
+ # Validate input types and domain constraints
29
+ input_meta = @__analyzer_result__.state[:input_meta] || {}
30
+ violations = Input::Validator.validate_context(context, input_meta)
31
+
32
+ raise Errors::InputValidationError, violations unless violations.empty?
33
+
34
+ keys.each do |key|
35
+ puts Kumi::Explain.call(self, key, inputs: context)
36
+ end
37
+
38
+ nil
39
+ end
40
+
41
+ def schema(&block)
42
+ @__syntax_tree__ = Kumi::Parser::Dsl.build_syntax_tree(&block).freeze
43
+ @__analyzer_result__ = Analyzer.analyze!(@__syntax_tree__).freeze
44
+ @__compiled_schema__ = Compiler.compile(@__syntax_tree__, analyzer: @__analyzer_result__).freeze
45
+
46
+ Inspector.new(@__syntax_tree__, @__analyzer_result__, @__compiled_schema__)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,43 @@
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
+ def initialize(compiled_schema, analysis, context)
15
+ @compiled_schema = compiled_schema # Kumi::CompiledSchema
16
+ @analysis = analysis # Analyzer result (for deps)
17
+ @context = context.is_a?(EvaluationWrapper) ? context : EvaluationWrapper.new(context)
18
+ end
19
+
20
+ # Hash‑like read of one or many bindings
21
+ def evaluate(*key_names)
22
+ if key_names.empty?
23
+ @compiled_schema.evaluate(@context)
24
+ else
25
+ @compiled_schema.evaluate(@context, *key_names)
26
+ end
27
+ end
28
+
29
+ def slice(*key_names)
30
+ return {} if key_names.empty?
31
+
32
+ evaluate(*key_names)
33
+ end
34
+
35
+ def [](key_name)
36
+ evaluate(key_name)[key_name]
37
+ end
38
+
39
+ def input
40
+ @context
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Syntax
5
+ module Declarations
6
+ Attribute = Struct.new(:name, :expression) do
7
+ include Node
8
+ def children = [expression]
9
+ end
10
+
11
+ Trait = Struct.new(:name, :expression) do
12
+ include Node
13
+ def children = [expression]
14
+ end
15
+
16
+ # For field metadata declarations inside input blocks
17
+ FieldDecl = Struct.new(:name, :domain, :type) do
18
+ include Node
19
+ def children = []
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Syntax
5
+ module Expressions
6
+ CallExpression = Struct.new(:fn_name, :args) do
7
+ include Node
8
+ def children = args
9
+ end
10
+ CascadeExpression = Struct.new(:cases) do
11
+ include Node
12
+ def children = cases
13
+ end
14
+
15
+ WhenCaseExpression = Struct.new(:condition, :result) do
16
+ include Node
17
+ def children = [condition, result]
18
+ end
19
+
20
+ ListExpression = Struct.new(:elements) do
21
+ include Node
22
+ def children = elements
23
+
24
+ def size
25
+ elements.size
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Syntax
5
+ # A struct to hold standardized source location information.
6
+ Location = Struct.new(:file, :line, :column, keyword_init: true)
7
+
8
+ # Base module included by all AST nodes to provide a standard
9
+ # interface for accessing source location information..
10
+ module Node
11
+ attr_accessor :loc
12
+
13
+ def initialize(*args, loc: nil, **kwargs)
14
+ @loc = loc
15
+ super(*args, **kwargs)
16
+ freeze
17
+ end
18
+
19
+ def ==(other)
20
+ other.is_a?(self.class) &&
21
+ # for Struct-based nodes
22
+ (if respond_to?(:members)
23
+ members.all? { |m| self[m] == other[m] }
24
+ else
25
+ instance_variables.reject { |iv| iv == :@loc }
26
+ .all? do |iv|
27
+ instance_variable_get(iv) ==
28
+ other.instance_variable_get(iv)
29
+ end
30
+ end
31
+ )
32
+ end
33
+ alias eql? ==
34
+
35
+ def hash
36
+ values = if respond_to?(:members)
37
+ members.map { |m| self[m] }
38
+ else
39
+ instance_variables.reject { |iv| iv == :@loc }
40
+ .map { |iv| instance_variable_get(iv) }
41
+ end
42
+ [self.class, *values].hash
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Syntax
5
+ # Represents the root of the Abstract Syntax Tree.
6
+ # It holds all the top-level declarations parsed from the source.
7
+ Root = Struct.new(:inputs, :attributes, :traits) do
8
+ include Node
9
+ def children = [inputs, attributes, traits]
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "node"
4
+
5
+ module Kumi
6
+ module Syntax
7
+ module TerminalExpressions
8
+ # Leaf expressions that represent a value or reference and terminate a branch.
9
+
10
+ Literal = Struct.new(:value) do
11
+ include Node
12
+ def children = []
13
+ end
14
+
15
+ # For field usage/reference in expressions (input.field_name)
16
+ FieldRef = Struct.new(:name) do
17
+ include Node
18
+ def children = []
19
+ end
20
+
21
+ Binding = Struct.new(:name) do
22
+ include Node
23
+ def children = []
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Syntax
5
+ include Declarations
6
+ include Expressions
7
+ include TerminalExpressions
8
+ end
9
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Types
5
+ # Builds complex type structures
6
+ class Builder
7
+ def self.array(elem_type)
8
+ raise ArgumentError, "Invalid array element type: #{elem_type}" unless Validator.valid_type?(elem_type)
9
+
10
+ { array: elem_type }
11
+ end
12
+
13
+ def self.hash(key_type, val_type)
14
+ raise ArgumentError, "Invalid hash key type: #{key_type}" unless Validator.valid_type?(key_type)
15
+ raise ArgumentError, "Invalid hash value type: #{val_type}" unless Validator.valid_type?(val_type)
16
+
17
+ { hash: [key_type, val_type] }
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Types
5
+ # Handles type compatibility and unification
6
+ class Compatibility
7
+ # Check if two types are compatible
8
+ def self.compatible?(type1, type2)
9
+ # Any type is compatible with anything
10
+ return true if type1 == :any || type2 == :any
11
+
12
+ # Exact match
13
+ return true if type1 == type2
14
+
15
+ # Numeric compatibility
16
+ return true if numeric_compatible?(type1, type2)
17
+
18
+ # Array compatibility
19
+ return array_compatible?(type1, type2) if array_types?(type1, type2)
20
+
21
+ # Hash compatibility
22
+ return hash_compatible?(type1, type2) if hash_types?(type1, type2)
23
+
24
+ false
25
+ end
26
+
27
+ # Find the most specific common type between two types
28
+ def self.unify(type1, type2)
29
+ return type1 if type1 == type2
30
+
31
+ # :any unifies to the other type (more specific)
32
+ return type2 if type1 == :any
33
+ return type1 if type2 == :any
34
+
35
+ # Numeric unification
36
+ if numeric_compatible?(type1, type2)
37
+ return :integer if type1 == :integer && type2 == :integer
38
+
39
+ return :float # One or both are float
40
+ end
41
+
42
+ # Array unification
43
+ if array_types?(type1, type2)
44
+ elem1 = type1[:array]
45
+ elem2 = type2[:array]
46
+ unified_elem = unify(elem1, elem2)
47
+ return Kumi::Types.array(unified_elem)
48
+ end
49
+
50
+ # Hash unification
51
+ if hash_types?(type1, type2)
52
+ key1, val1 = type1[:hash]
53
+ key2, val2 = type2[:hash]
54
+ unified_key = unify(key1, key2)
55
+ unified_val = unify(val1, val2)
56
+ return Kumi::Types.hash(unified_key, unified_val)
57
+ end
58
+
59
+ # Fall back to :any for incompatible types
60
+ :any
61
+ end
62
+
63
+ def self.numeric_compatible?(type1, type2)
64
+ numeric_types = %i[integer float]
65
+ numeric_types.include?(type1) && numeric_types.include?(type2)
66
+ end
67
+
68
+ def self.array_types?(type1, type2)
69
+ Validator.array_type?(type1) && Validator.array_type?(type2)
70
+ end
71
+
72
+ def self.hash_types?(type1, type2)
73
+ Validator.hash_type?(type1) && Validator.hash_type?(type2)
74
+ end
75
+
76
+ def self.array_compatible?(type1, type2)
77
+ compatible?(type1[:array], type2[:array])
78
+ end
79
+
80
+ def self.hash_compatible?(type1, type2)
81
+ compatible?(type1[:hash][0], type2[:hash][0]) &&
82
+ compatible?(type1[:hash][1], type2[:hash][1])
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Types
5
+ # Formats types for display and debugging
6
+ class Formatter
7
+ # Convert types to string representation
8
+ def self.type_to_s(type)
9
+ case type
10
+ when Hash
11
+ if type[:array]
12
+ "array(#{type_to_s(type[:array])})"
13
+ elsif type[:hash]
14
+ "hash(#{type_to_s(type[:hash][0])}, #{type_to_s(type[:hash][1])})"
15
+ else
16
+ type.to_s
17
+ end
18
+ else
19
+ type.to_s
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end