kalc 0.3

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2012 Chris Parker
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,74 @@
1
+ h1. kalc
2
+
3
+ h2. Introduction
4
+
5
+ kalc is a small functional programming language that (gasp) borrows a lot of
6
+ its syntax from the Excel formula language.
7
+
8
+ h2. ikalc
9
+
10
+ kalc comes with its own repl, known as ikalc. It evaluates when you press return.
11
+
12
+ h2. Syntax
13
+
14
+ kalc is a tiny language, and it has very little syntax. It does support
15
+ functions, variable assignment, and arithmetic.
16
+
17
+ h3. Arithmetic
18
+
19
+ <pre>
20
+ 1 + 1 / (10 * 100) - 3 + 3 - (3 - 2)
21
+ 1 > 1
22
+ SUM(1, 2, 3, 4, 5)
23
+ </pre>
24
+
25
+ Arithmetic is standard infix with nesting via parenthesis.
26
+
27
+ h3. Logical operations
28
+
29
+ <pre>
30
+ 1 > 2 ? 1 : 3 # Ternary
31
+ (1 || 2) > 3
32
+ 1 > 2 or 3 < 2 # false
33
+ OR(1 > 2, 3 < 2, 8 == 8) # true
34
+ </pre>
35
+
36
+ h3. Variable assignment
37
+
38
+ <pre>
39
+ a = 1
40
+ b = 2
41
+ d = a + b
42
+ </pre>
43
+
44
+ h3. Creating functions
45
+
46
+ <pre>
47
+ DEFINE FOO(a, b) {
48
+ a + b
49
+ }
50
+ </pre>
51
+
52
+ There are a few examples of functions in lib/stdlib.kalc
53
+
54
+ h3. Loops
55
+
56
+ There are no looping mechanisms to speak of, but recursion works well.
57
+
58
+ <pre>
59
+ DEFINE SAMPLE_LOOP(a) {
60
+ PUTS(a)
61
+ IF(a == 1, 1, SAMPLE_LOOP(a - 1))
62
+ }
63
+ </pre>
64
+
65
+ h4. More inside
66
+
67
+ Not everything is documented yet. As you can see, it is a mix of
68
+ a lot of different ideas. The goal is to have an excel-like language
69
+ that is somewhat functional.
70
+
71
+ h2. Contributing
72
+
73
+ Fork on GitHub and after you've committed tested patches, send a pull request.
74
+
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ kalc_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ $LOAD_PATH.unshift(kalc_dir) unless $LOAD_PATH.include?(kalc_dir)
5
+
6
+ require "kalc"
7
+
8
+ repl = Kalc::Repl.new.run
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ kalc_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ $LOAD_PATH.unshift(kalc_dir) unless $LOAD_PATH.include?(kalc_dir)
5
+
6
+ require "kalc"
7
+ require "pp"
8
+
9
+ input = File.read(ARGV.first)
10
+
11
+ grammar = Kalc::Grammar.new
12
+ g = grammar.parse(input)
13
+
14
+ transform = Kalc::Transform.new
15
+ ast = transform.apply(g)
16
+
17
+
18
+ interpreter = Kalc::Interpreter.new
19
+ puts interpreter.run(ast)
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "kalc/version"
4
+ #
5
+ Gem::Specification.new do |s|
6
+ s.name = "kalc"
7
+ s.version = Kalc::VERSION
8
+ s.authors = ["Chris Parker"]
9
+ s.email = ["mrcsparker@gmail.com"]
10
+ s.homepage = "https://github.com/mrcsparker/kalc"
11
+ s.summary = %q{Small calculation language.}
12
+ s.description = %q{Calculation language slightly based on Excel's formula language.}
13
+
14
+ s.rubyforge_project = "kalc"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ # specify any dependencies here; for example:
22
+ s.add_development_dependency "rake"
23
+ s.add_development_dependency "rspec"
24
+ s.add_runtime_dependency "parslet", "~> 1.2"
25
+ end
@@ -0,0 +1,14 @@
1
+ require 'parslet'
2
+
3
+ require 'kalc/version'
4
+ require 'kalc/grammar'
5
+ require 'kalc/ast'
6
+ require 'kalc/transform'
7
+ require 'kalc/environment'
8
+ require 'kalc/interpreter'
9
+ require 'kalc/repl'
10
+
11
+ module Kalc
12
+
13
+ end
14
+
@@ -0,0 +1,198 @@
1
+ module Kalc
2
+ module Ast
3
+
4
+ class Commands
5
+ attr_reader :commands
6
+
7
+ def initialize(commands)
8
+ @commands = commands
9
+ end
10
+
11
+ def eval(context)
12
+ last = nil
13
+ @commands.each do |command|
14
+ last = command.eval(context)
15
+ end
16
+ last
17
+ end
18
+ end
19
+
20
+
21
+ class Expressions
22
+ attr_reader :expressions
23
+
24
+ def initialize(expressions)
25
+ @expressions = expressions
26
+ end
27
+
28
+ def eval(context)
29
+ last = nil
30
+ @expressions.each do |exp|
31
+ last = exp.eval(context)
32
+ end
33
+ last
34
+ end
35
+ end
36
+
37
+ class BooleanValue
38
+ attr_reader :value
39
+
40
+ def initialize(value)
41
+ @value = value
42
+ end
43
+
44
+ def eval(context)
45
+ @value == 'TRUE' ? true : false
46
+ end
47
+ end
48
+
49
+ class FloatingPointNumber
50
+ attr_reader :value
51
+
52
+ def initialize(value)
53
+ @value = value
54
+ end
55
+
56
+ def eval(context)
57
+ Float(@value)
58
+ end
59
+ end
60
+
61
+ class Arithmetic
62
+ attr_reader :left
63
+ attr_reader :right
64
+ attr_reader :operator
65
+
66
+ def initialize(left, right, operator)
67
+ @left = left
68
+ @right = right
69
+ @operator = operator
70
+ end
71
+
72
+ def eval(context)
73
+
74
+ @left = @left.eval(context)
75
+ @right = @right.eval(context)
76
+
77
+ case @operator.to_s.strip
78
+ when '&&'
79
+ @left && @right
80
+ when 'and'
81
+ @left && @right
82
+ when '||'
83
+ @left || @right
84
+ when 'or'
85
+ @left || @right
86
+ when '<='
87
+ @left <= @right
88
+ when '>='
89
+ @left >= @right
90
+ when '=='
91
+ @left == @right
92
+ when '!='
93
+ @left != @right
94
+ when '-'
95
+ @left - @right
96
+ when '+'
97
+ @left + @right
98
+ when '*'
99
+ @left * @right
100
+ when '/'
101
+ @left / @right
102
+ when '%'
103
+ @left % @right
104
+ when '<'
105
+ @left < @right
106
+ when '>'
107
+ @left > @right
108
+ end
109
+ end
110
+ end
111
+
112
+ class Conditional
113
+ attr_reader :condition, :true_cond, :false_cond
114
+
115
+ def initialize(condition, true_cond, false_cond)
116
+ @condition = condition
117
+ @true_cond = true_cond
118
+ @false_cond = false_cond
119
+ end
120
+
121
+ def eval(context)
122
+ @condition ? true_cond : false_cond
123
+ end
124
+ end
125
+
126
+ class Identifier
127
+ attr_reader :identifier
128
+ attr_reader :value
129
+
130
+ def initialize(identifier, value)
131
+ @variable = identifier.to_s.strip
132
+ @value = value
133
+ end
134
+
135
+ def eval(context)
136
+ context.add_variable(@variable, @value.eval(context))
137
+ end
138
+ end
139
+
140
+ class Variable
141
+ attr_reader :variable
142
+ def initialize(variable)
143
+ @variable = variable
144
+ end
145
+
146
+ def eval(context)
147
+ context.get_variable(@variable)
148
+ end
149
+ end
150
+
151
+ class StringValue
152
+ attr_reader :value
153
+
154
+ def initialize(value)
155
+ @value = value
156
+ end
157
+
158
+ def eval(context)
159
+ value.to_s
160
+ end
161
+ end
162
+
163
+ class FunctionCall
164
+ attr_reader :name
165
+ attr_reader :variable_list
166
+
167
+ def initialize(name, variable_list)
168
+ @name = name
169
+ @variable_list = variable_list
170
+ end
171
+
172
+ def eval(context)
173
+ to_call = context.get_function(@name)
174
+ to_call.call(context, *@variable_list) if to_call
175
+ end
176
+ end
177
+
178
+ class FunctionDefinition
179
+ def initialize(name, argument_list, body)
180
+ @name = name
181
+ @argument_list = argument_list
182
+ @body = body
183
+ end
184
+
185
+ def eval(context)
186
+ context.add_function(@name.to_sym, lambda { |parent_context, *args|
187
+ dup_body = Marshal.load(Marshal.dump(@body))
188
+ cxt = Environment.new(parent_context)
189
+ args.each_with_index do |arg, idx|
190
+ cxt.add_variable(@argument_list[idx].value, arg.eval(cxt))
191
+ end
192
+ dup_body.eval(cxt)
193
+ })
194
+ nil
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,43 @@
1
+ module Kalc
2
+ class Environment
3
+
4
+ attr_reader :functions
5
+ attr_reader :variables
6
+
7
+ def initialize(parent = nil)
8
+ @functions = {}
9
+ @variables = {}
10
+ @parent = parent
11
+ yield self if block_given?
12
+ end
13
+
14
+ def add_function(name, value)
15
+ @functions.update({ name.to_s.strip => value })
16
+ end
17
+
18
+ def get_function(name)
19
+ if fun = @functions[name.to_s.strip]
20
+ fun
21
+ elsif !@parent.nil?
22
+ @parent.get_function(name)
23
+ else
24
+ nil
25
+ end
26
+ end
27
+
28
+ def add_variable(name, value)
29
+ @variables.update({ name.to_s.strip => value })
30
+ value
31
+ end
32
+
33
+ def get_variable(name)
34
+ if var = @variables[name.to_s.strip]
35
+ var
36
+ elsif !@parent.nil?
37
+ @parent.get_variable(name)
38
+ else
39
+ nil
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,273 @@
1
+ # Started with https://github.com/postmodern/cparser code
2
+ # which is based on http://www.lysator.liu.se/c/ANSI-C-grammar-y.html
3
+ # and worked from there
4
+ #
5
+ # The second link really helped when it came to functions
6
+
7
+ class Kalc::Grammar < Parslet::Parser
8
+
9
+ rule(:new_line) { match('[\n\r]').repeat(1) }
10
+
11
+ rule(:space) { match('[ \t\v\n\f]') }
12
+ rule(:spaces) { space.repeat(1) }
13
+ rule(:space?) { space.maybe }
14
+ rule(:spaces?) { space.repeat }
15
+
16
+ rule(:digit) { match('[0-9]') }
17
+ rule(:digits) { digit.repeat(1) }
18
+ rule(:digits?) { digit.repeat }
19
+
20
+ rule(:alpha) { match('[_a-zA-Z]') }
21
+
22
+ rule(:new_line) { match('[\n\r]').repeat(1) }
23
+ rule(:separator) { str(';') >> spaces? }
24
+
25
+ def self.symbols(symbols)
26
+ symbols.each do |name, symbol|
27
+ rule(name) { str(symbol) >> spaces? }
28
+ end
29
+ end
30
+
31
+ symbols :left_paren => '(',
32
+ :right_paren => ')',
33
+ :left_brace => '{',
34
+ :right_brace => '}',
35
+ :comma => ',',
36
+ :colon => ':'
37
+
38
+ def self.operators(operators={})
39
+ trailing_chars = Hash.new { |hash,symbol| hash[symbol] = [] }
40
+
41
+ operators.each_value do |symbol|
42
+ operators.each_value do |op|
43
+ if op[0,symbol.length] == symbol
44
+ char = op[symbol.length,1]
45
+
46
+ unless (char.nil? || char.empty?)
47
+ trailing_chars[symbol] << char
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ operators.each do |name,symbol|
54
+ trailing = trailing_chars[symbol]
55
+
56
+ if trailing.empty?
57
+ rule(name) { str(symbol).as(:operator) >> spaces? }
58
+ else
59
+ pattern = "[#{Regexp.escape(trailing.join)}]"
60
+
61
+ rule(name) {
62
+ (str(symbol) >> match(pattern).absnt?).as(:operator) >> spaces?
63
+ }
64
+ end
65
+ end
66
+ end
67
+
68
+ operators :logical_and => '&&',
69
+ :string_and => 'and',
70
+ :logical_or => '||',
71
+ :string_or => 'or',
72
+ :less_equal => '<=',
73
+ :greater_equal => '>=',
74
+ :equal => '==',
75
+ :not_equal => '!=',
76
+
77
+ :assign => '=',
78
+ :question_mark => '?',
79
+
80
+ :subtract => '-',
81
+ :add => '+',
82
+ :multiply => '*',
83
+ :divide => '/',
84
+ :modulus => '%',
85
+
86
+ :less => '<',
87
+ :greater => '>'
88
+
89
+ rule(:true_keyword) {
90
+ str('TRUE') >> spaces?
91
+ }
92
+
93
+ rule(:false_keyword) {
94
+ str('FALSE') >> spaces?
95
+ }
96
+
97
+ rule(:boolean) {
98
+ (true_keyword | false_keyword).as(:boolean)
99
+ }
100
+
101
+ rule(:string) {
102
+ str('"') >>
103
+ (
104
+ str('\\') >> any |
105
+ str('"').absnt? >> any
106
+ ).repeat.as(:string) >>
107
+ str('"')
108
+ }
109
+
110
+ rule(:number) {
111
+ (match('[+-]').maybe >> digits >> (str('.') >> digits).maybe).as(:number) >> spaces?
112
+ }
113
+
114
+ rule(:identifier) {
115
+ (alpha >> (alpha | digit).repeat) >> spaces?
116
+ }
117
+
118
+ rule(:argument) {
119
+ identifier.as(:argument)
120
+ }
121
+
122
+ # Should look like 'Name'
123
+ rule(:variable) {
124
+ identifier | (str("'") >> identifier >> str("'")) >> spaces?
125
+ }
126
+
127
+ # Does not self-evaluate
128
+ # Use to call function: FUNCTION_NAME(variable_list, ..)
129
+ rule(:variable_list) {
130
+ conditional_expression >> (comma >> conditional_expression).repeat
131
+ }
132
+
133
+ rule(:paren_variable_list) {
134
+ (left_paren >> variable_list.repeat >> right_paren).as(:paren_list)
135
+ }
136
+
137
+ # Does not self-evaluate
138
+ # Used to create function: DEF FUNCTION_NAME(argument_list, ..)
139
+ rule(:argument_list) {
140
+ argument >> (comma >> argument).repeat
141
+ }
142
+
143
+ rule(:paren_argument_list) {
144
+ (left_paren >> argument_list.repeat >> right_paren).as(:paren_list)
145
+ }
146
+
147
+ # Atoms can self-evaluate
148
+ # This where the grammar starts
149
+ rule(:atom) {
150
+ boolean | variable.as(:variable) | number | string | paren_expression
151
+ }
152
+
153
+ # (1 + 2)
154
+ rule(:paren_expression) {
155
+ left_paren >> conditional_expression >> right_paren
156
+ }
157
+
158
+ # IF(1, 2, 3)
159
+ # AND(1, 2, ...)
160
+ rule(:function_call_expression) {
161
+ (identifier.as(:name) >> paren_variable_list.as(:variable_list)).as(:function_call) |
162
+ atom
163
+ }
164
+
165
+ # 1 * 2
166
+ rule(:multiplicative_expression) {
167
+ function_call_expression.as(:left) >>
168
+ (multiply | divide | modulus) >>
169
+ multiplicative_expression.as(:right) |
170
+ function_call_expression
171
+ }
172
+
173
+ # 1 + 2
174
+ rule(:additive_expression) {
175
+ multiplicative_expression.as(:left) >>
176
+ (add | subtract) >>
177
+ additive_expression.as(:right) |
178
+ multiplicative_expression
179
+ }
180
+
181
+ # 1 < 2
182
+ # 1 > 2
183
+ # 1 <= 2
184
+ # 1 >= 2
185
+ rule(:relational_expression) {
186
+ additive_expression.as(:left) >>
187
+ (less | greater | less_equal | greater_equal) >>
188
+ relational_expression.as(:right) |
189
+ additive_expression
190
+ }
191
+
192
+ # 1 == 2
193
+ rule(:equality_expression) {
194
+ relational_expression.as(:left) >>
195
+ (equal | not_equal) >>
196
+ equality_expression.as(:right) |
197
+ relational_expression
198
+ }
199
+
200
+ # 1 && 2
201
+ rule(:logical_and_expression) {
202
+ equality_expression.as(:left) >>
203
+ (logical_and | string_and) >>
204
+ logical_and_expression.as(:right) |
205
+ equality_expression
206
+ }
207
+
208
+ # 1 || 2
209
+ rule(:logical_or_expression) {
210
+ logical_and_expression.as(:left) >>
211
+ (logical_or | string_or) >>
212
+ logical_or_expression.as(:right) |
213
+ logical_and_expression
214
+ }
215
+
216
+ # 1 > 2 ? 3 : 4
217
+ rule(:conditional_expression) {
218
+ logical_or_expression.as(:condition) >>
219
+ question_mark >>
220
+ conditional_expression.as(:true) >>
221
+ colon >>
222
+ conditional_expression.as(:false) |
223
+ logical_or_expression
224
+ }
225
+
226
+ # 'a' = 1
227
+ # We don't allow for nested assignments:
228
+ # IF('a' = 1, 1, 2)
229
+ rule(:assignment_expression) {
230
+ (variable.as(:identifier) >>
231
+ assign >>
232
+ assignment_expression.as(:value)).as(:assign) |
233
+ conditional_expression
234
+ }
235
+
236
+ rule(:expression) {
237
+ assignment_expression
238
+ }
239
+
240
+ rule(:expressions) {
241
+ expression >> (separator >> expressions).repeat
242
+ }
243
+
244
+ rule(:function_body) {
245
+ expressions.as(:expressions)
246
+ }
247
+
248
+ rule(:function_definition_expression) {
249
+ (str('DEFINE') >> spaces? >> identifier.as(:name) >>
250
+ paren_argument_list.as(:argument_list) >>
251
+ left_brace >> function_body.as(:body) >> right_brace).as(:function_definition) |
252
+ expressions.as(:expressions)
253
+ }
254
+
255
+ rule(:function_definition_expressions) {
256
+ function_definition_expression >> separator.maybe >> function_definition_expressions.repeat
257
+ }
258
+
259
+ rule(:commands) {
260
+ function_definition_expressions | expressions
261
+ }
262
+
263
+ rule(:line) {
264
+ commands.as(:commands)
265
+ }
266
+
267
+ rule(:lines) {
268
+ line >> (new_line >> lines).repeat
269
+ }
270
+
271
+ root :lines
272
+ end
273
+
@@ -0,0 +1,119 @@
1
+ # Code inspired by https://github.com/txus/schemer
2
+
3
+ require 'pp'
4
+
5
+ module Kalc
6
+ class Interpreter
7
+
8
+ attr_reader :env
9
+
10
+ def initialize
11
+ @env = Environment.new do |env|
12
+
13
+ env.add_function(:IF, lambda { |cxt, cond, if_true, if_false|
14
+ cond.eval(cxt) ? if_true.eval(cxt) : if_false.eval(cxt)
15
+ })
16
+
17
+ env.add_function(:OR, lambda { |cxt, *args|
18
+ retval = false
19
+ args.each do |arg|
20
+ if arg.eval(cxt) == true
21
+ retval = true
22
+ break
23
+ end
24
+ end
25
+ retval
26
+ })
27
+
28
+ env.add_function(:NOT, lambda { |cxt, val|
29
+ !val.eval(cxt)
30
+ })
31
+
32
+ env.add_function(:AND, lambda { |cxt, *args|
33
+ retval = true
34
+ args.each do |arg|
35
+ if arg.eval(cxt) == false
36
+ retval = false
37
+ break
38
+ end
39
+ end
40
+ retval
41
+ })
42
+
43
+ # Math
44
+ env.add_function(:ABS, lambda { |cxt, val|
45
+ val.eval(cxt).abs
46
+ })
47
+
48
+ env.add_function(:DEGREES, lambda { | cxt, val|
49
+ val.eval(cxt) * (180.0 / Math::PI)
50
+ })
51
+
52
+ env.add_function(:PRODUCT, lambda { |cxt, *args|
53
+ args.map { |a| a.eval(cxt) }.inject(:*)
54
+ })
55
+
56
+ env.add_function(:RADIANS, lambda { |cxt, val|
57
+ val.eval(cxt) * (Math::PI / 180.0)
58
+ })
59
+
60
+ env.add_function(:ROUND, lambda { |cxt, num, digits|
61
+ num.eval(cxt).round(digits.eval(cxt))
62
+ })
63
+
64
+ env.add_function(:SUM, lambda { |cxt, *args|
65
+ args.map { |a| a.eval(cxt) }.inject(:+)
66
+ })
67
+
68
+ env.add_function(:TRUNC, lambda { |cxt, val|
69
+ Integer(val.eval(cxt))
70
+ })
71
+
72
+ env.add_function(:LN, lambda { |cxt, val|
73
+ Math.log(val.eval(cxt))
74
+ })
75
+
76
+ math_funs =
77
+ [ 'acos', 'acosh', 'asin', 'asinh', 'atan', 'atanh',
78
+ 'cbrt', 'cos', 'cosh',
79
+ 'erf', 'erfc', 'exp',
80
+ 'log', 'log2', 'log10',
81
+ 'sin', 'sinh', 'sqrt',
82
+ 'tan', 'tanh',
83
+ ]
84
+
85
+ math_funs.each do |math_fun|
86
+ env.add_function(math_fun.upcase.to_sym, lambda { |cxt, val|
87
+ Math.send(math_fun.to_sym, val.eval(cxt))
88
+ })
89
+ end
90
+
91
+ env.add_function(:P, lambda { |cxt, *output|
92
+ p output
93
+ })
94
+
95
+ env.add_function(:PP, lambda { |cxt, *output|
96
+ pp output
97
+ })
98
+
99
+ env.add_function(:PUTS, lambda { |cxt, output|
100
+ puts output.eval(cxt)
101
+ })
102
+
103
+ end
104
+
105
+ end
106
+
107
+ def load_stdlibs(grammar, transform)
108
+ stdlib = "#{File.dirname(__FILE__)}/stdlib.kalc"
109
+ input = File.read(stdlib)
110
+ g = grammar.parse(input)
111
+ ast = transform.apply(g)
112
+ run(ast)
113
+ end
114
+
115
+ def run(ast)
116
+ ast.eval(@env)
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,76 @@
1
+ require 'readline'
2
+ require 'pp'
3
+
4
+ require 'parslet/convenience'
5
+
6
+ module Kalc
7
+ class Repl
8
+
9
+ def run
10
+
11
+ puts heading
12
+
13
+ puts "= Loading grammar"
14
+ @grammar = Kalc::Grammar.new
15
+
16
+ puts "= Loading transform"
17
+ @transform = Kalc::Transform.new
18
+
19
+ puts "= Loading interpreter"
20
+ @interpreter = Kalc::Interpreter.new
21
+ @interpreter.load_stdlibs(@grammar, @transform)
22
+
23
+ puts "You are ready to go. Have fun!"
24
+ puts ""
25
+
26
+ ast = nil
27
+
28
+ function_list = [
29
+ 'quit', 'exit', 'functions', 'variables', 'ast'
30
+ ] + @interpreter.env.functions.map { |f| f.first }
31
+
32
+ begin
33
+ comp = proc { |s| function_list.grep( /^#{Regexp.escape(s)}/ ) }
34
+ Readline.completion_append_character = ""
35
+ Readline.completion_proc = comp
36
+
37
+ while input = Readline.readline("kalc-#{Kalc::VERSION} > ", true)
38
+ begin
39
+ case
40
+ when (input == 'quit' || input == 'exit')
41
+ break
42
+ when input == "functions"
43
+ puts @interpreter.env.functions.map { |f| f.first }.join(", ")
44
+ when input == 'variables'
45
+ puts @interpreter.env.variables.map { |v| "#{v[0]} = #{v[1]}" }.join("\n\r")
46
+ when input == 'ast'
47
+ pp ast
48
+ when input != ""
49
+ g = @grammar.parse_with_debug(input)
50
+ ast = @transform.apply(g)
51
+ puts @interpreter.run(ast)
52
+ end
53
+ rescue Parslet::ParseFailed => e
54
+ puts e, g.root.error_tree
55
+ rescue Exception => e
56
+ puts e
57
+ puts e.backtrace
58
+ end
59
+ end
60
+ rescue Exception => e
61
+ puts e
62
+ puts e.backtrace
63
+ end
64
+ end
65
+
66
+ def heading
67
+ %q{
68
+ This is Kalc, a small line-based language.
69
+ More information about Kalc can be found at https://github.com/mrcsparker/kalc.
70
+
71
+ Kalc is free software, provided as is, with no warranty.
72
+ }
73
+ end
74
+
75
+ end
76
+ end
@@ -0,0 +1,17 @@
1
+ DEFINE SQUARE(x) {
2
+ x * x * x
3
+ }
4
+
5
+ DEFINE FIB(x) {
6
+ IF(x == 0, 0, IF(x == 1, 1, (FIB(x - 1) + FIB(x - 2))))
7
+ }
8
+
9
+ DEFINE FACTORIAL(x) {
10
+ IF(x == 0, 1, (x * FACTORIAL(x - 1)))
11
+ }
12
+
13
+ DEFINE TOWERS_OF_HANOI(x) {
14
+ IF(x == 1, 1, (2 * TOWERS_OF_HANOI(x - 1) + 1))
15
+ }
16
+
17
+
@@ -0,0 +1,75 @@
1
+ module Kalc
2
+ class Transform < Parslet::Transform
3
+
4
+ rule(:commands => sequence(:commands)) {
5
+ Ast::Commands.new(commands)
6
+ }
7
+
8
+ rule(:commands => simple(:commands)) {
9
+ Ast::Commands.new([commands])
10
+ }
11
+
12
+ rule(:expressions => sequence(:expressions)) {
13
+ Ast::Expressions.new(expressions)
14
+ }
15
+
16
+ rule(:expressions => simple(:expressions)) {
17
+ Ast::Expressions.new([expressions])
18
+ }
19
+
20
+ rule(:string => simple(:string)) {
21
+ Ast::StringValue.new(string)
22
+ }
23
+
24
+ rule(:boolean => simple(:boolean)) {
25
+ Ast::BooleanValue.new(boolean)
26
+ }
27
+
28
+ rule(:number => simple(:number)) {
29
+ Ast::FloatingPointNumber.new(number)
30
+ }
31
+
32
+ rule(:left => simple(:left), :right => simple(:right), :operator => simple(:operator)) {
33
+ Ast::Arithmetic.new(left, right, operator)
34
+ }
35
+
36
+ rule(:condition => simple(:condition), :true => simple(:true_cond), :false => simple(:false_cond)) {
37
+ Ast::Conditional.new(condition, true_cond, false_cond)
38
+ }
39
+
40
+ rule(:variable => simple(:variable)) {
41
+ Ast::Variable.new(variable)
42
+ }
43
+
44
+ rule(:identifier => simple(:identifier)) {
45
+ identifier
46
+ }
47
+
48
+ rule(:assign => {:value => simple(:value), :identifier => simple(:identifier), :operator => simple(:operator)}) {
49
+ Ast::Identifier.new(identifier, value)
50
+ }
51
+
52
+ rule(:paren_list => sequence(:paren_list)) {
53
+ paren_list
54
+ }
55
+
56
+ rule(:paren_list => "()") {
57
+ []
58
+ }
59
+
60
+ rule(:function_call => {:name => simple(:name),
61
+ :variable_list => sequence(:variable_list)}) {
62
+ Ast::FunctionCall.new(name, variable_list)
63
+ }
64
+
65
+ rule(:argument => simple(:argument)) {
66
+ Ast::Identifier.new(argument, argument)
67
+ }
68
+
69
+ rule(:function_definition => {:name => simple(:name),
70
+ :argument_list => sequence(:argument_list),
71
+ :body => simple(:body)}) {
72
+ Ast::FunctionDefinition.new(name, argument_list, body)
73
+ }
74
+ end
75
+ end
@@ -0,0 +1,3 @@
1
+ module Kalc
2
+ VERSION = "0.3"
3
+ end
@@ -0,0 +1,109 @@
1
+ require 'spec_helper'
2
+
3
+ describe Kalc::Grammar do
4
+ let(:grammar) { Kalc::Grammar.new }
5
+
6
+ context "integers" do
7
+ 1.upto(10) do |i|
8
+ it { grammar.should parse("#{i}") }
9
+ end
10
+ end
11
+
12
+ context "floats" do
13
+ 1.upto(10) do |i|
14
+ 1.upto(10) do |n|
15
+ it { grammar.should parse("#{i}.#{n}") }
16
+ end
17
+ end
18
+ end
19
+
20
+ context "basic integer math" do
21
+ 1.upto(10) do |i|
22
+ it { grammar.should parse("#{i} - #{i}") }
23
+ it { grammar.should parse("#{i} + #{i}") }
24
+ it { grammar.should parse("#{i} * #{i}") }
25
+ it { grammar.should parse("#{i} / #{i}") }
26
+ it { grammar.should parse("#{i} % #{i}") }
27
+ end
28
+ end
29
+
30
+ context "basic float math" do
31
+ 1.upto(10) do |i|
32
+ 1.upto(10) do |n|
33
+ it { grammar.should parse("#{i}.#{n} - #{i}.#{n}") }
34
+ it { grammar.should parse("#{i}.#{n} + #{i}.#{n}") }
35
+ it { grammar.should parse("#{i}.#{n} * #{i}.#{n}") }
36
+ it { grammar.should parse("#{i}.#{n} / #{i}.#{n}") }
37
+ it { grammar.should parse("#{i}.#{n} % #{i}.#{n}") }
38
+ end
39
+ end
40
+ end
41
+
42
+ context "Logical expressions" do
43
+ it { grammar.should parse("3 && 1") }
44
+ it { grammar.should_not parse("&& 1") }
45
+
46
+ it { grammar.should parse("3 || 1") }
47
+ it { grammar.should_not parse("|| 1") }
48
+ end
49
+
50
+ context "Comparison expressions" do
51
+ it { grammar.should parse("3 > 1") }
52
+ it { grammar.should parse("3 < 1") }
53
+ it { grammar.should parse("3 >= 1") }
54
+ it { grammar.should parse("3 <= 1") }
55
+
56
+ it { grammar.should_not parse("> 1") }
57
+ it { grammar.should_not parse("< 1") }
58
+ it { grammar.should_not parse(">= 1") }
59
+ it { grammar.should_not parse("<= 1") }
60
+ end
61
+
62
+ context "Equality" do
63
+ it { grammar.should parse("3 == 1") }
64
+ it { grammar.should parse("2 != 1") }
65
+ end
66
+
67
+ context "Block" do
68
+ it { grammar.should parse("(2 + 1)") }
69
+ it { grammar.should parse("(2 + 1) + 1") }
70
+ it { grammar.should parse("(2 + 1) * (1 / 2) + 3") }
71
+ it { grammar.should parse("(2 + 1) + (1 + 2) + ((3 + 2) / (2 + 1)) * 9") }
72
+ it { grammar.should parse("(2 + 1) - (1)") }
73
+ it { grammar.should parse("(2 ) + ( 1)") }
74
+ it { grammar.should parse("((2) + ( 1 ))") }
75
+ end
76
+
77
+ context "Ternary expressions" do
78
+ it { grammar.should parse("3 > 2 ? 1 : 5") }
79
+ it { grammar.should parse("3 > 2 || 4 <= 5 ? 1 : 5") }
80
+ it { grammar.should parse("(3 > (2 + 4)) ? 1 : 5") }
81
+ it { grammar.should parse("IF(2 > 3, 3 > 2 ? 1 : 5, 7)") }
82
+ it { grammar.should parse("3 > 2 ? 1 : 5 > 4 ? 7 : 5") }
83
+ end
84
+
85
+ context "AND statements" do
86
+ it { grammar.should parse("AND(1, 2, 3)") }
87
+ it { grammar.should parse("AND(1, 2, 3, 4, 5, 6)") }
88
+ it { grammar.should parse("AND(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)") }
89
+ end
90
+
91
+ context "OR statements" do
92
+ it { grammar.should parse("OR(1, 2, 3)") }
93
+ it { grammar.should parse("OR(1, 2, 3, 4, 5, 6)") }
94
+ it { grammar.should parse("OR(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)") }
95
+ end
96
+
97
+ context "IF statements" do
98
+ it { grammar.should parse("IF(3, 2, 1)") }
99
+ it { grammar.should parse("IF(3 > 1, 2, 1)") }
100
+ it { grammar.should parse("IF((3 > 1) || (2 < 1), 2, 1)") }
101
+ it { grammar.should parse("IF(OR(1 > 3, 2 < 5), 3, 2)") }
102
+ end
103
+
104
+ context "Nested IF statements" do
105
+ it { grammar.should parse("IF(3 > 2, IF(2 < 3, 1, 3), 5)") }
106
+ it { grammar.should parse("IF(3 > 2, IF(2 < 3, 1, 3), IF(5 > 1, 3, 9))") }
107
+ end
108
+
109
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ describe Kalc::Interpreter do
4
+ before(:each) do
5
+ @grammar = Kalc::Grammar.new
6
+ @transform = Kalc::Transform.new
7
+ end
8
+
9
+ it { evaluate("2 + 2").should == 4 }
10
+ it { evaluate("1 + 1").should == 2.0 }
11
+ it { evaluate("4 + 1").should == 5 }
12
+ it { evaluate("5 + 5").should == 10 }
13
+
14
+ it { evaluate("10 > 9").should == true }
15
+ it { evaluate("10 < 9").should == false }
16
+
17
+ it { evaluate("10 + 19 + 11 * 3").should == 62 }
18
+
19
+ it { evaluate("10 >= 10").should == true }
20
+
21
+ private
22
+ def evaluate(expression)
23
+ g = @grammar.parse(expression)
24
+ ast = @transform.apply(g)
25
+ Kalc::Interpreter.new.run(ast)
26
+ end
27
+ end
@@ -0,0 +1,3 @@
1
+ require 'parslet'
2
+ require 'parslet/rig/rspec'
3
+ require 'kalc'
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: kalc
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.3'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Chris Parker
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-03-06 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rake
16
+ requirement: &70212126361020 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70212126361020
25
+ - !ruby/object:Gem::Dependency
26
+ name: rspec
27
+ requirement: &70212126360600 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *70212126360600
36
+ - !ruby/object:Gem::Dependency
37
+ name: parslet
38
+ requirement: &70212126360100 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: '1.2'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *70212126360100
47
+ description: Calculation language slightly based on Excel's formula language.
48
+ email:
49
+ - mrcsparker@gmail.com
50
+ executables:
51
+ - ikalc
52
+ - kalc
53
+ extensions: []
54
+ extra_rdoc_files: []
55
+ files:
56
+ - Gemfile
57
+ - LICENSE
58
+ - README.textile
59
+ - bin/ikalc
60
+ - bin/kalc
61
+ - kalc.gemspec
62
+ - lib/kalc.rb
63
+ - lib/kalc/ast.rb
64
+ - lib/kalc/environment.rb
65
+ - lib/kalc/grammar.rb
66
+ - lib/kalc/interpreter.rb
67
+ - lib/kalc/repl.rb
68
+ - lib/kalc/stdlib.kalc
69
+ - lib/kalc/transform.rb
70
+ - lib/kalc/version.rb
71
+ - spec/grammar_spec.rb
72
+ - spec/interpreter_spec.rb
73
+ - spec/spec_helper.rb
74
+ homepage: https://github.com/mrcsparker/kalc
75
+ licenses: []
76
+ post_install_message:
77
+ rdoc_options: []
78
+ require_paths:
79
+ - lib
80
+ required_ruby_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ required_rubygems_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ! '>='
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
92
+ requirements: []
93
+ rubyforge_project: kalc
94
+ rubygems_version: 1.8.15
95
+ signing_key:
96
+ specification_version: 3
97
+ summary: Small calculation language.
98
+ test_files:
99
+ - spec/grammar_spec.rb
100
+ - spec/interpreter_spec.rb
101
+ - spec/spec_helper.rb