kumi 0.0.6 → 0.0.7
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 +33 -176
- data/README.md +33 -2
- data/docs/SYNTAX.md +2 -7
- data/docs/features/array-broadcasting.md +1 -1
- data/docs/schema_metadata/broadcasts.md +53 -0
- data/docs/schema_metadata/cascades.md +45 -0
- data/docs/schema_metadata/declarations.md +54 -0
- data/docs/schema_metadata/dependencies.md +57 -0
- data/docs/schema_metadata/evaluation_order.md +29 -0
- data/docs/schema_metadata/examples.md +95 -0
- data/docs/schema_metadata/inferred_types.md +46 -0
- data/docs/schema_metadata/inputs.md +86 -0
- data/docs/schema_metadata.md +108 -0
- data/lib/kumi/analyzer/passes/broadcast_detector.rb +52 -57
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +8 -8
- data/lib/kumi/analyzer/passes/input_collector.rb +2 -2
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +15 -16
- data/lib/kumi/analyzer/passes/toposorter.rb +23 -23
- data/lib/kumi/analyzer/passes/type_checker.rb +7 -9
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -2
- data/lib/kumi/analyzer/passes/type_inferencer.rb +24 -24
- data/lib/kumi/analyzer/passes/unsat_detector.rb +11 -13
- data/lib/kumi/analyzer.rb +5 -5
- data/lib/kumi/compiler.rb +39 -45
- data/lib/kumi/error_reporting.rb +1 -1
- data/lib/kumi/explain.rb +12 -0
- data/lib/kumi/export/node_registry.rb +2 -2
- data/lib/kumi/json_schema/generator.rb +63 -0
- data/lib/kumi/json_schema/validator.rb +25 -0
- data/lib/kumi/json_schema.rb +14 -0
- data/lib/kumi/{parser → ruby_parser}/build_context.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/declaration_reference_proxy.rb +3 -3
- data/lib/kumi/{parser → ruby_parser}/dsl.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/dsl_cascade_builder.rb +2 -2
- data/lib/kumi/{parser → ruby_parser}/expression_converter.rb +14 -14
- data/lib/kumi/{parser → ruby_parser}/guard_rails.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/input_builder.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/input_field_proxy.rb +4 -4
- data/lib/kumi/{parser → ruby_parser}/input_proxy.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/nested_input.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/parser.rb +11 -10
- data/lib/kumi/{parser → ruby_parser}/schema_builder.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/sugar.rb +1 -1
- data/lib/kumi/ruby_parser.rb +10 -0
- data/lib/kumi/schema.rb +10 -4
- data/lib/kumi/schema_instance.rb +6 -6
- data/lib/kumi/schema_metadata.rb +524 -0
- data/lib/kumi/vectorization_metadata.rb +4 -4
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +14 -0
- metadata +28 -15
- data/lib/generators/trait_engine/templates/schema_spec.rb.erb +0 -27
@@ -0,0 +1,95 @@
|
|
1
|
+
# Schema Metadata Examples
|
2
|
+
|
3
|
+
For comprehensive API documentation with detailed examples, see the YARD documentation in the SchemaMetadata class.
|
4
|
+
|
5
|
+
## Basic Usage
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class TaxSchema
|
9
|
+
extend Kumi::Schema
|
10
|
+
|
11
|
+
schema do
|
12
|
+
input do
|
13
|
+
integer :income, domain: 0..1_000_000
|
14
|
+
string :filing_status, domain: %w[single married]
|
15
|
+
integer :age, domain: 18..100
|
16
|
+
end
|
17
|
+
|
18
|
+
trait :adult, (input.age >= 18)
|
19
|
+
trait :high_income, (input.income > 100_000)
|
20
|
+
|
21
|
+
value :tax_rate do
|
22
|
+
on high_income, 0.25
|
23
|
+
base 0.15
|
24
|
+
end
|
25
|
+
|
26
|
+
value :tax_amount, input.income * tax_rate
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Access schema metadata - clean object interface!
|
31
|
+
metadata = TaxSchema.schema_metadata
|
32
|
+
|
33
|
+
# Processed semantic metadata (rich, transformed from AST)
|
34
|
+
puts metadata.inputs
|
35
|
+
# => { :income => { type: :integer, domain: {...}, required: true }, ... }
|
36
|
+
|
37
|
+
puts metadata.values
|
38
|
+
# => { :tax_rate => { type: :float, cascade: {...} }, ... }
|
39
|
+
|
40
|
+
puts metadata.traits
|
41
|
+
# => { :adult => { type: :boolean, condition: "input.age >= 18" }, ... }
|
42
|
+
|
43
|
+
# Raw analyzer state (direct from analysis passes)
|
44
|
+
puts metadata.evaluation_order
|
45
|
+
# => [:adult, :high_income, :tax_rate, :tax_amount]
|
46
|
+
|
47
|
+
puts metadata.dependencies
|
48
|
+
# => { :tax_amount => [#<Edge to: :tax_rate>, #<Edge to: :income>], ... }
|
49
|
+
|
50
|
+
puts metadata.inferred_types
|
51
|
+
# => { :adult => :boolean, :tax_rate => :float, :tax_amount => :float }
|
52
|
+
|
53
|
+
# Serializable processed hash
|
54
|
+
processed_hash = metadata.to_h
|
55
|
+
puts processed_hash.keys
|
56
|
+
# => [:inputs, :values, :traits, :functions]
|
57
|
+
|
58
|
+
# Raw analyzer state (contains AST nodes)
|
59
|
+
raw_state = metadata.analyzer_state
|
60
|
+
puts raw_state.keys
|
61
|
+
# => [:declarations, :inputs, :dependencies, :dependents, :leaves, :evaluation_order, :inferred_types, :cascades, :broadcasts]
|
62
|
+
```
|
63
|
+
|
64
|
+
## Tool Integration
|
65
|
+
|
66
|
+
```ruby
|
67
|
+
# Form generator example
|
68
|
+
def generate_form_fields(schema_class)
|
69
|
+
metadata = schema_class.schema_metadata
|
70
|
+
|
71
|
+
metadata.inputs.map do |field_name, field_info|
|
72
|
+
case field_info[:type]
|
73
|
+
when :integer
|
74
|
+
create_number_input(field_name, field_info[:domain])
|
75
|
+
when :string
|
76
|
+
create_select_input(field_name, field_info[:domain])
|
77
|
+
when :boolean
|
78
|
+
create_checkbox_input(field_name)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Dependency analysis example
|
84
|
+
def analyze_field_dependencies(schema_class, field_name)
|
85
|
+
metadata = schema_class.schema_metadata
|
86
|
+
|
87
|
+
# Find what depends on this field
|
88
|
+
dependents = metadata.dependents[field_name] || []
|
89
|
+
|
90
|
+
# Find what this field depends on
|
91
|
+
dependencies = metadata.dependencies[field_name]&.map(&:to) || []
|
92
|
+
|
93
|
+
{ affects: dependents, requires: dependencies }
|
94
|
+
end
|
95
|
+
```
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# Inferred Types Metadata
|
2
|
+
|
3
|
+
Type inference results for all declarations based on expression analysis.
|
4
|
+
|
5
|
+
## Access
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
metadata = MySchema.schema_metadata
|
9
|
+
types = metadata.inferred_types
|
10
|
+
```
|
11
|
+
|
12
|
+
## Structure
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
# Returns Hash<Symbol, Object>
|
16
|
+
{
|
17
|
+
declaration_name => type_specification
|
18
|
+
}
|
19
|
+
```
|
20
|
+
|
21
|
+
## Example
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
metadata.inferred_types
|
25
|
+
# => {
|
26
|
+
# :adult => :boolean,
|
27
|
+
# :age_group => :string,
|
28
|
+
# :tax_rate => :float,
|
29
|
+
# :count => :integer,
|
30
|
+
# :item_prices => { array: :float },
|
31
|
+
# :categories => { array: :string }
|
32
|
+
# }
|
33
|
+
```
|
34
|
+
|
35
|
+
## Type Values
|
36
|
+
|
37
|
+
- `:boolean`, `:string`, `:integer`, `:float`, `:any`
|
38
|
+
- `{ array: element_type }` for arrays
|
39
|
+
- `{ hash: { key: key_type, value: value_type } }` for hashes
|
40
|
+
|
41
|
+
## Usage
|
42
|
+
|
43
|
+
- Type checking
|
44
|
+
- Code generation
|
45
|
+
- Editor support
|
46
|
+
- Runtime validation
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# Input Metadata
|
2
|
+
|
3
|
+
Raw input field metadata extracted from `input` blocks during analysis.
|
4
|
+
|
5
|
+
## Access
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
metadata = MySchema.schema_metadata
|
9
|
+
|
10
|
+
# Processed input metadata (recommended for tools)
|
11
|
+
inputs = metadata.inputs
|
12
|
+
|
13
|
+
# Raw input metadata (advanced usage)
|
14
|
+
raw_inputs = metadata.analyzer_state[:inputs]
|
15
|
+
```
|
16
|
+
|
17
|
+
## Raw Structure
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
# Raw analyzer state format
|
21
|
+
{
|
22
|
+
field_name => {
|
23
|
+
type: Symbol, # :integer, :string, :float, :boolean, :array, etc.
|
24
|
+
domain: Range|Array, # optional domain constraints
|
25
|
+
children: Hash # for array/hash types
|
26
|
+
}
|
27
|
+
}
|
28
|
+
```
|
29
|
+
|
30
|
+
## Processed Structure
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
# Processed metadata format (via metadata.inputs)
|
34
|
+
{
|
35
|
+
field_name => {
|
36
|
+
type: Symbol, # normalized type
|
37
|
+
domain: Hash, # normalized domain metadata
|
38
|
+
required: Boolean # always true currently
|
39
|
+
}
|
40
|
+
}
|
41
|
+
```
|
42
|
+
|
43
|
+
## Examples
|
44
|
+
|
45
|
+
**Processed Input Metadata:**
|
46
|
+
```ruby
|
47
|
+
metadata.inputs
|
48
|
+
# => {
|
49
|
+
# :age => {
|
50
|
+
# type: :integer,
|
51
|
+
# domain: { type: :range, min: 0, max: 120, exclusive_end: false },
|
52
|
+
# required: true
|
53
|
+
# },
|
54
|
+
# :name => { type: :string, required: true },
|
55
|
+
# :active => { type: :boolean, required: true }
|
56
|
+
# }
|
57
|
+
```
|
58
|
+
|
59
|
+
**Raw Input Metadata:**
|
60
|
+
```ruby
|
61
|
+
metadata.analyzer_state[:inputs]
|
62
|
+
# => {
|
63
|
+
# :age => { type: :integer, domain: 0..120 },
|
64
|
+
# :name => { type: :string },
|
65
|
+
# :line_items => {
|
66
|
+
# type: :array,
|
67
|
+
# children: {
|
68
|
+
# :price => { type: :float, domain: 0..Float::INFINITY },
|
69
|
+
# :quantity => { type: :integer, domain: 1..100 }
|
70
|
+
# }
|
71
|
+
# }
|
72
|
+
# }
|
73
|
+
```
|
74
|
+
|
75
|
+
**Domain Types:**
|
76
|
+
- Range: `18..65`, `0..Float::INFINITY`
|
77
|
+
- Array: `%w[active inactive suspended]`
|
78
|
+
- Proc: Custom validation functions
|
79
|
+
|
80
|
+
## Usage
|
81
|
+
|
82
|
+
Form generators use this metadata to:
|
83
|
+
- Create appropriate input controls
|
84
|
+
- Set validation rules
|
85
|
+
- Build nested forms for arrays
|
86
|
+
- Generate type-safe schemas
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# Schema Metadata
|
2
|
+
|
3
|
+
Kumi's SchemaMetadata interface provides structured access to analyzed schema information for building external tools like form generators, documentation systems, and analysis utilities.
|
4
|
+
|
5
|
+
## Primary Interface
|
6
|
+
|
7
|
+
SchemaMetadata is the main interface for extracting metadata from Kumi schemas:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
metadata = MySchema.schema_metadata
|
11
|
+
```
|
12
|
+
|
13
|
+
See the comprehensive API documentation in the SchemaMetadata class for detailed method documentation, examples, and usage patterns.
|
14
|
+
|
15
|
+
## Processed Metadata (Tool-Friendly)
|
16
|
+
|
17
|
+
These methods return clean, serializable data structures:
|
18
|
+
|
19
|
+
| Method | Returns | Description |
|
20
|
+
|--------|---------|-------------|
|
21
|
+
| `inputs` | Hash | Input field metadata with normalized types and domains |
|
22
|
+
| `values` | Hash | Value declarations with dependencies and expressions |
|
23
|
+
| `traits` | Hash | Trait conditions with dependency information |
|
24
|
+
| `functions` | Hash | Function registry info for functions used in schema |
|
25
|
+
| `to_h` | Hash | Complete processed metadata (inputs, values, traits, functions) |
|
26
|
+
| `to_json` | String | JSON serialization of processed metadata |
|
27
|
+
| `to_json_schema` | Hash | JSON Schema document for input validation |
|
28
|
+
|
29
|
+
## Raw Analyzer State (Advanced)
|
30
|
+
|
31
|
+
Direct access to internal analyzer results:
|
32
|
+
|
33
|
+
| Method | Returns | Description |
|
34
|
+
|--------|---------|-------------|
|
35
|
+
| [`declarations`](schema_metadata/declarations.md) | Hash | Raw AST declaration nodes by name |
|
36
|
+
| [`dependencies`](schema_metadata/dependencies.md) | Hash | Dependency graph with Edge objects |
|
37
|
+
| `dependents` | Hash | Reverse dependency lookup |
|
38
|
+
| `leaves` | Hash | Leaf nodes (no dependencies) by type |
|
39
|
+
| [`evaluation_order`](schema_metadata/evaluation_order.md) | Array | Topologically sorted evaluation order |
|
40
|
+
| [`inferred_types`](schema_metadata/inferred_types.md) | Hash | Type inference results for declarations |
|
41
|
+
| [`cascades`](schema_metadata/cascades.md) | Hash | Cascade mutual exclusion analysis |
|
42
|
+
| [`broadcasts`](schema_metadata/broadcasts.md) | Hash | Array broadcasting operation metadata |
|
43
|
+
| `analyzer_state` | Hash | Complete raw analyzer state with AST nodes |
|
44
|
+
|
45
|
+
Note: Raw `inputs` metadata is available via `analyzer_state[:inputs]` but the processed `inputs` method is recommended for tool development.
|
46
|
+
|
47
|
+
## Usage Patterns
|
48
|
+
|
49
|
+
```ruby
|
50
|
+
# Tool development - use processed metadata
|
51
|
+
metadata = MySchema.schema_metadata
|
52
|
+
form_fields = metadata.inputs.map { |name, info| create_field(name, info) }
|
53
|
+
documentation = metadata.values.map { |name, info| document_value(name, info) }
|
54
|
+
|
55
|
+
# Advanced analysis - use raw state when needed
|
56
|
+
dependency_graph = metadata.dependencies
|
57
|
+
ast_nodes = metadata.declarations
|
58
|
+
evaluation_sequence = metadata.evaluation_order
|
59
|
+
```
|
60
|
+
|
61
|
+
## Data Structure Examples
|
62
|
+
|
63
|
+
### Processed Input Metadata
|
64
|
+
```ruby
|
65
|
+
metadata.inputs
|
66
|
+
# => {
|
67
|
+
# :age => { type: :integer, domain: { type: :range, min: 18, max: 65 }, required: true },
|
68
|
+
# :name => { type: :string, required: true },
|
69
|
+
# :items => { type: :array, required: true }
|
70
|
+
# }
|
71
|
+
```
|
72
|
+
|
73
|
+
### Processed Value Metadata
|
74
|
+
```ruby
|
75
|
+
metadata.values
|
76
|
+
# => {
|
77
|
+
# :tax_amount => {
|
78
|
+
# type: :float,
|
79
|
+
# dependencies: [:income, :tax_rate],
|
80
|
+
# computed: true,
|
81
|
+
# expression: "multiply(input.income, tax_rate)"
|
82
|
+
# }
|
83
|
+
# }
|
84
|
+
```
|
85
|
+
|
86
|
+
### Clean Public Interface Examples
|
87
|
+
```ruby
|
88
|
+
# Processed dependency information (clean hashes)
|
89
|
+
metadata.dependencies
|
90
|
+
# => { :tax_amount => [{ to: :income, conditional: false }, { to: :tax_rate, conditional: false }] }
|
91
|
+
|
92
|
+
# Processed declaration metadata (clean hashes)
|
93
|
+
metadata.declarations
|
94
|
+
# => { :adult => { type: :trait, expression: ">=(input.age, 18)" }, :tax_amount => { type: :value, expression: "multiply(input.income, tax_rate)" } }
|
95
|
+
|
96
|
+
# Type inference results (clean data)
|
97
|
+
metadata.inferred_types
|
98
|
+
# => { :adult => :boolean, :tax_amount => :float, :item_totals => { array: :float } }
|
99
|
+
```
|
100
|
+
|
101
|
+
### Raw Analyzer State (Advanced Usage)
|
102
|
+
```ruby
|
103
|
+
# Complete raw state hash with internal objects (AST nodes, Edge objects)
|
104
|
+
metadata.analyzer_state
|
105
|
+
# => { declarations: {AST nodes...}, dependencies: {Edge objects...}, ... }
|
106
|
+
```
|
107
|
+
|
108
|
+
See `docs/schema_metadata/` for detailed examples.
|
@@ -4,34 +4,33 @@ module Kumi
|
|
4
4
|
module Analyzer
|
5
5
|
module Passes
|
6
6
|
# Detects which operations should be broadcast over arrays
|
7
|
-
# DEPENDENCIES: :
|
8
|
-
# PRODUCES: :
|
7
|
+
# DEPENDENCIES: :inputs, :declarations
|
8
|
+
# PRODUCES: :broadcasts
|
9
9
|
class BroadcastDetector < PassBase
|
10
10
|
def run(errors)
|
11
|
-
input_meta = get_state(:
|
12
|
-
definitions = get_state(:
|
13
|
-
|
11
|
+
input_meta = get_state(:inputs) || {}
|
12
|
+
definitions = get_state(:declarations) || {}
|
13
|
+
|
14
14
|
# Find array fields with their element types
|
15
15
|
array_fields = find_array_fields(input_meta)
|
16
|
-
|
16
|
+
|
17
17
|
# Build compiler metadata
|
18
18
|
compiler_metadata = {
|
19
19
|
array_fields: array_fields,
|
20
20
|
vectorized_operations: {},
|
21
21
|
reduction_operations: {}
|
22
22
|
}
|
23
|
-
|
23
|
+
|
24
24
|
# Track which values are vectorized for type inference
|
25
25
|
vectorized_values = {}
|
26
|
-
|
26
|
+
|
27
27
|
# Analyze traits first, then values (to handle dependencies)
|
28
|
-
traits = definitions.select { |
|
29
|
-
values = definitions.select { |
|
30
|
-
|
28
|
+
traits = definitions.select { |_name, decl| decl.is_a?(Kumi::Syntax::TraitDeclaration) }
|
29
|
+
values = definitions.select { |_name, decl| decl.is_a?(Kumi::Syntax::ValueDeclaration) }
|
30
|
+
|
31
31
|
(traits.to_a + values.to_a).each do |name, decl|
|
32
32
|
result = analyze_value_vectorization(name, decl.expression, array_fields, vectorized_values, errors)
|
33
|
-
|
34
|
-
|
33
|
+
|
35
34
|
case result[:type]
|
36
35
|
when :vectorized
|
37
36
|
compiler_metadata[:vectorized_operations][name] = result[:info]
|
@@ -44,8 +43,8 @@ module Kumi
|
|
44
43
|
vectorized_values[name] = { vectorized: false }
|
45
44
|
end
|
46
45
|
end
|
47
|
-
|
48
|
-
state.with(:
|
46
|
+
|
47
|
+
state.with(:broadcasts, compiler_metadata.freeze)
|
49
48
|
end
|
50
49
|
|
51
50
|
private
|
@@ -53,12 +52,12 @@ module Kumi
|
|
53
52
|
def find_array_fields(input_meta)
|
54
53
|
result = {}
|
55
54
|
input_meta.each do |name, meta|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
}
|
61
|
-
|
55
|
+
next unless meta[:type] == :array && meta[:children]
|
56
|
+
|
57
|
+
result[name] = {
|
58
|
+
element_fields: meta[:children].keys,
|
59
|
+
element_types: meta[:children].transform_values { |v| v[:type] || :any }
|
60
|
+
}
|
62
61
|
end
|
63
62
|
result
|
64
63
|
end
|
@@ -71,7 +70,7 @@ module Kumi
|
|
71
70
|
else
|
72
71
|
{ type: :scalar }
|
73
72
|
end
|
74
|
-
|
73
|
+
|
75
74
|
when Kumi::Syntax::DeclarationReference
|
76
75
|
# Check if this references a vectorized value
|
77
76
|
vector_info = vectorized_values[expr.name]
|
@@ -80,19 +79,19 @@ module Kumi
|
|
80
79
|
else
|
81
80
|
{ type: :scalar }
|
82
81
|
end
|
83
|
-
|
82
|
+
|
84
83
|
when Kumi::Syntax::CallExpression
|
85
84
|
analyze_call_vectorization(name, expr, array_fields, vectorized_values, errors)
|
86
|
-
|
85
|
+
|
87
86
|
when Kumi::Syntax::CascadeExpression
|
88
87
|
analyze_cascade_vectorization(name, expr, array_fields, vectorized_values, errors)
|
89
|
-
|
88
|
+
|
90
89
|
else
|
91
90
|
{ type: :scalar }
|
92
91
|
end
|
93
92
|
end
|
94
93
|
|
95
|
-
def analyze_call_vectorization(
|
94
|
+
def analyze_call_vectorization(_name, expr, array_fields, vectorized_values, errors)
|
96
95
|
# Check if this is a reduction function using function registry metadata
|
97
96
|
if FunctionRegistry.reducer?(expr.fn_name)
|
98
97
|
# Only treat as reduction if the argument is actually vectorized
|
@@ -103,11 +102,11 @@ module Kumi
|
|
103
102
|
# Not a vectorized reduction - just a regular function call
|
104
103
|
{ type: :scalar }
|
105
104
|
end
|
106
|
-
|
105
|
+
|
107
106
|
else
|
108
107
|
# Special case: all?, any?, none? functions with vectorized trait arguments should be treated as vectorized
|
109
108
|
# for cascade condition purposes (they get transformed during compilation)
|
110
|
-
if [
|
109
|
+
if %i[all? any? none?].include?(expr.fn_name) && expr.args.length == 1
|
111
110
|
arg = expr.args.first
|
112
111
|
if arg.is_a?(Kumi::Syntax::ArrayExpression) && arg.elements.length == 1
|
113
112
|
trait_ref = arg.elements.first
|
@@ -116,28 +115,28 @@ module Kumi
|
|
116
115
|
end
|
117
116
|
end
|
118
117
|
end
|
119
|
-
|
118
|
+
|
120
119
|
# ANY function with vectorized arguments becomes vectorized (with broadcasting)
|
121
120
|
arg_infos = expr.args.map { |arg| analyze_argument_vectorization(arg, array_fields, vectorized_values) }
|
122
|
-
|
121
|
+
|
123
122
|
if arg_infos.any? { |info| info[:vectorized] }
|
124
123
|
# Check for dimension mismatches when multiple arguments are vectorized
|
125
|
-
vectorized_sources = arg_infos.select { |info| info[:vectorized] }.
|
126
|
-
|
124
|
+
vectorized_sources = arg_infos.select { |info| info[:vectorized] }.filter_map { |info| info[:array_source] }.uniq
|
125
|
+
|
127
126
|
if vectorized_sources.length > 1
|
128
127
|
# Multiple different array sources - this is a dimension mismatch
|
129
128
|
# Generate enhanced error message with type information
|
130
129
|
enhanced_message = build_dimension_mismatch_error(expr, arg_infos, array_fields, vectorized_sources)
|
131
|
-
|
130
|
+
|
132
131
|
report_error(errors, enhanced_message, location: expr.loc, type: :semantic)
|
133
|
-
return { type: :scalar }
|
132
|
+
return { type: :scalar } # Treat as scalar to prevent further errors
|
134
133
|
end
|
135
|
-
|
134
|
+
|
136
135
|
# 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
|
-
}}
|
136
|
+
{ type: :vectorized, info: {
|
137
|
+
operation: expr.fn_name,
|
138
|
+
vectorized_args: arg_infos.map.with_index { |info, i| [i, info[:vectorized]] }.to_h
|
139
|
+
} }
|
141
140
|
else
|
142
141
|
{ type: :scalar }
|
143
142
|
end
|
@@ -152,7 +151,7 @@ module Kumi
|
|
152
151
|
else
|
153
152
|
{ vectorized: false }
|
154
153
|
end
|
155
|
-
|
154
|
+
|
156
155
|
when Kumi::Syntax::DeclarationReference
|
157
156
|
# Check if this references a vectorized value
|
158
157
|
vector_info = vectorized_values[arg.name]
|
@@ -162,47 +161,44 @@ module Kumi
|
|
162
161
|
else
|
163
162
|
{ vectorized: false }
|
164
163
|
end
|
165
|
-
|
164
|
+
|
166
165
|
when Kumi::Syntax::CallExpression
|
167
166
|
# Recursively check
|
168
167
|
result = analyze_value_vectorization(nil, arg, array_fields, vectorized_values, [])
|
169
168
|
{ vectorized: result[:type] == :vectorized, source: :expression }
|
170
|
-
|
169
|
+
|
171
170
|
else
|
172
171
|
{ vectorized: false }
|
173
172
|
end
|
174
173
|
end
|
175
174
|
|
176
|
-
def extract_array_source(info,
|
175
|
+
def extract_array_source(info, _array_fields)
|
177
176
|
case info[:source]
|
178
177
|
when :array_field_access
|
179
178
|
info[:path]&.first
|
180
179
|
when :cascade_condition_with_vectorized_trait
|
181
180
|
# For cascades, we'd need to trace back to the original source
|
182
|
-
nil
|
183
|
-
else
|
184
|
-
nil
|
181
|
+
nil # TODO: Could be enhanced to trace through trait dependencies
|
185
182
|
end
|
186
183
|
end
|
187
184
|
|
188
|
-
def analyze_cascade_vectorization(
|
185
|
+
def analyze_cascade_vectorization(_name, expr, array_fields, vectorized_values, errors)
|
189
186
|
# A cascade is vectorized if:
|
190
187
|
# 1. Any of its result expressions are vectorized, OR
|
191
188
|
# 2. Any of its conditions reference vectorized values (traits or arrays)
|
192
189
|
vectorized_results = []
|
193
190
|
vectorized_conditions = []
|
194
|
-
|
191
|
+
|
195
192
|
expr.cases.each do |case_expr|
|
196
193
|
# Check if result is vectorized
|
197
194
|
result_info = analyze_value_vectorization(nil, case_expr.result, array_fields, vectorized_values, errors)
|
198
195
|
vectorized_results << (result_info[:type] == :vectorized)
|
199
|
-
|
196
|
+
|
200
197
|
# Check if condition is vectorized
|
201
198
|
condition_info = analyze_value_vectorization(nil, case_expr.condition, array_fields, vectorized_values, errors)
|
202
199
|
vectorized_conditions << (condition_info[:type] == :vectorized)
|
203
|
-
|
204
200
|
end
|
205
|
-
|
201
|
+
|
206
202
|
if vectorized_results.any? || vectorized_conditions.any?
|
207
203
|
{ type: :vectorized, info: { source: :cascade_with_vectorized_conditions_or_results } }
|
208
204
|
else
|
@@ -213,9 +209,9 @@ module Kumi
|
|
213
209
|
def build_dimension_mismatch_error(_expr, arg_infos, array_fields, vectorized_sources)
|
214
210
|
# Build detailed error message with type information
|
215
211
|
summary = "Cannot broadcast operation across arrays from different sources: #{vectorized_sources.join(', ')}. "
|
216
|
-
|
212
|
+
|
217
213
|
problem_desc = "Problem: Multiple operands are arrays from different sources:\n"
|
218
|
-
|
214
|
+
|
219
215
|
vectorized_args = arg_infos.select { |info| info[:vectorized] }
|
220
216
|
vectorized_args.each_with_index do |arg_info, index|
|
221
217
|
array_source = arg_info[:array_source]
|
@@ -225,10 +221,10 @@ module Kumi
|
|
225
221
|
type_desc = determine_array_type(array_source, array_fields)
|
226
222
|
problem_desc += " - Operand #{index + 1} resolves to #{type_desc} from array '#{array_source}'\n"
|
227
223
|
end
|
228
|
-
|
224
|
+
|
229
225
|
explanation = "Direct operations on arrays from different sources is ambiguous and not supported. " \
|
230
226
|
"Vectorized operations can only work on fields from the same array input."
|
231
|
-
|
227
|
+
|
232
228
|
"#{summary}#{problem_desc}#{explanation}"
|
233
229
|
end
|
234
230
|
|
@@ -244,8 +240,7 @@ module Kumi
|
|
244
240
|
"array(mixed)"
|
245
241
|
end
|
246
242
|
end
|
247
|
-
|
248
243
|
end
|
249
244
|
end
|
250
245
|
end
|
251
|
-
end
|
246
|
+
end
|
@@ -4,8 +4,8 @@ module Kumi
|
|
4
4
|
module Analyzer
|
5
5
|
module Passes
|
6
6
|
# RESPONSIBILITY: Build dependency graph and detect conditional dependencies in cascades
|
7
|
-
# DEPENDENCIES: :
|
8
|
-
# PRODUCES: :
|
7
|
+
# DEPENDENCIES: :declarations from NameIndexer, :inputs from InputCollector
|
8
|
+
# PRODUCES: :dependencies, :dependents, :leaves - Dependency analysis results
|
9
9
|
# INTERFACE: new(schema, state).run(errors)
|
10
10
|
class DependencyResolver < PassBase
|
11
11
|
# Enhanced edge with conditional flag and cascade metadata
|
@@ -24,8 +24,8 @@ module Kumi
|
|
24
24
|
include Syntax
|
25
25
|
|
26
26
|
def run(errors)
|
27
|
-
definitions = get_state(:
|
28
|
-
input_meta = get_state(:
|
27
|
+
definitions = get_state(:declarations)
|
28
|
+
input_meta = get_state(:inputs)
|
29
29
|
|
30
30
|
dependency_graph = Hash.new { |h, k| h[k] = [] }
|
31
31
|
reverse_dependencies = Hash.new { |h, k| h[k] = [] }
|
@@ -41,14 +41,14 @@ module Kumi
|
|
41
41
|
# Compute transitive closure of reverse dependencies
|
42
42
|
transitive_dependents = compute_transitive_closure(reverse_dependencies)
|
43
43
|
|
44
|
-
state.with(:
|
45
|
-
.with(:
|
46
|
-
.with(:
|
44
|
+
state.with(:dependencies, dependency_graph.transform_values(&:freeze).freeze)
|
45
|
+
.with(:dependents, transitive_dependents.freeze)
|
46
|
+
.with(:leaves, leaf_map.transform_values(&:freeze).freeze)
|
47
47
|
end
|
48
48
|
|
49
49
|
private
|
50
50
|
|
51
|
-
def process_node(node, decl, graph, reverse_deps, leaves, definitions,
|
51
|
+
def process_node(node, decl, graph, reverse_deps, leaves, definitions, _input_meta, errors, context)
|
52
52
|
case node
|
53
53
|
when DeclarationReference
|
54
54
|
report_error(errors, "undefined reference to `#{node.name}`", location: node.loc) unless definitions.key?(node.name)
|
@@ -5,7 +5,7 @@ module Kumi
|
|
5
5
|
module Passes
|
6
6
|
# RESPONSIBILITY: Collect field metadata from input declarations and validate consistency
|
7
7
|
# DEPENDENCIES: :definitions
|
8
|
-
# PRODUCES: :
|
8
|
+
# PRODUCES: :inputs - Hash mapping field names to {type:, domain:} metadata
|
9
9
|
# INTERFACE: new(schema, state).run(errors)
|
10
10
|
class InputCollector < PassBase
|
11
11
|
def run(errors)
|
@@ -30,7 +30,7 @@ module Kumi
|
|
30
30
|
end
|
31
31
|
end
|
32
32
|
|
33
|
-
state.with(:
|
33
|
+
state.with(:inputs, freeze_nested_hash(input_meta))
|
34
34
|
end
|
35
35
|
|
36
36
|
private
|
@@ -5,7 +5,7 @@ module Kumi
|
|
5
5
|
module Passes
|
6
6
|
# RESPONSIBILITY: Build definitions index and detect duplicate names
|
7
7
|
# DEPENDENCIES: None (first pass in pipeline)
|
8
|
-
# PRODUCES: :
|
8
|
+
# PRODUCES: :declarations - Hash mapping names to declaration nodes
|
9
9
|
# INTERFACE: new(schema, state).run(errors)
|
10
10
|
class NameIndexer < PassBase
|
11
11
|
def run(errors)
|
@@ -16,7 +16,7 @@ module Kumi
|
|
16
16
|
definitions[decl.name] = decl
|
17
17
|
end
|
18
18
|
|
19
|
-
state.with(:
|
19
|
+
state.with(:declarations, definitions.freeze)
|
20
20
|
end
|
21
21
|
end
|
22
22
|
end
|