kumi 0.0.5 → 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 +76 -174
- data/README.md +205 -52
- data/{documents → docs}/AST.md +29 -29
- data/{documents → docs}/SYNTAX.md +95 -8
- data/docs/features/README.md +45 -0
- data/docs/features/analysis-cascade-mutual-exclusion.md +89 -0
- data/docs/features/analysis-type-inference.md +42 -0
- data/docs/features/analysis-unsat-detection.md +71 -0
- data/docs/features/array-broadcasting.md +170 -0
- data/docs/features/input-declaration-system.md +42 -0
- data/docs/features/performance.md +16 -0
- 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/examples/federal_tax_calculator_2024.rb +11 -6
- data/lib/kumi/analyzer/constant_evaluator.rb +1 -1
- data/lib/kumi/analyzer/passes/broadcast_detector.rb +246 -0
- data/lib/kumi/analyzer/passes/{definition_validator.rb → declaration_validator.rb} +4 -4
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +78 -38
- data/lib/kumi/analyzer/passes/input_collector.rb +91 -30
- data/lib/kumi/analyzer/passes/name_indexer.rb +2 -2
- data/lib/kumi/analyzer/passes/pass_base.rb +1 -1
- data/lib/kumi/analyzer/passes/semantic_constraint_validator.rb +24 -25
- data/lib/kumi/analyzer/passes/toposorter.rb +44 -8
- data/lib/kumi/analyzer/passes/type_checker.rb +34 -14
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +2 -2
- data/lib/kumi/analyzer/passes/type_inferencer.rb +130 -21
- data/lib/kumi/analyzer/passes/unsat_detector.rb +134 -56
- data/lib/kumi/analyzer/passes/visitor_pass.rb +2 -2
- data/lib/kumi/analyzer.rb +16 -17
- data/lib/kumi/compiler.rb +188 -16
- data/lib/kumi/constraint_relationship_solver.rb +6 -6
- data/lib/kumi/domain/validator.rb +0 -4
- data/lib/kumi/error_reporting.rb +1 -1
- data/lib/kumi/explain.rb +32 -20
- data/lib/kumi/export/node_registry.rb +26 -12
- data/lib/kumi/export/node_serializers.rb +1 -1
- data/lib/kumi/function_registry/collection_functions.rb +14 -9
- data/lib/kumi/function_registry/function_builder.rb +4 -3
- data/lib/kumi/function_registry.rb +8 -2
- data/lib/kumi/input/type_matcher.rb +3 -0
- data/lib/kumi/input/validator.rb +0 -3
- 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/ruby_parser/declaration_reference_proxy.rb +36 -0
- data/lib/kumi/{parser → ruby_parser}/dsl.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/dsl_cascade_builder.rb +5 -5
- data/lib/kumi/{parser → ruby_parser}/expression_converter.rb +20 -20
- data/lib/kumi/{parser → ruby_parser}/guard_rails.rb +1 -1
- data/lib/kumi/{parser → ruby_parser}/input_builder.rb +41 -10
- data/lib/kumi/ruby_parser/input_field_proxy.rb +46 -0
- data/lib/kumi/{parser → ruby_parser}/input_proxy.rb +4 -4
- data/lib/kumi/ruby_parser/nested_input.rb +15 -0
- data/lib/kumi/{parser → ruby_parser}/parser.rb +11 -10
- data/lib/kumi/{parser → ruby_parser}/schema_builder.rb +11 -10
- data/lib/kumi/{parser → ruby_parser}/sugar.rb +62 -10
- 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/syntax/array_expression.rb +15 -0
- data/lib/kumi/syntax/call_expression.rb +11 -0
- data/lib/kumi/syntax/cascade_expression.rb +11 -0
- data/lib/kumi/syntax/case_expression.rb +11 -0
- data/lib/kumi/syntax/declaration_reference.rb +11 -0
- data/lib/kumi/syntax/hash_expression.rb +11 -0
- data/lib/kumi/syntax/input_declaration.rb +12 -0
- data/lib/kumi/syntax/input_element_reference.rb +12 -0
- data/lib/kumi/syntax/input_reference.rb +12 -0
- data/lib/kumi/syntax/literal.rb +11 -0
- data/lib/kumi/syntax/trait_declaration.rb +11 -0
- data/lib/kumi/syntax/value_declaration.rb +11 -0
- data/lib/kumi/vectorization_metadata.rb +108 -0
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +14 -0
- metadata +55 -25
- data/lib/generators/trait_engine/templates/schema_spec.rb.erb +0 -27
- data/lib/kumi/domain.rb +0 -8
- data/lib/kumi/input.rb +0 -8
- data/lib/kumi/syntax/declarations.rb +0 -26
- data/lib/kumi/syntax/expressions.rb +0 -34
- data/lib/kumi/syntax/terminal_expressions.rb +0 -30
- data/lib/kumi/syntax.rb +0 -9
- /data/{documents → docs}/DSL.md +0 -0
- /data/{documents → docs}/FUNCTIONS.md +0 -0
@@ -3,18 +3,29 @@
|
|
3
3
|
module Kumi
|
4
4
|
module Analyzer
|
5
5
|
module Passes
|
6
|
-
# RESPONSIBILITY: Build dependency graph and
|
7
|
-
# DEPENDENCIES: :
|
8
|
-
# PRODUCES: :
|
6
|
+
# RESPONSIBILITY: Build dependency graph and detect conditional dependencies in cascades
|
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
|
-
#
|
12
|
-
DependencyEdge
|
11
|
+
# Enhanced edge with conditional flag and cascade metadata
|
12
|
+
class DependencyEdge
|
13
|
+
attr_reader :to, :type, :via, :conditional, :cascade_owner
|
14
|
+
|
15
|
+
def initialize(to:, type:, via:, conditional: false, cascade_owner: nil)
|
16
|
+
@to = to
|
17
|
+
@type = type
|
18
|
+
@via = via
|
19
|
+
@conditional = conditional
|
20
|
+
@cascade_owner = cascade_owner
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
13
24
|
include Syntax
|
14
25
|
|
15
26
|
def run(errors)
|
16
|
-
definitions = get_state(:
|
17
|
-
input_meta = get_state(:
|
27
|
+
definitions = get_state(:declarations)
|
28
|
+
input_meta = get_state(:inputs)
|
18
29
|
|
19
30
|
dependency_graph = Hash.new { |h, k| h[k] = [] }
|
20
31
|
reverse_dependencies = Hash.new { |h, k| h[k] = [] }
|
@@ -22,7 +33,7 @@ module Kumi
|
|
22
33
|
|
23
34
|
each_decl do |decl|
|
24
35
|
# Traverse the expression for each declaration, passing context down
|
25
|
-
visit_with_context(decl.expression) do |node, context|
|
36
|
+
visit_with_context(decl.expression, { decl_name: decl.name }) do |node, context|
|
26
37
|
process_node(node, decl, dependency_graph, reverse_dependencies, leaf_map, definitions, input_meta, errors, context)
|
27
38
|
end
|
28
39
|
end
|
@@ -30,38 +41,83 @@ module Kumi
|
|
30
41
|
# Compute transitive closure of reverse dependencies
|
31
42
|
transitive_dependents = compute_transitive_closure(reverse_dependencies)
|
32
43
|
|
33
|
-
state.with(:
|
34
|
-
.with(:
|
35
|
-
.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)
|
36
47
|
end
|
37
48
|
|
38
49
|
private
|
39
50
|
|
40
|
-
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)
|
41
52
|
case node
|
42
|
-
when
|
53
|
+
when DeclarationReference
|
43
54
|
report_error(errors, "undefined reference to `#{node.name}`", location: node.loc) unless definitions.key?(node.name)
|
44
|
-
|
45
|
-
|
46
|
-
|
55
|
+
|
56
|
+
# Determine if this is a conditional dependency
|
57
|
+
conditional = context[:in_cascade_branch] || context[:in_cascade_base] || false
|
58
|
+
cascade_owner = conditional ? (context[:cascade_owner] || context[:decl_name]) : nil
|
59
|
+
|
60
|
+
add_dependency_edge(graph, reverse_deps, decl.name, node.name, :ref, context[:via],
|
61
|
+
conditional: conditional,
|
62
|
+
cascade_owner: cascade_owner)
|
63
|
+
when InputReference
|
47
64
|
add_dependency_edge(graph, reverse_deps, decl.name, node.name, :key, context[:via])
|
48
|
-
leaves[decl.name] << node
|
65
|
+
leaves[decl.name] << node
|
66
|
+
when InputElementReference
|
67
|
+
# adds the root input declaration as a dependency
|
68
|
+
root_input_declr_name = node.path.first
|
69
|
+
add_dependency_edge(graph, reverse_deps, decl.name, root_input_declr_name, :key, context[:via])
|
49
70
|
when Literal
|
50
71
|
leaves[decl.name] << node
|
51
72
|
end
|
52
73
|
end
|
53
74
|
|
54
|
-
def add_dependency_edge(graph, reverse_deps, from, to, type, via)
|
55
|
-
edge = DependencyEdge.new(
|
75
|
+
def add_dependency_edge(graph, reverse_deps, from, to, type, via, conditional: false, cascade_owner: nil)
|
76
|
+
edge = DependencyEdge.new(
|
77
|
+
to: to,
|
78
|
+
type: type,
|
79
|
+
via: via,
|
80
|
+
conditional: conditional,
|
81
|
+
cascade_owner: cascade_owner
|
82
|
+
)
|
56
83
|
graph[from] << edge
|
57
84
|
reverse_deps[to] << from
|
58
85
|
end
|
59
86
|
|
60
|
-
#
|
87
|
+
# Custom visitor that understands cascade structure
|
88
|
+
def visit_with_context(node, context = {}, &block)
|
89
|
+
return unless node
|
90
|
+
|
91
|
+
yield(node, context)
|
92
|
+
|
93
|
+
case node
|
94
|
+
when CascadeExpression
|
95
|
+
# Visit condition nodes and result expressions (non-base cases)
|
96
|
+
node.cases[0...-1].each do |when_case|
|
97
|
+
if when_case.condition
|
98
|
+
# Visit condition normally
|
99
|
+
visit_with_context(when_case.condition, context, &block)
|
100
|
+
end
|
101
|
+
# Visit result expressions as conditional dependencies
|
102
|
+
conditional_context = context.merge(in_cascade_branch: true, cascade_owner: context[:decl_name])
|
103
|
+
visit_with_context(when_case.result, conditional_context, &block)
|
104
|
+
end
|
105
|
+
|
106
|
+
# Visit base case with conditional flag
|
107
|
+
if node.cases.last
|
108
|
+
base_context = context.merge(in_cascade_base: true)
|
109
|
+
visit_with_context(node.cases.last.result, base_context, &block)
|
110
|
+
end
|
111
|
+
when CallExpression
|
112
|
+
new_context = context.merge(via: node.fn_name)
|
113
|
+
node.children.each { |child| visit_with_context(child, new_context, &block) }
|
114
|
+
else
|
115
|
+
node.children.each { |child| visit_with_context(child, context, &block) } if node.respond_to?(:children)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
61
119
|
def compute_transitive_closure(reverse_dependencies)
|
62
120
|
transitive = {}
|
63
|
-
|
64
|
-
# Collect all keys first to avoid iteration issues
|
65
121
|
all_keys = reverse_dependencies.keys
|
66
122
|
|
67
123
|
all_keys.each do |key|
|
@@ -75,7 +131,6 @@ module Kumi
|
|
75
131
|
|
76
132
|
visited.add(current)
|
77
133
|
|
78
|
-
# Get direct dependents
|
79
134
|
direct_dependents = reverse_dependencies[current] || []
|
80
135
|
direct_dependents.each do |dependent|
|
81
136
|
next if visited.include?(dependent)
|
@@ -90,21 +145,6 @@ module Kumi
|
|
90
145
|
|
91
146
|
transitive
|
92
147
|
end
|
93
|
-
|
94
|
-
# Custom visitor that passes context (like function name) down the tree
|
95
|
-
def visit_with_context(node, context = {}, &block)
|
96
|
-
return unless node
|
97
|
-
|
98
|
-
yield(node, context)
|
99
|
-
|
100
|
-
new_context = if node.is_a?(Expressions::CallExpression)
|
101
|
-
{ via: node.fn_name }
|
102
|
-
else
|
103
|
-
context
|
104
|
-
end
|
105
|
-
|
106
|
-
node.children.each { |child| visit_with_context(child, new_context, &block) }
|
107
|
-
end
|
108
148
|
end
|
109
149
|
end
|
110
150
|
end
|
@@ -5,17 +5,15 @@ 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
|
-
include Syntax::TerminalExpressions
|
12
|
-
|
13
11
|
def run(errors)
|
14
12
|
input_meta = {}
|
15
13
|
|
16
14
|
schema.inputs.each do |field_decl|
|
17
|
-
unless field_decl.is_a?(
|
18
|
-
report_error(errors, "Expected
|
15
|
+
unless field_decl.is_a?(Kumi::Syntax::InputDeclaration)
|
16
|
+
report_error(errors, "Expected InputDeclaration node, got #{field_decl.class}", location: field_decl.loc)
|
19
17
|
next
|
20
18
|
end
|
21
19
|
|
@@ -23,40 +21,103 @@ module Kumi
|
|
23
21
|
existing = input_meta[name]
|
24
22
|
|
25
23
|
if existing
|
26
|
-
# Check for compatibility
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
24
|
+
# Check for compatibility and merge
|
25
|
+
merged_meta = merge_field_metadata(existing, field_decl, errors)
|
26
|
+
input_meta[name] = merged_meta if merged_meta
|
27
|
+
else
|
28
|
+
# New field - collect its metadata
|
29
|
+
input_meta[name] = collect_field_metadata(field_decl, errors)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
state.with(:inputs, freeze_nested_hash(input_meta))
|
34
|
+
end
|
32
35
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
36
|
+
private
|
37
|
+
|
38
|
+
def collect_field_metadata(field_decl, errors)
|
39
|
+
validate_domain_type(field_decl, errors) if field_decl.domain
|
40
|
+
|
41
|
+
metadata = {
|
42
|
+
type: field_decl.type,
|
43
|
+
domain: field_decl.domain
|
44
|
+
}
|
45
|
+
|
46
|
+
# Process children if present
|
47
|
+
if field_decl.children && !field_decl.children.empty?
|
48
|
+
children_meta = {}
|
49
|
+
field_decl.children.each do |child_decl|
|
50
|
+
unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
|
51
|
+
report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
|
52
|
+
next
|
37
53
|
end
|
54
|
+
children_meta[child_decl.name] = collect_field_metadata(child_decl, errors)
|
55
|
+
end
|
56
|
+
metadata[:children] = children_meta
|
57
|
+
end
|
38
58
|
|
39
|
-
|
40
|
-
|
59
|
+
metadata
|
60
|
+
end
|
41
61
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
62
|
+
def merge_field_metadata(existing, field_decl, errors)
|
63
|
+
name = field_decl.name
|
64
|
+
|
65
|
+
# Check for type compatibility
|
66
|
+
if existing[:type] != field_decl.type && field_decl.type && existing[:type]
|
67
|
+
report_error(errors,
|
68
|
+
"Field :#{name} declared with conflicting types: #{existing[:type]} vs #{field_decl.type}",
|
69
|
+
location: field_decl.loc)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Check for domain compatibility
|
73
|
+
if existing[:domain] != field_decl.domain && field_decl.domain && existing[:domain]
|
74
|
+
report_error(errors,
|
75
|
+
"Field :#{name} declared with conflicting domains: #{existing[:domain].inspect} vs #{field_decl.domain.inspect}",
|
76
|
+
location: field_decl.loc)
|
77
|
+
end
|
78
|
+
|
79
|
+
# Validate domain type if provided
|
80
|
+
validate_domain_type(field_decl, errors) if field_decl.domain
|
81
|
+
|
82
|
+
# Merge metadata (later declarations override nil values)
|
83
|
+
merged = {
|
84
|
+
type: field_decl.type || existing[:type],
|
85
|
+
domain: field_decl.domain || existing[:domain]
|
86
|
+
}
|
87
|
+
|
88
|
+
# Merge children if present
|
89
|
+
if field_decl.children && !field_decl.children.empty?
|
90
|
+
existing_children = existing[:children] || {}
|
91
|
+
new_children = {}
|
92
|
+
|
93
|
+
field_decl.children.each do |child_decl|
|
94
|
+
unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
|
95
|
+
report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
|
96
|
+
next
|
97
|
+
end
|
98
|
+
|
99
|
+
child_name = child_decl.name
|
100
|
+
new_children[child_name] = if existing_children[child_name]
|
101
|
+
merge_field_metadata(existing_children[child_name], child_decl, errors)
|
102
|
+
else
|
103
|
+
collect_field_metadata(child_decl, errors)
|
104
|
+
end
|
53
105
|
end
|
106
|
+
|
107
|
+
merged[:children] = new_children
|
108
|
+
elsif existing[:children]
|
109
|
+
merged[:children] = existing[:children]
|
54
110
|
end
|
55
111
|
|
56
|
-
|
112
|
+
merged
|
57
113
|
end
|
58
114
|
|
59
|
-
|
115
|
+
def freeze_nested_hash(hash)
|
116
|
+
hash.each_value do |value|
|
117
|
+
freeze_nested_hash(value) if value.is_a?(Hash)
|
118
|
+
end
|
119
|
+
hash.freeze
|
120
|
+
end
|
60
121
|
|
61
122
|
def validate_domain_type(field_decl, errors)
|
62
123
|
domain = field_decl.domain
|
@@ -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
|
@@ -27,7 +27,7 @@ module Kumi
|
|
27
27
|
attr_reader :schema, :state
|
28
28
|
|
29
29
|
# Iterate over all declarations (attributes and traits) in the schema
|
30
|
-
# @yield [Syntax::
|
30
|
+
# @yield [Syntax::Attribute|Syntax::Trait] Each declaration
|
31
31
|
def each_decl(&block)
|
32
32
|
schema.attributes.each(&block)
|
33
33
|
schema.traits.each(&block)
|
@@ -10,7 +10,7 @@ module Kumi
|
|
10
10
|
#
|
11
11
|
# This pass enforces semantic constraints that must hold regardless of which parser
|
12
12
|
# was used to construct the AST. It validates:
|
13
|
-
# 1. Cascade conditions are only trait references (
|
13
|
+
# 1. Cascade conditions are only trait references (DeclarationReference nodes)
|
14
14
|
# 2. Trait expressions evaluate to boolean values (CallExpression nodes)
|
15
15
|
# 3. Function names exist in the function registry
|
16
16
|
# 4. Expression types are valid for their context
|
@@ -24,19 +24,19 @@ module Kumi
|
|
24
24
|
|
25
25
|
private
|
26
26
|
|
27
|
-
def validate_semantic_constraints(node,
|
27
|
+
def validate_semantic_constraints(node, _decl, errors)
|
28
28
|
case node
|
29
|
-
when
|
29
|
+
when Kumi::Syntax::TraitDeclaration
|
30
30
|
validate_trait_expression(node, errors)
|
31
|
-
when
|
31
|
+
when Kumi::Syntax::CaseExpression
|
32
32
|
validate_cascade_condition(node, errors)
|
33
|
-
when
|
33
|
+
when Kumi::Syntax::CallExpression
|
34
34
|
validate_function_call(node, errors)
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
38
|
def validate_trait_expression(trait, errors)
|
39
|
-
return if trait.expression.is_a?(
|
39
|
+
return if trait.expression.is_a?(Kumi::Syntax::CallExpression)
|
40
40
|
|
41
41
|
report_error(
|
42
42
|
errors,
|
@@ -48,22 +48,22 @@ module Kumi
|
|
48
48
|
|
49
49
|
def validate_cascade_condition(when_case, errors)
|
50
50
|
condition = when_case.condition
|
51
|
-
|
51
|
+
|
52
52
|
case condition
|
53
|
-
when
|
53
|
+
when Kumi::Syntax::DeclarationReference
|
54
54
|
# Valid: trait reference
|
55
|
-
|
56
|
-
when
|
55
|
+
nil
|
56
|
+
when Kumi::Syntax::CallExpression
|
57
57
|
# Valid if it's a boolean composition of traits (all?, any?, none?)
|
58
58
|
return if boolean_trait_composition?(condition)
|
59
|
-
|
59
|
+
|
60
60
|
# For now, allow other CallExpressions - they'll be validated by other passes
|
61
|
-
|
62
|
-
when
|
61
|
+
nil
|
62
|
+
when Kumi::Syntax::Literal
|
63
63
|
# Allow literal conditions (like true/false) - they might be valid
|
64
|
-
|
64
|
+
nil
|
65
65
|
else
|
66
|
-
# Only reject truly invalid conditions like
|
66
|
+
# Only reject truly invalid conditions like InputReference or complex expressions
|
67
67
|
report_error(
|
68
68
|
errors,
|
69
69
|
"cascade condition must be trait reference",
|
@@ -75,10 +75,10 @@ module Kumi
|
|
75
75
|
|
76
76
|
def validate_function_call(call_expr, errors)
|
77
77
|
fn_name = call_expr.fn_name
|
78
|
-
|
78
|
+
|
79
79
|
# Skip validation if FunctionRegistry is being mocked for testing
|
80
80
|
return if function_registry_mocked?
|
81
|
-
|
81
|
+
|
82
82
|
return if FunctionRegistry.supported?(fn_name)
|
83
83
|
|
84
84
|
report_error(
|
@@ -96,15 +96,14 @@ module Kumi
|
|
96
96
|
|
97
97
|
def function_registry_mocked?
|
98
98
|
# Check if FunctionRegistry is being mocked (for tests)
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
end
|
99
|
+
|
100
|
+
# Try to access a method that doesn't exist in the real registry
|
101
|
+
# If it's mocked, this won't raise an error
|
102
|
+
FunctionRegistry.respond_to?(:confirm_support!)
|
103
|
+
rescue StandardError
|
104
|
+
false
|
106
105
|
end
|
107
106
|
end
|
108
107
|
end
|
109
108
|
end
|
110
|
-
end
|
109
|
+
end
|
@@ -3,17 +3,17 @@
|
|
3
3
|
module Kumi
|
4
4
|
module Analyzer
|
5
5
|
module Passes
|
6
|
-
# RESPONSIBILITY: Compute topological ordering of declarations
|
7
|
-
# DEPENDENCIES: :
|
8
|
-
# PRODUCES: :
|
6
|
+
# RESPONSIBILITY: Compute topological ordering of declarations, allowing safe conditional cycles
|
7
|
+
# DEPENDENCIES: :dependencies from DependencyResolver, :declarations from NameIndexer, :cascades from UnsatDetector
|
8
|
+
# PRODUCES: :evaluation_order - Array of declaration names in evaluation order
|
9
9
|
# INTERFACE: new(schema, state).run(errors)
|
10
10
|
class Toposorter < PassBase
|
11
11
|
def run(errors)
|
12
|
-
dependency_graph = get_state(:
|
13
|
-
definitions = get_state(:
|
12
|
+
dependency_graph = get_state(:dependencies, required: false) || {}
|
13
|
+
definitions = get_state(:declarations, required: false) || {}
|
14
14
|
|
15
15
|
order = compute_topological_order(dependency_graph, definitions, errors)
|
16
|
-
state.with(:
|
16
|
+
state.with(:evaluation_order, order)
|
17
17
|
end
|
18
18
|
|
19
19
|
private
|
@@ -22,17 +22,26 @@ module Kumi
|
|
22
22
|
temp_marks = Set.new
|
23
23
|
perm_marks = Set.new
|
24
24
|
order = []
|
25
|
+
cascades = get_state(:cascades) || {}
|
25
26
|
|
26
|
-
visit_node = lambda do |node|
|
27
|
+
visit_node = lambda do |node, path = []|
|
27
28
|
return if perm_marks.include?(node)
|
28
29
|
|
29
30
|
if temp_marks.include?(node)
|
31
|
+
# Check if this is a safe conditional cycle
|
32
|
+
cycle_path = path + [node]
|
33
|
+
return if safe_conditional_cycle?(cycle_path, graph, cascades)
|
34
|
+
|
35
|
+
# Allow this cycle - it's safe due to cascade mutual exclusion
|
36
|
+
|
30
37
|
report_unexpected_cycle(temp_marks, node, errors)
|
38
|
+
|
31
39
|
return
|
32
40
|
end
|
33
41
|
|
34
42
|
temp_marks << node
|
35
|
-
|
43
|
+
current_path = path + [node]
|
44
|
+
Array(graph[node]).each { |edge| visit_node.call(edge.to, current_path) }
|
36
45
|
temp_marks.delete(node)
|
37
46
|
perm_marks << node
|
38
47
|
|
@@ -50,6 +59,33 @@ module Kumi
|
|
50
59
|
order.freeze
|
51
60
|
end
|
52
61
|
|
62
|
+
def safe_conditional_cycle?(cycle_path, graph, cascades)
|
63
|
+
return false if cycle_path.nil? || cycle_path.size < 2
|
64
|
+
|
65
|
+
# Find where the cycle starts - look for the first occurrence of the repeated node
|
66
|
+
last_node = cycle_path.last
|
67
|
+
return false if last_node.nil?
|
68
|
+
|
69
|
+
cycle_start = cycle_path.index(last_node)
|
70
|
+
return false unless cycle_start && cycle_start < cycle_path.size - 1
|
71
|
+
|
72
|
+
cycle_nodes = cycle_path[cycle_start..]
|
73
|
+
|
74
|
+
# Check if all edges in the cycle are conditional
|
75
|
+
cycle_nodes.each_cons(2) do |from, to|
|
76
|
+
edges = graph[from] || []
|
77
|
+
edge = edges.find { |e| e.to == to }
|
78
|
+
|
79
|
+
return false unless edge&.conditional
|
80
|
+
|
81
|
+
# Check if the cascade has mutually exclusive conditions
|
82
|
+
cascade_meta = cascades[edge.cascade_owner]
|
83
|
+
return false unless cascade_meta&.dig(:all_mutually_exclusive)
|
84
|
+
end
|
85
|
+
|
86
|
+
true
|
87
|
+
end
|
88
|
+
|
53
89
|
def report_unexpected_cycle(temp_marks, current_node, errors)
|
54
90
|
cycle_path = temp_marks.to_a.join(" → ") + " → #{current_node}"
|
55
91
|
|
@@ -4,12 +4,12 @@ module Kumi
|
|
4
4
|
module Analyzer
|
5
5
|
module Passes
|
6
6
|
# RESPONSIBILITY: Validate function call arity and argument types against FunctionRegistry
|
7
|
-
# DEPENDENCIES: :
|
7
|
+
# DEPENDENCIES: :inferred_types from TypeInferencer
|
8
8
|
# PRODUCES: None (validation only)
|
9
9
|
# INTERFACE: new(schema, state).run(errors)
|
10
10
|
class TypeChecker < VisitorPass
|
11
11
|
def run(errors)
|
12
|
-
visit_nodes_of_type(
|
12
|
+
visit_nodes_of_type(Kumi::Syntax::CallExpression, errors: errors) do |node, _decl, errs|
|
13
13
|
validate_function_call(node, errs)
|
14
14
|
end
|
15
15
|
state
|
@@ -47,11 +47,31 @@ module Kumi
|
|
47
47
|
types = signature[:param_types]
|
48
48
|
return if types.nil? || (signature[:arity].negative? && node.args.empty?)
|
49
49
|
|
50
|
+
# Skip type checking for vectorized operations
|
51
|
+
broadcast_meta = get_state(:broadcasts, required: false)
|
52
|
+
return if broadcast_meta && is_part_of_vectorized_operation?(node, broadcast_meta)
|
53
|
+
|
50
54
|
node.args.each_with_index do |arg, i|
|
51
55
|
validate_argument_type(arg, i, types[i], node.fn_name, errors)
|
52
56
|
end
|
53
57
|
end
|
54
58
|
|
59
|
+
def is_part_of_vectorized_operation?(node, broadcast_meta)
|
60
|
+
# Check if this node is part of a vectorized or reduction operation
|
61
|
+
# This is a simplified check - in a real implementation we'd need to track context
|
62
|
+
node.args.any? do |arg|
|
63
|
+
case arg
|
64
|
+
when Kumi::Syntax::DeclarationReference
|
65
|
+
broadcast_meta[:vectorized_operations]&.key?(arg.name) ||
|
66
|
+
broadcast_meta[:reduction_operations]&.key?(arg.name)
|
67
|
+
when Kumi::Syntax::InputElementReference
|
68
|
+
broadcast_meta[:array_fields]&.key?(arg.path.first)
|
69
|
+
else
|
70
|
+
false
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
55
75
|
def validate_argument_type(arg, index, expected_type, fn_name, errors)
|
56
76
|
return if expected_type.nil? || expected_type == Kumi::Types::ANY
|
57
77
|
|
@@ -67,15 +87,15 @@ module Kumi
|
|
67
87
|
|
68
88
|
def get_expression_type(expr)
|
69
89
|
case expr
|
70
|
-
when
|
90
|
+
when Kumi::Syntax::Literal
|
71
91
|
# Inferred type from literal value
|
72
92
|
Kumi::Types.infer_from_value(expr.value)
|
73
93
|
|
74
|
-
when
|
94
|
+
when Kumi::Syntax::InputReference
|
75
95
|
# Declared type from input block (user-specified)
|
76
96
|
get_declared_field_type(expr.name)
|
77
97
|
|
78
|
-
when
|
98
|
+
when Kumi::Syntax::DeclarationReference
|
79
99
|
# Inferred type from type inference results
|
80
100
|
get_inferred_declaration_type(expr.name)
|
81
101
|
|
@@ -88,24 +108,24 @@ module Kumi
|
|
88
108
|
|
89
109
|
def get_declared_field_type(field_name)
|
90
110
|
# Get explicitly declared type from input metadata
|
91
|
-
input_meta = get_state(:
|
111
|
+
input_meta = get_state(:inputs, required: false) || {}
|
92
112
|
field_meta = input_meta[field_name]
|
93
113
|
field_meta&.dig(:type) || Kumi::Types::ANY
|
94
114
|
end
|
95
115
|
|
96
116
|
def get_inferred_declaration_type(decl_name)
|
97
117
|
# Get inferred type from type inference results
|
98
|
-
decl_types = get_state(:
|
118
|
+
decl_types = get_state(:inferred_types, required: true)
|
99
119
|
decl_types[decl_name] || Kumi::Types::ANY
|
100
120
|
end
|
101
121
|
|
102
122
|
def describe_expression_type(expr, type)
|
103
123
|
case expr
|
104
|
-
when
|
124
|
+
when Kumi::Syntax::Literal
|
105
125
|
"`#{expr.value}` of type #{type} (literal value)"
|
106
126
|
|
107
|
-
when
|
108
|
-
input_meta = get_state(:
|
127
|
+
when Kumi::Syntax::InputReference
|
128
|
+
input_meta = get_state(:inputs, required: false) || {}
|
109
129
|
field_meta = input_meta[expr.name]
|
110
130
|
|
111
131
|
if field_meta&.dig(:type)
|
@@ -117,17 +137,17 @@ module Kumi
|
|
117
137
|
"undeclared input field `#{expr.name}` (inferred as #{type})"
|
118
138
|
end
|
119
139
|
|
120
|
-
when
|
140
|
+
when Kumi::Syntax::DeclarationReference
|
121
141
|
# This type was inferred from the declaration's expression
|
122
142
|
"reference to declaration `#{expr.name}` of inferred type #{type}"
|
123
143
|
|
124
|
-
when
|
144
|
+
when Kumi::Syntax::CallExpression
|
125
145
|
"result of function `#{expr.fn_name}` returning #{type}"
|
126
146
|
|
127
|
-
when
|
147
|
+
when Kumi::Syntax::ArrayExpression
|
128
148
|
"list expression of type #{type}"
|
129
149
|
|
130
|
-
when
|
150
|
+
when Kumi::Syntax::CascadeExpression
|
131
151
|
"cascade expression of type #{type}"
|
132
152
|
|
133
153
|
else
|
@@ -4,12 +4,12 @@ module Kumi
|
|
4
4
|
module Analyzer
|
5
5
|
module Passes
|
6
6
|
# RESPONSIBILITY: Validate consistency between declared and inferred types
|
7
|
-
# DEPENDENCIES: :
|
7
|
+
# DEPENDENCIES: :inputs from InputCollector, :inferred_types from TypeInferencer
|
8
8
|
# PRODUCES: None (validation only)
|
9
9
|
# INTERFACE: new(schema, state).run(errors)
|
10
10
|
class TypeConsistencyChecker < PassBase
|
11
11
|
def run(errors)
|
12
|
-
input_meta = get_state(:
|
12
|
+
input_meta = get_state(:inputs, required: false) || {}
|
13
13
|
|
14
14
|
# First, validate that all declared types are valid
|
15
15
|
validate_declared_types(input_meta, errors)
|