kumi 0.0.6 → 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 +34 -177
- data/README.md +41 -7
- 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/examples/game_of_life.rb +1 -1
- data/examples/static_analysis_errors.rb +7 -7
- data/lib/kumi/analyzer.rb +20 -20
- data/lib/kumi/compiler.rb +44 -50
- 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 +13 -7
- data/lib/kumi/schema_metadata.rb +524 -0
- 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/lib/kumi.rb +14 -0
- data/migrate_to_core_iterative.rb +938 -0
- data/scripts/generate_function_docs.rb +9 -9
- metadata +85 -69
- data/lib/generators/trait_engine/templates/schema_spec.rb.erb +0 -27
- 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 -251
- 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 -110
- data/lib/kumi/analyzer/passes/toposorter.rb +0 -108
- data/lib/kumi/analyzer/passes/type_checker.rb +0 -162
- 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 -406
- 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 -281
- 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/parser/build_context.rb +0 -25
- data/lib/kumi/parser/declaration_reference_proxy.rb +0 -36
- data/lib/kumi/parser/dsl.rb +0 -12
- data/lib/kumi/parser/dsl_cascade_builder.rb +0 -136
- data/lib/kumi/parser/expression_converter.rb +0 -126
- data/lib/kumi/parser/guard_rails.rb +0 -43
- data/lib/kumi/parser/input_builder.rb +0 -125
- data/lib/kumi/parser/input_field_proxy.rb +0 -46
- data/lib/kumi/parser/input_proxy.rb +0 -29
- data/lib/kumi/parser/nested_input.rb +0 -15
- data/lib/kumi/parser/parser.rb +0 -68
- data/lib/kumi/parser/schema_builder.rb +0 -173
- data/lib/kumi/parser/sugar.rb +0 -261
- 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
@@ -0,0 +1,153 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# RESPONSIBILITY: Build dependency graph and detect conditional dependencies in cascades
|
8
|
+
# DEPENDENCIES: :declarations from NameIndexer, :inputs from InputCollector
|
9
|
+
# PRODUCES: :dependencies, :dependents, :leaves - Dependency analysis results
|
10
|
+
# INTERFACE: new(schema, state).run(errors)
|
11
|
+
class DependencyResolver < PassBase
|
12
|
+
# Enhanced edge with conditional flag and cascade metadata
|
13
|
+
class DependencyEdge
|
14
|
+
attr_reader :to, :type, :via, :conditional, :cascade_owner
|
15
|
+
|
16
|
+
def initialize(to:, type:, via:, conditional: false, cascade_owner: nil)
|
17
|
+
@to = to
|
18
|
+
@type = type
|
19
|
+
@via = via
|
20
|
+
@conditional = conditional
|
21
|
+
@cascade_owner = cascade_owner
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
include Syntax
|
26
|
+
|
27
|
+
def run(errors)
|
28
|
+
definitions = get_state(:declarations)
|
29
|
+
input_meta = get_state(:inputs)
|
30
|
+
|
31
|
+
dependency_graph = Hash.new { |h, k| h[k] = [] }
|
32
|
+
reverse_dependencies = Hash.new { |h, k| h[k] = [] }
|
33
|
+
leaf_map = Hash.new { |h, k| h[k] = Set.new }
|
34
|
+
|
35
|
+
each_decl do |decl|
|
36
|
+
# Traverse the expression for each declaration, passing context down
|
37
|
+
visit_with_context(decl.expression, { decl_name: decl.name }) do |node, context|
|
38
|
+
process_node(node, decl, dependency_graph, reverse_dependencies, leaf_map, definitions, input_meta, errors, context)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Compute transitive closure of reverse dependencies
|
43
|
+
transitive_dependents = compute_transitive_closure(reverse_dependencies)
|
44
|
+
|
45
|
+
state.with(:dependencies, dependency_graph.transform_values(&:freeze).freeze)
|
46
|
+
.with(:dependents, transitive_dependents.freeze)
|
47
|
+
.with(:leaves, leaf_map.transform_values(&:freeze).freeze)
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def process_node(node, decl, graph, reverse_deps, leaves, definitions, _input_meta, errors, context)
|
53
|
+
case node
|
54
|
+
when DeclarationReference
|
55
|
+
report_error(errors, "undefined reference to `#{node.name}`", location: node.loc) unless definitions.key?(node.name)
|
56
|
+
|
57
|
+
# Determine if this is a conditional dependency
|
58
|
+
conditional = context[:in_cascade_branch] || context[:in_cascade_base] || false
|
59
|
+
cascade_owner = conditional ? (context[:cascade_owner] || context[:decl_name]) : nil
|
60
|
+
|
61
|
+
add_dependency_edge(graph, reverse_deps, decl.name, node.name, :ref, context[:via],
|
62
|
+
conditional: conditional,
|
63
|
+
cascade_owner: cascade_owner)
|
64
|
+
when InputReference
|
65
|
+
add_dependency_edge(graph, reverse_deps, decl.name, node.name, :key, context[:via])
|
66
|
+
leaves[decl.name] << node
|
67
|
+
when InputElementReference
|
68
|
+
# adds the root input declaration as a dependency
|
69
|
+
root_input_declr_name = node.path.first
|
70
|
+
add_dependency_edge(graph, reverse_deps, decl.name, root_input_declr_name, :key, context[:via])
|
71
|
+
when Literal
|
72
|
+
leaves[decl.name] << node
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def add_dependency_edge(graph, reverse_deps, from, to, type, via, conditional: false, cascade_owner: nil)
|
77
|
+
edge = DependencyEdge.new(
|
78
|
+
to: to,
|
79
|
+
type: type,
|
80
|
+
via: via,
|
81
|
+
conditional: conditional,
|
82
|
+
cascade_owner: cascade_owner
|
83
|
+
)
|
84
|
+
graph[from] << edge
|
85
|
+
reverse_deps[to] << from
|
86
|
+
end
|
87
|
+
|
88
|
+
# Custom visitor that understands cascade structure
|
89
|
+
def visit_with_context(node, context = {}, &block)
|
90
|
+
return unless node
|
91
|
+
|
92
|
+
yield(node, context)
|
93
|
+
|
94
|
+
case node
|
95
|
+
when CascadeExpression
|
96
|
+
# Visit condition nodes and result expressions (non-base cases)
|
97
|
+
node.cases[0...-1].each do |when_case|
|
98
|
+
if when_case.condition
|
99
|
+
# Visit condition normally
|
100
|
+
visit_with_context(when_case.condition, context, &block)
|
101
|
+
end
|
102
|
+
# Visit result expressions as conditional dependencies
|
103
|
+
conditional_context = context.merge(in_cascade_branch: true, cascade_owner: context[:decl_name])
|
104
|
+
visit_with_context(when_case.result, conditional_context, &block)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Visit base case with conditional flag
|
108
|
+
if node.cases.last
|
109
|
+
base_context = context.merge(in_cascade_base: true)
|
110
|
+
visit_with_context(node.cases.last.result, base_context, &block)
|
111
|
+
end
|
112
|
+
when CallExpression
|
113
|
+
new_context = context.merge(via: node.fn_name)
|
114
|
+
node.children.each { |child| visit_with_context(child, new_context, &block) }
|
115
|
+
else
|
116
|
+
node.children.each { |child| visit_with_context(child, context, &block) } if node.respond_to?(:children)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def compute_transitive_closure(reverse_dependencies)
|
121
|
+
transitive = {}
|
122
|
+
all_keys = reverse_dependencies.keys
|
123
|
+
|
124
|
+
all_keys.each do |key|
|
125
|
+
visited = Set.new
|
126
|
+
to_visit = [key]
|
127
|
+
dependents = Set.new
|
128
|
+
|
129
|
+
while to_visit.any?
|
130
|
+
current = to_visit.shift
|
131
|
+
next if visited.include?(current)
|
132
|
+
|
133
|
+
visited.add(current)
|
134
|
+
|
135
|
+
direct_dependents = reverse_dependencies[current] || []
|
136
|
+
direct_dependents.each do |dependent|
|
137
|
+
next if visited.include?(dependent)
|
138
|
+
|
139
|
+
dependents << dependent
|
140
|
+
to_visit << dependent
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
transitive[key] = dependents.to_a
|
145
|
+
end
|
146
|
+
|
147
|
+
transitive
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# RESPONSIBILITY: Collect field metadata from input declarations and validate consistency
|
8
|
+
# DEPENDENCIES: :definitions
|
9
|
+
# PRODUCES: :inputs - Hash mapping field names to {type:, domain:} metadata
|
10
|
+
# INTERFACE: new(schema, state).run(errors)
|
11
|
+
class InputCollector < PassBase
|
12
|
+
def run(errors)
|
13
|
+
input_meta = {}
|
14
|
+
|
15
|
+
schema.inputs.each do |field_decl|
|
16
|
+
unless field_decl.is_a?(Kumi::Syntax::InputDeclaration)
|
17
|
+
report_error(errors, "Expected InputDeclaration node, got #{field_decl.class}", location: field_decl.loc)
|
18
|
+
next
|
19
|
+
end
|
20
|
+
|
21
|
+
name = field_decl.name
|
22
|
+
existing = input_meta[name]
|
23
|
+
|
24
|
+
if existing
|
25
|
+
# Check for compatibility and merge
|
26
|
+
merged_meta = merge_field_metadata(existing, field_decl, errors)
|
27
|
+
input_meta[name] = merged_meta if merged_meta
|
28
|
+
else
|
29
|
+
# New field - collect its metadata
|
30
|
+
input_meta[name] = collect_field_metadata(field_decl, errors)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
state.with(:inputs, freeze_nested_hash(input_meta))
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def collect_field_metadata(field_decl, errors)
|
40
|
+
validate_domain_type(field_decl, errors) if field_decl.domain
|
41
|
+
|
42
|
+
metadata = {
|
43
|
+
type: field_decl.type,
|
44
|
+
domain: field_decl.domain
|
45
|
+
}
|
46
|
+
|
47
|
+
# Process children if present
|
48
|
+
if field_decl.children && !field_decl.children.empty?
|
49
|
+
children_meta = {}
|
50
|
+
field_decl.children.each do |child_decl|
|
51
|
+
unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
|
52
|
+
report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
|
53
|
+
next
|
54
|
+
end
|
55
|
+
children_meta[child_decl.name] = collect_field_metadata(child_decl, errors)
|
56
|
+
end
|
57
|
+
metadata[:children] = children_meta
|
58
|
+
end
|
59
|
+
|
60
|
+
metadata
|
61
|
+
end
|
62
|
+
|
63
|
+
def merge_field_metadata(existing, field_decl, errors)
|
64
|
+
name = field_decl.name
|
65
|
+
|
66
|
+
# Check for type compatibility
|
67
|
+
if existing[:type] != field_decl.type && field_decl.type && existing[:type]
|
68
|
+
report_error(errors,
|
69
|
+
"Field :#{name} declared with conflicting types: #{existing[:type]} vs #{field_decl.type}",
|
70
|
+
location: field_decl.loc)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Check for domain compatibility
|
74
|
+
if existing[:domain] != field_decl.domain && field_decl.domain && existing[:domain]
|
75
|
+
report_error(errors,
|
76
|
+
"Field :#{name} declared with conflicting domains: #{existing[:domain].inspect} vs #{field_decl.domain.inspect}",
|
77
|
+
location: field_decl.loc)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Validate domain type if provided
|
81
|
+
validate_domain_type(field_decl, errors) if field_decl.domain
|
82
|
+
|
83
|
+
# Merge metadata (later declarations override nil values)
|
84
|
+
merged = {
|
85
|
+
type: field_decl.type || existing[:type],
|
86
|
+
domain: field_decl.domain || existing[:domain]
|
87
|
+
}
|
88
|
+
|
89
|
+
# Merge children if present
|
90
|
+
if field_decl.children && !field_decl.children.empty?
|
91
|
+
existing_children = existing[:children] || {}
|
92
|
+
new_children = {}
|
93
|
+
|
94
|
+
field_decl.children.each do |child_decl|
|
95
|
+
unless child_decl.is_a?(Kumi::Syntax::InputDeclaration)
|
96
|
+
report_error(errors, "Expected InputDeclaration node in children, got #{child_decl.class}", location: child_decl.loc)
|
97
|
+
next
|
98
|
+
end
|
99
|
+
|
100
|
+
child_name = child_decl.name
|
101
|
+
new_children[child_name] = if existing_children[child_name]
|
102
|
+
merge_field_metadata(existing_children[child_name], child_decl, errors)
|
103
|
+
else
|
104
|
+
collect_field_metadata(child_decl, errors)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
merged[:children] = new_children
|
109
|
+
elsif existing[:children]
|
110
|
+
merged[:children] = existing[:children]
|
111
|
+
end
|
112
|
+
|
113
|
+
merged
|
114
|
+
end
|
115
|
+
|
116
|
+
def freeze_nested_hash(hash)
|
117
|
+
hash.each_value do |value|
|
118
|
+
freeze_nested_hash(value) if value.is_a?(Hash)
|
119
|
+
end
|
120
|
+
hash.freeze
|
121
|
+
end
|
122
|
+
|
123
|
+
def validate_domain_type(field_decl, errors)
|
124
|
+
domain = field_decl.domain
|
125
|
+
return if valid_domain_type?(domain)
|
126
|
+
|
127
|
+
report_error(errors,
|
128
|
+
"Field :#{field_decl.name} has invalid domain constraint: #{domain.inspect}. Domain must be a Range, Array, or Proc",
|
129
|
+
location: field_decl.loc)
|
130
|
+
end
|
131
|
+
|
132
|
+
def valid_domain_type?(domain)
|
133
|
+
domain.is_a?(Range) || domain.is_a?(Array) || domain.is_a?(Proc)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# RESPONSIBILITY: Build definitions index and detect duplicate names
|
8
|
+
# DEPENDENCIES: None (first pass in pipeline)
|
9
|
+
# PRODUCES: :declarations - Hash mapping names to declaration nodes
|
10
|
+
# INTERFACE: new(schema, state).run(errors)
|
11
|
+
class NameIndexer < PassBase
|
12
|
+
def run(errors)
|
13
|
+
definitions = {}
|
14
|
+
|
15
|
+
each_decl do |decl|
|
16
|
+
report_error(errors, "duplicated definition `#{decl.name}`", location: decl.loc) if definitions.key?(decl.name)
|
17
|
+
definitions[decl.name] = decl
|
18
|
+
end
|
19
|
+
|
20
|
+
state.with(:declarations, definitions.freeze)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# Base class for analyzer passes with simple immutable state
|
8
|
+
class PassBase
|
9
|
+
include Kumi::Syntax
|
10
|
+
include Kumi::Core::ErrorReporting
|
11
|
+
|
12
|
+
# @param schema [Syntax::Root] The schema to analyze
|
13
|
+
# @param state [AnalysisState] Current analysis state
|
14
|
+
def initialize(schema, state)
|
15
|
+
@schema = schema
|
16
|
+
@state = state
|
17
|
+
end
|
18
|
+
|
19
|
+
# Main pass execution - subclasses implement this
|
20
|
+
# @param errors [Array] Error accumulator array
|
21
|
+
# @return [AnalysisState] New state after pass execution
|
22
|
+
def run(errors)
|
23
|
+
raise NotImplementedError, "#{self.class.name} must implement #run"
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
attr_reader :schema, :state
|
29
|
+
|
30
|
+
# Iterate over all declarations (attributes and traits) in the schema
|
31
|
+
# @yield [Syntax::Attribute|Syntax::Trait] Each declaration
|
32
|
+
def each_decl(&block)
|
33
|
+
schema.attributes.each(&block)
|
34
|
+
schema.traits.each(&block)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get state value - compatible with old interface
|
38
|
+
def get_state(key, required: true)
|
39
|
+
raise StandardError, "Required state key '#{key}' not found" if required && !state.key?(key)
|
40
|
+
|
41
|
+
state[key]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Add error to the error list
|
45
|
+
def add_error(errors, location, message)
|
46
|
+
errors << ErrorReporter.create_error(message, location: location, type: :semantic)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# RESPONSIBILITY: Validate DSL semantic constraints at the AST level
|
8
|
+
# DEPENDENCIES: :definitions
|
9
|
+
# PRODUCES: None (validation only)
|
10
|
+
# INTERFACE: new(schema, state).run(errors)
|
11
|
+
#
|
12
|
+
# This pass enforces semantic constraints that must hold regardless of which parser
|
13
|
+
# was used to construct the AST. It validates:
|
14
|
+
# 1. Cascade conditions are only trait references (DeclarationReference nodes)
|
15
|
+
# 2. Trait expressions evaluate to boolean values (CallExpression nodes)
|
16
|
+
# 3. Function names exist in the function registry
|
17
|
+
# 4. Expression types are valid for their context
|
18
|
+
class SemanticConstraintValidator < VisitorPass
|
19
|
+
def run(errors)
|
20
|
+
each_decl do |decl|
|
21
|
+
visit(decl) { |node| validate_semantic_constraints(node, decl, errors) }
|
22
|
+
end
|
23
|
+
state
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def validate_semantic_constraints(node, _decl, errors)
|
29
|
+
case node
|
30
|
+
when Kumi::Syntax::TraitDeclaration
|
31
|
+
validate_trait_expression(node, errors)
|
32
|
+
when Kumi::Syntax::CaseExpression
|
33
|
+
validate_cascade_condition(node, errors)
|
34
|
+
when Kumi::Syntax::CallExpression
|
35
|
+
validate_function_call(node, errors)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_trait_expression(trait, errors)
|
40
|
+
return if trait.expression.is_a?(Kumi::Syntax::CallExpression)
|
41
|
+
|
42
|
+
report_error(
|
43
|
+
errors,
|
44
|
+
"trait `#{trait.name}` must have a boolean expression",
|
45
|
+
location: trait.loc,
|
46
|
+
type: :semantic
|
47
|
+
)
|
48
|
+
end
|
49
|
+
|
50
|
+
def validate_cascade_condition(when_case, errors)
|
51
|
+
condition = when_case.condition
|
52
|
+
|
53
|
+
case condition
|
54
|
+
when Kumi::Syntax::DeclarationReference
|
55
|
+
# Valid: trait reference
|
56
|
+
nil
|
57
|
+
when Kumi::Syntax::CallExpression
|
58
|
+
# Valid if it's a boolean composition of traits (all?, any?, none?)
|
59
|
+
return if boolean_trait_composition?(condition)
|
60
|
+
|
61
|
+
# For now, allow other CallExpressions - they'll be validated by other passes
|
62
|
+
nil
|
63
|
+
when Kumi::Syntax::Literal
|
64
|
+
# Allow literal conditions (like true/false) - they might be valid
|
65
|
+
nil
|
66
|
+
else
|
67
|
+
# Only reject truly invalid conditions like InputReference or complex expressions
|
68
|
+
report_error(
|
69
|
+
errors,
|
70
|
+
"cascade condition must be trait reference",
|
71
|
+
location: when_case.loc,
|
72
|
+
type: :semantic
|
73
|
+
)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def validate_function_call(call_expr, errors)
|
78
|
+
fn_name = call_expr.fn_name
|
79
|
+
|
80
|
+
# Skip validation if Kumi::Registry.is being mocked for testing
|
81
|
+
return if function_registry_mocked?
|
82
|
+
|
83
|
+
return if Kumi::Registry.supported?(fn_name)
|
84
|
+
|
85
|
+
report_error(
|
86
|
+
errors,
|
87
|
+
"unknown function `#{fn_name}`",
|
88
|
+
location: call_expr.loc,
|
89
|
+
type: :semantic
|
90
|
+
)
|
91
|
+
end
|
92
|
+
|
93
|
+
def boolean_trait_composition?(call_expr)
|
94
|
+
# Allow boolean composition functions that operate on trait collections
|
95
|
+
%i[all? any? none?].include?(call_expr.fn_name)
|
96
|
+
end
|
97
|
+
|
98
|
+
def function_registry_mocked?
|
99
|
+
# Check if Kumi::Registry.is being mocked (for tests)
|
100
|
+
|
101
|
+
# Try to access a method that doesn't exist in the real registry
|
102
|
+
# If it's mocked, this won't raise an error
|
103
|
+
Kumi::Registry.respond_to?(:confirm_support!)
|
104
|
+
rescue StandardError
|
105
|
+
false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Core
|
5
|
+
module Analyzer
|
6
|
+
module Passes
|
7
|
+
# RESPONSIBILITY: Compute topological ordering of declarations, allowing safe conditional cycles
|
8
|
+
# DEPENDENCIES: :dependencies from DependencyResolver, :declarations from NameIndexer, :cascades from UnsatDetector
|
9
|
+
# PRODUCES: :evaluation_order - Array of declaration names in evaluation order
|
10
|
+
# INTERFACE: new(schema, state).run(errors)
|
11
|
+
class Toposorter < PassBase
|
12
|
+
def run(errors)
|
13
|
+
dependency_graph = get_state(:dependencies, required: false) || {}
|
14
|
+
definitions = get_state(:declarations, required: false) || {}
|
15
|
+
|
16
|
+
order = compute_topological_order(dependency_graph, definitions, errors)
|
17
|
+
state.with(:evaluation_order, order)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def compute_topological_order(graph, definitions, errors)
|
23
|
+
temp_marks = Set.new
|
24
|
+
perm_marks = Set.new
|
25
|
+
order = []
|
26
|
+
cascades = get_state(:cascades) || {}
|
27
|
+
|
28
|
+
visit_node = lambda do |node, path = []|
|
29
|
+
return if perm_marks.include?(node)
|
30
|
+
|
31
|
+
if temp_marks.include?(node)
|
32
|
+
# Check if this is a safe conditional cycle
|
33
|
+
cycle_path = path + [node]
|
34
|
+
return if safe_conditional_cycle?(cycle_path, graph, cascades)
|
35
|
+
|
36
|
+
# Allow this cycle - it's safe due to cascade mutual exclusion
|
37
|
+
|
38
|
+
report_unexpected_cycle(temp_marks, node, errors)
|
39
|
+
|
40
|
+
return
|
41
|
+
end
|
42
|
+
|
43
|
+
temp_marks << node
|
44
|
+
current_path = path + [node]
|
45
|
+
Array(graph[node]).each { |edge| visit_node.call(edge.to, current_path) }
|
46
|
+
temp_marks.delete(node)
|
47
|
+
perm_marks << node
|
48
|
+
|
49
|
+
# Only include declaration nodes in the final order
|
50
|
+
order << node if definitions.key?(node)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Visit all nodes in the graph
|
54
|
+
graph.each_key { |node| visit_node.call(node) }
|
55
|
+
|
56
|
+
# Also visit any definitions that aren't in the dependency graph
|
57
|
+
# (i.e., declarations with no dependencies)
|
58
|
+
definitions.each_key { |node| visit_node.call(node) }
|
59
|
+
|
60
|
+
order.freeze
|
61
|
+
end
|
62
|
+
|
63
|
+
def safe_conditional_cycle?(cycle_path, graph, cascades)
|
64
|
+
return false if cycle_path.nil? || cycle_path.size < 2
|
65
|
+
|
66
|
+
# Find where the cycle starts - look for the first occurrence of the repeated node
|
67
|
+
last_node = cycle_path.last
|
68
|
+
return false if last_node.nil?
|
69
|
+
|
70
|
+
cycle_start = cycle_path.index(last_node)
|
71
|
+
return false unless cycle_start && cycle_start < cycle_path.size - 1
|
72
|
+
|
73
|
+
cycle_nodes = cycle_path[cycle_start..]
|
74
|
+
|
75
|
+
# Check if all edges in the cycle are conditional
|
76
|
+
cycle_nodes.each_cons(2) do |from, to|
|
77
|
+
edges = graph[from] || []
|
78
|
+
edge = edges.find { |e| e.to == to }
|
79
|
+
|
80
|
+
return false unless edge&.conditional
|
81
|
+
|
82
|
+
# Check if the cascade has mutually exclusive conditions
|
83
|
+
cascade_meta = cascades[edge.cascade_owner]
|
84
|
+
return false unless cascade_meta&.dig(:all_mutually_exclusive)
|
85
|
+
end
|
86
|
+
|
87
|
+
true
|
88
|
+
end
|
89
|
+
|
90
|
+
def report_unexpected_cycle(temp_marks, current_node, errors)
|
91
|
+
cycle_path = temp_marks.to_a.join(" → ") + " → #{current_node}"
|
92
|
+
|
93
|
+
# Try to find the first declaration in the cycle for location info
|
94
|
+
first_decl = find_declaration_by_name(temp_marks.first || current_node)
|
95
|
+
location = first_decl&.loc
|
96
|
+
|
97
|
+
report_error(errors, "cycle detected: #{cycle_path}", location: location)
|
98
|
+
end
|
99
|
+
|
100
|
+
def find_declaration_by_name(name)
|
101
|
+
return nil unless schema
|
102
|
+
|
103
|
+
schema.attributes.find { |attr| attr.name == name } ||
|
104
|
+
schema.traits.find { |trait| trait.name == name }
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|