calc.rb |
|
---|---|
EBNF Parser for EBNF.Produces an Abstract Synatx Tree in S-Expression form for the input grammar file |
require 'ebnf'
require 'ebnf/terminals'
require 'ebnf/peg/parser'
require 'sxp'
require 'logger'
class Calc
include EBNF::PEG::Parser |
Abstract syntax tree from parse @return [ArrayEBNF::Rule] |
attr_reader :ast |
The calculator grammar comes from a Wikipedia entry on Parsing Expression Grammar, with some small concessions.
This, in turn, is turned into S-Expression with sub-rules added for embedded rules, which allow them to be accessed independently:
|
|
The calculator evaluates values from each rule and applies operators resulting in the calculated result. |
|
[1] Expr := Sum
|
production(:Expr, clear_packrat: true) do |value|
value.first[:Sum]
end |
[2] Sum := Product (('+' | '-') Product)*
|
production(:Sum, clear_packrat: true) do |value|
product, operations = value.first[:Product], value.last[:_Sum_1] |
Operations are an array of tuples: [['+', 2], ['-', 3]] |
operations.inject(product) {|accumulator, vv| accumulator.send(*vv)}
end |
(('+' | '-') Product)*
Turn [{Sum3: "+"}, {Product: N}] into ["+" N] |
production(:_Sum_2) do |value|
value.map(&:values).flatten
end |
[3] Product := Power (('*' | '/') Power)*
|
production(:Product, clear_packrat: true) do |value|
power, operations = value.first[:Power], value.last[:_Product_1] |
Operations are an array of tuples: [['*', 2], ['/', 3]] |
operations.inject(power) {|accumulator, vv| accumulator.send(*vv)}
end |
(('*' | '/') Power)*
Turn [{Product3: ""}, {Power: N}] into ["" N] |
production(:_Product_2) do |value|
value.map(&:values).flatten
end |
[4] Power := Value ('^' Power)?
|
production(:Power, clear_packrat: true) do |value|
val, pow = value.first[:Value], value.last[:_Power_1]
pow ? val.pow(pow) : val
end |
('^' Power)?
|
production(:_Power_2) {|value| value.last[:Power]} |
[5] Value := [0-9]+ | '(' Expr ')'
|
production(:Value, clear_packrat: true) do |value|
case value
when String then value.to_i
when Array then value[1][:Expr]
end
end |
Terminals don't require any special processing, but we could optimize by creating a regular expression such as |
|
Instantiate the calculator using the EBNF grammar. @param [Hash{Symbol => Object}] options @option options [Boolean] :trace Trace level. 0(debug), 1(info), 2(warn), 3(error). |
def initialize(**options) |
Intantiate grammar from ebnf.ebnf |
ebnf = File.expand_path("../calc.ebnf", __FILE__) |
Perform PEG-specific transformation to the associated rules, which will be passed directly to the parser. |
@rules = EBNF.parse(File.open(ebnf)).make_peg.ast
@options = options.dup |
If the |
if @options.has_key?(:trace)
@options[:logger] = Logger.new(STDERR)
@options[:logger].level = @options[:trace]
@options[:logger].formatter = lambda {|severity, datetime, progname, msg| "#{severity} #{msg}\n"}
end
end |
Evaluate an expression Evaluates each line of input. @param [String] input |
def evaluate(input)
result = parse(input, :Expr, @rules, **@options) |
This is called for each Expr |
puts result
end
end |