pg-verify 0.1.0
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 +7 -0
- data/.rspec +3 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +98 -0
- data/README.md +29 -0
- data/Rakefile +62 -0
- data/bin/console +15 -0
- data/bin/pg-verify.rb +18 -0
- data/bin/setup +8 -0
- data/calc.ebnf +21 -0
- data/data/config/pg-verify.yml +66 -0
- data/data/nusmv.sample.smv +179 -0
- data/data/project-template/.gitignore.resource +4 -0
- data/data/project-template/.pg-verify.yml +0 -0
- data/data/project-template/README.md +18 -0
- data/data/project-template/addon/.keep +0 -0
- data/data/project-template/program-graph.rb.resource +103 -0
- data/devpg +5 -0
- data/doc/examples/railroad_crossing.rb +61 -0
- data/doc/examples/train-tree.rb +43 -0
- data/doc/examples/weidezaun.rb +99 -0
- data/doc/examples/weidezaun.txt +29 -0
- data/doc/expose/definition.png +0 -0
- data/doc/expose/diagram.png +0 -0
- data/doc/expose/expose.md +359 -0
- data/doc/expose/validity.png +0 -0
- data/exe/pg-verify +4 -0
- data/integration_tests/ruby_dsl/001_states.rb +10 -0
- data/integration_tests/ruby_dsl/002_transitions.rb +10 -0
- data/integration_tests/ruby_dsl/003_actions.rb +14 -0
- data/integration_tests/ruby_dsl/004_guards.rb +18 -0
- data/integration_tests/ruby_dsl/005_variables.rb +16 -0
- data/integration_tests/ruby_dsl/006_state_variables.rb +26 -0
- data/integration_tests/ruby_dsl/007_variable_initialization.rb +28 -0
- data/integration_tests/ruby_dsl/008_state_initialization.rb +19 -0
- data/integration_tests/ruby_dsl/009_shared_variables.rb +26 -0
- data/integration_tests/ruby_dsl/010_complex_guards.rb +18 -0
- data/integration_tests/ruby_dsl/011_complex_actions.rb +16 -0
- data/integration_tests/ruby_dsl/012_error_components.rb +9 -0
- data/integration_tests/ruby_dsl/013_hazards.rb +25 -0
- data/integration_tests/ruby_dsl/014_tau_transitions.rb +26 -0
- data/integration_tests/ruby_dsl/015_basic_dcca.rb +19 -0
- data/integration_tests/ruby_dsl/016_pressure_tank.rb +146 -0
- data/lib/pg-verify/cli/cli.rb +235 -0
- data/lib/pg-verify/core/cmd_runner.rb +151 -0
- data/lib/pg-verify/core/core.rb +38 -0
- data/lib/pg-verify/core/extensions/array_extensions.rb +11 -0
- data/lib/pg-verify/core/extensions/enumerable_extensions.rb +19 -0
- data/lib/pg-verify/core/extensions/nil_extensions.rb +7 -0
- data/lib/pg-verify/core/extensions/string_extensions.rb +84 -0
- data/lib/pg-verify/core/shell/colorizer.rb +136 -0
- data/lib/pg-verify/core/shell/shell.rb +0 -0
- data/lib/pg-verify/core/util.rb +146 -0
- data/lib/pg-verify/doctor/doctor.rb +180 -0
- data/lib/pg-verify/ebnf_parser/ast.rb +31 -0
- data/lib/pg-verify/ebnf_parser/ebnf_parser.rb +26 -0
- data/lib/pg-verify/ebnf_parser/expression_parser.rb +177 -0
- data/lib/pg-verify/ebnf_parser/expression_parser2.rb +422 -0
- data/lib/pg-verify/ebnf_parser/expressions.ebnf +33 -0
- data/lib/pg-verify/ebnf_parser/expressions.peg +52 -0
- data/lib/pg-verify/ebnf_parser/parser_result.rb +26 -0
- data/lib/pg-verify/interpret/component_context.rb +125 -0
- data/lib/pg-verify/interpret/graph_context.rb +85 -0
- data/lib/pg-verify/interpret/interpret.rb +142 -0
- data/lib/pg-verify/interpret/pg_script.rb +72 -0
- data/lib/pg-verify/interpret/spec/ltl_builder.rb +90 -0
- data/lib/pg-verify/interpret/spec/spec_context.rb +32 -0
- data/lib/pg-verify/interpret/spec/spec_set_context.rb +67 -0
- data/lib/pg-verify/interpret/transition_context.rb +55 -0
- data/lib/pg-verify/model/allocation_set.rb +28 -0
- data/lib/pg-verify/model/assignment.rb +34 -0
- data/lib/pg-verify/model/component.rb +40 -0
- data/lib/pg-verify/model/dcca/hazard.rb +16 -0
- data/lib/pg-verify/model/dcca.rb +67 -0
- data/lib/pg-verify/model/expression.rb +106 -0
- data/lib/pg-verify/model/graph.rb +58 -0
- data/lib/pg-verify/model/model.rb +10 -0
- data/lib/pg-verify/model/parsed_expression.rb +77 -0
- data/lib/pg-verify/model/simulation/trace.rb +43 -0
- data/lib/pg-verify/model/simulation/variable_state.rb +23 -0
- data/lib/pg-verify/model/source_location.rb +45 -0
- data/lib/pg-verify/model/specs/spec.rb +44 -0
- data/lib/pg-verify/model/specs/spec_result.rb +25 -0
- data/lib/pg-verify/model/specs/spec_set.rb +43 -0
- data/lib/pg-verify/model/specs/specification.rb +50 -0
- data/lib/pg-verify/model/transition.rb +41 -0
- data/lib/pg-verify/model/validation/assignment_to_state_variable_validation.rb +26 -0
- data/lib/pg-verify/model/validation/empty_state_set_validation.rb +18 -0
- data/lib/pg-verify/model/validation/errors.rb +119 -0
- data/lib/pg-verify/model/validation/foreign_assignment_validation.rb +30 -0
- data/lib/pg-verify/model/validation/unknown_token_validation.rb +35 -0
- data/lib/pg-verify/model/validation/validation.rb +23 -0
- data/lib/pg-verify/model/variable.rb +47 -0
- data/lib/pg-verify/model/variable_set.rb +84 -0
- data/lib/pg-verify/nusmv/nusmv.rb +23 -0
- data/lib/pg-verify/nusmv/runner.rb +124 -0
- data/lib/pg-verify/puml/puml.rb +23 -0
- data/lib/pg-verify/shell/loading/line_animation.rb +36 -0
- data/lib/pg-verify/shell/loading/loading_animation.rb +80 -0
- data/lib/pg-verify/shell/loading/loading_prompt.rb +43 -0
- data/lib/pg-verify/shell/loading/no_animation.rb +20 -0
- data/lib/pg-verify/shell/shell.rb +30 -0
- data/lib/pg-verify/simulation/simulation.rb +7 -0
- data/lib/pg-verify/simulation/simulator.rb +90 -0
- data/lib/pg-verify/simulation/state.rb +53 -0
- data/lib/pg-verify/transform/hash_transformation.rb +104 -0
- data/lib/pg-verify/transform/nusmv_transformation.rb +261 -0
- data/lib/pg-verify/transform/puml_transformation.rb +89 -0
- data/lib/pg-verify/transform/transform.rb +8 -0
- data/lib/pg-verify/version.rb +5 -0
- data/lib/pg-verify.rb +47 -0
- data/pg-verify.gemspec +38 -0
- data/sig/pg-verify.rbs +4 -0
- metadata +226 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
module PgVerify
|
|
2
|
+
module Doctor
|
|
3
|
+
|
|
4
|
+
Warning = Struct.new(:title, :text)
|
|
5
|
+
|
|
6
|
+
class DoctorError < PgVerify::Core::Error
|
|
7
|
+
def initialize(warnings)
|
|
8
|
+
@warnings = warnings
|
|
9
|
+
end
|
|
10
|
+
def formatted()
|
|
11
|
+
is_are, s = @warnings.length == 1 ? ["is", ""] : ["are", "s"]
|
|
12
|
+
header = "There #{is_are} #{@warnings.length} warning#{s} for your installation:"
|
|
13
|
+
string = @warnings.each_with_index.map { |w, i|
|
|
14
|
+
title = "#{i + 1}) #{w.title}".c_warn
|
|
15
|
+
body = w.text.indented(str: " ")
|
|
16
|
+
"#{title}\n#{body}"
|
|
17
|
+
}.join("\n\n")
|
|
18
|
+
return header, string
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.check()
|
|
23
|
+
checks = Doctor.methods
|
|
24
|
+
.select { |method| method.to_s.start_with?("check_") }
|
|
25
|
+
.map { |sym| sym.to_s.sub("check_", "").to_sym }
|
|
26
|
+
.sort
|
|
27
|
+
warnings = checks.map { |check| run_check(check) }.flatten.compact
|
|
28
|
+
raise DoctorError.new(warnings) unless warnings.empty?
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.run_check(symbol)
|
|
32
|
+
Shell::LoadingPrompt.while_loading("Checking #{symbol.to_s.gsub('_', ' ').c_string}") {
|
|
33
|
+
warnings = ([self.send(:"check_#{symbol}")] || []).flatten.compact
|
|
34
|
+
state = warnings.empty? ? :success : :error
|
|
35
|
+
msg = warnings.empty? ? "Ok" : "#{warnings.length} warning(s)!"
|
|
36
|
+
# Allow returning :skip from the check to mark the check as skipped
|
|
37
|
+
if warnings.length == 1 && warnings.first == :skip
|
|
38
|
+
state, msg, warnings = :empty, "Skipped!", []
|
|
39
|
+
end
|
|
40
|
+
Shell::LoadingPrompt::LoadingResult.new(warnings, msg, state: state)
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.check_01_Can_find_NuSMV()
|
|
45
|
+
return [] unless PgVerify::NuSMV::Runner.new.find_nusmv_path.nil?
|
|
46
|
+
return Warning.new("Unable to locate the NuSMV executable",
|
|
47
|
+
"Make sure to install NuSMV by unpacking it and placing the entire folder into\n" \
|
|
48
|
+
"the #{'addon'.c_file} directory of your project. " \
|
|
49
|
+
"(#{File.expand_path('addon').c_sidenote})\n" \
|
|
50
|
+
"Alternatively you can set the #{'nusmv.path'.c_string} in the configuration."
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.check_02_Can_run_NuSMV()
|
|
55
|
+
path = PgVerify::NuSMV::Runner.new.find_nusmv_path
|
|
56
|
+
return :skip if path.nil?
|
|
57
|
+
|
|
58
|
+
# Test by evaluating some example smv file
|
|
59
|
+
example_file = File.join(PgVerify.root, "data", "nusmv.sample.smv")
|
|
60
|
+
return [] if Core::CMDRunner.run_for_exit_code("#{path} #{example_file}") == 0
|
|
61
|
+
|
|
62
|
+
return Warning.new("Unable to run the NuSMV executable",
|
|
63
|
+
"NuSMV could be found here: #{path.c_file}\n" \
|
|
64
|
+
"However it could not be executed. Here are a few things to try:\n" \
|
|
65
|
+
" - Make sure the file is executable\n" \
|
|
66
|
+
" - Make sure you have the required permissions"
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def self.check_03_Run_integration_tests()
|
|
71
|
+
return :skip if PgVerify::NuSMV::Runner.new.find_nusmv_path.nil?
|
|
72
|
+
|
|
73
|
+
warnings = []
|
|
74
|
+
|
|
75
|
+
test_files = Dir[File.join(PgVerify.root, "integration_tests", "ruby_dsl", "*.rb")].sort
|
|
76
|
+
warnings += test_files.map { |test_file|
|
|
77
|
+
model = Interpret::PgScript.new.interpret(test_file).first
|
|
78
|
+
PgVerify::Model::Validation.validate!(model)
|
|
79
|
+
results = NuSMV::Runner.new().run_specs(model)
|
|
80
|
+
failures = results.reject(&:success)
|
|
81
|
+
next if failures.empty?
|
|
82
|
+
|
|
83
|
+
test_name = File.basename(test_file, '.*').gsub("_", "-")
|
|
84
|
+
failures_s = failures.map { |f| "#{f.spec.text} (#{f.spec.expression.to_s.c_blue})" }
|
|
85
|
+
show_command = "$ pg-verify show nusmv --script #{File.expand_path(test_file)}".c_cyan
|
|
86
|
+
test_command = "$ pg-verify test --script #{File.expand_path(test_file)}".c_cyan
|
|
87
|
+
Warning.new("Failed integration test in #{test_name}",
|
|
88
|
+
"The test #{test_name.c_string} contains the following unsatisfied specifications:\n" \
|
|
89
|
+
"\t- #{failures_s.join("\n\t- ")}\n" \
|
|
90
|
+
"These specifications should be valid if pg-verify works as expected.\n" \
|
|
91
|
+
"You can use the following commands to debug this:\n" \
|
|
92
|
+
" #{show_command}\n #{test_command}"
|
|
93
|
+
)
|
|
94
|
+
}.compact
|
|
95
|
+
|
|
96
|
+
warnings += test_files.map { |test_file|
|
|
97
|
+
model = Interpret::PgScript.new.interpret(test_file).first
|
|
98
|
+
next if model.hazards.empty?
|
|
99
|
+
require 'set'
|
|
100
|
+
|
|
101
|
+
dcca = Model::DCCA.new(model, NuSMV::Runner.new)
|
|
102
|
+
result = dcca.perform()
|
|
103
|
+
|
|
104
|
+
expected_cut_sets = File.read(test_file)
|
|
105
|
+
.split("\n")
|
|
106
|
+
.find { |l| l.include?("Expected Cut Sets") }
|
|
107
|
+
.scan(/\{([^}]+)\}/).flatten
|
|
108
|
+
.map { |string| string.split(",").map(&:strip).map(&:to_sym) }
|
|
109
|
+
.map { |cut_set| Set.new(cut_set) }
|
|
110
|
+
|
|
111
|
+
actual_cut_sets = result.values.first.map { |cut_set| Set.new(cut_set) }
|
|
112
|
+
|
|
113
|
+
next if Set.new(actual_cut_sets) == Set.new(expected_cut_sets)
|
|
114
|
+
|
|
115
|
+
Warning.new("Failed DCCA for #{File.basename(test_file)}",
|
|
116
|
+
"The DCCA for hazard #{result.keys.first.expression.to_s.c_blue} yielded unexpected cut sets:\n" \
|
|
117
|
+
"Expected: #{expected_cut_sets}\n" \
|
|
118
|
+
"Got : #{actual_cut_sets}\n"
|
|
119
|
+
)
|
|
120
|
+
}.compact
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
return warnings
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def self.check_04_Check_project_files()
|
|
127
|
+
warnings = []
|
|
128
|
+
script = Settings.ruby_dsl.default_script_name
|
|
129
|
+
|
|
130
|
+
return Warning.new("Failed to find model definition at #{script}",
|
|
131
|
+
"If you run PG verify without any arguments it expects a program graph definition\n" \
|
|
132
|
+
"at your working directory, which should be named #{script.c_file} by default.\n" \
|
|
133
|
+
"Based on your current working directory this would be this full path:\n" \
|
|
134
|
+
"\t#{File.expand_path(script).c_file}"
|
|
135
|
+
) unless File.file?(script)
|
|
136
|
+
|
|
137
|
+
begin
|
|
138
|
+
Interpret::PgScript.new.interpret(script)
|
|
139
|
+
rescue Exception => e
|
|
140
|
+
return Warning.new("Failed to interpret model at #{script}",
|
|
141
|
+
"Your default model definition at #{script.c_file}\n" \
|
|
142
|
+
"(Full path: #{File.expand_path(script).c_sidenote})\n" \
|
|
143
|
+
"Could not be interpreted. Make sure there are no syntax errors."
|
|
144
|
+
)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
return []
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# def self.check_03_Can_find_PlantUML()
|
|
151
|
+
# return [] unless PgVerify::Puml.find_path.nil?
|
|
152
|
+
# return Warning.new("Unable to find the PlantUML executable",
|
|
153
|
+
# "Make sure to install PlantUML by dropping the jar into\n" \
|
|
154
|
+
# "the #{'addon'.c_file} directory of your project. " \
|
|
155
|
+
# "(#{File.expand_path('addon').c_sidenote})\n" \
|
|
156
|
+
# "You can get it from here: #{"https://plantuml.com/download".c_string}\n" \
|
|
157
|
+
# "pg-verify will fall back to using the PlantUML web-server otherwise."
|
|
158
|
+
# )
|
|
159
|
+
# end
|
|
160
|
+
|
|
161
|
+
# def self.check_04_Can_run_PlantUML()
|
|
162
|
+
# path = PgVerify::Puml.find_path
|
|
163
|
+
# return :skip if path.blank?
|
|
164
|
+
|
|
165
|
+
# return [] if Core::CMDRunner.run_for_exit_code("java -jar #{path} -help") == 0
|
|
166
|
+
# puts "java -jar #{path} -help"
|
|
167
|
+
|
|
168
|
+
# return Warning.new("Unable to run the PlantUML executable",
|
|
169
|
+
# "PlantUML could be found here: #{path.c_file}\n" \
|
|
170
|
+
# "However it could not be executed. Here are a few things to try:\n" \
|
|
171
|
+
# " - Make sure the file is executable\n" \
|
|
172
|
+
# " - Make sure you have the required permissions"
|
|
173
|
+
# )
|
|
174
|
+
# end
|
|
175
|
+
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Require all module files
|
|
180
|
+
Dir[File.join(__dir__, "**", '*.rb')].sort.each { |file| require file }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module PgVerify
|
|
2
|
+
module EbnfParser
|
|
3
|
+
|
|
4
|
+
class Ast
|
|
5
|
+
|
|
6
|
+
attr_accessor :map
|
|
7
|
+
|
|
8
|
+
def initialize(map)
|
|
9
|
+
@map = map
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def find_variables(element=@map)
|
|
13
|
+
# TODO: Actually parse the variables
|
|
14
|
+
# puts "REC #{element.class}: #{element}"
|
|
15
|
+
# puts "\n"
|
|
16
|
+
# ret = []
|
|
17
|
+
# ret += element.map { |val| find_variables(val) }.flatten if element.is_a?(Array)
|
|
18
|
+
# element.each { |key, value|
|
|
19
|
+
# # We found a variable if we found a value which is not a number nor a boolean constant
|
|
20
|
+
# if key == :Value && value.is_a?(String) && !(/\d+/.match?(value)) && value != 'true' && value != 'false'
|
|
21
|
+
# ret << value
|
|
22
|
+
# end
|
|
23
|
+
# ret += find_variables(value)
|
|
24
|
+
# } if element.is_a?(Hash)
|
|
25
|
+
# return ret
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
require 'ebnf'
|
|
2
|
+
require 'ebnf/terminals'
|
|
3
|
+
require 'ebnf/peg/parser'
|
|
4
|
+
require 'sxp'
|
|
5
|
+
require 'logger'
|
|
6
|
+
require 'json'
|
|
7
|
+
|
|
8
|
+
# Require all module files
|
|
9
|
+
Dir[File.join(__dir__, "**", '*.rb')].sort.each { |file| require file }
|
|
10
|
+
|
|
11
|
+
module PgVerify
|
|
12
|
+
module EbnfParser
|
|
13
|
+
|
|
14
|
+
def self.parse_expression(expression, type: :Expression)
|
|
15
|
+
parser = ExpressionParser.new(type: type)
|
|
16
|
+
error, ast = nil, nil
|
|
17
|
+
begin
|
|
18
|
+
ast = parser.parse!(expression)
|
|
19
|
+
rescue EBNF::PEG::Parser::Error => e
|
|
20
|
+
error = e
|
|
21
|
+
end
|
|
22
|
+
return ParserResult.new(ast, error)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
module PgVerify
|
|
2
|
+
module EbnfParser
|
|
3
|
+
|
|
4
|
+
class ExpressionParser
|
|
5
|
+
include EBNF::PEG::Parser
|
|
6
|
+
|
|
7
|
+
# Abstract syntax tree from parse
|
|
8
|
+
attr_reader :ast
|
|
9
|
+
attr_accessor :rules
|
|
10
|
+
attr_accessor :options
|
|
11
|
+
attr_accessor :type
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# production(:Assignment, clear_packrat: true) do |value|
|
|
15
|
+
# # puts"Assignment:".ljust(25) + " #{value}"
|
|
16
|
+
# value
|
|
17
|
+
# end
|
|
18
|
+
|
|
19
|
+
# production(:BoolExpr, clear_packrat: true) do |value|
|
|
20
|
+
# # puts"BoolExpr:".ljust(25) + " #{value}"
|
|
21
|
+
# value
|
|
22
|
+
# end
|
|
23
|
+
|
|
24
|
+
# production(:Equivalence, clear_packrat: true) do |value|
|
|
25
|
+
# # puts"Equivalence:".ljust(25) + " #{value}"
|
|
26
|
+
# value
|
|
27
|
+
# end
|
|
28
|
+
|
|
29
|
+
# production(:Implication, clear_packrat: true) do |value|
|
|
30
|
+
# # puts"Implication:".ljust(25) + " #{value}"
|
|
31
|
+
# value
|
|
32
|
+
# end
|
|
33
|
+
|
|
34
|
+
# production(:Disjunction, clear_packrat: true) do |value|
|
|
35
|
+
# # puts"Disjunction:".ljust(25) + " #{value}"
|
|
36
|
+
# value
|
|
37
|
+
# end
|
|
38
|
+
|
|
39
|
+
# production(:Konjunction, clear_packrat: true) do |value|
|
|
40
|
+
# # puts"Konjunction:".ljust(25) + " #{value}"
|
|
41
|
+
# value
|
|
42
|
+
# end
|
|
43
|
+
|
|
44
|
+
# production(:Negation, clear_packrat: true) do |value|
|
|
45
|
+
# puts"Negation:".ljust(25) + "#{value.class} #{value}"
|
|
46
|
+
# value
|
|
47
|
+
# end
|
|
48
|
+
|
|
49
|
+
# production(:Comparison, clear_packrat: true) do |value|
|
|
50
|
+
# # puts"Comparison:".ljust(25) + " #{value}"
|
|
51
|
+
# value
|
|
52
|
+
# end
|
|
53
|
+
|
|
54
|
+
# production(:IntExpr, clear_packrat: true) do |value|
|
|
55
|
+
# # puts"IntExpr:".ljust(25) + " #{value}"
|
|
56
|
+
# value
|
|
57
|
+
# end
|
|
58
|
+
|
|
59
|
+
# production(:Sum, clear_packrat: true) do |value|
|
|
60
|
+
# # puts"Sum:".ljust(25) + " #{value}"
|
|
61
|
+
# value
|
|
62
|
+
# end
|
|
63
|
+
|
|
64
|
+
# production(:Product, clear_packrat: true) do |value|
|
|
65
|
+
# # puts"Product:".ljust(25) + " #{value}"
|
|
66
|
+
# valuex
|
|
67
|
+
# end
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# [{:Value=>"3"}, {:_Product_1=>"* 5 * 5"}]
|
|
72
|
+
production(:Product) do |value|
|
|
73
|
+
val = value.first[:Value]
|
|
74
|
+
prod1 = value.last[:_Product_1]
|
|
75
|
+
ret = "#{val} #{prod1}"
|
|
76
|
+
puts"Product:".ljust(25) + " #{value} -> #{ret}"
|
|
77
|
+
ret
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# A list of product operations plus values ['+ 4', '+ 4']
|
|
81
|
+
production(:_Product_1) do |value|
|
|
82
|
+
ret = value.join(' ')
|
|
83
|
+
# puts"Product1:".ljust(25) + " #{value} -> #{ret}"
|
|
84
|
+
ret
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# A product operation plus value '+ 4'
|
|
88
|
+
production(:_Product_2) do |value|
|
|
89
|
+
op = value.first[:_Product_3]
|
|
90
|
+
val = value.last[:Value]
|
|
91
|
+
ret = "#{op} #{val}"
|
|
92
|
+
puts "Product2:".ljust(25) + " #{value} -> #{ret}"
|
|
93
|
+
ret
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# A product operation '*' '/'
|
|
97
|
+
production(:_Product_3) do |value|
|
|
98
|
+
operation = value
|
|
99
|
+
value
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# An atomic value like '3', 'true' or 'variable'
|
|
103
|
+
production(:Value) do |value|
|
|
104
|
+
value
|
|
105
|
+
end
|
|
106
|
+
# An expression in braces
|
|
107
|
+
production(:_Value_1) do |value|
|
|
108
|
+
value.is_a?(Hash) ? value[:Expression] : value
|
|
109
|
+
end
|
|
110
|
+
# A number like '42' or '-7'
|
|
111
|
+
production(:NUMBER) do |value|
|
|
112
|
+
value
|
|
113
|
+
end
|
|
114
|
+
# A boolean value like 'true' or 'false'
|
|
115
|
+
production(:BOOL) do |value|
|
|
116
|
+
value
|
|
117
|
+
end
|
|
118
|
+
# A boolean expression in braces like '(3 < 2 <-> true)'
|
|
119
|
+
production(:_BOOL_1) do |value|
|
|
120
|
+
value.is_a?(Hash) ? value[:BoolExpr] : value
|
|
121
|
+
value
|
|
122
|
+
end
|
|
123
|
+
# Integer comparison operators like '>' and '!='
|
|
124
|
+
production(:CMPOP) do |value|
|
|
125
|
+
value
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def initialize(type: :Expression)
|
|
130
|
+
@type = type
|
|
131
|
+
@options = {}
|
|
132
|
+
# @options[:logger] = Logger.new(STDERR)
|
|
133
|
+
# @options[:logger].level = :info
|
|
134
|
+
# @options[:logger].formatter = lambda {|severity, datetime, progname, msg| "#{severity} #{msg}\n"}
|
|
135
|
+
|
|
136
|
+
# Intantiate the grammar
|
|
137
|
+
ebnf_file = File.expand_path("expressions.ebnf", __dir__)
|
|
138
|
+
# Perform PEG-specific transformation to the associated rules, which will be passed directly to the parser.
|
|
139
|
+
@rules = EBNF.parse(File.open(ebnf_file)).make_peg.ast
|
|
140
|
+
|
|
141
|
+
skeletton = EBNF.parse(File.open(ebnf_file)).make_peg.to_s.split("\n").reverse.map { |line|
|
|
142
|
+
rule = line.split(/\s+/)[1]
|
|
143
|
+
comment = "# Rule: '#{line.split(/\s+/).join(" ")}'"
|
|
144
|
+
puts_line = 'puts "' + rule + ': \'#{input}\' -> \'#{output}\'"'
|
|
145
|
+
body = "\toutput = input\n\t#{puts_line}\n\toutput"
|
|
146
|
+
method = "production(:#{rule}) do |input|\n#{body}\nend"
|
|
147
|
+
[comment, method].join("\n")
|
|
148
|
+
}.join("\n\n")
|
|
149
|
+
File.write(File.expand_path("expressions.rb", __dir__), skeletton)
|
|
150
|
+
|
|
151
|
+
# TODO: Remove
|
|
152
|
+
File.write(File.expand_path("expressions.peg", __dir__), EBNF.parse(File.open(ebnf_file)).make_peg)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def parse!(input)
|
|
156
|
+
raise "Empty expression!" if input.nil? || input.empty?
|
|
157
|
+
ast_map = parse(input, @type, @rules, **@options)
|
|
158
|
+
Ast.new(ast_map)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# def evaluate!(input, type: :Expression)
|
|
162
|
+
# # TODO: Change this to actually eval (Also in specs)
|
|
163
|
+
# parse(input, type, @rules, **@options)
|
|
164
|
+
# end
|
|
165
|
+
|
|
166
|
+
# def accepts?(input, type: :Expression)
|
|
167
|
+
# begin
|
|
168
|
+
# evaluate!(input, type: type)
|
|
169
|
+
# return true
|
|
170
|
+
# rescue EBNF::PEG::Parser::Error => e
|
|
171
|
+
# return false
|
|
172
|
+
# end
|
|
173
|
+
# end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
end
|
|
177
|
+
end
|