omghax-einstein 0.1.1 → 0.2.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.
Files changed (45) hide show
  1. data/Benchmarks.txt +48 -0
  2. data/History.txt +9 -0
  3. data/Manifest.txt +14 -21
  4. data/README.txt +1 -8
  5. data/Rakefile +26 -13
  6. data/einstein.gemspec +24 -0
  7. data/lib/einstein.rb +10 -41
  8. data/lib/einstein/evaluator.rb +102 -0
  9. data/lib/einstein/expression.rb +37 -0
  10. data/lib/einstein/parser.racc +41 -0
  11. data/lib/einstein/parser.racc.rb +333 -0
  12. data/lib/einstein/parser.rex +42 -0
  13. data/lib/einstein/parser.rex.rb +101 -0
  14. data/lib/einstein/pretty_printer.rb +98 -0
  15. data/lib/einstein/processor.rb +12 -0
  16. data/lib/einstein/version.rb +3 -3
  17. data/tasks/benchmark.rake +28 -0
  18. data/test/helper.rb +2 -1
  19. data/test/test_einstein.rb +20 -0
  20. data/test/test_evaluator.rb +86 -0
  21. data/test/test_parser.rb +35 -30
  22. data/test/test_pretty_printer.rb +81 -0
  23. metadata +31 -31
  24. data/config/hoe.rb +0 -62
  25. data/config/requirements.rb +0 -17
  26. data/lib/einstein/generated_parser.rb +0 -337
  27. data/lib/einstein/nodes.rb +0 -155
  28. data/lib/einstein/parser.rb +0 -60
  29. data/lib/einstein/tokenizer.rb +0 -112
  30. data/lib/einstein/visitors.rb +0 -303
  31. data/lib/parser.y +0 -79
  32. data/script/destroy +0 -14
  33. data/script/generate +0 -14
  34. data/script/txt2html +0 -74
  35. data/tasks/deployment.rake +0 -34
  36. data/tasks/environment.rake +0 -7
  37. data/tasks/website.rake +0 -17
  38. data/test/test_evaluate.rb +0 -127
  39. data/test/test_pretty_print.rb +0 -119
  40. data/test/test_tokenizer.rb +0 -68
  41. data/website/index.html +0 -120
  42. data/website/index.txt +0 -58
  43. data/website/javascripts/rounded_corners_lite.inc.js +0 -285
  44. data/website/stylesheets/screen.css +0 -138
  45. data/website/template.rhtml +0 -48
data/Benchmarks.txt ADDED
@@ -0,0 +1,48 @@
1
+ == Version 0.1.1
2
+
3
+ First version to include benchmarking. Each type of node in the AST had its
4
+ own Einstein::Node subclass.
5
+
6
+ Benchmarking 10000 run(s) of Einstein.parse
7
+ user system total real
8
+ 1 1.020000 0.020000 1.040000 ( 1.053689)
9
+ 2 + 5 3.150000 0.010000 3.160000 ( 3.182302)
10
+ x * y 2.690000 0.010000 2.700000 ( 2.712514)
11
+ (x ** 2) / 8 - (y << z) 10.770000 0.040000 10.810000 ( 10.866615)
12
+ Benchmarking 10000 run(s) of Einstein.evaluate
13
+ user system total real
14
+ 1 1.710000 0.000000 1.710000 ( 1.719042)
15
+ 2 + 5 4.110000 0.020000 4.130000 ( 4.148186)
16
+ x * y 3.920000 0.010000 3.930000 ( 3.949004)
17
+ (x ** 2) / 8 - (y << z) 13.440000 0.040000 13.480000 ( 13.548173)
18
+ Benchmarking 10000 run(s) of evaluate on a pre-parsed expression
19
+ user system total real
20
+ 1 0.530000 0.000000 0.530000 ( 0.538565)
21
+ 2 + 5 1.000000 0.000000 1.000000 ( 1.003800)
22
+ x * y 1.080000 0.010000 1.090000 ( 1.090432)
23
+ (x ** 2) / 8 - (y << z) 2.490000 0.010000 2.500000 ( 2.518389)
24
+
25
+ == Version 0.2.0
26
+
27
+ All node classes were removed, and Einstein was re-implemented using only
28
+ s-expressions. The custom Einstein::Tokenizer class was removed as well, and
29
+ replaced with one generated by rex.
30
+
31
+ Benchmarking 10000 run(s) of Einstein.parse
32
+ user system total real
33
+ 1 0.600000 0.030000 0.630000 ( 0.629743)
34
+ 2 + 5 1.300000 0.000000 1.300000 ( 1.310801)
35
+ x * y 1.280000 0.010000 1.290000 ( 1.298743)
36
+ (x ** 2) / 8 - (y << z) 4.570000 0.020000 4.590000 ( 4.611955)
37
+ Benchmarking 10000 run(s) of Einstein.evaluate
38
+ user system total real
39
+ 1 0.780000 0.000000 0.780000 ( 0.794553)
40
+ 2 + 5 1.580000 0.010000 1.590000 ( 1.595382)
41
+ x * y 1.600000 0.000000 1.600000 ( 1.613572)
42
+ (x ** 2) / 8 - (y << z) 5.380000 0.020000 5.400000 ( 5.436614)
43
+ Benchmarking 10000 run(s) of evaluate on a pre-parsed expression
44
+ user system total real
45
+ 1 0.080000 0.000000 0.080000 ( 0.077623)
46
+ 2 + 5 0.230000 0.000000 0.230000 ( 0.232722)
47
+ x * y 0.320000 0.000000 0.320000 ( 0.326163)
48
+ (x ** 2) / 8 - (y << z) 0.670000 0.000000 0.670000 ( 0.682789)
data/History.txt CHANGED
@@ -1,3 +1,12 @@
1
+ == 0.2.0 2008-05-10
2
+
3
+ * 2 major enhancements:
4
+ * Removed the custom tokenizer in favor of using a rex-based one instead.
5
+ * Huge changes to the internal structure of Einstein. Eliminated the node
6
+ classes in favor of modeling the syntax tree as an s-expression. This
7
+ makes the code much simpler, and I have a feeling it will be faster as
8
+ well, but I need to run some benchmarks to be sure.
9
+
1
10
  == 0.1.1 2008-05-08
2
11
 
3
12
  * 1 major bugfix:
data/Manifest.txt CHANGED
@@ -1,31 +1,24 @@
1
+ Benchmarks.txt
1
2
  History.txt
2
3
  License.txt
3
4
  Manifest.txt
4
5
  README.txt
5
6
  Rakefile
6
- config/hoe.rb
7
- config/requirements.rb
7
+ einstein.gemspec
8
8
  lib/einstein.rb
9
- lib/einstein/generated_parser.rb
10
- lib/einstein/nodes.rb
11
- lib/einstein/parser.rb
12
- lib/einstein/tokenizer.rb
9
+ lib/einstein/evaluator.rb
10
+ lib/einstein/expression.rb
11
+ lib/einstein/parser.racc
12
+ lib/einstein/parser.racc.rb
13
+ lib/einstein/parser.rex
14
+ lib/einstein/parser.rex.rb
15
+ lib/einstein/pretty_printer.rb
16
+ lib/einstein/processor.rb
13
17
  lib/einstein/version.rb
14
- lib/einstein/visitors.rb
15
- lib/parser.y
16
- script/destroy
17
- script/generate
18
- script/txt2html
19
18
  setup.rb
20
- tasks/deployment.rake
21
- tasks/environment.rake
22
- tasks/website.rake
23
- test/test_evaluate.rb
19
+ tasks/benchmark.rake
24
20
  test/helper.rb
21
+ test/test_einstein.rb
22
+ test/test_evaluator.rb
25
23
  test/test_parser.rb
26
- test/test_pretty_print.rb
27
- website/index.html
28
- website/index.txt
29
- website/javascripts/rounded_corners_lite.inc.js
30
- website/stylesheets/screen.css
31
- website/template.rhtml
24
+ test/test_pretty_printer.rb
data/README.txt CHANGED
@@ -40,19 +40,12 @@ You can then evaluate this tree for different values of x:
40
40
  Or you can return an s-expression representation of the tree:
41
41
 
42
42
  >> ast.to_sexp
43
- => [:raise, [:resolve, "x"], [:lit, 2]]
43
+ => [:exponent, [:resolve, "x"], [:lit, 2]]
44
44
 
45
45
  == Authors
46
46
 
47
47
  Copyright (c) 2008 by Dray Lacy
48
48
 
49
- == Acknowledgements
50
-
51
- Large portions of the parser were taken from the RKelly project by Aaron
52
- Patterson, which was itself derived from rbnarcissus by Paul Sowden.
53
-
54
- Thanks Aaron and Paul!
55
-
56
49
  == License
57
50
 
58
51
  Einstein is licensed under the MIT License.
data/Rakefile CHANGED
@@ -1,20 +1,33 @@
1
- require 'config/requirements'
2
- require 'config/hoe' # setup Hoe + all gem configuration
1
+ require 'rubygems'
2
+ require 'hoe'
3
+ require './lib/einstein.rb'
3
4
 
5
+ # Load all tasks from /tasks.
4
6
  Dir['tasks/**/*.rake'].each { |rake| load rake }
5
7
 
6
- GENERATED_PARSER = "lib/einstein/generated_parser.rb"
8
+ Hoe.new('einstein', Einstein::VERSION::STRING) do |p|
9
+ p.developer 'Dray Lacy', 'dray@izea.com'
10
+ p.description = p.summary = 'Safe arithmetic parser for Ruby apps'
11
+ p.url = 'http://github.com/omghax/einstein'
12
+ p.test_globs = ['test/**/test_*.rb']
13
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
14
+ end
15
+
16
+ GENERATED_PARSER = 'lib/einstein/parser.racc.rb'
17
+ GENERATED_LEXER = 'lib/einstein/parser.rex.rb'
7
18
 
8
- file GENERATED_PARSER => "lib/parser.y" do |t|
9
- if ENV['DEBUG']
10
- sh "racc -g -v -o #{t.name} #{t.prerequisites.first}"
11
- else
12
- sh "racc -o #{t.name} #{t.prerequisites.first}"
13
- end
19
+ file GENERATED_LEXER => 'lib/einstein/parser.rex' do |t|
20
+ sh "rex -o #{t.name} #{t.prerequisites.first}"
14
21
  end
15
22
 
16
- task :parser => GENERATED_PARSER
23
+ file GENERATED_PARSER => 'lib/einstein/parser.racc' do |t|
24
+ sh "racc -o #{t.name} #{t.prerequisites.first}"
25
+ end
26
+
27
+ task :parser => [GENERATED_LEXER, GENERATED_PARSER]
28
+
29
+ # Make sure the parser's up-to-date when we test.
30
+ Rake::Task["test"].prerequisites << :parser
17
31
 
18
- # make sure the parser's up-to-date when we test
19
- Rake::Task[:test].prerequisites << :parser
20
- Rake::Task[:check_manifest].prerequisites << :parser
32
+ # Make sure the generated parser gets included in the manifest.
33
+ Rake::Task["check_manifest"].prerequisites << :parser
data/einstein.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{einstein}
3
+ s.version = "0.2.0"
4
+
5
+ s.specification_version = 2 if s.respond_to? :specification_version=
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Dray Lacy"]
9
+ s.date = %q{2008-05-15}
10
+ s.description = %q{Safe arithmetic parser for Ruby apps}
11
+ s.email = ["dray@izea.com"]
12
+ s.extra_rdoc_files = ["Benchmarks.txt", "History.txt", "License.txt", "Manifest.txt", "README.txt"]
13
+ s.files = ["Benchmarks.txt", "History.txt", "License.txt", "Manifest.txt", "README.txt", "Rakefile", "einstein.gemspec", "lib/einstein.rb", "lib/einstein/evaluator.rb", "lib/einstein/expression.rb", "lib/einstein/parser.racc", "lib/einstein/parser.racc.rb", "lib/einstein/parser.rex", "lib/einstein/parser.rex.rb", "lib/einstein/pretty_printer.rb", "lib/einstein/processor.rb", "lib/einstein/version.rb", "setup.rb", "tasks/benchmark.rake", "test/helper.rb", "test/test_einstein.rb", "test/test_evaluator.rb", "test/test_parser.rb", "test/test_pretty_printer.rb"]
14
+ s.has_rdoc = true
15
+ s.homepage = %q{http://github.com/omghax/einstein}
16
+ s.rdoc_options = ["--main", "README.txt"]
17
+ s.require_paths = ["lib"]
18
+ s.rubyforge_project = %q{einstein}
19
+ s.rubygems_version = %q{1.1.1}
20
+ s.summary = %q{Safe arithmetic parser for Ruby apps}
21
+ s.test_files = ["test/test_einstein.rb", "test/test_evaluator.rb", "test/test_parser.rb", "test/test_pretty_printer.rb"]
22
+
23
+ s.add_dependency(%q<hoe>, [">= 1.5.1"])
24
+ end
data/lib/einstein.rb CHANGED
@@ -1,50 +1,19 @@
1
+ # Add ./lib to the load path.
1
2
  $:.unshift File.dirname(__FILE__)
3
+ require 'einstein/parser.racc.rb'
4
+ require 'einstein/parser.rex.rb'
5
+ require 'einstein/expression'
6
+ require 'einstein/version'
2
7
 
3
- require "einstein/parser"
4
-
5
- # == Basic Usage
6
- #
7
- # If you're only interested in evaluating expressions, you can simply call
8
- # Einstein.evaluate:
9
- #
10
- # Einstein.evaluate("1 + 2 + 3") # => 6
11
- #
12
- # You can use variables as well, but you must supply their values:
13
- #
14
- # Einstein.evaluate("x * 4", :x => 5) # => 20
15
- # Einstein.evaluate("x + y + z", :x => 3, :y => 4, :z => 5) # => 12
16
- #
17
- # == Prepared Statements
18
- #
19
- # If you're going to be performing the same operation over a large set of
20
- # different values, and performance is a concern, you can have Einstein parse
21
- # the expression once beforehand, and return a statement object (technically,
22
- # an instance of Einstein::Nodes::StatementNode). You can then pass your
23
- # values directly to this statement for much faster processing, rather than
24
- # making Einstein parse the same formula over and over again each time.
25
- #
26
- # # This will return a prepared statement.
27
- # stmt = Einstein.parse("x + y")
28
- #
29
- # # You can then evaluate this statement against different inputs.
30
- # stmt.evaluate(:x => 1, :y => 2) # => 3
31
- # stmt.evaluate(:x => 25, :y => 30) # => 55
32
8
  module Einstein
33
- # Base class for all Einstein errors.
34
- class Error < StandardError
35
- end
36
-
37
- # Raised when a variable's name cannot be resolved.
38
- class ResolveError < Error
39
- end
40
-
41
- # Raised when division by zero occurs.
42
- class ZeroDivisionError < Error
9
+ # Raised when an undefined variable is referenced.
10
+ class ResolveError < StandardError # :nodoc:
43
11
  end
44
12
 
45
- # Parse the given +expression+ and return the AST as
13
+ # Parse the given +expression+ and return the AST as an instance of
14
+ # Einstein::Expression.
46
15
  def self.parse(expression)
47
- Parser.new.parse(expression)
16
+ Expression.new(Parser.new.scan_str(expression))
48
17
  end
49
18
 
50
19
  # Evaluate the given +expression+ with the given +scope+. Any variables
@@ -0,0 +1,102 @@
1
+ require 'einstein/processor'
2
+
3
+ module Einstein
4
+ # This processor walks the AST and evaluates the values of the nodes.
5
+ class Evaluator < Processor
6
+ # Initialize a new instance of this processor with the given +scope+, as a
7
+ # hash. This +scope+ should provide a mapping of variable names to
8
+ # values.
9
+ def initialize(scope = {})
10
+ # Convert symbol keys into strings.
11
+ @scope = scope.inject({}) do |hash, (key, value)|
12
+ hash[key.to_s] = value
13
+ hash
14
+ end
15
+ end
16
+
17
+ # Returns the value of +exp+.
18
+ def process_lit(exp)
19
+ exp[1]
20
+ end
21
+
22
+ # Returns the value of +exp+.
23
+ def process_unary_plus(exp)
24
+ process(exp[1])
25
+ end
26
+
27
+ # Returns the negated value of +exp+.
28
+ def process_unary_minus(exp)
29
+ -process(exp[1])
30
+ end
31
+
32
+ # Performs a bitwise NOT operation on the value of +exp+.
33
+ def process_bitwise_not(exp)
34
+ ~process(exp[1])
35
+ end
36
+
37
+ # Raises the left value of +exp+ by the right value of +exp+.
38
+ def process_exponent(exp)
39
+ process(exp[1]) ** process(exp[2])
40
+ end
41
+
42
+ # Multiplies the left and right values of +exp+.
43
+ def process_multiply(exp)
44
+ process(exp[1]) * process(exp[2])
45
+ end
46
+
47
+ # Divides the left value of +exp+ by the right value of +exp+. Will raise
48
+ # ZeroDivisionError if the right value of +exp+ evaluates to 0.
49
+ def process_divide(exp)
50
+ process(exp[1]) / process(exp[2])
51
+ end
52
+
53
+ # Performs a modulus operation for the left and right values of +exp+.
54
+ def process_modulus(exp)
55
+ process(exp[1]) % process(exp[2])
56
+ end
57
+
58
+ # Adds the left and right values of +exp+.
59
+ def process_add(exp)
60
+ process(exp[1]) + process(exp[2])
61
+ end
62
+
63
+ # Subtracts the right value of +exp+ by the left value of +exp+.
64
+ def process_subtract(exp)
65
+ process(exp[1]) - process(exp[2])
66
+ end
67
+
68
+ # Performs a bitwise left shift of the left value of +exp+ by the number
69
+ # of bits specified in the right value of +exp+.
70
+ def process_lshift(exp)
71
+ process(exp[1]) << process(exp[2])
72
+ end
73
+
74
+ # Performs a bitwise right shift of the left value of +exp+ by the number
75
+ # of bits specified in the right value of +exp+.
76
+ def process_rshift(exp)
77
+ process(exp[1]) >> process(exp[2])
78
+ end
79
+
80
+ # Performs a bitwise AND with the left and right values of +exp+.
81
+ def process_bitwise_and(exp)
82
+ process(exp[1]) & process(exp[2])
83
+ end
84
+
85
+ # Performs a bitwise XOR with the left and right values of +exp+.
86
+ def process_bitwise_xor(exp)
87
+ process(exp[1]) ^ process(exp[2])
88
+ end
89
+
90
+ # Performs a bitwise OR with the left and right values of +exp+.
91
+ def process_bitwise_or(exp)
92
+ process(exp[1]) | process(exp[2])
93
+ end
94
+
95
+ # Performs a lookup for the value of +exp+ inside this processor's scope.
96
+ # Raises ResolveError if the variable is not in scope.
97
+ def process_resolve(exp)
98
+ raise ResolveError, "Undefined variable: #{exp[1]}" unless @scope.has_key?(exp[1])
99
+ @scope[exp[1]]
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,37 @@
1
+ require 'einstein/evaluator'
2
+ require 'einstein/pretty_printer'
3
+
4
+ module Einstein
5
+ # Node representing an entire Einstein statement. This is the root-level
6
+ # node of the parser. This node's +exp+ is the tree of expressions that
7
+ # make up a single logical statement.
8
+ class Expression
9
+ # Initialize a new expression wrapping the given +sexp+, which is expected
10
+ # to be an s-expression represented as a set of nested arrays.
11
+ #
12
+ # Example:
13
+ # Expression.new([:lit, 10]) # => 10
14
+ # Expression.new([:multiply, [:lit, 10], [:lit, 5]]) # => (10 * 5)
15
+ def initialize(sexp)
16
+ @sexp = sexp
17
+ end
18
+
19
+ # The s-expression given to this Expression's constructor.
20
+ attr_reader :sexp
21
+ alias_method :to_sexp, :sexp
22
+
23
+ # Evaluate this node against the given +scope+. Returns a numeric value
24
+ # calculated by walking the AST with an instance of EvaluateProcessor.
25
+ def evaluate(scope = {})
26
+ Evaluator.new(scope).process(to_sexp)
27
+ end
28
+
29
+ # Performs a "pretty print" of this expression.
30
+ def to_s
31
+ PrettyPrinter.new.process(to_sexp)
32
+ end
33
+
34
+ # Also use #to_s for inspecting a node in IRB.
35
+ alias_method :inspect, :to_s
36
+ end
37
+ end
@@ -0,0 +1,41 @@
1
+ class Einstein::Parser
2
+
3
+ rule
4
+ exp : bitwise_or
5
+
6
+ literal : IDENT { result = [:resolve, val.first] }
7
+ | NUMBER { result = [:lit, val.first] }
8
+
9
+ primary : '(' exp ')' { result = val[1] }
10
+ | literal
11
+
12
+ unary : primary
13
+ | '+' unary { result = [:unary_plus, val[1]] }
14
+ | '-' unary { result = [:unary_minus, val[1]] }
15
+ | '~' unary { result = [:bitwise_not, val[1]] }
16
+
17
+ exponent : unary
18
+ | exponent MULT2 unary { result = [:exponent, val[0], val[2]] }
19
+
20
+ multiplicative : exponent
21
+ | multiplicative '*' exponent { result = [:multiply, val[0], val[2]] }
22
+ | multiplicative '/' exponent { result = [:divide, val[0], val[2]] }
23
+ | multiplicative '%' exponent { result = [:modulus, val[0], val[2]] }
24
+
25
+ additive : multiplicative
26
+ | additive '+' multiplicative { result = [:add, val[0], val[2]] }
27
+ | additive '-' multiplicative { result = [:subtract, val[0], val[2]] }
28
+
29
+ shift : additive
30
+ | shift LSHIFT additive { result = [:lshift, val[0], val[2]] }
31
+ | shift RSHIFT additive { result = [:rshift, val[0], val[2]] }
32
+
33
+ bitwise_and : shift
34
+ | bitwise_and '&' shift { result = [:bitwise_and, val[0], val[2]] }
35
+
36
+ bitwise_or : bitwise_and
37
+ | bitwise_or '|' bitwise_and { result = [:bitwise_or, val[0], val[2]] }
38
+ | bitwise_or '^' bitwise_and { result = [:bitwise_xor, val[0], val[2]] }
39
+
40
+ ---- header ----
41
+ require 'einstein/parser.rex.rb'