kumi 0.0.9 → 0.0.11

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 (103) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +18 -0
  3. data/CLAUDE.md +18 -258
  4. data/README.md +188 -121
  5. data/docs/AST.md +1 -1
  6. data/docs/FUNCTIONS.md +52 -8
  7. data/docs/VECTOR_SEMANTICS.md +286 -0
  8. data/docs/compiler_design_principles.md +86 -0
  9. data/docs/features/README.md +15 -2
  10. data/docs/features/hierarchical-broadcasting.md +349 -0
  11. data/docs/features/javascript-transpiler.md +148 -0
  12. data/docs/features/performance.md +1 -3
  13. data/docs/features/s-expression-printer.md +2 -2
  14. data/docs/schema_metadata.md +7 -7
  15. data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
  16. data/examples/game_of_life.rb +2 -4
  17. data/lib/kumi/analyzer.rb +34 -14
  18. data/lib/kumi/compiler.rb +4 -283
  19. data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +717 -66
  20. data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
  21. data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
  22. data/lib/kumi/core/analyzer/passes/input_collector.rb +118 -99
  23. data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
  24. data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
  25. data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
  26. data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
  27. data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +28 -0
  28. data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
  29. data/lib/kumi/core/analyzer/passes/type_checker.rb +9 -5
  30. data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
  31. data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
  32. data/lib/kumi/core/analyzer/passes/unsat_detector.rb +92 -48
  33. data/lib/kumi/core/analyzer/plans.rb +52 -0
  34. data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
  35. data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
  36. data/lib/kumi/core/compiler/access_builder.rb +36 -0
  37. data/lib/kumi/core/compiler/access_planner.rb +219 -0
  38. data/lib/kumi/core/compiler/accessors/base.rb +69 -0
  39. data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
  40. data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
  41. data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
  42. data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
  43. data/lib/kumi/core/compiler_base.rb +137 -0
  44. data/lib/kumi/core/error_reporter.rb +6 -5
  45. data/lib/kumi/core/errors.rb +4 -0
  46. data/lib/kumi/core/explain.rb +157 -205
  47. data/lib/kumi/core/export/node_builders.rb +2 -2
  48. data/lib/kumi/core/export/node_serializers.rb +1 -1
  49. data/lib/kumi/core/function_registry/collection_functions.rb +100 -6
  50. data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
  51. data/lib/kumi/core/function_registry/function_builder.rb +142 -53
  52. data/lib/kumi/core/function_registry/logical_functions.rb +173 -3
  53. data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
  54. data/lib/kumi/core/function_registry.rb +138 -98
  55. data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
  56. data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
  57. data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
  58. data/lib/kumi/core/ir/execution_engine.rb +50 -0
  59. data/lib/kumi/core/ir.rb +58 -0
  60. data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
  61. data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
  62. data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +37 -16
  63. data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
  64. data/lib/kumi/core/ruby_parser/parser.rb +1 -1
  65. data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
  66. data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
  67. data/lib/kumi/errors.rb +2 -0
  68. data/lib/kumi/js.rb +23 -0
  69. data/lib/kumi/registry.rb +17 -22
  70. data/lib/kumi/runtime/executable.rb +213 -0
  71. data/lib/kumi/schema.rb +15 -4
  72. data/lib/kumi/schema_metadata.rb +2 -2
  73. data/lib/kumi/support/ir_dump.rb +491 -0
  74. data/lib/kumi/support/s_expression_printer.rb +17 -16
  75. data/lib/kumi/syntax/array_expression.rb +6 -6
  76. data/lib/kumi/syntax/call_expression.rb +4 -4
  77. data/lib/kumi/syntax/cascade_expression.rb +4 -4
  78. data/lib/kumi/syntax/case_expression.rb +4 -4
  79. data/lib/kumi/syntax/declaration_reference.rb +4 -4
  80. data/lib/kumi/syntax/hash_expression.rb +4 -4
  81. data/lib/kumi/syntax/input_declaration.rb +6 -5
  82. data/lib/kumi/syntax/input_element_reference.rb +5 -5
  83. data/lib/kumi/syntax/input_reference.rb +5 -5
  84. data/lib/kumi/syntax/literal.rb +4 -4
  85. data/lib/kumi/syntax/location.rb +5 -0
  86. data/lib/kumi/syntax/node.rb +33 -34
  87. data/lib/kumi/syntax/root.rb +6 -6
  88. data/lib/kumi/syntax/trait_declaration.rb +4 -4
  89. data/lib/kumi/syntax/value_declaration.rb +4 -4
  90. data/lib/kumi/version.rb +1 -1
  91. data/lib/kumi.rb +6 -15
  92. data/scripts/analyze_broadcast_methods.rb +68 -0
  93. data/scripts/analyze_cascade_methods.rb +74 -0
  94. data/scripts/check_broadcasting_coverage.rb +51 -0
  95. data/scripts/find_dead_code.rb +114 -0
  96. metadata +36 -9
  97. data/docs/features/array-broadcasting.md +0 -170
  98. data/lib/kumi/cli.rb +0 -449
  99. data/lib/kumi/core/compiled_schema.rb +0 -43
  100. data/lib/kumi/core/evaluation_wrapper.rb +0 -40
  101. data/lib/kumi/core/schema_instance.rb +0 -111
  102. data/lib/kumi/core/vectorization_metadata.rb +0 -110
  103. data/migrate_to_core_iterative.rb +0 -938
@@ -13,13 +13,13 @@ module Kumi
13
13
 
14
14
  def key(name, type: :any, domain: nil)
15
15
  normalized_type = normalize_type(type, name)
16
- @context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, normalized_type, [], loc: @context.current_location)
16
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, normalized_type, [], nil, loc: @context.current_location)
17
17
  end
18
18
 
19
19
  %i[integer float string boolean any scalar].each do |type_name|
20
20
  define_method(type_name) do |name, type: nil, domain: nil|
21
21
  actual_type = type || (type_name == :scalar ? :any : type_name)
22
- @context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, actual_type, [], loc: @context.current_location)
22
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(name, domain, actual_type, [], nil, loc: @context.current_location)
23
23
  end
24
24
  end
25
25
 
@@ -40,7 +40,7 @@ module Kumi
40
40
  end
41
41
 
42
42
  def method_missing(method_name, *_args)
43
- allowed_methods = "'key', 'integer', 'float', 'string', 'boolean', 'any', 'scalar', 'array', and 'hash'"
43
+ allowed_methods = "'key', 'integer', 'float', 'string', 'boolean', 'any', 'scalar', 'array', 'hash', and 'element'"
44
44
  raise_syntax_error("Unknown method '#{method_name}' in input block. Only #{allowed_methods} are allowed.",
45
45
  location: @context.current_location)
46
46
  end
@@ -63,7 +63,7 @@ module Kumi
63
63
  elem_type = elem_spec.is_a?(Hash) && elem_spec[:type] ? elem_spec[:type] : :any
64
64
 
65
65
  array_type = create_array_type(field_name, elem_type)
66
- @context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, array_type, [], loc: @context.current_location)
66
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, array_type, [], :field, loc: @context.current_location)
67
67
  end
68
68
 
69
69
  def create_array_type(field_name, elem_type)
@@ -81,7 +81,7 @@ module Kumi
81
81
  val_type = extract_type(val_spec)
82
82
 
83
83
  hash_type = create_hash_type(field_name, key_type, val_type)
84
- @context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, hash_type, [], loc: @context.current_location)
84
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, hash_type, [], nil, loc: @context.current_location)
85
85
  end
86
86
 
87
87
  def extract_type(spec)
@@ -98,14 +98,16 @@ module Kumi
98
98
  domain = options[:domain]
99
99
 
100
100
  # Collect children by creating a nested context
101
- children = collect_array_children(&block)
101
+ children, _, using_elements = collect_array_children(&block)
102
102
 
103
- # Create the InputDeclaration with children
103
+ # Create the InputDeclaration with children and access_mode
104
+ access_mode = using_elements ? :element : :field
104
105
  @context.inputs << Kumi::Syntax::InputDeclaration.new(
105
106
  field_name,
106
107
  domain,
107
108
  :array,
108
109
  children,
110
+ access_mode,
109
111
  loc: @context.current_location
110
112
  )
111
113
  end
@@ -119,7 +121,58 @@ module Kumi
119
121
  # Execute the block in the nested context
120
122
  nested_builder.instance_eval(&block)
121
123
 
122
- nested_inputs
124
+ # Determine element type based on what was declared
125
+ elem_type = determine_element_type(nested_builder, nested_inputs)
126
+
127
+ # Check if element() was used
128
+ using_elements = nested_builder.instance_variable_get(:@using_elements) || false
129
+
130
+ [nested_inputs, elem_type, using_elements]
131
+ end
132
+
133
+ def determine_element_type(_builder, inputs)
134
+ # Since element() always creates named children now,
135
+ # we just use the standard logic
136
+ if inputs.any?
137
+ # If fields were declared, it's a hash/object structure
138
+ :hash
139
+ else
140
+ # No fields declared, default to :any
141
+ :any
142
+ end
143
+ end
144
+
145
+ def primitive_element_type?(elem_type)
146
+ %i[string integer float boolean bool any symbol].include?(elem_type)
147
+ end
148
+
149
+ # New method: element() declaration - always requires a name
150
+ def element(type_spec, name, &block)
151
+ @using_elements = true
152
+ if block_given?
153
+ # Named element with nested structure: element(:array, :rows) do ... end
154
+ # These DO set @using_elements to enable element access mode for multi-dimensional arrays
155
+ case type_spec
156
+ when :array
157
+ create_array_field_with_block(name, {}, &block)
158
+ when :field
159
+ # Create nested object structure
160
+ create_object_element(name, &block)
161
+ else
162
+ raise_syntax_error("element(#{type_spec.inspect}, #{name.inspect}) with block only supports :array or :field types",
163
+ location: @context.current_location)
164
+ end
165
+ else
166
+ # Named primitive element: element(:boolean, :active)
167
+ # Only primitive elements mark the parent as using element access
168
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(name, nil, type_spec, [], nil, loc: @context.current_location)
169
+ end
170
+ end
171
+
172
+ def create_object_element(name, &block)
173
+ # Similar to create_array_field_with_block but for objects
174
+ children, = collect_array_children(&block)
175
+ @context.inputs << Kumi::Syntax::InputDeclaration.new(name, nil, :field, children, nil, loc: @context.current_location)
123
176
  end
124
177
  end
125
178
  end
@@ -45,7 +45,7 @@ module Kumi
45
45
  end
46
46
 
47
47
  def build_syntax_tree
48
- Root.new(@context.inputs, @context.attributes, @context.traits)
48
+ Root.new(@context.inputs, @context.values, @context.traits)
49
49
  end
50
50
 
51
51
  def handle_parse_error(error)
@@ -19,7 +19,7 @@ module Kumi
19
19
  validate_value_args(name, expr, blk)
20
20
 
21
21
  expression = blk ? build_cascade(&blk) : ensure_syntax(expr)
22
- @context.attributes << Kumi::Syntax::ValueDeclaration.new(name, expression, loc: @context.current_location)
22
+ @context.values << Kumi::Syntax::ValueDeclaration.new(name, expression, loc: @context.current_location)
23
23
  end
24
24
 
25
25
  def trait(*args, **kwargs)
@@ -90,7 +90,7 @@ module Kumi
90
90
  location: @context.current_location)
91
91
  end
92
92
 
93
- has_expr = !expr.nil?
93
+ has_expr = !expr.is_a?(NilClass)
94
94
  has_block = blk
95
95
 
96
96
  if has_expr && has_block
@@ -254,6 +254,13 @@ module Kumi
254
254
  ast_node = to_ast_node
255
255
  Sugar.create_call_expression(:subtract, [Sugar.ensure_literal(0), ast_node])
256
256
  end
257
+
258
+ # Override Ruby's built-in nil? method to transform into == nil
259
+ define_method(:nil?) do
260
+ ast_node = to_ast_node
261
+ nil_literal = Kumi::Syntax::Literal.new(nil)
262
+ Sugar.create_call_expression(:==, [ast_node, nil_literal])
263
+ end
257
264
  end
258
265
  end
259
266
  end
data/lib/kumi/errors.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kumi
2
4
  module Errors
3
5
  include Core::Errors
data/lib/kumi/js.rb ADDED
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Js
5
+ # JavaScript transpiler for Kumi schemas
6
+ # Extends the existing compiler architecture to output JavaScript instead of Ruby lambdas
7
+
8
+ # Export a compiled schema to JavaScript
9
+ def self.compile(schema_class, **options)
10
+ syntax_tree = schema_class.__syntax_tree__
11
+ analyzer_result = schema_class.__analyzer_result__
12
+
13
+ compiler = Compiler.new(syntax_tree, analyzer_result)
14
+ compiler.compile(**options)
15
+ end
16
+
17
+ # Export to JavaScript file
18
+ def self.export_to_file(schema_class, filename, **options)
19
+ js_code = compile(schema_class, **options)
20
+ File.write(filename, js_code)
21
+ end
22
+ end
23
+ end
data/lib/kumi/registry.rb CHANGED
@@ -1,36 +1,31 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Kumi
4
+ # Public facade for the function registry.
5
+ # Delegates to Kumi::Core::FunctionRegistry.
2
6
  module Registry
3
- extend Core::FunctionRegistry
4
- Entry = Core::FunctionRegistry::FunctionBuilder::Entry
5
-
6
- @functions = Core::FunctionRegistry::CORE_FUNCTIONS.transform_values(&:dup)
7
- @frozen = false
8
- @lock = Mutex.new
9
-
10
- class FrozenError < RuntimeError; end
7
+ Entry = Core::FunctionRegistry::FunctionBuilder::Entry
8
+ FrozenError = Core::FunctionRegistry::FrozenError
11
9
 
12
10
  class << self
13
- def reset!
14
- @lock.synchronize do
15
- @functions = Core::FunctionRegistry::CORE_FUNCTIONS.transform_values(&:dup)
16
- @frozen = false
17
- end
11
+ def auto_register(*mods)
12
+ Core::FunctionRegistry.auto_register(*mods)
18
13
  end
19
14
 
20
- def register(name, &block)
21
- @lock.synchronize do
22
- raise FrozenError, "registry is frozen" if @frozen
23
-
15
+ def method_missing(name, ...)
16
+ if Core::FunctionRegistry.respond_to?(name)
17
+ Core::FunctionRegistry.public_send(name, ...)
18
+ else
24
19
  super
25
20
  end
26
21
  end
27
22
 
23
+ def respond_to_missing?(name, include_private = false)
24
+ Core::FunctionRegistry.respond_to?(name, include_private) || super
25
+ end
26
+
28
27
  def freeze!
29
- @lock.synchronize do
30
- @functions.each_value(&:freeze)
31
- @functions.freeze
32
- @frozen = true
33
- end
28
+ Core::FunctionRegistry.freeze!
34
29
  end
35
30
  end
36
31
  end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kumi
4
+ module Runtime
5
+ # Executable / Reader: evaluation interface for compiled schemas
6
+ #
7
+ # BUILD:
8
+ # - Executable.from_analysis(state) consumes:
9
+ # * :ir_module (lowered IR)
10
+ # * :access_plans (for AccessBuilder)
11
+ # * function registry
12
+ # - Builds accessor lambdas once per plan id.
13
+ #
14
+ # EVALUATION:
15
+ # - program.read(inputs, mode: :public|:wrapped, target: nil)
16
+ # * mode=:public → returns “user values” (scalars and plain Ruby arrays). Vec results are exposed as their lifted scalar.
17
+ # * mode=:wrapped → returns internal VM structures for introspection:
18
+ # - Scalars as {k: :scalar, v: ...}
19
+ # - Vec twins available as :name__vec (and :name for lifted scalar)
20
+ # * target: symbol → short-circuit after computing the requested declaration and its dependencies.
21
+ #
22
+ # NAMING & TWINS (TODO: we are not exposing for now):
23
+ # - Every vectorized declaration with indices has:
24
+ # * :name__vec → internal vec form (rows with idx)
25
+ # * :name → lifted scalar form (nested arrays shaped by scope)
26
+ # - only :name is visible (TODO: For now, we do not expose the twins)
27
+ #
28
+ # CACHING / MEMOIZATION:
29
+ # - Values are computed once per evaluation; dependent requests reuse cached slots.
30
+ #
31
+ # ERROR SURFACE:
32
+ # - VM errors are wrapped as Kumi::Core::Errors::RuntimeError with op context (decl/op index).
33
+ # - Accessors raise descriptive KeyError for missing fields/arrays (policy-aware).
34
+ #
35
+ # DEBUGGING:
36
+ # - DEBUG_LOWER=1 to print IR at build time
37
+ # - DEBUG_VM_ARGS=1 to trace VM execution
38
+ # - Accessors can be debugged independently with DEBUG_ACCESSOR_OPS=1
39
+ class Executable
40
+ def self.from_analysis(state, registry: nil)
41
+ ir = state.fetch(:ir_module)
42
+ access_plans = state.fetch(:access_plans)
43
+ input_metadata = state[:input_metadata] || {}
44
+ accessors = Kumi::Core::Compiler::AccessBuilder.build(access_plans)
45
+
46
+ access_meta = {}
47
+ access_plans.each_value do |plans|
48
+ plans.each do |p|
49
+ access_meta[p.accessor_key] = { mode: p.mode, scope: p.scope }
50
+ end
51
+ end
52
+
53
+ # Use the internal functions hash that VM expects
54
+ registry ||= Kumi::Registry.functions
55
+ new(ir: ir, accessors: accessors, access_meta: access_meta, registry: registry, input_metadata: input_metadata)
56
+ end
57
+
58
+ def initialize(ir:, accessors:, access_meta:, registry:, input_metadata:)
59
+ @ir = ir.freeze
60
+ @acc = accessors.freeze
61
+ @meta = access_meta.freeze
62
+ @reg = registry
63
+ @input_metadata = input_metadata.freeze
64
+ @decl = @ir.decls.map { |d| [d.name, d] }.to_h
65
+ end
66
+
67
+ def decl?(name) = @decl.key?(name)
68
+
69
+ def read(input, mode: :ruby)
70
+ Run.new(self, input, mode: mode, input_metadata: @input_metadata)
71
+ end
72
+
73
+ # API compatibility for backward compatibility
74
+ def evaluate(ctx, *key_names)
75
+ target_keys = key_names.empty? ? @decl.keys : validate_keys(key_names)
76
+
77
+ # Handle context wrapping for backward compatibility
78
+ input = ctx.respond_to?(:ctx) ? ctx.ctx : ctx
79
+
80
+ target_keys.each_with_object({}) do |key, result|
81
+ result[key] = eval_decl(key, input, mode: :ruby)
82
+ end
83
+ end
84
+
85
+ def eval_decl(name, input, mode: :ruby)
86
+ raise Kumi::Core::Errors::RuntimeError, "unknown decl #{name}" unless decl?(name)
87
+
88
+ out = Kumi::Core::IR::ExecutionEngine.run(@ir, { input: input, target: name },
89
+ accessors: @acc, registry: @reg).fetch(name)
90
+
91
+ mode == :ruby ? unwrap(@decl[name], out) : out
92
+ end
93
+
94
+ private
95
+
96
+ def validate_keys(keys)
97
+ unknown_keys = keys - @decl.keys
98
+ return keys if unknown_keys.empty?
99
+
100
+ raise Kumi::Errors::RuntimeError, "No binding named #{unknown_keys.first}"
101
+ end
102
+
103
+ private
104
+
105
+ def unwrap(_decl, v)
106
+ v[:k] == :scalar ? v[:v] : v # no grouping needed
107
+ end
108
+ end
109
+
110
+ class Run
111
+ def initialize(program, input, mode:, input_metadata:)
112
+ @program = program
113
+ @input = input
114
+ @mode = mode
115
+ @input_metadata = input_metadata
116
+ @cache = {}
117
+ end
118
+
119
+ def get(name)
120
+ @cache[name] ||= @program.eval_decl(name, @input, mode: @mode)
121
+ end
122
+
123
+ def [](name)
124
+ get(name)
125
+ end
126
+
127
+ def slice(*keys)
128
+ return {} if keys.empty?
129
+ keys.each_with_object({}) { |key, result| result[key] = get(key) }
130
+ end
131
+
132
+ def compiled_schema
133
+ @program
134
+ end
135
+
136
+ def method_missing(sym, *args, **kwargs, &blk)
137
+ return super unless args.empty? && kwargs.empty? && @program.decl?(sym)
138
+
139
+ get(sym)
140
+ end
141
+
142
+ def respond_to_missing?(sym, priv = false)
143
+ @program.decl?(sym) || super
144
+ end
145
+
146
+ def update(**changes)
147
+ changes.each do |field, value|
148
+ # Validate field exists
149
+ raise ArgumentError, "unknown input field: #{field}" unless input_field_exists?(field)
150
+
151
+ # Validate domain constraints
152
+ validate_domain_constraint(field, value)
153
+
154
+ # Update the input data
155
+ @input = deep_merge(@input, { field => value })
156
+ end
157
+
158
+ # Clear cache after all updates
159
+ @cache.clear
160
+ self
161
+ end
162
+
163
+ def wrapped!
164
+ @mode = :wrapped
165
+ @cache.clear
166
+ self
167
+ end
168
+
169
+ def ruby!
170
+ @mode = :ruby
171
+ @cache.clear
172
+ self
173
+ end
174
+
175
+ private
176
+
177
+ def input_field_exists?(field)
178
+ # Check if field is declared in input block
179
+ @input_metadata.key?(field) || @input.key?(field)
180
+ end
181
+
182
+ def validate_domain_constraint(field, value)
183
+ field_meta = @input_metadata[field]
184
+ return unless field_meta&.dig(:domain)
185
+
186
+ domain = field_meta[:domain]
187
+ return unless violates_domain?(value, domain)
188
+
189
+ raise ArgumentError, "value #{value} is not in domain #{domain}"
190
+ end
191
+
192
+ def violates_domain?(value, domain)
193
+ case domain
194
+ when Range
195
+ !domain.include?(value)
196
+ when Array
197
+ !domain.include?(value)
198
+ when Proc
199
+ # For Proc domains, we can't statically analyze
200
+ false
201
+ else
202
+ false
203
+ end
204
+ end
205
+
206
+ def deep_merge(a, b)
207
+ return b unless a.is_a?(Hash) && b.is_a?(Hash)
208
+
209
+ a.merge(b) { |_k, v1, v2| deep_merge(v1, v2) }
210
+ end
211
+ end
212
+ end
213
+ end
data/lib/kumi/schema.rb CHANGED
@@ -8,27 +8,29 @@ module Kumi
8
8
 
9
9
  Inspector = Struct.new(:syntax_tree, :analyzer_result, :compiled_schema) do
10
10
  def inspect
11
- "#<#{self.class} syntax_tree: #{syntax_tree.inspect}, analyzer_result: #{analyzer_result.inspect}, schema: #{schema.inspect}>"
11
+ "#<#{self.class} syntax_tree: #{syntax_tree.inspect}, analyzer_result: #{analyzer_result.inspect}, compiled_schema: #{compiled_schema.inspect}>"
12
12
  end
13
13
  end
14
14
 
15
15
  def from(context)
16
+ # VERY IMPORTANT: This method is overriden on specs in order to use dual mode.
17
+
16
18
  raise("No schema defined") unless @__compiled_schema__
17
19
 
18
20
  # Validate input types and domain constraints
19
- input_meta = @__analyzer_result__.state[:inputs] || {}
21
+ input_meta = @__analyzer_result__.state[:input_metadata] || {}
20
22
  violations = Core::Input::Validator.validate_context(context, input_meta)
21
23
 
22
24
  raise Errors::InputValidationError, violations unless violations.empty?
23
25
 
24
- Core::SchemaInstance.new(@__compiled_schema__, @__analyzer_result__.state, context)
26
+ @__compiled_schema__.read(context, mode: :ruby)
25
27
  end
26
28
 
27
29
  def explain(context, *keys)
28
30
  raise("No schema defined") unless @__compiled_schema__
29
31
 
30
32
  # Validate input types and domain constraints
31
- input_meta = @__analyzer_result__.state[:inputs] || {}
33
+ input_meta = @__analyzer_result__.state[:input_metadata] || {}
32
34
  violations = Core::Input::Validator.validate_context(context, input_meta)
33
35
 
34
36
  raise Errors::InputValidationError, violations unless violations.empty?
@@ -40,8 +42,17 @@ module Kumi
40
42
  nil
41
43
  end
42
44
 
45
+ def build_syntax_tree(&block)
46
+ @__syntax_tree__ = Core::RubyParser::Dsl.build_syntax_tree(&block).freeze
47
+ end
48
+
43
49
  def schema(&block)
50
+ # from_location = caller_locations(1, 1).first
51
+ # raise "Called from #{from_location.path}:#{from_location.lineno}"
44
52
  @__syntax_tree__ = Core::RubyParser::Dsl.build_syntax_tree(&block).freeze
53
+
54
+ puts Support::SExpressionPrinter.print(@__syntax_tree__, indent: 2) if ENV["KUMI_DEBUG"] || ENV["KUMI_PRINT_SYNTAX_TREE"]
55
+
45
56
  @__analyzer_result__ = Analyzer.analyze!(@__syntax_tree__).freeze
46
57
  @__compiled_schema__ = Compiler.compile(@__syntax_tree__, analyzer: @__analyzer_result__).freeze
47
58
 
@@ -341,9 +341,9 @@ module Kumi
341
341
  end
342
342
 
343
343
  def extract_inputs
344
- return {} unless @state[:inputs]
344
+ return {} unless @state[:input_metadata]
345
345
 
346
- @state[:inputs].transform_values do |field_info|
346
+ @state[:input_metadata].transform_values do |field_info|
347
347
  {
348
348
  type: normalize_type(field_info[:type]),
349
349
  domain: normalize_domain(field_info[:domain]),