interrotron 0.0.1

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.
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - jruby-19mode # JRuby in 1.9 mode
5
+ - rbx-19mode
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in interrotron.gemspec
4
+ gemspec
5
+ gem 'rake'
6
+ gem 'hashie', require: "hashie/mash"
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Andrew Cholakian
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,83 @@
1
+ # Interrotron
2
+
3
+ [![Build Status](https://secure.travis-ci.org/andrewvc/interrotron.png?branch=master)](http://travis-ci.org/andrewvc/interrotron)
4
+
5
+ A simple non-turing complete lisp meant to be embedded in apps as a rules engine. It is intentionally designed to limit the harm evaluated code can do (in contrast to a straight ruby 'eval') and is constrained to:
6
+
7
+ * Be totally sandboxed by default
8
+ * Always finish executing (no infinite loops)
9
+ * Let you easily add variables and functions (simply pass in a hash defining them)
10
+ * Be a small, single file
11
+
12
+ ## Installation
13
+
14
+ Either add the `interrotron` gem, or just copy and paste [interratron.rb](https://github.com/andrewvc/interrotron/blob/master/lib/interrotron.rb)
15
+
16
+ ## Usage
17
+
18
+ ```ruby
19
+ # Injecting a variable and evaluating a function is easy!
20
+ Interrotron.run('(> 51 custom_var)', :custom_var => 10)
21
+ # => true
22
+
23
+ #You can inject functions just as easily
24
+ Interrotron.run("(doubler (+ 2 2))", :doubler => proc {|a| a*2 })
25
+ # => 8
26
+
27
+ # You can even pre-compile scripts for speed / re-use!
28
+ tron = Interrotron.new(:is_valid => proc {|a| a.reverse == 'oof'})
29
+ compiled = tron.compile("(is_valid my_param)")
30
+ compiled.call(:my_param => 'foo')
31
+ # => true
32
+ compiled.call(:my_param => 'bar')
33
+ #=> false
34
+
35
+ # Since interrotron is meant for business rules, it handles dates as a
36
+ # native type as instances of ruby's DateTime class. You can use literals
37
+ # for that like so:
38
+ Interrotron.run('(> #dt{2010-09-04} start_date)', start_date: DateTime.parse('2012-12-12'))
39
+ # => true
40
+
41
+ # You can, of course, create arbitarily complex exprs
42
+ Interrotron.run("(if false
43
+ (+ 4 -3)
44
+ (- 10 (+ 2 (+ 1 1))))")
45
+ # => 6
46
+
47
+ # Additionally, it is possible to constrain execution to a maximum number of
48
+ # operations by passing in a third argument
49
+ Interrotron.run("str (+ 1 2) (+ 3 4) (+ 5 7))", {}, 4)
50
+ # => raises Interrotron::OpsThresholdError since 4 operations were executed
51
+
52
+ ```
53
+
54
+ The following functions and variables are built in to Interrotron (and more are on the way!):
55
+ ```clojure
56
+ (if pred then else) ; it's an if / else statement
57
+ (cond pred1 clause1 pred2 clause2 true fallbackclause) ; like a case statement
58
+ (and e1, e2, ...) ; logical and, returns last arg if true
59
+ (or e1, e2, ...) ; logical or, returns first true arg
60
+ (not expr) ; negates
61
+ (! expr) ; negates
62
+ (identity expr) ; returns its argument
63
+ (str s1, s2, ...) ; converts its args to strings, also concatenates them
64
+ (floor expr) ; equiv to num.floor
65
+ (ceil expr) ; equiv to num.ceil
66
+ (round expr) ; equiv to num.round
67
+ (max lst) ; returns the largest element in a list
68
+ (min lst) ; returns the smallest element in a list
69
+ (to_i expr) ; int conversion
70
+ (to_f expr) ; float conversion
71
+ (rand) ; returns a random float between 0 and 1
72
+ (upcase str) ; uppercases a string
73
+ (downcase) ; lowercases a string
74
+ (now) ; returns the current DateTime
75
+ ```
76
+
77
+ ## Contributing
78
+
79
+ 1. Fork it
80
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
81
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
82
+ 4. Push to the branch (`git push origin my-new-feature`)
83
+ 5. Create new Pull Request
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require "rspec/core/rake_task"
4
+
5
+ Bundler::GemHelper.install_tasks
6
+
7
+ RSpec::Core::RakeTask.new('spec') do |t|
8
+ t.rspec_opts = '--tag ~integration'
9
+ end
10
+
11
+ RSpec::Core::RakeTask.new('spec:integration') do |t|
12
+ t.pattern = 'spec/integration/*_spec.rb'
13
+ end
14
+
15
+ task :default => :spec
16
+
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/interrotron/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Andrew Cholakian"]
6
+ gem.email = ["andrew@andrewvc.com"]
7
+ gem.description = %q{A tiny, embeddable, lisp VM}
8
+ gem.summary = %q{A lisp VM meant to run with guarantees on execution for business rules}
9
+ gem.homepage = ""
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "interrotron"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Interrotron::VERSION
17
+ gem.add_development_dependency "rspec", "~> 2.6"
18
+ end
@@ -0,0 +1,240 @@
1
+ require "interrotron/version"
2
+ require 'date'
3
+ require 'hashie/mash'
4
+
5
+ # This is a Lispish DSL meant to define business rules
6
+ # in environments where you do *not* want a turing complete language.
7
+ # It comes with a very small number of builtin functions, all overridable.
8
+ #
9
+ # It is meant to aid in creating a DSL that can be executed in environments
10
+ # where code injection could be dangerous.
11
+ #
12
+ # To compile and run, you could, for example:
13
+ # Interrotron.new().compile('(+ a_custom_var 2)').call("a_custom_var" => 4)
14
+ # Interrotron.new().compile.run('(+ a_custom_var 4)', :vars => {"a_custom_var" => 2})
15
+ # => 6
16
+ # You can inject your own custom functions and constants via the :vars option.
17
+ #
18
+ # Additionally, you can cap the number of operations exected with the :max_ops option
19
+ # This is of limited value since recursion is not a feature
20
+ #
21
+ class Interrotron
22
+ class ParserError < StandardError; end
23
+ class InvalidTokenError < ParserError; end
24
+ class SyntaxError < ParserError; end
25
+ class UndefinedVarError < ParserError; end
26
+ class OpsThresholdError < StandardError; end
27
+ class InterroArgumentError < StandardError; end
28
+
29
+ class Macro
30
+ def initialize(&block)
31
+ @block = block
32
+ end
33
+ def call(*args)
34
+ @block.call(*args)
35
+ end
36
+ end
37
+
38
+ class Token
39
+ attr_accessor :type, :value
40
+ def initialize(type,value)
41
+ @type = type
42
+ @value = value
43
+ end
44
+ end
45
+
46
+ TOKENS = [
47
+ [:lpar, /\(/],
48
+ [:rpar, /\)/],
49
+ [:fn, /fn/],
50
+ [:var, /[A-Za-z_><\+\>\<\!\=\*\/\%\-]+/],
51
+ [:num, /(\-?[0-9]+(\.[0-9]+)?)/],
52
+ [:datetime, /#dt\{([^\{]+)\}/, {capture: 1}],
53
+ [:spc, /\s+/, {discard: true}],
54
+ [:str, /"([^"\\]*(\\.[^"\\]*)*)"/, {capture: 1}],
55
+ [:str, /'([^'\\]*(\\.[^'\\]*)*)'/, {capture: 1}]
56
+ ]
57
+
58
+ # Quote a ruby variable as a interrotron one
59
+ def self.qvar(val)
60
+ Token.new(:var, val.to_s)
61
+ end
62
+
63
+ DEFAULT_VARS = Hashie::Mash.new({
64
+ 'if' => Macro.new {|i,pred,t_clause,f_clause| i.vm_eval(pred) ? t_clause : f_clause },
65
+ 'cond' => Macro.new {|i,*args|
66
+ raise InterroArgumentError, "Cond requires at least 3 args" unless args.length >= 3
67
+ raise InterroArgumentError, "Cond requires an even # of args!" unless args.length.even?
68
+ res = qvar('nil')
69
+ args.each_slice(2).any? {|slice|
70
+ pred, expr = slice
71
+ res = expr if i.vm_eval(pred)
72
+ }
73
+ res
74
+ },
75
+ 'and' => Macro.new {|i,*args| args.all? {|a| i.vm_eval(a)} ? args.last : qvar('false') },
76
+ 'or' => Macro.new {|i,*args| args.detect {|a| i.vm_eval(a) } || qvar('false') },
77
+ 'array' => proc {|*args| args},
78
+ 'identity' => proc {|a| a},
79
+ 'not' => proc {|a| !a},
80
+ '!' => proc {|a| !a},
81
+ '>' => proc {|a,b| a > b},
82
+ '<' => proc {|a,b| a < b},
83
+ '>=' => proc {|a,b| a >= b},
84
+ '<=' => proc {|a,b| a <= b},
85
+ '=' => proc {|a,b| a == b},
86
+ '!=' => proc {|a,b| a != b},
87
+ 'true' => true,
88
+ 'false' => false,
89
+ 'nil' => nil,
90
+ '+' => proc {|*args| args.reduce(&:+)},
91
+ '-' => proc {|*args| args.reduce(&:-)},
92
+ '*' => proc {|*args| args.reduce(&:*)},
93
+ '/' => proc {|a,b| a / b},
94
+ '%' => proc {|a,b| a % b},
95
+ 'floor' => proc {|a| a.floor},
96
+ 'ceil' => proc {|a| a.ceil},
97
+ 'round' => proc {|a| a.round},
98
+ 'max' => proc {|arr| arr.max},
99
+ 'min' => proc {|arr| arr.min},
100
+ 'first' => proc {|arr| arr.first},
101
+ 'last' => proc {|arr| arr.last},
102
+ 'length' => proc {|arr| arr.length},
103
+ 'to_i' => proc {|a| a.to_i},
104
+ 'to_f' => proc {|a| a.to_f},
105
+ 'rand' => proc { rand },
106
+ 'upcase' => proc {|a| a.upcase},
107
+ 'downcase' => proc {|a| a.downcase},
108
+ 'now' => proc { DateTime.now },
109
+ 'str' => proc {|*args| args.reduce("") {|m,a| m + a.to_s}}
110
+ })
111
+
112
+ def initialize(vars={},max_ops=nil)
113
+ @max_ops = max_ops
114
+ @instance_default_vars = DEFAULT_VARS.merge(vars)
115
+ end
116
+
117
+ def reset!
118
+ @op_count = 0
119
+ @stack = [@instance_default_vars]
120
+ end
121
+
122
+ # Converts a string to a flat array of Token objects
123
+ def lex(str)
124
+ return [] if str.nil?
125
+ tokens = []
126
+ while str.length > 0
127
+ matched_any = TOKENS.any? {|name,matcher,opts|
128
+ opts ||= {}
129
+ matches = matcher.match(str)
130
+ if !matches || !matches.pre_match.empty?
131
+ false
132
+ else
133
+ mlen = matches[0].length
134
+ str = str[mlen..-1]
135
+ m = matches[opts[:capture] || 0]
136
+ tokens << Token.new(name, m) unless opts[:discard] == true
137
+ true
138
+ end
139
+ }
140
+ raise InvalidTokenError, "Invalid token at: #{str}" unless matched_any
141
+ end
142
+ tokens
143
+ end
144
+
145
+ # Transforms token values to ruby types
146
+ def cast(t)
147
+ new_val = case t.type
148
+ when :num
149
+ t.value =~ /\./ ? t.value.to_f : t.value.to_i
150
+ when :datetime
151
+ DateTime.parse(t.value)
152
+ else
153
+ t.value
154
+ end
155
+ t.value = new_val
156
+ t
157
+ end
158
+
159
+ def parse(tokens)
160
+ return [] if tokens.empty?
161
+ expr = []
162
+ t = tokens.shift
163
+ if t.type == :lpar
164
+ while t = tokens[0]
165
+ if t.type == :lpar
166
+ expr << parse(tokens)
167
+ else
168
+ tokens.shift
169
+ break if t.type == :rpar
170
+ expr << cast(t)
171
+ end
172
+ end
173
+ elsif t.type != :rpar
174
+ tokens.shift
175
+ expr << cast(t)
176
+ #raise SyntaxError, "Expected :lparen, got #{t} while parsing #{tokens}"
177
+ end
178
+ expr
179
+ end
180
+
181
+ def resolve_token(token)
182
+ case token.type
183
+ when :var
184
+ frame = @stack.reverse.find {|frame| frame.has_key?(token.value) }
185
+ raise UndefinedVarError, "Var '#{token.value}' is undefined!" unless frame
186
+ frame[token.value]
187
+ else
188
+ token.value
189
+ end
190
+ end
191
+
192
+ def register_op
193
+ return unless @max_ops
194
+ @op_count += 1
195
+ raise OpsThresholdError, "Exceeded max ops(#{@max_ops}) allowed!" if @op_count && @op_count > @max_ops
196
+ end
197
+
198
+ def vm_eval(expr,max_ops=nil)
199
+ return resolve_token(expr) if expr.is_a?(Token)
200
+ return nil if expr.empty?
201
+ register_op
202
+
203
+ head = vm_eval(expr[0])
204
+ if head.is_a?(Macro)
205
+ expanded = head.call(self, *expr[1..-1])
206
+ vm_eval(expanded)
207
+ else
208
+ args = expr[1..-1].map {|e|vm_eval(e)}
209
+
210
+ head.is_a?(Proc) ? head.call(*args) : head
211
+ end
212
+ end
213
+
214
+ # Returns a Proc than can be executed with #call
215
+ # Use if you want to repeatedly execute one script, this
216
+ # Will only lex/parse once
217
+ def compile(str)
218
+ tokens = lex(str)
219
+ ast = parse(tokens)
220
+
221
+ proc {|vars,max_ops|
222
+ reset!
223
+ @max_ops = max_ops
224
+ @stack = [@instance_default_vars.merge(vars)]
225
+ vm_eval(ast)
226
+ }
227
+ end
228
+
229
+ def self.compile(str)
230
+ Interrotron.new().compile(str)
231
+ end
232
+
233
+ def run(str,vars={},max_ops=nil)
234
+ compile(str).call(vars,max_ops)
235
+ end
236
+
237
+ def self.run(str,vars={},max_ops=nil)
238
+ Interrotron.new().run(str,vars,max_ops)
239
+ end
240
+ end
@@ -0,0 +1,3 @@
1
+ class Interrotron
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,154 @@
1
+ require 'interrotron'
2
+
3
+ describe "running" do
4
+
5
+ def run(s,vars={},max_ops=nil)
6
+ Interrotron.run(s,vars,max_ops)
7
+ end
8
+
9
+ it "should exec identity correctly" do
10
+ run("(identity 2)").should == 2
11
+ end
12
+
13
+ describe "and" do
14
+ it "should return true if all truthy" do
15
+ run("(and 1 true 'ohai')").should be_true
16
+ end
17
+ it "should return false if not all truthy" do
18
+ run("(and 1 true false)").should be_false
19
+ end
20
+ end
21
+
22
+ describe "or" do
23
+ it "should return true if all truthy" do
24
+ run("(or 1 true 'ohai')").should be_true
25
+ end
26
+ it "should return true if some truthy" do
27
+ run("(or 1 true false)").should be_true
28
+ end
29
+ it "should return false if all falsey" do
30
+ run("(or nil false)").should be_false
31
+ end
32
+ end
33
+
34
+ describe "evaluating a single tokens outside on sexpr" do
35
+ it "simple values should return themselves" do
36
+ run("28").should == 28
37
+ end
38
+ it "vars should dereference" do
39
+ run("true").should == true
40
+ end
41
+ end
42
+
43
+ describe "nested expressions" do
44
+ it "should execute a simple nested expr correctly" do
45
+ run("(+ (* 2 2) (% 5 4))").should == 5
46
+ end
47
+
48
+ it "should execute complex nested exprs correctly" do
49
+ run("(if false (+ 4 -3) (- 10 (+ 2 (+ 1 1))))").should == 6
50
+ end
51
+ end
52
+
53
+ describe "custom vars" do
54
+ it "should define custom vars" do
55
+ run("my_var", "my_var" => 123).should == 123
56
+ end
57
+ it "should properly execute proc custom vars" do
58
+ run("(my_proc 4)", "my_proc" => proc {|a| a*2 }).should == 8
59
+ end
60
+ end
61
+
62
+ describe "date times" do
63
+ it "should parse and compare them properly" do
64
+ run('(> #dt{2010-09-04} start_date)', start_date: DateTime.parse('2012-12-12'))
65
+ end
66
+ end
67
+
68
+ describe "cond" do
69
+ it "should work for a simple case where there is a match" do
70
+ run("(cond (> 1 2) (* 2 2)
71
+ (< 5 10) 'ohai')").should == 'ohai'
72
+ end
73
+ it "should return nil when no matches available" do
74
+ run("(cond (> 1 2) (* 2 2)
75
+ false 'ohai')").should == nil
76
+ end
77
+ it "should support true as a fallthrough clause" do
78
+ run("(cond (> 1 2) (* 2 2)
79
+ false 'ohai'
80
+ true 'backup')").should == 'backup'
81
+ end
82
+ end
83
+
84
+ describe "intermediate compilation" do
85
+ it "should support compiled scripts" do
86
+ # Setup an interrotron obj with some default vals
87
+ tron = Interrotron.new(:is_valid => proc {|a| a.reverse == 'oof'})
88
+ compiled = tron.compile("(is_valid my_param)")
89
+ compiled.call(:my_param => 'foo').should == true
90
+ compiled.call(:my_param => 'bar').should == false
91
+ end
92
+ end
93
+
94
+ describe "higher order functions" do
95
+ it "should support calculating a fn at the head" do
96
+ run('((or * +) 5 5)').should == 25
97
+ end
98
+ end
99
+
100
+ describe "array" do
101
+ it "should return a ruby array" do
102
+ run("(array 1 2 3)").should == [1, 2, 3]
103
+ end
104
+
105
+ it "should detect max vals correctly" do
106
+ run("(max (array 82 10 100 99.5))").should == 100
107
+ end
108
+
109
+ it "should detect min vals correctly" do
110
+ run("(min (array 82 10 100 99.5))").should == 10
111
+ end
112
+
113
+ it "should let you get the head" do
114
+ run("(first (array 1 2 3))").should == 1
115
+ end
116
+
117
+ it "should let you get the tail" do
118
+ run("(last (array 1 2 3))").should == 3
119
+ end
120
+
121
+ it "should let you get the length" do
122
+ run("(length (array 1 2 3 'bob'))").should == 4
123
+ end
124
+
125
+ it "should implement detect correctly in the positive case" do
126
+ pending "not now"
127
+ #run("(detect (> 10 n) (array 1 5 30 1))").should
128
+ end
129
+ end
130
+
131
+ describe "functions" do
132
+ it "should have access to vars they've bound" do
133
+ pending
134
+ run("((fn (n) (* n 2)) 5)").should == 10
135
+ end
136
+ end
137
+
138
+ describe "readme examples" do
139
+ it "should execute the simple custom var one" do
140
+ Interrotron.run('(> 51 custom_var)', 'custom_var' => 10).should == true
141
+ end
142
+ end
143
+
144
+ describe "op counter" do
145
+ it "should not stop scripts under or at the threshold" do
146
+ run("(str (+ 1 2) (+ 3 4) (+ 5 7))", {}, 4)
147
+ end
148
+ it "should terminate with the proper exception if over the threshold" do
149
+ proc {
150
+ run("(str (+ 1 2) (+ 3 4) (+ 5 7))", {}, 3)
151
+ }.should raise_exception(Interrotron::OpsThresholdError)
152
+ end
153
+ end
154
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: interrotron
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Andrew Cholakian
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-05-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70244487965700 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '2.6'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70244487965700
25
+ description: A tiny, embeddable, lisp VM
26
+ email:
27
+ - andrew@andrewvc.com
28
+ executables: []
29
+ extensions: []
30
+ extra_rdoc_files: []
31
+ files:
32
+ - .gitignore
33
+ - .travis.yml
34
+ - Gemfile
35
+ - LICENSE
36
+ - README.md
37
+ - Rakefile
38
+ - interrotron.gemspec
39
+ - lib/interrotron.rb
40
+ - lib/interrotron/version.rb
41
+ - spec/interrotron_spec.rb
42
+ homepage: ''
43
+ licenses: []
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
+ none: false
56
+ requirements:
57
+ - - ! '>='
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubyforge_project:
62
+ rubygems_version: 1.8.10
63
+ signing_key:
64
+ specification_version: 3
65
+ summary: A lisp VM meant to run with guarantees on execution for business rules
66
+ test_files:
67
+ - spec/interrotron_spec.rb