kumi 0.0.10 → 0.0.12
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/CHANGELOG.md +23 -0
- data/CLAUDE.md +7 -231
- data/README.md +5 -5
- data/docs/SYNTAX.md +66 -0
- data/docs/VECTOR_SEMANTICS.md +286 -0
- data/docs/features/hierarchical-broadcasting.md +67 -1
- data/docs/features/input-declaration-system.md +16 -0
- data/docs/features/s-expression-printer.md +2 -2
- data/lib/kumi/analyzer.rb +34 -12
- data/lib/kumi/compiler.rb +2 -12
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +157 -64
- data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +1 -1
- data/lib/kumi/core/analyzer/passes/input_access_planner_pass.rb +47 -0
- data/lib/kumi/core/analyzer/passes/input_collector.rb +123 -101
- data/lib/kumi/core/analyzer/passes/join_reduce_planning_pass.rb +293 -0
- data/lib/kumi/core/analyzer/passes/lower_to_ir_pass.rb +993 -0
- data/lib/kumi/core/analyzer/passes/pass_base.rb +2 -2
- data/lib/kumi/core/analyzer/passes/scope_resolution_pass.rb +346 -0
- data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +2 -1
- data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
- data/lib/kumi/core/analyzer/passes/type_checker.rb +3 -3
- data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +2 -2
- data/lib/kumi/core/analyzer/passes/{type_inferencer.rb → type_inferencer_pass.rb} +4 -4
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +2 -2
- data/lib/kumi/core/analyzer/plans.rb +52 -0
- data/lib/kumi/core/analyzer/structs/access_plan.rb +20 -0
- data/lib/kumi/core/analyzer/structs/input_meta.rb +29 -0
- data/lib/kumi/core/compiler/access_builder.rb +36 -0
- data/lib/kumi/core/compiler/access_planner.rb +219 -0
- data/lib/kumi/core/compiler/accessors/base.rb +69 -0
- data/lib/kumi/core/compiler/accessors/each_indexed_accessor.rb +84 -0
- data/lib/kumi/core/compiler/accessors/materialize_accessor.rb +55 -0
- data/lib/kumi/core/compiler/accessors/ravel_accessor.rb +73 -0
- data/lib/kumi/core/compiler/accessors/read_accessor.rb +41 -0
- data/lib/kumi/core/compiler_base.rb +2 -2
- data/lib/kumi/core/error_reporter.rb +6 -5
- data/lib/kumi/core/errors.rb +4 -0
- data/lib/kumi/core/explain.rb +157 -205
- data/lib/kumi/core/export/node_builders.rb +2 -2
- data/lib/kumi/core/export/node_serializers.rb +1 -1
- data/lib/kumi/core/function_registry/collection_functions.rb +21 -10
- data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
- data/lib/kumi/core/function_registry/function_builder.rb +142 -55
- data/lib/kumi/core/function_registry/logical_functions.rb +5 -5
- data/lib/kumi/core/function_registry/stat_functions.rb +2 -2
- data/lib/kumi/core/function_registry.rb +126 -108
- data/lib/kumi/core/input/validator.rb +1 -1
- data/lib/kumi/core/ir/execution_engine/combinators.rb +117 -0
- data/lib/kumi/core/ir/execution_engine/interpreter.rb +336 -0
- data/lib/kumi/core/ir/execution_engine/values.rb +46 -0
- data/lib/kumi/core/ir/execution_engine.rb +50 -0
- data/lib/kumi/core/ir.rb +58 -0
- data/lib/kumi/core/ruby_parser/build_context.rb +2 -2
- data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +0 -12
- data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +36 -15
- data/lib/kumi/core/ruby_parser/input_builder.rb +30 -9
- data/lib/kumi/core/ruby_parser/parser.rb +1 -1
- data/lib/kumi/core/ruby_parser/schema_builder.rb +2 -2
- data/lib/kumi/core/ruby_parser/sugar.rb +7 -0
- data/lib/kumi/core/types/validator.rb +1 -1
- data/lib/kumi/registry.rb +14 -79
- data/lib/kumi/runtime/executable.rb +213 -0
- data/lib/kumi/schema.rb +14 -3
- data/lib/kumi/schema_metadata.rb +2 -2
- data/lib/kumi/support/ir_dump.rb +491 -0
- data/lib/kumi/support/s_expression_printer.rb +1 -1
- data/lib/kumi/syntax/location.rb +5 -0
- data/lib/kumi/syntax/node.rb +0 -1
- data/lib/kumi/syntax/root.rb +2 -2
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +6 -15
- metadata +37 -19
- data/lib/kumi/core/cascade_executor_builder.rb +0 -132
- data/lib/kumi/core/compiled_schema.rb +0 -43
- data/lib/kumi/core/compiler/expression_compiler.rb +0 -146
- data/lib/kumi/core/compiler/function_invoker.rb +0 -55
- data/lib/kumi/core/compiler/path_traversal_compiler.rb +0 -158
- data/lib/kumi/core/compiler/reference_compiler.rb +0 -46
- data/lib/kumi/core/evaluation_wrapper.rb +0 -40
- data/lib/kumi/core/nested_structure_utils.rb +0 -78
- data/lib/kumi/core/schema_instance.rb +0 -115
- data/lib/kumi/core/vectorized_function_builder.rb +0 -88
- data/lib/kumi/js/compiler.rb +0 -878
- data/lib/kumi/js/function_registry.rb +0 -333
- data/migrate_to_core_iterative.rb +0 -938
@@ -15,39 +15,36 @@ module Kumi
|
|
15
15
|
end
|
16
16
|
|
17
17
|
def on(*args)
|
18
|
-
|
19
|
-
validate_on_args(args, "on", on_loc)
|
18
|
+
validate_on_args(args, "on", @loc)
|
20
19
|
|
21
20
|
trait_names = args[0..-2]
|
22
21
|
expr = args.last
|
23
22
|
|
24
|
-
trait_bindings = convert_trait_names_to_bindings(trait_names,
|
23
|
+
trait_bindings = convert_trait_names_to_bindings(trait_names, @loc)
|
25
24
|
condition = @context.fn(:cascade_and, *trait_bindings)
|
26
25
|
result = ensure_syntax(expr)
|
27
26
|
add_case(condition, result)
|
28
27
|
end
|
29
28
|
|
30
29
|
def on_any(*args)
|
31
|
-
|
32
|
-
validate_on_args(args, "on_any", on_loc)
|
30
|
+
validate_on_args(args, "on_any", @loc)
|
33
31
|
|
34
32
|
trait_names = args[0..-2]
|
35
33
|
expr = args.last
|
36
34
|
|
37
|
-
trait_bindings = convert_trait_names_to_bindings(trait_names,
|
35
|
+
trait_bindings = convert_trait_names_to_bindings(trait_names, @loc)
|
38
36
|
condition = create_fn(:any?, trait_bindings)
|
39
37
|
result = ensure_syntax(expr)
|
40
38
|
add_case(condition, result)
|
41
39
|
end
|
42
40
|
|
43
41
|
def on_none(*args)
|
44
|
-
|
45
|
-
validate_on_args(args, "on_none", on_loc)
|
42
|
+
validate_on_args(args, "on_none", @loc)
|
46
43
|
|
47
44
|
trait_names = args[0..-2]
|
48
45
|
expr = args.last
|
49
46
|
|
50
|
-
trait_bindings = convert_trait_names_to_bindings(trait_names,
|
47
|
+
trait_bindings = convert_trait_names_to_bindings(trait_names, @loc)
|
51
48
|
condition = create_fn(:none?, trait_bindings)
|
52
49
|
result = ensure_syntax(expr)
|
53
50
|
add_case(condition, result)
|
@@ -71,11 +68,6 @@ module Kumi
|
|
71
68
|
|
72
69
|
private
|
73
70
|
|
74
|
-
def current_location
|
75
|
-
caller_info = caller_locations(1, 1).first
|
76
|
-
Location.new(file: caller_info.path, line: caller_info.lineno, column: 0)
|
77
|
-
end
|
78
|
-
|
79
71
|
def validate_on_args(args, method_name, location)
|
80
72
|
raise_error("cascade '#{method_name}' requires at least one trait name", location) if args.empty?
|
81
73
|
|
@@ -92,7 +84,36 @@ module Kumi
|
|
92
84
|
when DeclarationReference
|
93
85
|
name # Already a binding from method_missing
|
94
86
|
else
|
95
|
-
|
87
|
+
# TODO: MOVE THESE TO ANALYZER- GRAMMAR CHECKS!
|
88
|
+
case name
|
89
|
+
when Kumi::Syntax::CallExpression
|
90
|
+
if name.fn_name == :==
|
91
|
+
raise_error(
|
92
|
+
"cascade conditions must be bare trait identifiers, not expressions like 'ref(...) == ...'. " \
|
93
|
+
"Use fn(:==, ref(:tier), \"gold\") or define the comparison as a separate trait.",
|
94
|
+
location
|
95
|
+
)
|
96
|
+
else
|
97
|
+
raise_error(
|
98
|
+
"cascade conditions must be bare trait identifiers, not function calls (CallExpression). " \
|
99
|
+
"Define the function call as a separate trait first.",
|
100
|
+
location
|
101
|
+
)
|
102
|
+
end
|
103
|
+
when Kumi::Syntax::DeclarationReference
|
104
|
+
raise_error(
|
105
|
+
"cascade conditions must be bare trait identifiers, not value references (DeclarationReference). " \
|
106
|
+
"Use just 'my_trait' instead of 'ref(:my_trait)'.",
|
107
|
+
location
|
108
|
+
)
|
109
|
+
else
|
110
|
+
expression_type = name.class.name.split("::").last
|
111
|
+
raise_error(
|
112
|
+
"cascade conditions must be bare trait identifiers, not #{expression_type} expressions. " \
|
113
|
+
"Define complex expressions as separate traits first.",
|
114
|
+
location
|
115
|
+
)
|
116
|
+
end
|
96
117
|
end
|
97
118
|
end
|
98
119
|
end
|
@@ -33,10 +33,14 @@ module Kumi
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
|
36
|
-
def hash(name_or_key_type, val_type = nil, **kwargs)
|
37
|
-
|
38
|
-
|
39
|
-
|
36
|
+
def hash(name_or_key_type, val_type = nil, **kwargs, &block)
|
37
|
+
if block_given?
|
38
|
+
create_hash_field_with_block(name_or_key_type, kwargs, &block)
|
39
|
+
elsif val_type.nil?
|
40
|
+
create_hash_field(name_or_key_type, kwargs)
|
41
|
+
else
|
42
|
+
Kumi::Core::Types.hash(name_or_key_type, val_type)
|
43
|
+
end
|
40
44
|
end
|
41
45
|
|
42
46
|
def method_missing(method_name, *_args)
|
@@ -63,7 +67,7 @@ module Kumi
|
|
63
67
|
elem_type = elem_spec.is_a?(Hash) && elem_spec[:type] ? elem_spec[:type] : :any
|
64
68
|
|
65
69
|
array_type = create_array_type(field_name, elem_type)
|
66
|
-
@context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, array_type, [], :
|
70
|
+
@context.inputs << Kumi::Syntax::InputDeclaration.new(field_name, domain, array_type, [], :field, loc: @context.current_location)
|
67
71
|
end
|
68
72
|
|
69
73
|
def create_array_type(field_name, elem_type)
|
@@ -101,7 +105,7 @@ module Kumi
|
|
101
105
|
children, _, using_elements = collect_array_children(&block)
|
102
106
|
|
103
107
|
# Create the InputDeclaration with children and access_mode
|
104
|
-
access_mode = using_elements ? :element : :
|
108
|
+
access_mode = using_elements ? :element : :field
|
105
109
|
@context.inputs << Kumi::Syntax::InputDeclaration.new(
|
106
110
|
field_name,
|
107
111
|
domain,
|
@@ -155,11 +159,11 @@ module Kumi
|
|
155
159
|
case type_spec
|
156
160
|
when :array
|
157
161
|
create_array_field_with_block(name, {}, &block)
|
158
|
-
when :
|
162
|
+
when :field
|
159
163
|
# Create nested object structure
|
160
164
|
create_object_element(name, &block)
|
161
165
|
else
|
162
|
-
raise_syntax_error("element(#{type_spec.inspect}, #{name.inspect}) with block only supports :array or :
|
166
|
+
raise_syntax_error("element(#{type_spec.inspect}, #{name.inspect}) with block only supports :array or :field types",
|
163
167
|
location: @context.current_location)
|
164
168
|
end
|
165
169
|
else
|
@@ -172,7 +176,24 @@ module Kumi
|
|
172
176
|
def create_object_element(name, &block)
|
173
177
|
# Similar to create_array_field_with_block but for objects
|
174
178
|
children, = collect_array_children(&block)
|
175
|
-
@context.inputs << Kumi::Syntax::InputDeclaration.new(name, nil, :
|
179
|
+
@context.inputs << Kumi::Syntax::InputDeclaration.new(name, nil, :field, children, nil, loc: @context.current_location)
|
180
|
+
end
|
181
|
+
|
182
|
+
def create_hash_field_with_block(field_name, options, &block)
|
183
|
+
domain = options[:domain]
|
184
|
+
|
185
|
+
# Collect children by creating a nested context (reuse array logic)
|
186
|
+
children, = collect_array_children(&block)
|
187
|
+
|
188
|
+
# Create the InputDeclaration with children and :field access_mode for hash objects
|
189
|
+
@context.inputs << Kumi::Syntax::InputDeclaration.new(
|
190
|
+
field_name,
|
191
|
+
domain,
|
192
|
+
:hash,
|
193
|
+
children,
|
194
|
+
:field,
|
195
|
+
loc: @context.current_location
|
196
|
+
)
|
176
197
|
end
|
177
198
|
end
|
178
199
|
end
|
@@ -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.
|
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.
|
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
|
@@ -5,7 +5,7 @@ module Kumi
|
|
5
5
|
module Types
|
6
6
|
# Validates type definitions and structures
|
7
7
|
class Validator
|
8
|
-
VALID_TYPES = %i[string integer float boolean any symbol regexp time date datetime array].freeze
|
8
|
+
VALID_TYPES = %i[string integer float boolean any symbol regexp time date datetime array hash].freeze
|
9
9
|
|
10
10
|
def self.valid_type?(type)
|
11
11
|
return true if VALID_TYPES.include?(type)
|
data/lib/kumi/registry.rb
CHANGED
@@ -1,96 +1,31 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Kumi
|
4
|
-
# Public
|
5
|
-
#
|
6
|
-
# Usage:
|
7
|
-
# Kumi::Registry.register(:my_function) do |x|
|
8
|
-
# x * 2
|
9
|
-
# end
|
4
|
+
# Public facade for the function registry.
|
5
|
+
# Delegates to Kumi::Core::FunctionRegistry.
|
10
6
|
module Registry
|
11
|
-
|
12
|
-
|
13
|
-
Entry = Core::FunctionRegistry::FunctionBuilder::Entry
|
14
|
-
|
15
|
-
@functions = Core::FunctionRegistry::CORE_FUNCTIONS.transform_values(&:dup)
|
16
|
-
@frozen = false
|
17
|
-
@lock = Mutex.new
|
18
|
-
|
19
|
-
class FrozenError < RuntimeError; end
|
7
|
+
Entry = Core::FunctionRegistry::FunctionBuilder::Entry
|
8
|
+
FrozenError = Core::FunctionRegistry::FrozenError
|
20
9
|
|
21
10
|
class << self
|
22
|
-
def
|
23
|
-
|
24
|
-
@functions = Core::FunctionRegistry::CORE_FUNCTIONS.transform_values(&:dup)
|
25
|
-
@frozen = false
|
26
|
-
end
|
11
|
+
def auto_register(*mods)
|
12
|
+
Core::FunctionRegistry.auto_register(*mods)
|
27
13
|
end
|
28
14
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
# end
|
35
|
-
#
|
36
|
-
# # Use in schema:
|
37
|
-
# value :doubled, fn(:double, input.number)
|
38
|
-
def register(name, &block)
|
39
|
-
@lock.synchronize do
|
40
|
-
raise FrozenError, "registry is frozen" if @frozen
|
41
|
-
raise ArgumentError, "Function #{name.inspect} already registered" if @functions.key?(name)
|
42
|
-
|
43
|
-
fn_lambda = block.is_a?(Proc) ? block : ->(*args) { yield(*args) }
|
44
|
-
@functions[name] = Entry.new(
|
45
|
-
fn: fn_lambda,
|
46
|
-
arity: fn_lambda.arity,
|
47
|
-
param_types: [:any],
|
48
|
-
return_type: :any,
|
49
|
-
description: nil,
|
50
|
-
inverse: nil,
|
51
|
-
reducer: false
|
52
|
-
)
|
15
|
+
def method_missing(name, ...)
|
16
|
+
if Core::FunctionRegistry.respond_to?(name)
|
17
|
+
Core::FunctionRegistry.public_send(name, ...)
|
18
|
+
else
|
19
|
+
super
|
53
20
|
end
|
54
21
|
end
|
55
22
|
|
56
|
-
|
57
|
-
|
58
|
-
# Example:
|
59
|
-
# Kumi::Registry.register_with_metadata(
|
60
|
-
# :add_tax,
|
61
|
-
# ->(amount, rate) { amount * (1 + rate) },
|
62
|
-
# arity: 2,
|
63
|
-
# param_types: [:float, :float],
|
64
|
-
# return_type: :float,
|
65
|
-
# description: "Adds tax to an amount",
|
66
|
-
# )
|
67
|
-
#
|
68
|
-
# # Use in schema:
|
69
|
-
# value :total, fn(:add_tax, input.price, input.tax_rate)
|
70
|
-
def register_with_metadata(name, fn_lambda, arity:, param_types: [:any], return_type: :any, description: nil, inverse: nil,
|
71
|
-
reducer: false)
|
72
|
-
@lock.synchronize do
|
73
|
-
raise FrozenError, "registry is frozen" if @frozen
|
74
|
-
raise ArgumentError, "Function #{name.inspect} already registered" if @functions.key?(name)
|
75
|
-
|
76
|
-
@functions[name] = Entry.new(
|
77
|
-
fn: fn_lambda,
|
78
|
-
arity: arity,
|
79
|
-
param_types: param_types,
|
80
|
-
return_type: return_type,
|
81
|
-
description: description,
|
82
|
-
inverse: inverse,
|
83
|
-
reducer: reducer
|
84
|
-
)
|
85
|
-
end
|
23
|
+
def respond_to_missing?(name, include_private = false)
|
24
|
+
Core::FunctionRegistry.respond_to?(name, include_private) || super
|
86
25
|
end
|
87
26
|
|
88
27
|
def freeze!
|
89
|
-
|
90
|
-
@functions.each_value(&:freeze)
|
91
|
-
@functions.freeze
|
92
|
-
@frozen = true
|
93
|
-
end
|
28
|
+
Core::FunctionRegistry.freeze!
|
94
29
|
end
|
95
30
|
end
|
96
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
@@ -13,22 +13,24 @@ module Kumi
|
|
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[:
|
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
|
-
|
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[:
|
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
|
|
data/lib/kumi/schema_metadata.rb
CHANGED
@@ -341,9 +341,9 @@ module Kumi
|
|
341
341
|
end
|
342
342
|
|
343
343
|
def extract_inputs
|
344
|
-
return {} unless @state[:
|
344
|
+
return {} unless @state[:input_metadata]
|
345
345
|
|
346
|
-
@state[:
|
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]),
|