kumi 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CLAUDE.md +1 -1
- data/README.md +8 -5
- data/examples/game_of_life.rb +1 -1
- data/examples/static_analysis_errors.rb +7 -7
- data/lib/kumi/analyzer.rb +15 -15
- data/lib/kumi/compiler.rb +6 -6
- data/lib/kumi/core/analyzer/analysis_state.rb +39 -0
- data/lib/kumi/core/analyzer/constant_evaluator.rb +59 -0
- data/lib/kumi/core/analyzer/passes/broadcast_detector.rb +248 -0
- data/lib/kumi/core/analyzer/passes/declaration_validator.rb +45 -0
- data/lib/kumi/core/analyzer/passes/dependency_resolver.rb +153 -0
- data/lib/kumi/core/analyzer/passes/input_collector.rb +139 -0
- data/lib/kumi/core/analyzer/passes/name_indexer.rb +26 -0
- data/lib/kumi/core/analyzer/passes/pass_base.rb +52 -0
- data/lib/kumi/core/analyzer/passes/semantic_constraint_validator.rb +111 -0
- data/lib/kumi/core/analyzer/passes/toposorter.rb +110 -0
- data/lib/kumi/core/analyzer/passes/type_checker.rb +162 -0
- data/lib/kumi/core/analyzer/passes/type_consistency_checker.rb +48 -0
- data/lib/kumi/core/analyzer/passes/type_inferencer.rb +236 -0
- data/lib/kumi/core/analyzer/passes/unsat_detector.rb +406 -0
- data/lib/kumi/core/analyzer/passes/visitor_pass.rb +44 -0
- data/lib/kumi/core/atom_unsat_solver.rb +396 -0
- data/lib/kumi/core/compiled_schema.rb +43 -0
- data/lib/kumi/core/constraint_relationship_solver.rb +641 -0
- data/lib/kumi/core/domain/enum_analyzer.rb +55 -0
- data/lib/kumi/core/domain/range_analyzer.rb +85 -0
- data/lib/kumi/core/domain/validator.rb +82 -0
- data/lib/kumi/core/domain/violation_formatter.rb +42 -0
- data/lib/kumi/core/error_reporter.rb +166 -0
- data/lib/kumi/core/error_reporting.rb +97 -0
- data/lib/kumi/core/errors.rb +120 -0
- data/lib/kumi/core/evaluation_wrapper.rb +40 -0
- data/lib/kumi/core/explain.rb +295 -0
- data/lib/kumi/core/export/deserializer.rb +41 -0
- data/lib/kumi/core/export/errors.rb +14 -0
- data/lib/kumi/core/export/node_builders.rb +142 -0
- data/lib/kumi/core/export/node_registry.rb +54 -0
- data/lib/kumi/core/export/node_serializers.rb +158 -0
- data/lib/kumi/core/export/serializer.rb +25 -0
- data/lib/kumi/core/export.rb +35 -0
- data/lib/kumi/core/function_registry/collection_functions.rb +202 -0
- data/lib/kumi/core/function_registry/comparison_functions.rb +33 -0
- data/lib/kumi/core/function_registry/conditional_functions.rb +38 -0
- data/lib/kumi/core/function_registry/function_builder.rb +95 -0
- data/lib/kumi/core/function_registry/logical_functions.rb +44 -0
- data/lib/kumi/core/function_registry/math_functions.rb +74 -0
- data/lib/kumi/core/function_registry/string_functions.rb +57 -0
- data/lib/kumi/core/function_registry/type_functions.rb +53 -0
- data/lib/kumi/{function_registry.rb → core/function_registry.rb} +28 -36
- data/lib/kumi/core/input/type_matcher.rb +97 -0
- data/lib/kumi/core/input/validator.rb +51 -0
- data/lib/kumi/core/input/violation_creator.rb +52 -0
- data/lib/kumi/core/json_schema/generator.rb +65 -0
- data/lib/kumi/core/json_schema/validator.rb +27 -0
- data/lib/kumi/core/json_schema.rb +16 -0
- data/lib/kumi/core/ruby_parser/build_context.rb +27 -0
- data/lib/kumi/core/ruby_parser/declaration_reference_proxy.rb +38 -0
- data/lib/kumi/core/ruby_parser/dsl.rb +14 -0
- data/lib/kumi/core/ruby_parser/dsl_cascade_builder.rb +138 -0
- data/lib/kumi/core/ruby_parser/expression_converter.rb +128 -0
- data/lib/kumi/core/ruby_parser/guard_rails.rb +45 -0
- data/lib/kumi/core/ruby_parser/input_builder.rb +127 -0
- data/lib/kumi/core/ruby_parser/input_field_proxy.rb +48 -0
- data/lib/kumi/core/ruby_parser/input_proxy.rb +31 -0
- data/lib/kumi/core/ruby_parser/nested_input.rb +17 -0
- data/lib/kumi/core/ruby_parser/parser.rb +71 -0
- data/lib/kumi/core/ruby_parser/schema_builder.rb +175 -0
- data/lib/kumi/core/ruby_parser/sugar.rb +263 -0
- data/lib/kumi/core/ruby_parser.rb +12 -0
- data/lib/kumi/core/schema_instance.rb +111 -0
- data/lib/kumi/core/types/builder.rb +23 -0
- data/lib/kumi/core/types/compatibility.rb +96 -0
- data/lib/kumi/core/types/formatter.rb +26 -0
- data/lib/kumi/core/types/inference.rb +42 -0
- data/lib/kumi/core/types/normalizer.rb +72 -0
- data/lib/kumi/core/types/validator.rb +37 -0
- data/lib/kumi/core/types.rb +66 -0
- data/lib/kumi/core/vectorization_metadata.rb +110 -0
- data/lib/kumi/errors.rb +1 -112
- data/lib/kumi/registry.rb +37 -0
- data/lib/kumi/schema.rb +5 -5
- data/lib/kumi/schema_metadata.rb +3 -3
- 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 +5 -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/node.rb +34 -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/migrate_to_core_iterative.rb +938 -0
- data/scripts/generate_function_docs.rb +9 -9
- metadata +75 -72
- data/lib/kumi/analyzer/analysis_state.rb +0 -37
- data/lib/kumi/analyzer/constant_evaluator.rb +0 -57
- data/lib/kumi/analyzer/passes/broadcast_detector.rb +0 -246
- data/lib/kumi/analyzer/passes/declaration_validator.rb +0 -43
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +0 -151
- data/lib/kumi/analyzer/passes/input_collector.rb +0 -137
- data/lib/kumi/analyzer/passes/name_indexer.rb +0 -24
- data/lib/kumi/analyzer/passes/pass_base.rb +0 -50
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +0 -109
- data/lib/kumi/analyzer/passes/toposorter.rb +0 -108
- data/lib/kumi/analyzer/passes/type_checker.rb +0 -160
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +0 -46
- data/lib/kumi/analyzer/passes/type_inferencer.rb +0 -232
- data/lib/kumi/analyzer/passes/unsat_detector.rb +0 -404
- data/lib/kumi/analyzer/passes/visitor_pass.rb +0 -42
- data/lib/kumi/atom_unsat_solver.rb +0 -394
- data/lib/kumi/compiled_schema.rb +0 -41
- data/lib/kumi/constraint_relationship_solver.rb +0 -638
- data/lib/kumi/domain/enum_analyzer.rb +0 -53
- data/lib/kumi/domain/range_analyzer.rb +0 -83
- data/lib/kumi/domain/validator.rb +0 -80
- data/lib/kumi/domain/violation_formatter.rb +0 -40
- data/lib/kumi/error_reporter.rb +0 -164
- data/lib/kumi/error_reporting.rb +0 -95
- data/lib/kumi/evaluation_wrapper.rb +0 -38
- data/lib/kumi/explain.rb +0 -293
- data/lib/kumi/export/deserializer.rb +0 -39
- data/lib/kumi/export/errors.rb +0 -12
- data/lib/kumi/export/node_builders.rb +0 -140
- data/lib/kumi/export/node_registry.rb +0 -52
- data/lib/kumi/export/node_serializers.rb +0 -156
- data/lib/kumi/export/serializer.rb +0 -23
- data/lib/kumi/export.rb +0 -33
- data/lib/kumi/function_registry/collection_functions.rb +0 -200
- data/lib/kumi/function_registry/comparison_functions.rb +0 -31
- data/lib/kumi/function_registry/conditional_functions.rb +0 -36
- data/lib/kumi/function_registry/function_builder.rb +0 -93
- data/lib/kumi/function_registry/logical_functions.rb +0 -42
- data/lib/kumi/function_registry/math_functions.rb +0 -72
- data/lib/kumi/function_registry/string_functions.rb +0 -54
- data/lib/kumi/function_registry/type_functions.rb +0 -51
- data/lib/kumi/input/type_matcher.rb +0 -95
- data/lib/kumi/input/validator.rb +0 -49
- data/lib/kumi/input/violation_creator.rb +0 -50
- data/lib/kumi/json_schema/generator.rb +0 -63
- data/lib/kumi/json_schema/validator.rb +0 -25
- data/lib/kumi/json_schema.rb +0 -14
- data/lib/kumi/ruby_parser/build_context.rb +0 -25
- data/lib/kumi/ruby_parser/declaration_reference_proxy.rb +0 -36
- data/lib/kumi/ruby_parser/dsl.rb +0 -12
- data/lib/kumi/ruby_parser/dsl_cascade_builder.rb +0 -136
- data/lib/kumi/ruby_parser/expression_converter.rb +0 -126
- data/lib/kumi/ruby_parser/guard_rails.rb +0 -43
- data/lib/kumi/ruby_parser/input_builder.rb +0 -125
- data/lib/kumi/ruby_parser/input_field_proxy.rb +0 -46
- data/lib/kumi/ruby_parser/input_proxy.rb +0 -29
- data/lib/kumi/ruby_parser/nested_input.rb +0 -15
- data/lib/kumi/ruby_parser/parser.rb +0 -69
- data/lib/kumi/ruby_parser/schema_builder.rb +0 -173
- data/lib/kumi/ruby_parser/sugar.rb +0 -261
- data/lib/kumi/ruby_parser.rb +0 -10
- data/lib/kumi/schema_instance.rb +0 -109
- data/lib/kumi/types/builder.rb +0 -21
- data/lib/kumi/types/compatibility.rb +0 -94
- data/lib/kumi/types/formatter.rb +0 -24
- data/lib/kumi/types/inference.rb +0 -40
- data/lib/kumi/types/normalizer.rb +0 -70
- data/lib/kumi/types/validator.rb +0 -35
- data/lib/kumi/types.rb +0 -64
- data/lib/kumi/vectorization_metadata.rb +0 -108
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 652ef5f59f7c4469c86ecda4add1f650121d75d333231ae50306a7bf2ce7725c
|
4
|
+
data.tar.gz: 367a335e09a9d5cefaecbfa8e870c99ac49153a771f2451e3cfa8f73531d0701
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2dece8cc4284177c7f45a97768ea0c228cf41210d85cff985850a5e98f64c4ce02cc2c97cc2a20897d1f192422390ad39357b0d883e4e6aebbae1d0abc41cfe8
|
7
|
+
data.tar.gz: cb817018fab35b9594053d5bbd7fa4adb31c79a92d7e968a2739f9ffd16d194a3a45aeb75b6fcc28743fadcd93da83c9227cfe81d14bb2f9d917c18e1fb5d16b
|
data/CLAUDE.md
CHANGED
@@ -62,7 +62,7 @@ Kumi is a Declarative logic and rules engine framework with static analysis for
|
|
62
62
|
**Compiler** (`lib/kumi/compiler.rb`):
|
63
63
|
- Transforms analyzed syntax tree into executable lambda functions
|
64
64
|
- Maps each expression type to a compilation method
|
65
|
-
- Handles function calls via `
|
65
|
+
- Handles function calls via `Kumi::Registry`
|
66
66
|
- Produces `CompiledSchema` with executable bindings
|
67
67
|
|
68
68
|
**Function Registry** (`lib/kumi/function_registry.rb`):
|
data/README.md
CHANGED
@@ -8,7 +8,6 @@ Kumi is a Declarative logic and rules engine framework with static analysis for
|
|
8
8
|
It is well-suited for scenarios with complex, interdependent calculations, enforcing validation and consistency across your business rules while maintaining performance.
|
9
9
|
|
10
10
|
|
11
|
-
|
12
11
|
## What can you build?
|
13
12
|
|
14
13
|
Calculate U.S. federal taxes:
|
@@ -118,16 +117,14 @@ value :monthly_payment, fn(:pmt, rate: 0.05/12, nper: 36, pv: -loan_amount)
|
|
118
117
|
```
|
119
118
|
Note: You can find a list all core functions in [docs/FUNCTIONS.md](docs/FUNCTIONS.md)
|
120
119
|
|
121
|
-
These primitives are statically analyzed during schema definition to catch logical errors before runtime.
|
122
|
-
|
123
120
|
</details>
|
124
121
|
|
125
122
|
<details>
|
126
|
-
<summary><strong>🔍 Static Analysis</strong> - Catch
|
123
|
+
<summary><strong>🔍 Static Analysis</strong> - Catch errors at definition time and provides rich metadata</summary>
|
127
124
|
|
128
125
|
### Static Analysis
|
129
126
|
|
130
|
-
Kumi catches business logic errors that cause runtime failures or silent bugs:
|
127
|
+
Kumi catches many types of business logic errors that cause runtime failures or silent bugs:
|
131
128
|
|
132
129
|
```ruby
|
133
130
|
module InsurancePolicyPricer
|
@@ -361,6 +358,12 @@ The SchemaMetadata interface provides both processed metadata for tool developme
|
|
361
358
|
|
362
359
|
</details>
|
363
360
|
|
361
|
+
## Beyond Rules: What the Metadata Unlocks
|
362
|
+
* **Auto-generated forms** – compile schema → field spec → React form
|
363
|
+
* **Scenario explorer** – derive all trait combinations, Monte Carlo outcomes
|
364
|
+
* **Coverage dashboard** – flag branches never hit in prod
|
365
|
+
* **Schema diff** – highlight behaviour changes across versions
|
366
|
+
|
364
367
|
## Usage
|
365
368
|
|
366
369
|
**Suitable for:**
|
data/examples/game_of_life.rb
CHANGED
@@ -13,7 +13,7 @@ begin
|
|
13
13
|
cells[neighbor_index]
|
14
14
|
end.sum
|
15
15
|
end
|
16
|
-
Kumi::FunctionRegistry.register_with_metadata(:neighbor_cells_sum, method(:neighbor_cells_sum_method),
|
16
|
+
Kumi::Core::FunctionRegistry.register_with_metadata(:neighbor_cells_sum, method(:neighbor_cells_sum_method),
|
17
17
|
return_type: :integer, arity: 5,
|
18
18
|
param_types: %i[array integer integer integer integer],
|
19
19
|
description: "Get neighbor cells for Conway's Game of Life")
|
@@ -22,7 +22,7 @@ begin
|
|
22
22
|
value :yearly_rate, monthly_rate * 12
|
23
23
|
end
|
24
24
|
end
|
25
|
-
rescue Kumi::Errors::SemanticError => e
|
25
|
+
rescue Kumi::Core::Errors::SemanticError => e
|
26
26
|
puts " → #{e.message}"
|
27
27
|
end
|
28
28
|
|
@@ -48,7 +48,7 @@ begin
|
|
48
48
|
end
|
49
49
|
end
|
50
50
|
end
|
51
|
-
rescue Kumi::Errors::SemanticError => e
|
51
|
+
rescue Kumi::Core::Errors::SemanticError => e
|
52
52
|
puts " → #{e.message}"
|
53
53
|
end
|
54
54
|
|
@@ -71,7 +71,7 @@ begin
|
|
71
71
|
value :invalid_sum, input.name + input.age
|
72
72
|
end
|
73
73
|
end
|
74
|
-
rescue Kumi::Errors::TypeError => e
|
74
|
+
rescue Kumi::Core::Errors::TypeError => e
|
75
75
|
puts " → #{e.message}"
|
76
76
|
end
|
77
77
|
|
@@ -94,7 +94,7 @@ begin
|
|
94
94
|
trait :impossible_score, input.score == 150
|
95
95
|
end
|
96
96
|
end
|
97
|
-
rescue Kumi::Errors::SemanticError => e
|
97
|
+
rescue Kumi::Core::Errors::SemanticError => e
|
98
98
|
puts " → #{e.message}"
|
99
99
|
end
|
100
100
|
|
@@ -114,7 +114,7 @@ begin
|
|
114
114
|
value :result, ref(:nonexistent_trait) ? 100 : 0
|
115
115
|
end
|
116
116
|
end
|
117
|
-
rescue Kumi::Errors::SemanticError => e
|
117
|
+
rescue Kumi::Core::Errors::SemanticError => e
|
118
118
|
puts " → #{e.message}"
|
119
119
|
end
|
120
120
|
|
@@ -134,7 +134,7 @@ begin
|
|
134
134
|
value :result, fn(:nonexistent_function, input.text)
|
135
135
|
end
|
136
136
|
end
|
137
|
-
rescue Kumi::Errors::TypeError => e
|
137
|
+
rescue Kumi::Core::Errors::TypeError => e
|
138
138
|
puts " → #{e.message}"
|
139
139
|
end
|
140
140
|
|
@@ -162,7 +162,7 @@ begin
|
|
162
162
|
value :result, ref(:undefined_declaration)
|
163
163
|
end
|
164
164
|
end
|
165
|
-
rescue Kumi::Errors::SemanticError => e
|
165
|
+
rescue Kumi::Core::Errors::SemanticError => e
|
166
166
|
puts " → " + e.message.split("\n").join("\n → ")
|
167
167
|
end
|
168
168
|
|
data/lib/kumi/analyzer.rb
CHANGED
@@ -7,21 +7,21 @@ module Kumi
|
|
7
7
|
module_function
|
8
8
|
|
9
9
|
DEFAULT_PASSES = [
|
10
|
-
Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
|
11
|
-
Passes::InputCollector, # 2. Collects field metadata from input declarations.
|
12
|
-
Passes::DeclarationValidator, # 3. Checks the basic structure of each rule.
|
13
|
-
Passes::SemanticConstraintValidator, # 4. Validates DSL semantic constraints at AST level.
|
14
|
-
Passes::DependencyResolver, # 5. Builds the dependency graph with conditional dependencies.
|
15
|
-
Passes::UnsatDetector, # 6. Detects unsatisfiable constraints and analyzes cascade mutual exclusion.
|
16
|
-
Passes::Toposorter, # 7. Creates the final evaluation order, allowing safe cycles.
|
17
|
-
Passes::BroadcastDetector, # 8. Detects which operations should be broadcast over arrays (must run before type inference).
|
18
|
-
Passes::TypeInferencer, # 9. Infers types for all declarations (uses vectorization metadata).
|
19
|
-
Passes::TypeConsistencyChecker, # 10. Validates declared vs inferred type consistency.
|
20
|
-
Passes::TypeChecker # 11. Validates types using inferred information.
|
10
|
+
Core::Analyzer::Passes::NameIndexer, # 1. Finds all names and checks for duplicates.
|
11
|
+
Core::Analyzer::Passes::InputCollector, # 2. Collects field metadata from input declarations.
|
12
|
+
Core::Analyzer::Passes::DeclarationValidator, # 3. Checks the basic structure of each rule.
|
13
|
+
Core::Analyzer::Passes::SemanticConstraintValidator, # 4. Validates DSL semantic constraints at AST level.
|
14
|
+
Core::Analyzer::Passes::DependencyResolver, # 5. Builds the dependency graph with conditional dependencies.
|
15
|
+
Core::Analyzer::Passes::UnsatDetector, # 6. Detects unsatisfiable constraints and analyzes cascade mutual exclusion.
|
16
|
+
Core::Analyzer::Passes::Toposorter, # 7. Creates the final evaluation order, allowing safe cycles.
|
17
|
+
Core::Analyzer::Passes::BroadcastDetector, # 8. Detects which operations should be broadcast over arrays (must run before type inference).
|
18
|
+
Core::Analyzer::Passes::TypeInferencer, # 9. Infers types for all declarations (uses vectorization metadata).
|
19
|
+
Core::Analyzer::Passes::TypeConsistencyChecker, # 10. Validates declared vs inferred type consistency.
|
20
|
+
Core::Analyzer::Passes::TypeChecker # 11. Validates types using inferred information.
|
21
21
|
].freeze
|
22
22
|
|
23
|
-
def analyze!(schema, passes: DEFAULT_PASSES, **opts)
|
24
|
-
state = AnalysisState.new(opts)
|
23
|
+
def self.analyze!(schema, passes: DEFAULT_PASSES, **opts)
|
24
|
+
state = Core::Analyzer::AnalysisState.new(opts)
|
25
25
|
errors = []
|
26
26
|
|
27
27
|
state = run_analysis_passes(schema, passes, state, errors)
|
@@ -35,7 +35,7 @@ module Kumi
|
|
35
35
|
begin
|
36
36
|
state = pass_instance.run(errors)
|
37
37
|
rescue StandardError => e
|
38
|
-
errors << ErrorReporter.create_error(e.message, location: nil, type: :semantic)
|
38
|
+
errors << Core::ErrorReporter.create_error(e.message, location: nil, type: :semantic)
|
39
39
|
end
|
40
40
|
end
|
41
41
|
state
|
@@ -62,7 +62,7 @@ module Kumi
|
|
62
62
|
end
|
63
63
|
|
64
64
|
# Handle both old and new error formats for backward compatibility
|
65
|
-
def format_errors(errors)
|
65
|
+
def self.format_errors(errors)
|
66
66
|
return "" if errors.empty?
|
67
67
|
|
68
68
|
errors.map(&:to_s).join("\n")
|
data/lib/kumi/compiler.rb
CHANGED
@@ -158,7 +158,7 @@ module Kumi
|
|
158
158
|
compile_declaration(decl)
|
159
159
|
end
|
160
160
|
|
161
|
-
CompiledSchema.new(@bindings.freeze)
|
161
|
+
Core::CompiledSchema.new(@bindings.freeze)
|
162
162
|
end
|
163
163
|
|
164
164
|
private
|
@@ -218,7 +218,7 @@ module Kumi
|
|
218
218
|
return false unless broadcast_meta
|
219
219
|
|
220
220
|
# Reduction functions are NOT vectorized operations - they consume arrays
|
221
|
-
return false if
|
221
|
+
return false if Kumi::Registry.reducer?(expr.fn_name)
|
222
222
|
|
223
223
|
expr.args.any? do |arg|
|
224
224
|
case arg
|
@@ -244,7 +244,7 @@ module Kumi
|
|
244
244
|
vectorized_function_call(name, values)
|
245
245
|
else
|
246
246
|
# All arguments are scalars - regular function call
|
247
|
-
fn =
|
247
|
+
fn = Kumi::Registry.fetch(name)
|
248
248
|
fn.call(*values)
|
249
249
|
end
|
250
250
|
rescue StandardError => e
|
@@ -257,7 +257,7 @@ module Kumi
|
|
257
257
|
|
258
258
|
def vectorized_function_call(fn_name, values)
|
259
259
|
# Get the function from registry
|
260
|
-
fn =
|
260
|
+
fn = Kumi::Registry.fetch(fn_name)
|
261
261
|
|
262
262
|
# Find array dimensions for broadcasting
|
263
263
|
array_values = values.select { |v| v.is_a?(Array) }
|
@@ -276,14 +276,14 @@ module Kumi
|
|
276
276
|
end
|
277
277
|
|
278
278
|
def invoke_function(name, arg_fns, ctx, loc)
|
279
|
-
fn =
|
279
|
+
fn = Kumi::Registry.fetch(name)
|
280
280
|
values = arg_fns.map { |fn| fn.call(ctx) }
|
281
281
|
fn.call(*values)
|
282
282
|
rescue StandardError => e
|
283
283
|
# Preserve original error class and backtrace while adding context
|
284
284
|
enhanced_message = "Error calling fn(:#{name}) at #{loc}: #{e.message}"
|
285
285
|
|
286
|
-
if e.is_a?(Kumi::Errors::Error)
|
286
|
+
if e.is_a?(Kumi::Core::Errors::Error)
|
287
287
|
# Re-raise Kumi errors with enhanced message but preserve type
|
288
288
|
e.define_singleton_method(:message) { enhanced_message }
|
289
289
|
raise e
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
# Simple immutable state wrapper to prevent accidental mutations between passes
|
7
|
+
class AnalysisState
|
8
|
+
def initialize(data = {})
|
9
|
+
@data = data.dup.freeze
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get a value (same as hash access)
|
13
|
+
def [](key)
|
14
|
+
@data[key]
|
15
|
+
end
|
16
|
+
|
17
|
+
# Check if key exists (same as hash)
|
18
|
+
def key?(key)
|
19
|
+
@data.key?(key)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Get all keys (same as hash)
|
23
|
+
def keys
|
24
|
+
@data.keys
|
25
|
+
end
|
26
|
+
|
27
|
+
# Create new state with additional data (simple and clean)
|
28
|
+
def with(key, value)
|
29
|
+
AnalysisState.new(@data.merge(key => value))
|
30
|
+
end
|
31
|
+
|
32
|
+
# Convert back to hash for final result
|
33
|
+
def to_h
|
34
|
+
@data.dup
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
class ConstantEvaluator
|
7
|
+
include Syntax
|
8
|
+
|
9
|
+
def initialize(definitions)
|
10
|
+
@definitions = definitions
|
11
|
+
@memo = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
OPERATORS = {
|
15
|
+
add: :+,
|
16
|
+
subtract: :-,
|
17
|
+
multiply: :*,
|
18
|
+
divide: :/
|
19
|
+
}.freeze
|
20
|
+
|
21
|
+
def evaluate(node, visited = Set.new)
|
22
|
+
return :unknown unless node
|
23
|
+
return @memo[node] if @memo.key?(node)
|
24
|
+
return node.value if node.is_a?(Literal)
|
25
|
+
|
26
|
+
result = case node
|
27
|
+
when DeclarationReference then evaluate_binding(node, visited)
|
28
|
+
when CallExpression then evaluate_call_expression(node, visited)
|
29
|
+
else :unknown
|
30
|
+
end
|
31
|
+
|
32
|
+
@memo[node] = result unless result == :unknown
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def evaluate_binding(node, visited)
|
39
|
+
return :unknown if visited.include?(node.name)
|
40
|
+
|
41
|
+
visited << node.name
|
42
|
+
definition = @definitions[node.name]
|
43
|
+
return :unknown unless definition
|
44
|
+
|
45
|
+
evaluate(definition.expression, visited)
|
46
|
+
end
|
47
|
+
|
48
|
+
def evaluate_call_expression(node, visited)
|
49
|
+
return :unknown unless OPERATORS.key?(node.fn_name)
|
50
|
+
|
51
|
+
args = node.args.map { |arg| evaluate(arg, visited) }
|
52
|
+
return :unknown if args.any?(:unknown)
|
53
|
+
|
54
|
+
args.reduce(OPERATORS[node.fn_name])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# Detects which operations should be broadcast over arrays
|
8
|
+
# DEPENDENCIES: :inputs, :declarations
|
9
|
+
# PRODUCES: :broadcasts
|
10
|
+
class BroadcastDetector < PassBase
|
11
|
+
def run(errors)
|
12
|
+
input_meta = get_state(:inputs) || {}
|
13
|
+
definitions = get_state(:declarations) || {}
|
14
|
+
|
15
|
+
# Find array fields with their element types
|
16
|
+
array_fields = find_array_fields(input_meta)
|
17
|
+
|
18
|
+
# Build compiler metadata
|
19
|
+
compiler_metadata = {
|
20
|
+
array_fields: array_fields,
|
21
|
+
vectorized_operations: {},
|
22
|
+
reduction_operations: {}
|
23
|
+
}
|
24
|
+
|
25
|
+
# Track which values are vectorized for type inference
|
26
|
+
vectorized_values = {}
|
27
|
+
|
28
|
+
# Analyze traits first, then values (to handle dependencies)
|
29
|
+
traits = definitions.select { |_name, decl| decl.is_a?(Kumi::Syntax::TraitDeclaration) }
|
30
|
+
values = definitions.select { |_name, decl| decl.is_a?(Kumi::Syntax::ValueDeclaration) }
|
31
|
+
|
32
|
+
(traits.to_a + values.to_a).each do |name, decl|
|
33
|
+
result = analyze_value_vectorization(name, decl.expression, array_fields, vectorized_values, errors)
|
34
|
+
|
35
|
+
case result[:type]
|
36
|
+
when :vectorized
|
37
|
+
compiler_metadata[:vectorized_operations][name] = result[:info]
|
38
|
+
# Store array source information for dimension checking
|
39
|
+
array_source = extract_array_source(result[:info], array_fields)
|
40
|
+
vectorized_values[name] = { vectorized: true, array_source: array_source }
|
41
|
+
when :reduction
|
42
|
+
compiler_metadata[:reduction_operations][name] = result[:info]
|
43
|
+
# Reduction produces scalar, not vectorized
|
44
|
+
vectorized_values[name] = { vectorized: false }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
state.with(:broadcasts, compiler_metadata.freeze)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def find_array_fields(input_meta)
|
54
|
+
result = {}
|
55
|
+
input_meta.each do |name, meta|
|
56
|
+
next unless meta[:type] == :array && meta[:children]
|
57
|
+
|
58
|
+
result[name] = {
|
59
|
+
element_fields: meta[:children].keys,
|
60
|
+
element_types: meta[:children].transform_values { |v| v[:type] || :any }
|
61
|
+
}
|
62
|
+
end
|
63
|
+
result
|
64
|
+
end
|
65
|
+
|
66
|
+
def analyze_value_vectorization(name, expr, array_fields, vectorized_values, errors)
|
67
|
+
case expr
|
68
|
+
when Kumi::Syntax::InputElementReference
|
69
|
+
if array_fields.key?(expr.path.first)
|
70
|
+
{ type: :vectorized, info: { source: :array_field_access, path: expr.path } }
|
71
|
+
else
|
72
|
+
{ type: :scalar }
|
73
|
+
end
|
74
|
+
|
75
|
+
when Kumi::Syntax::DeclarationReference
|
76
|
+
# Check if this references a vectorized value
|
77
|
+
vector_info = vectorized_values[expr.name]
|
78
|
+
if vector_info && vector_info[:vectorized]
|
79
|
+
{ type: :vectorized, info: { source: :vectorized_declaration, name: expr.name } }
|
80
|
+
else
|
81
|
+
{ type: :scalar }
|
82
|
+
end
|
83
|
+
|
84
|
+
when Kumi::Syntax::CallExpression
|
85
|
+
analyze_call_vectorization(name, expr, array_fields, vectorized_values, errors)
|
86
|
+
|
87
|
+
when Kumi::Syntax::CascadeExpression
|
88
|
+
analyze_cascade_vectorization(name, expr, array_fields, vectorized_values, errors)
|
89
|
+
|
90
|
+
else
|
91
|
+
{ type: :scalar }
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def analyze_call_vectorization(_name, expr, array_fields, vectorized_values, errors)
|
96
|
+
# Check if this is a reduction function using function registry metadata
|
97
|
+
if Kumi::Registry.reducer?(expr.fn_name)
|
98
|
+
# Only treat as reduction if the argument is actually vectorized
|
99
|
+
arg_info = analyze_argument_vectorization(expr.args.first, array_fields, vectorized_values)
|
100
|
+
if arg_info[:vectorized]
|
101
|
+
{ type: :reduction, info: { function: expr.fn_name, source: arg_info[:source] } }
|
102
|
+
else
|
103
|
+
# Not a vectorized reduction - just a regular function call
|
104
|
+
{ type: :scalar }
|
105
|
+
end
|
106
|
+
|
107
|
+
else
|
108
|
+
# Special case: all?, any?, none? functions with vectorized trait arguments should be treated as vectorized
|
109
|
+
# for cascade condition purposes (they get transformed during compilation)
|
110
|
+
if %i[all? any? none?].include?(expr.fn_name) && expr.args.length == 1
|
111
|
+
arg = expr.args.first
|
112
|
+
if arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.length == 1
|
113
|
+
trait_ref = arg.elements.first
|
114
|
+
if trait_ref.is_a?(Kumi::Syntax::DeclarationReference) && vectorized_values[trait_ref.name]&.[](:vectorized)
|
115
|
+
return { type: :vectorized, info: { source: :cascade_condition_with_vectorized_trait, trait: trait_ref.name } }
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# ANY function with vectorized arguments becomes vectorized (with broadcasting)
|
121
|
+
arg_infos = expr.args.map { |arg| analyze_argument_vectorization(arg, array_fields, vectorized_values) }
|
122
|
+
|
123
|
+
if arg_infos.any? { |info| info[:vectorized] }
|
124
|
+
# Check for dimension mismatches when multiple arguments are vectorized
|
125
|
+
vectorized_sources = arg_infos.select { |info| info[:vectorized] }.filter_map { |info| info[:array_source] }.uniq
|
126
|
+
|
127
|
+
if vectorized_sources.length > 1
|
128
|
+
# Multiple different array sources - this is a dimension mismatch
|
129
|
+
# Generate enhanced error message with type information
|
130
|
+
enhanced_message = build_dimension_mismatch_error(expr, arg_infos, array_fields, vectorized_sources)
|
131
|
+
|
132
|
+
report_error(errors, enhanced_message, location: expr.loc, type: :semantic)
|
133
|
+
return { type: :scalar } # Treat as scalar to prevent further errors
|
134
|
+
end
|
135
|
+
|
136
|
+
# This is a vectorized operation - ANY function supports broadcasting
|
137
|
+
{ type: :vectorized, info: {
|
138
|
+
operation: expr.fn_name,
|
139
|
+
vectorized_args: arg_infos.map.with_index { |info, i| [i, info[:vectorized]] }.to_h
|
140
|
+
} }
|
141
|
+
else
|
142
|
+
{ type: :scalar }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
def analyze_argument_vectorization(arg, array_fields, vectorized_values)
|
148
|
+
case arg
|
149
|
+
when Kumi::Syntax::InputElementReference
|
150
|
+
if array_fields.key?(arg.path.first)
|
151
|
+
{ vectorized: true, source: :array_field, array_source: arg.path.first }
|
152
|
+
else
|
153
|
+
{ vectorized: false }
|
154
|
+
end
|
155
|
+
|
156
|
+
when Kumi::Syntax::DeclarationReference
|
157
|
+
# Check if this references a vectorized value
|
158
|
+
vector_info = vectorized_values[arg.name]
|
159
|
+
if vector_info && vector_info[:vectorized]
|
160
|
+
array_source = vector_info[:array_source]
|
161
|
+
{ vectorized: true, source: :vectorized_value, array_source: array_source }
|
162
|
+
else
|
163
|
+
{ vectorized: false }
|
164
|
+
end
|
165
|
+
|
166
|
+
when Kumi::Syntax::CallExpression
|
167
|
+
# Recursively check
|
168
|
+
result = analyze_value_vectorization(nil, arg, array_fields, vectorized_values, [])
|
169
|
+
{ vectorized: result[:type] == :vectorized, source: :expression }
|
170
|
+
|
171
|
+
else
|
172
|
+
{ vectorized: false }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def extract_array_source(info, _array_fields)
|
177
|
+
case info[:source]
|
178
|
+
when :array_field_access
|
179
|
+
info[:path]&.first
|
180
|
+
when :cascade_condition_with_vectorized_trait
|
181
|
+
# For cascades, we'd need to trace back to the original source
|
182
|
+
nil # TODO: Could be enhanced to trace through trait dependencies
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
def analyze_cascade_vectorization(_name, expr, array_fields, vectorized_values, errors)
|
187
|
+
# A cascade is vectorized if:
|
188
|
+
# 1. Any of its result expressions are vectorized, OR
|
189
|
+
# 2. Any of its conditions reference vectorized values (traits or arrays)
|
190
|
+
vectorized_results = []
|
191
|
+
vectorized_conditions = []
|
192
|
+
|
193
|
+
expr.cases.each do |case_expr|
|
194
|
+
# Check if result is vectorized
|
195
|
+
result_info = analyze_value_vectorization(nil, case_expr.result, array_fields, vectorized_values, errors)
|
196
|
+
vectorized_results << (result_info[:type] == :vectorized)
|
197
|
+
|
198
|
+
# Check if condition is vectorized
|
199
|
+
condition_info = analyze_value_vectorization(nil, case_expr.condition, array_fields, vectorized_values, errors)
|
200
|
+
vectorized_conditions << (condition_info[:type] == :vectorized)
|
201
|
+
end
|
202
|
+
|
203
|
+
if vectorized_results.any? || vectorized_conditions.any?
|
204
|
+
{ type: :vectorized, info: { source: :cascade_with_vectorized_conditions_or_results } }
|
205
|
+
else
|
206
|
+
{ type: :scalar }
|
207
|
+
end
|
208
|
+
end
|
209
|
+
|
210
|
+
def build_dimension_mismatch_error(_expr, arg_infos, array_fields, vectorized_sources)
|
211
|
+
# Build detailed error message with type information
|
212
|
+
summary = "Cannot broadcast operation across arrays from different sources: #{vectorized_sources.join(', ')}. "
|
213
|
+
|
214
|
+
problem_desc = "Problem: Multiple operands are arrays from different sources:\n"
|
215
|
+
|
216
|
+
vectorized_args = arg_infos.select { |info| info[:vectorized] }
|
217
|
+
vectorized_args.each_with_index do |arg_info, index|
|
218
|
+
array_source = arg_info[:array_source]
|
219
|
+
next unless array_source && array_fields[array_source]
|
220
|
+
|
221
|
+
# Determine the type based on array field metadata
|
222
|
+
type_desc = determine_array_type(array_source, array_fields)
|
223
|
+
problem_desc += " - Operand #{index + 1} resolves to #{type_desc} from array '#{array_source}'\n"
|
224
|
+
end
|
225
|
+
|
226
|
+
explanation = "Direct operations on arrays from different sources is ambiguous and not supported. " \
|
227
|
+
"Vectorized operations can only work on fields from the same array input."
|
228
|
+
|
229
|
+
"#{summary}#{problem_desc}#{explanation}"
|
230
|
+
end
|
231
|
+
|
232
|
+
def determine_array_type(array_source, array_fields)
|
233
|
+
field_info = array_fields[array_source]
|
234
|
+
return "array(any)" unless field_info[:element_types]
|
235
|
+
|
236
|
+
# For nested arrays (like items.name where items is an array), this represents array(element_type)
|
237
|
+
element_types = field_info[:element_types].values.uniq
|
238
|
+
if element_types.length == 1
|
239
|
+
"array(#{element_types.first})"
|
240
|
+
else
|
241
|
+
"array(mixed)"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# RESPONSIBILITY: Perform local structural validation on each declaration
|
8
|
+
# DEPENDENCIES: :definitions
|
9
|
+
# PRODUCES: None (validation only)
|
10
|
+
# INTERFACE: new(schema, state).run(errors)
|
11
|
+
class DeclarationValidator < VisitorPass
|
12
|
+
def run(errors)
|
13
|
+
each_decl do |decl|
|
14
|
+
visit(decl) { |node| validate_node(node, errors) }
|
15
|
+
end
|
16
|
+
state
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def validate_node(node, errors)
|
22
|
+
case node
|
23
|
+
when Kumi::Syntax::ValueDeclaration
|
24
|
+
validate_attribute(node, errors)
|
25
|
+
when Kumi::Syntax::TraitDeclaration
|
26
|
+
validate_trait(node, errors)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def validate_attribute(node, errors)
|
31
|
+
return unless node.expression.nil?
|
32
|
+
|
33
|
+
report_error(errors, "attribute `#{node.name}` requires an expression", location: node.loc)
|
34
|
+
end
|
35
|
+
|
36
|
+
def validate_trait(node, errors)
|
37
|
+
return if node.expression.is_a?(Kumi::Syntax::CallExpression)
|
38
|
+
|
39
|
+
report_error(errors, "trait `#{node.name}` must wrap a CallExpression", location: node.loc)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|