kalc 0.3

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.
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