pg-verify 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|