kumi 0.0.0 → 0.0.4
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/.rubocop.yml +113 -3
- data/CHANGELOG.md +21 -1
- data/CLAUDE.md +387 -0
- data/README.md +270 -20
- data/docs/development/README.md +120 -0
- data/docs/development/error-reporting.md +361 -0
- data/documents/AST.md +126 -0
- data/documents/DSL.md +154 -0
- data/documents/FUNCTIONS.md +132 -0
- data/documents/SYNTAX.md +367 -0
- data/examples/deep_schema_compilation_and_evaluation_benchmark.rb +106 -0
- data/examples/federal_tax_calculator_2024.rb +112 -0
- data/examples/wide_schema_compilation_and_evaluation_benchmark.rb +80 -0
- data/lib/generators/trait_engine/templates/schema_spec.rb.erb +27 -0
- data/lib/kumi/analyzer/constant_evaluator.rb +51 -0
- data/lib/kumi/analyzer/passes/definition_validator.rb +42 -0
- data/lib/kumi/analyzer/passes/dependency_resolver.rb +71 -0
- data/lib/kumi/analyzer/passes/input_collector.rb +55 -0
- data/lib/kumi/analyzer/passes/name_indexer.rb +24 -0
- data/lib/kumi/analyzer/passes/pass_base.rb +67 -0
- data/lib/kumi/analyzer/passes/toposorter.rb +72 -0
- data/lib/kumi/analyzer/passes/type_checker.rb +139 -0
- data/lib/kumi/analyzer/passes/type_consistency_checker.rb +45 -0
- data/lib/kumi/analyzer/passes/type_inferencer.rb +125 -0
- data/lib/kumi/analyzer/passes/unsat_detector.rb +107 -0
- data/lib/kumi/analyzer/passes/visitor_pass.rb +41 -0
- data/lib/kumi/analyzer.rb +54 -0
- data/lib/kumi/atom_unsat_solver.rb +349 -0
- data/lib/kumi/compiled_schema.rb +41 -0
- data/lib/kumi/compiler.rb +127 -0
- data/lib/kumi/domain/enum_analyzer.rb +53 -0
- data/lib/kumi/domain/range_analyzer.rb +83 -0
- data/lib/kumi/domain/validator.rb +84 -0
- data/lib/kumi/domain/violation_formatter.rb +40 -0
- data/lib/kumi/domain.rb +8 -0
- data/lib/kumi/error_reporter.rb +164 -0
- data/lib/kumi/error_reporting.rb +95 -0
- data/lib/kumi/errors.rb +116 -0
- data/lib/kumi/evaluation_wrapper.rb +22 -0
- data/lib/kumi/explain.rb +281 -0
- data/lib/kumi/export/deserializer.rb +39 -0
- data/lib/kumi/export/errors.rb +12 -0
- data/lib/kumi/export/node_builders.rb +140 -0
- data/lib/kumi/export/node_registry.rb +38 -0
- data/lib/kumi/export/node_serializers.rb +156 -0
- data/lib/kumi/export/serializer.rb +23 -0
- data/lib/kumi/export.rb +33 -0
- data/lib/kumi/function_registry/collection_functions.rb +92 -0
- data/lib/kumi/function_registry/comparison_functions.rb +31 -0
- data/lib/kumi/function_registry/conditional_functions.rb +36 -0
- data/lib/kumi/function_registry/function_builder.rb +92 -0
- data/lib/kumi/function_registry/logical_functions.rb +42 -0
- data/lib/kumi/function_registry/math_functions.rb +72 -0
- data/lib/kumi/function_registry/string_functions.rb +54 -0
- data/lib/kumi/function_registry/type_functions.rb +51 -0
- data/lib/kumi/function_registry.rb +138 -0
- data/lib/kumi/input/type_matcher.rb +92 -0
- data/lib/kumi/input/validator.rb +52 -0
- data/lib/kumi/input/violation_creator.rb +50 -0
- data/lib/kumi/input.rb +8 -0
- data/lib/kumi/parser/build_context.rb +25 -0
- data/lib/kumi/parser/dsl.rb +12 -0
- data/lib/kumi/parser/dsl_cascade_builder.rb +125 -0
- data/lib/kumi/parser/expression_converter.rb +58 -0
- data/lib/kumi/parser/guard_rails.rb +43 -0
- data/lib/kumi/parser/input_builder.rb +94 -0
- data/lib/kumi/parser/input_proxy.rb +29 -0
- data/lib/kumi/parser/parser.rb +66 -0
- data/lib/kumi/parser/schema_builder.rb +172 -0
- data/lib/kumi/parser/sugar.rb +108 -0
- data/lib/kumi/schema.rb +49 -0
- data/lib/kumi/schema_instance.rb +43 -0
- data/lib/kumi/syntax/declarations.rb +23 -0
- data/lib/kumi/syntax/expressions.rb +30 -0
- data/lib/kumi/syntax/node.rb +46 -0
- data/lib/kumi/syntax/root.rb +12 -0
- data/lib/kumi/syntax/terminal_expressions.rb +27 -0
- data/lib/kumi/syntax.rb +9 -0
- data/lib/kumi/types/builder.rb +21 -0
- data/lib/kumi/types/compatibility.rb +86 -0
- data/lib/kumi/types/formatter.rb +24 -0
- data/lib/kumi/types/inference.rb +40 -0
- data/lib/kumi/types/normalizer.rb +70 -0
- data/lib/kumi/types/validator.rb +35 -0
- data/lib/kumi/types.rb +64 -0
- data/lib/kumi/version.rb +1 -1
- data/lib/kumi.rb +7 -3
- data/scripts/generate_function_docs.rb +59 -0
- data/test_impossible_cascade.rb +51 -0
- metadata +93 -10
- data/sig/kumi.rbs +0 -4
@@ -0,0 +1,139 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Analyzer
|
5
|
+
module Passes
|
6
|
+
# RESPONSIBILITY: Validate function call arity and argument types against FunctionRegistry
|
7
|
+
# DEPENDENCIES: None (can run independently)
|
8
|
+
# PRODUCES: None (validation only)
|
9
|
+
# INTERFACE: new(schema, state).run(errors)
|
10
|
+
class TypeChecker < VisitorPass
|
11
|
+
def run(errors)
|
12
|
+
visit_nodes_of_type(Expressions::CallExpression, errors: errors) do |node, _decl, errs|
|
13
|
+
validate_function_call(node, errs)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def validate_function_call(node, errors)
|
20
|
+
signature = get_function_signature(node, errors)
|
21
|
+
return unless signature
|
22
|
+
|
23
|
+
validate_arity(node, signature, errors)
|
24
|
+
validate_argument_types(node, signature, errors)
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_function_signature(node, errors)
|
28
|
+
FunctionRegistry.signature(node.fn_name)
|
29
|
+
rescue FunctionRegistry::UnknownFunction
|
30
|
+
# Use old format for backward compatibility, but node.loc provides better location
|
31
|
+
report_error(errors, "unsupported operator `#{node.fn_name}`", location: node.loc, type: :type)
|
32
|
+
nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def validate_arity(node, signature, errors)
|
36
|
+
expected_arity = signature[:arity]
|
37
|
+
actual_arity = node.args.size
|
38
|
+
|
39
|
+
return if expected_arity.negative? || expected_arity == actual_arity
|
40
|
+
|
41
|
+
report_error(errors, "operator `#{node.fn_name}` expects #{expected_arity} args, got #{actual_arity}", location: node.loc,
|
42
|
+
type: :type)
|
43
|
+
end
|
44
|
+
|
45
|
+
def validate_argument_types(node, signature, errors)
|
46
|
+
types = signature[:param_types]
|
47
|
+
return if types.nil? || (signature[:arity].negative? && node.args.empty?)
|
48
|
+
|
49
|
+
node.args.each_with_index do |arg, i|
|
50
|
+
validate_argument_type(arg, i, types[i], node.fn_name, errors)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def validate_argument_type(arg, index, expected_type, fn_name, errors)
|
55
|
+
return if expected_type.nil? || expected_type == Kumi::Types::ANY
|
56
|
+
|
57
|
+
# Get the inferred type for this argument
|
58
|
+
actual_type = get_expression_type(arg)
|
59
|
+
return if Kumi::Types.compatible?(actual_type, expected_type)
|
60
|
+
|
61
|
+
# Generate descriptive error message
|
62
|
+
source_desc = describe_expression_type(arg, actual_type)
|
63
|
+
report_error(errors, "argument #{index + 1} of `fn(:#{fn_name})` expects #{expected_type}, " \
|
64
|
+
"got #{source_desc}", location: arg.loc, type: :type)
|
65
|
+
end
|
66
|
+
|
67
|
+
def get_expression_type(expr)
|
68
|
+
case expr
|
69
|
+
when TerminalExpressions::Literal
|
70
|
+
# Inferred type from literal value
|
71
|
+
Kumi::Types.infer_from_value(expr.value)
|
72
|
+
|
73
|
+
when TerminalExpressions::FieldRef
|
74
|
+
# Declared type from input block (user-specified)
|
75
|
+
get_declared_field_type(expr.name)
|
76
|
+
|
77
|
+
when TerminalExpressions::Binding
|
78
|
+
# Inferred type from type inference results
|
79
|
+
get_inferred_declaration_type(expr.name)
|
80
|
+
|
81
|
+
else
|
82
|
+
# For complex expressions, we should have type inference results
|
83
|
+
# This is a simplified approach - in reality we'd need to track types for all expressions
|
84
|
+
Kumi::Types::ANY
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def get_declared_field_type(field_name)
|
89
|
+
# Get explicitly declared type from input metadata
|
90
|
+
input_meta = get_state(:input_meta, required: false) || {}
|
91
|
+
field_meta = input_meta[field_name]
|
92
|
+
field_meta&.dig(:type) || Kumi::Types::ANY
|
93
|
+
end
|
94
|
+
|
95
|
+
def get_inferred_declaration_type(decl_name)
|
96
|
+
# Get inferred type from type inference results
|
97
|
+
decl_types = get_state(:decl_types, required: true)
|
98
|
+
decl_types[decl_name] || Kumi::Types::ANY
|
99
|
+
end
|
100
|
+
|
101
|
+
def describe_expression_type(expr, type)
|
102
|
+
case expr
|
103
|
+
when TerminalExpressions::Literal
|
104
|
+
"`#{expr.value}` of type #{type} (literal value)"
|
105
|
+
|
106
|
+
when TerminalExpressions::FieldRef
|
107
|
+
input_meta = get_state(:input_meta, required: false) || {}
|
108
|
+
field_meta = input_meta[expr.name]
|
109
|
+
|
110
|
+
if field_meta&.dig(:type)
|
111
|
+
# Explicitly declared type
|
112
|
+
domain_desc = field_meta[:domain] ? " (domain: #{field_meta[:domain]})" : ""
|
113
|
+
"input field `#{expr.name}` of declared type #{type}#{domain_desc}"
|
114
|
+
else
|
115
|
+
# Undeclared field
|
116
|
+
"undeclared input field `#{expr.name}` (inferred as #{type})"
|
117
|
+
end
|
118
|
+
|
119
|
+
when TerminalExpressions::Binding
|
120
|
+
# This type was inferred from the declaration's expression
|
121
|
+
"reference to declaration `#{expr.name}` of inferred type #{type}"
|
122
|
+
|
123
|
+
when Expressions::CallExpression
|
124
|
+
"result of function `#{expr.fn_name}` returning #{type}"
|
125
|
+
|
126
|
+
when Expressions::ListExpression
|
127
|
+
"list expression of type #{type}"
|
128
|
+
|
129
|
+
when Expressions::CascadeExpression
|
130
|
+
"cascade expression of type #{type}"
|
131
|
+
|
132
|
+
else
|
133
|
+
"expression of type #{type}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Analyzer
|
5
|
+
module Passes
|
6
|
+
# RESPONSIBILITY: Validate consistency between declared and inferred types
|
7
|
+
# DEPENDENCIES: :input_meta from InputCollector, :decl_types from TypeInferencer
|
8
|
+
# PRODUCES: None (validation only)
|
9
|
+
# INTERFACE: new(schema, state).run(errors)
|
10
|
+
class TypeConsistencyChecker < PassBase
|
11
|
+
def run(errors)
|
12
|
+
input_meta = get_state(:input_meta, required: false) || {}
|
13
|
+
|
14
|
+
# First, validate that all declared types are valid
|
15
|
+
validate_declared_types(input_meta, errors)
|
16
|
+
|
17
|
+
# Then check basic consistency (placeholder for now)
|
18
|
+
# In a full implementation, this would do sophisticated usage analysis
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def validate_declared_types(input_meta, errors)
|
24
|
+
input_meta.each do |field_name, meta|
|
25
|
+
declared_type = meta[:type]
|
26
|
+
next unless declared_type # Skip fields without declared types
|
27
|
+
next if Kumi::Types.valid_type?(declared_type)
|
28
|
+
|
29
|
+
# Find the input field declaration for proper location information
|
30
|
+
field_decl = find_input_field_declaration(field_name)
|
31
|
+
location = field_decl&.loc
|
32
|
+
|
33
|
+
add_error(errors, location, "Invalid type declaration for field :#{field_name}: #{declared_type.inspect}")
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def find_input_field_declaration(field_name)
|
38
|
+
return nil unless schema
|
39
|
+
|
40
|
+
schema.inputs.find { |input_decl| input_decl.name == field_name }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Analyzer
|
5
|
+
module Passes
|
6
|
+
# RESPONSIBILITY: Infer types for all declarations based on expression analysis
|
7
|
+
# DEPENDENCIES: Toposorter (needs topo_order), DefinitionValidator (needs definitions)
|
8
|
+
# PRODUCES: decl_types hash mapping declaration names to inferred types
|
9
|
+
# INTERFACE: new(schema, state).run(errors)
|
10
|
+
class TypeInferencer < PassBase
|
11
|
+
def run(errors)
|
12
|
+
return if state[:decl_types] # Already run
|
13
|
+
|
14
|
+
types = {}
|
15
|
+
topo_order = get_state(:topo_order)
|
16
|
+
definitions = get_state(:definitions)
|
17
|
+
|
18
|
+
# Process declarations in topological order to ensure dependencies are resolved
|
19
|
+
topo_order.each do |name|
|
20
|
+
decl = definitions[name]
|
21
|
+
next unless decl
|
22
|
+
|
23
|
+
begin
|
24
|
+
inferred_type = infer_expression_type(decl.expression, types)
|
25
|
+
types[name] = inferred_type
|
26
|
+
rescue StandardError => e
|
27
|
+
add_error(errors, decl&.loc, "Type inference failed: #{e.message}")
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
set_state(:decl_types, types)
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def infer_expression_type(expr, type_context = {})
|
37
|
+
case expr
|
38
|
+
when Literal
|
39
|
+
Types.infer_from_value(expr.value)
|
40
|
+
when FieldRef
|
41
|
+
# Look up type from field metadata
|
42
|
+
input_meta = get_state(:input_meta, required: false) || {}
|
43
|
+
meta = input_meta[expr.name]
|
44
|
+
meta&.dig(:type) || :any
|
45
|
+
when Binding
|
46
|
+
type_context[expr.name] || :any
|
47
|
+
when CallExpression
|
48
|
+
infer_call_type(expr, type_context)
|
49
|
+
when ListExpression
|
50
|
+
infer_list_type(expr, type_context)
|
51
|
+
when CascadeExpression
|
52
|
+
infer_cascade_type(expr, type_context)
|
53
|
+
else
|
54
|
+
:any
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def infer_call_type(call_expr, type_context)
|
59
|
+
fn_name = call_expr.fn_name
|
60
|
+
args = call_expr.args
|
61
|
+
|
62
|
+
# Check if function exists in registry
|
63
|
+
unless FunctionRegistry.supported?(fn_name)
|
64
|
+
# Don't push error here - let existing TypeChecker handle it
|
65
|
+
return :any
|
66
|
+
end
|
67
|
+
|
68
|
+
signature = FunctionRegistry.signature(fn_name)
|
69
|
+
|
70
|
+
# Validate arity if not variable
|
71
|
+
if signature[:arity] >= 0 && args.size != signature[:arity]
|
72
|
+
# Don't push error here - let existing TypeChecker handle it
|
73
|
+
return :any
|
74
|
+
end
|
75
|
+
|
76
|
+
# Infer argument types
|
77
|
+
arg_types = args.map { |arg| infer_expression_type(arg, type_context) }
|
78
|
+
|
79
|
+
# Validate parameter types (warn but don't fail)
|
80
|
+
param_types = signature[:param_types] || []
|
81
|
+
if signature[:arity] >= 0 && param_types.size.positive?
|
82
|
+
arg_types.each_with_index do |arg_type, i|
|
83
|
+
expected_type = param_types[i] || param_types.last
|
84
|
+
next if expected_type.nil?
|
85
|
+
|
86
|
+
unless Types.compatible?(arg_type, expected_type)
|
87
|
+
# Could add warning here in future, but for now just infer best type
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
signature[:return_type] || :any
|
93
|
+
end
|
94
|
+
|
95
|
+
def infer_list_type(list_expr, type_context)
|
96
|
+
return Types.array(:any) if list_expr.elements.empty?
|
97
|
+
|
98
|
+
element_types = list_expr.elements.map { |elem| infer_expression_type(elem, type_context) }
|
99
|
+
|
100
|
+
# Try to unify all element types
|
101
|
+
unified_type = element_types.reduce { |acc, type| Types.unify(acc, type) }
|
102
|
+
Types.array(unified_type)
|
103
|
+
rescue StandardError
|
104
|
+
# If unification fails, fall back to generic array
|
105
|
+
Types.array(:any)
|
106
|
+
end
|
107
|
+
|
108
|
+
def infer_cascade_type(cascade_expr, type_context)
|
109
|
+
return :any if cascade_expr.cases.empty?
|
110
|
+
|
111
|
+
result_types = cascade_expr.cases.map do |case_stmt|
|
112
|
+
infer_expression_type(case_stmt.result, type_context)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Reduce all possible types into a single unified type
|
116
|
+
result_types.reduce { |unified, type| Types.unify(unified, type) } || :any
|
117
|
+
rescue StandardError
|
118
|
+
# Check if unification fails, fall back to base type
|
119
|
+
# TODO: understand if this right to fallback or we should raise
|
120
|
+
:any
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Analyzer
|
5
|
+
module Passes
|
6
|
+
class UnsatDetector < VisitorPass
|
7
|
+
include Syntax
|
8
|
+
|
9
|
+
COMPARATORS = %i[> < >= <= == !=].freeze
|
10
|
+
Atom = Kumi::AtomUnsatSolver::Atom
|
11
|
+
|
12
|
+
def run(errors)
|
13
|
+
definitions = get_state(:definitions)
|
14
|
+
@evaluator = ConstantEvaluator.new(definitions)
|
15
|
+
|
16
|
+
each_decl do |decl|
|
17
|
+
if decl.expression.is_a?(CascadeExpression)
|
18
|
+
# Special handling for cascade expressions
|
19
|
+
check_cascade_expression(decl, definitions, errors)
|
20
|
+
else
|
21
|
+
# Normal handling for non-cascade expressions
|
22
|
+
atoms = gather_atoms(decl.expression, definitions, Set.new)
|
23
|
+
next if atoms.empty?
|
24
|
+
|
25
|
+
add_error(errors, decl.loc, "conjunction `#{decl.name}` is impossible") if Kumi::AtomUnsatSolver.unsat?(atoms)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def gather_atoms(node, defs, visited, list = [])
|
33
|
+
return list unless node
|
34
|
+
|
35
|
+
# Use iterative approach with stack to avoid SystemStackError on deep graphs
|
36
|
+
stack = [node]
|
37
|
+
|
38
|
+
until stack.empty?
|
39
|
+
current = stack.pop
|
40
|
+
next unless current
|
41
|
+
|
42
|
+
if current.is_a?(CallExpression) && COMPARATORS.include?(current.fn_name)
|
43
|
+
lhs, rhs = current.args
|
44
|
+
list << Atom.new(current.fn_name, term(lhs, defs), term(rhs, defs))
|
45
|
+
elsif current.is_a?(Binding)
|
46
|
+
name = current.name
|
47
|
+
unless visited.include?(name)
|
48
|
+
visited << name
|
49
|
+
stack << defs[name].expression if defs.key?(name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
# Add children to stack for processing
|
54
|
+
current.children.each { |child| stack << child } if current.respond_to?(:children)
|
55
|
+
end
|
56
|
+
|
57
|
+
list
|
58
|
+
end
|
59
|
+
|
60
|
+
def check_cascade_expression(decl, definitions, errors)
|
61
|
+
# Analyze each cascade branch condition independently
|
62
|
+
# This is the correct behavior: each 'on' condition should be checked separately
|
63
|
+
# since only ONE will be evaluated at runtime (they're mutually exclusive by design)
|
64
|
+
|
65
|
+
decl.expression.cases.each_with_index do |when_case, _index|
|
66
|
+
# Skip the base case (it's typically a literal true condition)
|
67
|
+
next if when_case.condition.is_a?(Literal) && when_case.condition.value == true
|
68
|
+
|
69
|
+
# Skip single-trait 'on' branches: trait-level unsat detection covers these
|
70
|
+
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
|
71
|
+
list = when_case.condition.args.first
|
72
|
+
next if list.is_a?(ListExpression) && list.elements.size < 2
|
73
|
+
end
|
74
|
+
# Gather atoms from this individual condition only
|
75
|
+
condition_atoms = gather_atoms(when_case.condition, definitions, Set.new, [])
|
76
|
+
|
77
|
+
# Only flag if this individual condition is impossible
|
78
|
+
next unless !condition_atoms.empty? && Kumi::AtomUnsatSolver.unsat?(condition_atoms)
|
79
|
+
|
80
|
+
# For multi-trait on-clauses, report the trait names rather than the value name
|
81
|
+
if when_case.condition.is_a?(CallExpression) && when_case.condition.fn_name == :all?
|
82
|
+
list = when_case.condition.args.first
|
83
|
+
if list.is_a?(ListExpression) && list.elements.all?(Binding)
|
84
|
+
traits = list.elements.map(&:name).join(" AND ")
|
85
|
+
add_error(errors, decl.loc, "conjunction `#{traits}` is impossible")
|
86
|
+
next
|
87
|
+
end
|
88
|
+
end
|
89
|
+
add_error(errors, decl.loc, "conjunction `#{decl.name}` is impossible")
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def term(node, _defs)
|
94
|
+
case node
|
95
|
+
when FieldRef, Binding
|
96
|
+
val = @evaluator.evaluate(node)
|
97
|
+
val == :unknown ? node.name : val
|
98
|
+
when Literal
|
99
|
+
node.value
|
100
|
+
else
|
101
|
+
:unknown
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Analyzer
|
5
|
+
module Passes
|
6
|
+
# Base class for analyzer passes that need to traverse the AST using the visitor pattern
|
7
|
+
class VisitorPass < PassBase
|
8
|
+
# Visit a node and all its children using depth-first traversal
|
9
|
+
# @param node [Syntax::Node] The node to visit
|
10
|
+
# @yield [Syntax::Node] Each node in the traversal
|
11
|
+
def visit(node, &block)
|
12
|
+
return unless node
|
13
|
+
|
14
|
+
yield(node)
|
15
|
+
node.children.each { |child| visit(child, &block) }
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
# Helper to visit each declaration's expression tree
|
21
|
+
# @param errors [Array] Error accumulator
|
22
|
+
# @yield [Syntax::Node, Syntax::Declarations::Base] Each node and its containing declaration
|
23
|
+
def visit_all_expressions(errors)
|
24
|
+
each_decl do |decl|
|
25
|
+
visit(decl.expression) { |node| yield(node, decl, errors) }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Helper to visit only specific node types
|
30
|
+
# @param node_types [Array<Class>] Node types to match
|
31
|
+
# @param errors [Array] Error accumulator
|
32
|
+
# @yield [Syntax::Node, Syntax::Declarations::Base] Matching nodes and their declarations
|
33
|
+
def visit_nodes_of_type(*node_types, errors:)
|
34
|
+
visit_all_expressions(errors) do |node, decl, errs|
|
35
|
+
yield(node, decl, errs) if node_types.any? { |type| node.is_a?(type) }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Kumi
|
4
|
+
module Analyzer
|
5
|
+
Result = Struct.new(:definitions, :dependency_graph, :leaf_map, :topo_order, :decl_types, :state, keyword_init: true)
|
6
|
+
|
7
|
+
module_function
|
8
|
+
|
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::DefinitionValidator, # 3. Checks the basic structure of each rule.
|
13
|
+
Passes::DependencyResolver, # 4. Builds the dependency graph.
|
14
|
+
Passes::UnsatDetector, # 5. Detects unsatisfiable constraints in rules.
|
15
|
+
Passes::Toposorter, # 6. Creates the final evaluation order.
|
16
|
+
Passes::TypeInferencer, # 7. Infers types for all declarations (pure annotation).
|
17
|
+
Passes::TypeConsistencyChecker, # 8. Validates declared vs inferred type consistency.
|
18
|
+
Passes::TypeChecker # 9. Validates types using inferred information.
|
19
|
+
].freeze
|
20
|
+
|
21
|
+
def analyze!(schema, passes: DEFAULT_PASSES, **opts)
|
22
|
+
analysis_state = { opts: opts }
|
23
|
+
errors = []
|
24
|
+
|
25
|
+
passes.each { |klass| klass.new(schema, analysis_state).run(errors) }
|
26
|
+
|
27
|
+
unless errors.empty?
|
28
|
+
# Check if we have type-specific errors to raise more specific exception
|
29
|
+
type_errors = errors.select { |e| e.type == :type }
|
30
|
+
first_error_location = errors.first.location
|
31
|
+
|
32
|
+
raise Errors::TypeError.new(format_errors(errors), first_error_location) if type_errors.any?
|
33
|
+
|
34
|
+
raise Errors::SemanticError.new(format_errors(errors), first_error_location)
|
35
|
+
end
|
36
|
+
|
37
|
+
Result.new(
|
38
|
+
definitions: analysis_state[:definitions].freeze,
|
39
|
+
dependency_graph: analysis_state[:dependency_graph].freeze,
|
40
|
+
leaf_map: analysis_state[:leaf_map].freeze,
|
41
|
+
topo_order: analysis_state[:topo_order].freeze,
|
42
|
+
decl_types: analysis_state[:decl_types].freeze,
|
43
|
+
state: analysis_state.freeze
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Handle both old and new error formats for backward compatibility
|
48
|
+
def format_errors(errors)
|
49
|
+
return "" if errors.empty?
|
50
|
+
|
51
|
+
errors.map(&:to_s).join("\n")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|