class Highway::Compiler::Analyze::Analyzer
This class is responsible for semantic analysis of a parse tree. This is the second phase of the compiler.
Public Class Methods
new(registry:, interface:)
click to toggle source
Initialize an instance.
@param registry [Highway::Steps::Registry] The steps registry. @param reporter [Highway::Interface] The interface.
# File lib/highway/compiler/analyze/analyzer.rb, line 23 def initialize(registry:, interface:) @registry = registry @interface = interface end
Public Instance Methods
analyze(parse_tree:)
click to toggle source
Analyze
the parse tree.
The semantic analyzer validates the parse tree in terms of content, performs segmentation of values and resolves steps against the registry.
The semantic analyzer produces a semantic tree which is then used by build phase to generate a manifest.
@param parse_tree [Highway::Compiler::Parse::Tree::Root] The parse tree.
@return [Highway::Compiler::Analyze::Tree::Root]
# File lib/highway/compiler/analyze/analyzer.rb, line 40 def analyze(parse_tree:) sema_tree = Analyze::Tree::Root.new() sema_tree.add_stage(index: 0, name: "bootstrap", policy: :normal, ) sema_tree.add_stage(index: 1, name: "test", policy: :normal) sema_tree.add_stage(index: 2, name: "deploy", policy: :normal) sema_tree.add_stage(index: 3, name: "report", policy: :always) sema_tree.default_preset = "default" validate_preset_names(parse_tree: parse_tree) validate_variable_names(parse_tree: parse_tree) validate_variable_values(parse_tree: parse_tree) validate_step_names(parse_tree: parse_tree) validate_step_parameter_values(parse_tree: parse_tree) resolve_variables(parse_tree: parse_tree, sema_tree: sema_tree) resolve_steps(parse_tree: parse_tree, sema_tree: sema_tree) validate_variable_references(sema_tree: sema_tree) sema_tree end
Private Instance Methods
assert_preset_name_valid(value, keypath:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 180 def assert_preset_name_valid(value, keypath:) unless %r(^[a-z_]*$) =~ value @interface.fatal!("Invalid preset name: '#{value}' at: '#{Utilities::keypath_to_s(keypath)}'.") end end
assert_step_exists(value, keypath:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 206 def assert_step_exists(value, keypath:) unless @registry.get_by_name(value) @interface.fatal!("Unknown step: '#{value}' at: '#{Utilities::keypath_to_s(keypath)}'.") end end
assert_step_parameter_exists(value, expected:, keypath:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 239 def assert_step_parameter_exists(value, expected:, keypath:) unless value.include?(expected) @interface.fatal!("Missing value for required step parameter: '#{expected}' at: '#{Utilities::keypath_to_s(keypath)}'.") end end
assert_step_parameter_name_valid(value, expected:, keypath:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 212 def assert_step_parameter_name_valid(value, expected:, keypath:) unless expected.include?(value) expected_names = expected.map { |name| "'#{name}'" }.join(", ") @interface.fatal!("Unknown step parameter: '#{value}' at '#{Utilities::keypath_to_s(keypath)}'. Expected one of: [#{expected_names}].") end end
assert_step_parameter_value_valid(value, keypath:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 219 def assert_step_parameter_value_valid(value, keypath:) if value.is_a?(String) unless %r(^((?:[^\$]*(?:(?:\\\$)|(?<!\\)\$\((?:ENV:)?[A-Z_]+\))*)*)$) =~ value @interface.fatal!("Invalid step parameter value: '#{value}' at: '#{Utilities::keypath_to_s(keypath)}'.") end elsif value.is_a?(Array) value.each_with_index do |single_value, index| assert_step_parameter_value_valid(single_value, keypath: keypath + [index]) end elsif value.is_a?(Hash) value.each_pair do |key, single_value| assert_step_parameter_value_valid(single_value, keypath: keypath + [key]) end else unless value.is_a?(TrueClass) || value.is_a?(FalseClass) || value.is_a?(Numeric) || value.is_a?(NilClass) @interface.fatal!("Invalid step parameter value: '#{value}' at: '#{Utilities::keypath_to_s(keypath)}'.") end end end
assert_variable_name_valid(value, keypath:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 186 def assert_variable_name_valid(value, keypath:) unless %r(^[A-Z_][A-Z0-9_]*$) =~ value @interface.fatal!("Invalid variable name: '#{value}' at: '#{Utilities::keypath_to_s(keypath)}'.") end end
assert_variable_value_valid(value, keypath:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 192 def assert_variable_value_valid(value, keypath:) if value.is_a?(String) unless %r(^((?:[^\$]*(?:(?:\\\$)|(?<!\\)\$\((?:ENV:)?[A-Z_][A-Z0-9_]*\))*)*)$) =~ value @interface.fatal!("Invalid variable value: '#{value}' at: '#{Utilities::keypath_to_s(keypath)}'.") end elsif value.is_a?(Array) || value.is_a?(Hash) @interface.fatal!("Invalid variable value: '#{value}' at: '#{Utilities::keypath_to_s(keypath)}'.") else unless value.is_a?(TrueClass) || value.is_a?(FalseClass) || value.is_a?(Numeric) || value.is_a?(NilClass) @interface.fatal!("Invalid variable value: '#{value}' at: '#{Utilities::keypath_to_s(keypath)}'.") end end end
find_referenced_variable(sema_tree:, name:, preset:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 174 def find_referenced_variable(sema_tree:, name:, preset:) sema_tree.variables.find do |variable| variable.name == name && [preset, sema_tree.default_preset].include?(variable.preset) end end
resolve_steps(parse_tree:, sema_tree:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 132 def resolve_steps(parse_tree:, sema_tree:) parse_tree.steps.each do |step| klass = @registry.get_by_name(step.name) parameters = segmentize_value(step.parameters) sema_tree.add_step(index: step.index, name: step.name, step_class: klass, parameters: parameters, stage: step.stage, preset: step.preset) end end
resolve_variables(parse_tree:, sema_tree:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 125 def resolve_variables(parse_tree:, sema_tree:) parse_tree.variables.each do |variable| value = segmentize_value(variable.value) sema_tree.add_variable(name: variable.name, value: value, preset: variable.preset) end end
segmentize_value(value)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 140 def segmentize_value(value) if value.is_a?(String) Analyze::Tree::Values::Primitive.new( value.to_enum(:scan, %r((?<!\\)\$\(([A-Z0-9:_]+)\)|((?:[^\\\$]|\\\$)+))).map { Regexp.last_match }.map { |match| if match[1] if match[1][0, 4] == "ENV:" Analyze::Tree::Segments::Variable.new(match[1][4..-1], scope: :env) else Analyze::Tree::Segments::Variable.new(match[1], scope: :static) end elsif match[2] Analyze::Tree::Segments::Text.new(match[2]) end } ) elsif value.is_a?(Array) Analyze::Tree::Values::Array.new( value.map { |element| segmentize_value(element) } ) elsif value.is_a?(Hash) Analyze::Tree::Values::Hash.new( Utilities::hash_map(value) { |name, element| [name, segmentize_value(element)] } ) elsif value.is_a?(TrueClass) || value.is_a?(FalseClass) || value.is_a?(Numeric) || value.is_a?(NilClass) Analyze::Tree::Values::Primitive.new( [Analyze::Tree::Segments::Text.new(value.to_s)] ) end end
validate_preset_names(parse_tree:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 68 def validate_preset_names(parse_tree:) parse_tree.variables.each do |variable| assert_preset_name_valid(variable.preset, keypath: ["variables"]) end parse_tree.steps.each do |step| assert_preset_name_valid(step.preset, keypath: [step.stage]) end end
validate_step_names(parse_tree:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 89 def validate_step_names(parse_tree:) parse_tree.steps.each do |step| assert_step_exists(step.name, keypath: [step.stage, step.preset]) end end
validate_step_parameter_values(parse_tree:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 95 def validate_step_parameter_values(parse_tree:) parse_tree.steps.each do |step| step.parameters.each_pair do |param_name, param_value| assert_step_parameter_value_valid(param_value, keypath: [step.stage, step.preset, step.name, param_name]) end end end
validate_variable_names(parse_tree:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 77 def validate_variable_names(parse_tree:) parse_tree.variables.each do |variable| assert_variable_name_valid(variable.name, keypath: ["variables", variable.preset]) end end
validate_variable_references(sema_tree:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 103 def validate_variable_references(sema_tree:) sema_tree.variables.each do |variable| variable.value.select_variable_segments_with_scope(:static).each do |segment| unless (ref_variable = find_referenced_variable(sema_tree: sema_tree, name: segment.name, preset: variable.preset)) @interface.fatal!("Unknown variable: '#{segment.name}' referenced from: '#{Utilities::keypath_to_s(["variables", variable.preset, variable.name])}'.") end if ref_variable.value.select_variable_segments_with_scope(:static).any? { |other| other.name == variable.name } @interface.fatal!("Detected a reference cycle between: '#{Utilities::keypath_to_s(["variables", variable.preset, variable.name])}' and '#{Utilities::keypath_to_s(["variables", ref_variable.preset, ref_variable.name])}'.") end end end sema_tree.steps.each do |step| step.parameters.children.each_pair do |name, value| value.select_variable_segments_with_scope(:static).each do |segment| unless find_referenced_variable(sema_tree: sema_tree, name: segment.name, preset: step.preset) @interface.fatal!("Unknown variable: '#{segment.name}' referenced from: '#{Utilities::keypath_to_s([step.stage, step.preset, step.name, name])}'.") end end end end end
validate_variable_values(parse_tree:)
click to toggle source
# File lib/highway/compiler/analyze/analyzer.rb, line 83 def validate_variable_values(parse_tree:) parse_tree.variables.each do |variable| assert_variable_value_valid(variable.value, keypath: ["variables", variable.preset, variable.name]) end end