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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +18 -0
- data/CLAUDE.md +18 -258
- data/README.md +188 -121
- data/docs/AST.md +1 -1
- data/docs/FUNCTIONS.md +52 -8
- data/docs/VECTOR_SEMANTICS.md +286 -0
- data/docs/compiler_design_principles.md +86 -0
- data/docs/features/README.md +15 -2
- data/docs/features/hierarchical-broadcasting.md +349 -0
- data/docs/features/javascript-transpiler.md +148 -0
- data/docs/features/performance.md +1 -3
- data/docs/features/s-expression-printer.md +2 -2
- data/docs/schema_metadata.md +7 -7
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +21 -15
- data/examples/game_of_life.rb +2 -4
- data/lib/kumi/analyzer.rb +34 -14
- data/lib/kumi/compiler.rb +4 -283
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +717 -66
- 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 +118 -99
- 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 +28 -0
- data/lib/kumi/core/analyzer/passes/toposorter.rb +9 -3
- data/lib/kumi/core/analyzer/passes/type_checker.rb +9 -5
- 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 +92 -48
- 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 +137 -0
- 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 +100 -6
- data/lib/kumi/core/function_registry/conditional_functions.rb +14 -4
- data/lib/kumi/core/function_registry/function_builder.rb +142 -53
- data/lib/kumi/core/function_registry/logical_functions.rb +173 -3
- data/lib/kumi/core/function_registry/stat_functions.rb +156 -0
- data/lib/kumi/core/function_registry.rb +138 -98
- 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 +37 -16
- data/lib/kumi/core/ruby_parser/input_builder.rb +61 -8
- 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/errors.rb +2 -0
- data/lib/kumi/js.rb +23 -0
- data/lib/kumi/registry.rb +17 -22
- data/lib/kumi/runtime/executable.rb +213 -0
- data/lib/kumi/schema.rb +15 -4
- 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 +17 -16
- data/lib/kumi/syntax/array_expression.rb +6 -6
- data/lib/kumi/syntax/call_expression.rb +4 -4
- data/lib/kumi/syntax/cascade_expression.rb +4 -4
- data/lib/kumi/syntax/case_expression.rb +4 -4
- data/lib/kumi/syntax/declaration_reference.rb +4 -4
- data/lib/kumi/syntax/hash_expression.rb +4 -4
- data/lib/kumi/syntax/input_declaration.rb +6 -5
- data/lib/kumi/syntax/input_element_reference.rb +5 -5
- data/lib/kumi/syntax/input_reference.rb +5 -5
- data/lib/kumi/syntax/literal.rb +4 -4
- data/lib/kumi/syntax/location.rb +5 -0
- data/lib/kumi/syntax/node.rb +33 -34
- data/lib/kumi/syntax/root.rb +6 -6
- data/lib/kumi/syntax/trait_declaration.rb +4 -4
- data/lib/kumi/syntax/value_declaration.rb +4 -4
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +6 -15
- data/scripts/analyze_broadcast_methods.rb +68 -0
- data/scripts/analyze_cascade_methods.rb +74 -0
- data/scripts/check_broadcasting_coverage.rb +51 -0
- data/scripts/find_dead_code.rb +114 -0
- metadata +36 -9
- data/docs/features/array-broadcasting.md +0 -170
- data/lib/kumi/cli.rb +0 -449
- data/lib/kumi/core/compiled_schema.rb +0 -43
- data/lib/kumi/core/evaluation_wrapper.rb +0 -40
- data/lib/kumi/core/schema_instance.rb +0 -111
- data/lib/kumi/core/vectorization_metadata.rb +0 -110
- 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 '
|
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
|
-
|
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
|
@@ -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
|
data/lib/kumi/errors.rb
CHANGED
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
|
-
|
4
|
-
|
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
|
14
|
-
|
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
|
21
|
-
|
22
|
-
|
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
|
-
|
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},
|
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[:
|
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]),
|