shen-ruby 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY.md +25 -0
- data/README.md +82 -11
- data/bin/srrepl +13 -1
- data/k_lambda_spec/atom_spec.rb +85 -0
- data/k_lambda_spec/primitives/boolean_operations_spec.rb +136 -0
- data/k_lambda_spec/primitives/generic_functions_spec.rb +7 -0
- data/k_lambda_spec/spec_helper.rb +29 -0
- data/k_lambda_spec/support/shared_examples.rb +33 -0
- data/lib/kl/compiler.rb +140 -24
- data/lib/kl/environment.rb +73 -34
- data/lib/kl/primitives/assignments.rb +1 -5
- data/lib/kl/primitives/extensions.rb +12 -0
- data/lib/kl/reader.rb +10 -4
- data/lib/shen_ruby/version.rb +1 -1
- data/shen/lib/shen_ruby/shen.rb +12 -0
- data/spec/kl/environment_spec.rb +47 -71
- data/spec/kl/interop_spec.rb +68 -0
- data/spec/kl/reader_spec.rb +6 -0
- metadata +11 -3
- data/lib/kl/trampoline.rb +0 -14
data/HISTORY.md
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# ShenRuby Release History
|
2
|
+
|
3
|
+
## 0.2.0 - January 16, 2013
|
4
|
+
### Features
|
5
|
+
- Graceful handling of Control-C in ShenRuby REPL [Bruno Deferrari]
|
6
|
+
- Handle `do` in the K Lambda compiler so that it is eligible for tail call elimination [Bruno Deferrari]
|
7
|
+
- Ruby->Shen interop
|
8
|
+
- Shen functions can now be called from Ruby by invoking the corresponding method on the `ShenRuby::Shen` instance
|
9
|
+
- Ruby arrays are coerced back and forth to K Lambda lists
|
10
|
+
- Underscores in Ruby symbols are coerced back and forth to hyphens in Shen symbols
|
11
|
+
|
12
|
+
### Optimizations
|
13
|
+
Compared to ShenRuby 0.1.0, 50% faster Shen REPL launch and 38% faster execution of the Shen Test Suite achieved by:
|
14
|
+
- Faster application of K Lambda primitives when not partially applied
|
15
|
+
- Inlining of list primitives when not partially applied
|
16
|
+
|
17
|
+
### Bug Fixes
|
18
|
+
- [Issue 4](https://github.com/gregspurrier/shen-ruby/issues/4) -- `and` and `or` may now be partially applied
|
19
|
+
- [Issue 5](https://github.com/gregspurrier/shen-ruby/issues/4) -- the error raised by applying too many arguments to a function is now caught by `trap-error`.
|
20
|
+
|
21
|
+
|
22
|
+
## 0.1.0 - December 30, 2012
|
23
|
+
- First public release
|
24
|
+
- Shen 7.1
|
25
|
+
- Shen REPL available via `srrepl` executable
|
data/README.md
CHANGED
@@ -7,12 +7,12 @@ The ShenRuby project has two primary goals. The first is to be a low barrier-to-
|
|
7
7
|
|
8
8
|
Second, ShenRuby aims to enable hybrid applications implemented using a combination of Ruby and Shen. Ruby methods should be able to invoke functions written in Shen and vice versa. Performance is a secondary part of this goal. It should be good enough that, for most tasks, the choice between Ruby and Shen is based primarily on which language is best suited for solving the problem at hand.
|
9
9
|
|
10
|
-
ShenRuby 0.1.0
|
10
|
+
ShenRuby 0.1.0 began to satisfy the first goal by providing a Shen REPL accessible from the command line. The second goal is more ambitious and is the subject of ongoing work beginning with the 0.2.0 release and leading up to the eventual 1.0.0 release.
|
11
11
|
|
12
12
|
## Installation
|
13
|
-
NOTE: ShenRuby requires Ruby 1.9 language features. It has been tested with Ruby 1.9.3
|
13
|
+
NOTE: ShenRuby requires Ruby 1.9 language features. It has been tested with Ruby 1.9.3-p362. It is not yet working under JRuby or Rubinius.
|
14
14
|
|
15
|
-
ShenRuby 0.
|
15
|
+
ShenRuby 0.2.0 is the current release. To install it as gem, use the following command:
|
16
16
|
|
17
17
|
gem install shen-ruby
|
18
18
|
|
@@ -20,19 +20,18 @@ ShenRuby 0.1.0 is the current release. To install it as gem, use the following c
|
|
20
20
|
|
21
21
|
Once the gem has been installed, the Shen REPL can be launched via the `srrepl` (short for ShenRuby REPL) command. For example:
|
22
22
|
|
23
|
-
|
24
23
|
% srrepl
|
25
|
-
Loading
|
24
|
+
Loading.... Completed in 9.93 seconds.
|
26
25
|
|
27
26
|
Shen 2010, copyright (C) 2010 Mark Tarver
|
28
27
|
www.shenlanguage.org, version 7.1
|
29
28
|
running under Ruby, implementation: ruby 1.9.3
|
30
|
-
port 0.
|
29
|
+
port 0.2.0 ported by Greg Spurrier
|
31
30
|
|
32
31
|
|
33
32
|
(0-)
|
34
33
|
|
35
|
-
Please be patient: the Shen REPL takes a while to load (about
|
34
|
+
Please be patient: the Shen REPL takes a while to load (about 10 seconds on a 2.66 GHz MacBook Pro). This will be addressed in future releases.
|
36
35
|
|
37
36
|
The `(0-)` seen above is the Shen REPL prompt. The number in the prompt increases after each expression that is entered.
|
38
37
|
|
@@ -63,6 +62,73 @@ To exit the Shen REPL, execute the `quit` function:
|
|
63
62
|
(4-) (quit)
|
64
63
|
%
|
65
64
|
|
65
|
+
## Ruby<->Shen Interop
|
66
|
+
Bidirectional interaction between Ruby and Shen is a primary goal of ShenRuby. The following sections describe the currently supported means of collaboration between Shen and Ruby.
|
67
|
+
|
68
|
+
These APIs should be considered experimental and likely to evolve as ShenRuby progresses toward 1.0.
|
69
|
+
|
70
|
+
### Extending the Shen Environment with Ruby Functions
|
71
|
+
|
72
|
+
The Shen Environment may be extended with functions written in Ruby. Any instance method added to the ShenRuby::Shen class--whether through subclassing or adding methods to an instance's eigenclass--is available for invocation from within Shen.
|
73
|
+
|
74
|
+
For example, to add a `divides?` function to an existing Shen environment object:
|
75
|
+
|
76
|
+
require 'rubygems'
|
77
|
+
require 'shen_ruby'
|
78
|
+
|
79
|
+
shen = ShenRuby::Shen.new
|
80
|
+
class << shen
|
81
|
+
def divides?(a, b)
|
82
|
+
b % a == 0
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
### Evaluating Shen Expressions from Ruby
|
87
|
+
`ShenRuby::Shen#eval_string` takes a string and evaluates it as a Shen expression within the environment. For example, the `divides?` function added above may be invoked with:
|
88
|
+
|
89
|
+
shen.eval_string "(divides? 3 9)"
|
90
|
+
# => true
|
91
|
+
|
92
|
+
More commonly, though, `eval_string` is used with Shen `define` expressions to extend the environment with new functions that are implemented in Shen. For example, let's add a [Fizz Buzz](http://en.wikipedia.org/wiki/Fizz_buzz) function:
|
93
|
+
|
94
|
+
shen.eval_string <<-EOS
|
95
|
+
(define fizz-buzz
|
96
|
+
X -> "Fizz Buzz" where (and (divides? 3 X)
|
97
|
+
(divides? 5 X))
|
98
|
+
X -> "Fizz" where (divides? 3 X)
|
99
|
+
X -> "Buzz" where (divides? 5 X)
|
100
|
+
X -> (str X))
|
101
|
+
EOS
|
102
|
+
# => :"fizz-buzz"
|
103
|
+
|
104
|
+
### Invoking Shen Functions from Ruby
|
105
|
+
|
106
|
+
A better way to invoke most Shen functions from Ruby is to simply invoke the corresponding method on the Shen object. If the Shen function's name includes a hyphen, use an underscore instead.
|
107
|
+
|
108
|
+
For example, to use the `fizz-buzz` function defined in the previous section to compute the first 20 Fizz Buzz values:
|
109
|
+
|
110
|
+
(1..20).map { |x| shen.fizz_buzz(x) }
|
111
|
+
# => ["1", "2", "Fizz", "4", "Buzz", "Fizz", "7", "8", "Fizz", "Buzz", "11", "Fizz", "13", "14", "Fizz Buzz", "16", "17", "Fizz", "19", "Buzz"]
|
112
|
+
|
113
|
+
The above example uses Ruby's `map` function, but could also have used Shen's version, relying on ShenRuby's interop features to coerce Ruby arrays to and from Shen lists:
|
114
|
+
|
115
|
+
shen.map(:fizz_buzz, (1..20).to_a)
|
116
|
+
# => ["1", "2", "Fizz", "4", "Buzz", "Fizz", "7", "8", "Fizz", "Buzz", "11", "Fizz", "13", "14", "Fizz Buzz", "16", "17", "Fizz", "19", "Buzz"]
|
117
|
+
|
118
|
+
Note that the Ruby symbol `:fizz_buzz` is automatically coerced to the Shen symbol `fizz-buzz` so that refer to the function defined above.
|
119
|
+
|
120
|
+
As a final example, Ruby functions may be passed as arguments to higher-order Shen functions:
|
121
|
+
|
122
|
+
shen.map(lambda {|x| x * x}, [1, 2, 3, 4, 5])
|
123
|
+
# => [1, 4, 9, 16, 25]
|
124
|
+
|
125
|
+
#### Caveats
|
126
|
+
Shen function invocation via methods on the Shen object only works for normal functions. It will not work for special forms like `define`.
|
127
|
+
|
128
|
+
The array<->list and underscore<->hyphen automatic coercions do not take effect for Ruby functions that are added to the Shen environment or for the K Lambda [primitives](http://www.shenlanguage.org/documentation/shendoc.htm#The%20Primitive%20Functions%20of%20K%20Lambda) that form the basis of Shen. In practice, this should not be much of a limitation, but it is hoped that this restriction will be lifted in the future.
|
129
|
+
|
130
|
+
Shen functions cannot be passed as arguments to functions defined in Ruby. This restriction will be removed in the future.
|
131
|
+
|
66
132
|
## Shen Resources
|
67
133
|
|
68
134
|
The following resources may be helpful for those wanting to learn more about the Shen programming language:
|
@@ -78,17 +144,22 @@ The following resources may be helpful for those wanting to learn more about the
|
|
78
144
|
|
79
145
|
The following features and improvements are among those planned for ShenRuby as it approaches its 1.0 release:
|
80
146
|
|
81
|
-
- Ability to call Shen functions directly from Ruby
|
82
147
|
- Ability to call Ruby methods directly from Shen
|
83
148
|
- Support for command-line Shen scripts that under ShenRuby
|
84
|
-
- Support for
|
149
|
+
- Support for JRuby and Rubinius
|
85
150
|
- Improved performance
|
86
151
|
|
87
152
|
## Known Limitations
|
153
|
+
- The "Qi interpreter - chapter 13" test case in the Shen Test Suite and some of the benchmarks are currently failing with stack overflow errors.
|
154
|
+
- ShenRuby fails with a stack overflow when run under cygwin on Windows ([Issue #3](https://github.com/gregspurrier/shen-ruby/issues/3)). The Ruby environment installed by [RubyInstaller](http://rubyinstaller.org/), however, is capable of running ShenRuby. It is the recommended environment for running ShenRuby on Windows until the stack overflow issues seen on cygwin can be addressed.
|
155
|
+
- ShenRuby fails to load under JRuby ([Issue #6](https://github.com/gregspurrier/shen-ruby/issues/6)) and Rubinius ([Issue #])(https://github.com/gregspurrier/shen-ruby/issues/7)].
|
156
|
+
|
157
|
+
## Contributors
|
158
|
+
The following people are gratefully acknowledged for their contributions of code to ShenRuby:
|
88
159
|
|
89
|
-
|
160
|
+
- Bruno Deferrari
|
90
161
|
|
91
162
|
## License
|
92
163
|
Shen is Copyright (c) 2010-2012 Mark Tarver and released under the Shen License. A copy of the Shen License may be found in [shen/license.txt](https://github.com/gregspurrier/shen-ruby/blob/master/shen/license.txt). A detailed description of the license, along with questions and answers, may be found at http://shenlanguage.org/license.html. The entire contents of the [shen directory](https://github.com/gregspurrier/shen-ruby/tree/master/shen), including the implementation of the `ShenRuby::Shen` class, is part of Shen and is subject to the Shen license.
|
93
164
|
|
94
|
-
The remainder of ShenRuby--i.e., everything outside of the shen directory--is Copyright (c) 2012 Greg Spurrier and released under the MIT License. A copy of the MIT License may be found in [MIT_LICENSE.txt](https://github.com/gregspurrier/shen-ruby/blob/master/MIT_LICENSE.txt).
|
165
|
+
The remainder of ShenRuby--i.e., everything outside of the shen directory--is Copyright (c) 2012-2013 Greg Spurrier and released under the MIT License. A copy of the MIT License may be found in [MIT_LICENSE.txt](https://github.com/gregspurrier/shen-ruby/blob/master/MIT_LICENSE.txt).
|
data/bin/srrepl
CHANGED
@@ -1,6 +1,9 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
require 'shen_ruby'
|
3
3
|
|
4
|
+
# Leave gracefully if someone hits Control-C during loading
|
5
|
+
Signal.trap("INT") { puts "\nLeaving..."; exit 1 }
|
6
|
+
|
4
7
|
# Load the Shen Envinronment
|
5
8
|
print "Loading..."
|
6
9
|
STDOUT.flush
|
@@ -9,6 +12,11 @@ shen = ShenRuby::Shen.new
|
|
9
12
|
now = Time.now.to_f
|
10
13
|
puts ". Completed in %0.2f seconds.\n" % (now - start)
|
11
14
|
|
15
|
+
# Now that the Shen environment is loaded, repurpose the SIGINT
|
16
|
+
# handler to interrupt the current execution.
|
17
|
+
class ReplInterrupt < StandardError; end
|
18
|
+
Signal.trap("INT") { raise ReplInterrupt }
|
19
|
+
|
12
20
|
# Launch the REPL
|
13
21
|
command = :"shen-shen"
|
14
22
|
begin
|
@@ -17,7 +25,11 @@ rescue StandardError => e
|
|
17
25
|
# K Lambda simple errors are already handled by the Shen REPL. Therefore
|
18
26
|
# this must be another type of exception. Print it as such and reenter
|
19
27
|
# the REPL without re-display the initial credits.
|
20
|
-
|
28
|
+
if e.kind_of? ReplInterrupt
|
29
|
+
puts "Execution interrupted. If you are trying to exit the REPL, use (quit)."
|
30
|
+
else
|
31
|
+
puts "Ruby exception: #{e.message}"
|
32
|
+
end
|
21
33
|
command = :"shen-loop"
|
22
34
|
retry
|
23
35
|
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Atoms:' do
|
4
|
+
describe 'a string' do
|
5
|
+
it 'is self-evaluating' do
|
6
|
+
kl_eval('"string"').should == 'string'
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'a symbol' do
|
11
|
+
it 'is self-evaluating' do
|
12
|
+
kl_eval('symbol').should == :symbol
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'may include any of the legal characters for symbol' do
|
16
|
+
# See http://www.shenlanguage.org/documentation/shendoc.htm#The%20Syntax%20of%20Symbols
|
17
|
+
all_legal_chars = 'abcdefghijklmnopqrstuvwxyz' +
|
18
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
|
19
|
+
'0123456789' +
|
20
|
+
'=-*/+_?$!@~.><&%\'#`;:{}'
|
21
|
+
kl_eval(all_legal_chars).should == all_legal_chars.to_sym
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'may begin with any legal character other than a digit' do
|
25
|
+
legal_non_digits = 'abcdefghijklmnopqrstuvwxyz' +
|
26
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
|
27
|
+
'=-*/+_?$!@~.><&%\'#`;:{}'
|
28
|
+
legal_non_digits.each_char do |char|
|
29
|
+
kl_eval(char).should == char.to_sym
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'may not begin with a digit' do
|
34
|
+
digits = '01234567890'
|
35
|
+
digits.each_char do |char|
|
36
|
+
kl_eval(char).should_not be_kind_of Symbol
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'numbers' do
|
42
|
+
it 'parses integers as integers' do
|
43
|
+
result = kl_eval('123')
|
44
|
+
result.should == 123
|
45
|
+
result.should be_kind_of Fixnum
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'parses floating point numbers as reals' do
|
49
|
+
result = kl_eval('12.3')
|
50
|
+
result.should == 12.3
|
51
|
+
result.should be_kind_of Float
|
52
|
+
end
|
53
|
+
|
54
|
+
describe 'with leading sign characters' do
|
55
|
+
it 'recognizes negative numbers' do
|
56
|
+
kl_eval('-123').should == -123
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'treats any odd number of minuses as negative' do
|
60
|
+
kl_eval('---123').should == -123
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'treats any even number of minuses as positive' do
|
64
|
+
kl_eval('----123').should == 123
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'ignores leading plusses' do
|
68
|
+
kl_eval('+123').should == 123
|
69
|
+
kl_eval('--+-123').should == -123
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
describe 'true' do
|
75
|
+
it 'evaluates to boolean true rather than a symbol' do
|
76
|
+
kl_eval('true').should == true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe 'false' do
|
81
|
+
it 'evaluates to boolean false rather than a symbol' do
|
82
|
+
kl_eval('false').should == false
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Primitives for Boolean Operations' do
|
4
|
+
describe 'if special form' do
|
5
|
+
include_examples "non-partially-applicable function", %w(if true a b)
|
6
|
+
|
7
|
+
it 'evaluates its first argument' do
|
8
|
+
define_kl_do
|
9
|
+
kl_eval('(set flag clear)')
|
10
|
+
kl_eval('(if (kl-do (set flag set) true) a b)')
|
11
|
+
kl_eval('(value flag)').should == :set
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'when its first argument evaluates to true' do
|
15
|
+
it 'returns the normal form of its second argument' do
|
16
|
+
kl_eval('(if true (+ 1 2) 10)').should == 3
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'does not evaluate its third argument' do
|
20
|
+
define_kl_do
|
21
|
+
kl_eval('(set flag clear)')
|
22
|
+
kl_eval('(if true 1 (kl-do (set flag set) 2))')
|
23
|
+
kl_eval('(value flag)').should == :clear
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe 'when its first argument evaluates to false' do
|
28
|
+
it 'returns the normal form of its third argument' do
|
29
|
+
kl_eval('(if false 1 (+ 1 2))').should == 3
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'does not evaluate its second argument' do
|
33
|
+
define_kl_do
|
34
|
+
kl_eval('(set flag clear)')
|
35
|
+
kl_eval('(if false (kl-do (set flag set) 1) 2)')
|
36
|
+
kl_eval('(value flag)').should == :clear
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'and special form' do
|
42
|
+
it 'evaluates its first argument' do
|
43
|
+
define_kl_do
|
44
|
+
kl_eval('(set flag clear)')
|
45
|
+
kl_eval('(and (kl-do (set flag set) true) false)')
|
46
|
+
kl_eval('(value flag)').should == :set
|
47
|
+
end
|
48
|
+
|
49
|
+
describe 'when its first argument evaluates to true' do
|
50
|
+
it 'returns true if its second argument evaluates to true' do
|
51
|
+
kl_eval('(defun return-true () true)')
|
52
|
+
kl_eval('(and true (return-true))').should == true
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'returns false if its second argument evaluates to false' do
|
56
|
+
kl_eval('(defun return-false () false)')
|
57
|
+
kl_eval('(and true (return-false))').should == false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe 'when its first argument evaluates to false' do
|
62
|
+
it 'returns false' do
|
63
|
+
kl_eval('(and false true)').should == false
|
64
|
+
end
|
65
|
+
|
66
|
+
it 'does not evaluate it second argument' do
|
67
|
+
kl_eval('(set flag clear)')
|
68
|
+
kl_eval('(and false (kl-do (set flag set) true))').should == false
|
69
|
+
kl_eval('(value flag)').should == :clear
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe 'partial application' do
|
74
|
+
include_examples "partially-applicable function", %w(and true false)
|
75
|
+
|
76
|
+
it 'results in it no longer short-circuiting argument evaluation' do
|
77
|
+
define_kl_do
|
78
|
+
kl_eval('(set flag clear)')
|
79
|
+
kl_eval('((and false) (kl-do (set flag set) false)))').should == false
|
80
|
+
kl_eval('(value flag)').should == :set
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'may be passed as an argument to a higher-order function' do
|
85
|
+
kl_eval('((lambda F (F true false)) and)').should == false
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe 'or special form' do
|
90
|
+
it 'evaluates its first argument' do
|
91
|
+
define_kl_do
|
92
|
+
kl_eval('(set flag clear)')
|
93
|
+
kl_eval('(or (kl-do (set flag set) false) false)')
|
94
|
+
kl_eval('(value flag)').should == :set
|
95
|
+
end
|
96
|
+
|
97
|
+
describe 'when its first argument evaluates to false' do
|
98
|
+
it 'returns true if its second argument evaluates to true' do
|
99
|
+
kl_eval('(defun return-true () true)')
|
100
|
+
kl_eval('(or false (return-true))').should == true
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'returns false if its second argument evaluates to false' do
|
104
|
+
kl_eval('(defun return-false () false)')
|
105
|
+
kl_eval('(or false (return-false))').should == false
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
describe 'when its first argument evaluates to true' do
|
110
|
+
it 'returns true' do
|
111
|
+
kl_eval('(or true false)').should == true
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'does not evaluate it second argument' do
|
115
|
+
kl_eval('(set flag clear)')
|
116
|
+
kl_eval('(or true (kl-do (set flag set) false))').should == true
|
117
|
+
kl_eval('(value flag)').should == :clear
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe 'partial application' do
|
122
|
+
include_examples "partially-applicable function", %w(or false true)
|
123
|
+
|
124
|
+
it 'results in it no longer short-circuiting argument evaluation' do
|
125
|
+
define_kl_do
|
126
|
+
kl_eval('(set flag clear)')
|
127
|
+
kl_eval('((or true) (kl-do (set flag set) false)))').should == true
|
128
|
+
kl_eval('(value flag)').should == :set
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'may be passed as an argument to a higher-order function' do
|
133
|
+
kl_eval('((lambda F (F true true)) or)').should == true
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
lib_path = File.expand_path("../../lib", __FILE__)
|
2
|
+
$LOAD_PATH << lib_path unless $LOAD_PATH.include?(lib_path)
|
3
|
+
|
4
|
+
require 'kl'
|
5
|
+
require 'stringio'
|
6
|
+
|
7
|
+
# Load the support files
|
8
|
+
Dir["./k_lambda_spec/support/**/*.rb"].sort.each {|f| require f}
|
9
|
+
|
10
|
+
# Reset the K Lambda environment before every example
|
11
|
+
RSpec.configure do |config|
|
12
|
+
config.before(:each) do
|
13
|
+
@kl_env = Kl::Environment.new
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# Helper function that evaluates a string in the K Lambda environment and
|
18
|
+
# returns the result as a Ruby object.
|
19
|
+
def kl_eval(str)
|
20
|
+
form = Kl::Reader.new(StringIO.new(str)).next
|
21
|
+
@kl_env.__eval(form)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Defines the 'kl-do' function in the current K Lambda environment. This is
|
25
|
+
# used instead of 'do' because do is not an official K Lambda primitive
|
26
|
+
# and may not be found in all K Lambda implementations.
|
27
|
+
def define_kl_do
|
28
|
+
kl_eval('(defun kl-do (X Y) Y)')
|
29
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# args should be an array containing the components of an example expression.
|
2
|
+
# E.g., to test partial application of +, you could use:
|
3
|
+
#
|
4
|
+
# include_examples "a partially-applicable function", %w(+ 1 2)
|
5
|
+
#
|
6
|
+
# The expression will be evaluated in its fully-expanded form for reference
|
7
|
+
# and then compared against various partial application scenarios.
|
8
|
+
shared_examples "partially-applicable function" do |args|
|
9
|
+
full_expression = "(#{args.join(' ')})"
|
10
|
+
(0...(args.length - 1)).each do |arg_count|
|
11
|
+
description = "supports partial application of #{arg_count} argument"
|
12
|
+
description << "s" unless arg_count == 1
|
13
|
+
it description do
|
14
|
+
full_result = kl_eval(full_expression)
|
15
|
+
partial_expression = "((#{args[0..arg_count].join(' ')}) #{args[(arg_count + 1)..-1].join(' ')})"
|
16
|
+
kl_eval(partial_expression).should == full_result
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
shared_examples "non-partially-applicable function" do |args|
|
22
|
+
(0...(args.length - 1)).each do |arg_count|
|
23
|
+
description = "raises an error when applied to #{arg_count} argument"
|
24
|
+
description << "s" unless arg_count == 1
|
25
|
+
it description do
|
26
|
+
partial_expression = "(#{args[0..arg_count].join(' ')})"
|
27
|
+
expect {
|
28
|
+
kl_eval(partial_expression)
|
29
|
+
}.to raise_error(Kl::Error, "#{args[0]} expects #{args.length - 1} arguments but was given #{arg_count}")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
data/lib/kl/compiler.rb
CHANGED
@@ -1,5 +1,27 @@
|
|
1
1
|
module Kl
|
2
2
|
module Compiler
|
3
|
+
# The K Lambda primitives are all Ruby functions and never use
|
4
|
+
# trampolines. They are all regarded as System Functions and
|
5
|
+
# therefore are not allowed to be redefined by the user at
|
6
|
+
# run time. Therefore, if all of their arguments have been
|
7
|
+
# supplied, it is safe to directly invoke them rather than going
|
8
|
+
# incurring the overhead of using __apply. This table holds the
|
9
|
+
# arities of the primitives and is used to determine whether
|
10
|
+
# direct invocation is possible.
|
11
|
+
# Kl::Primitives::Extensions is purposely omitted from this list
|
12
|
+
# so that it may be overridden by Shen.
|
13
|
+
PRIMITIVE_ARITIES = {}
|
14
|
+
[Kl::Primitives::Arithmetic, Kl::Primitives::Assignments,
|
15
|
+
Kl::Primitives::Booleans, Kl::Primitives::ErrorHandling,
|
16
|
+
Kl::Primitives::GenericFunctions, Kl::Primitives::Lists,
|
17
|
+
Kl::Primitives::Streams, Kl::Primitives::Strings,
|
18
|
+
Kl::Primitives::Symbols, Kl::Primitives::Time,
|
19
|
+
Kl::Primitives::Vectors].each do |prim_mod|
|
20
|
+
prim_mod.instance_methods.each do |name|
|
21
|
+
PRIMITIVE_ARITIES[name] = prim_mod.instance_method(name).arity
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
3
25
|
class << self
|
4
26
|
def compile(form, lexical_vars, in_tail_pos)
|
5
27
|
case form
|
@@ -48,8 +70,20 @@ module Kl
|
|
48
70
|
compile_or(form, lexical_vars, in_tail_pos)
|
49
71
|
when :cond
|
50
72
|
compile_cond(form, lexical_vars, in_tail_pos)
|
73
|
+
when :do
|
74
|
+
compile_do(form, lexical_vars, in_tail_pos)
|
51
75
|
when :"trap-error"
|
52
76
|
compile_trap_error(form, lexical_vars, in_tail_pos)
|
77
|
+
# cons, hd, tl, and cons? are crucial to performance and are inlined
|
78
|
+
# when all of their arguments are available.
|
79
|
+
when :cons
|
80
|
+
compile_cons(form, lexical_vars, in_tail_pos)
|
81
|
+
when :hd
|
82
|
+
compile_hd(form, lexical_vars, in_tail_pos)
|
83
|
+
when :tl
|
84
|
+
compile_tl(form, lexical_vars, in_tail_pos)
|
85
|
+
when :"cons?"
|
86
|
+
compile_consp(form, lexical_vars, in_tail_pos)
|
53
87
|
else
|
54
88
|
compile_application(form, lexical_vars, in_tail_pos)
|
55
89
|
end
|
@@ -64,15 +98,17 @@ module Kl
|
|
64
98
|
unless arglist.all? {|a| a.kind_of? Symbol}
|
65
99
|
raise Kl::Error, 'function argument list may only contain symbols'
|
66
100
|
end
|
101
|
+
if PRIMITIVE_ARITIES.has_key?(name)
|
102
|
+
raise Kl::Error, "#{name} is primitive and may not be redefined"
|
103
|
+
end
|
67
104
|
|
68
105
|
extended_vars = add_vars(lexical_vars, arglist.to_a)
|
69
106
|
|
70
107
|
fn_name = escape_symbol(name)
|
71
108
|
fn_args = arglist.map { |arg| extended_vars[arg] }.join(",")
|
72
|
-
fn_arity = arglist.count
|
73
109
|
fn_body = compile(body, extended_vars, true)
|
74
110
|
|
75
|
-
"(@
|
111
|
+
"(@functions[#{fn_name}] = ::Kernel.lambda { |#{fn_args}| #{fn_body}}; #{fn_name})"
|
76
112
|
end
|
77
113
|
|
78
114
|
# (lambda VAR BODY)
|
@@ -134,16 +170,27 @@ module Kl
|
|
134
170
|
|
135
171
|
# (and EXPR1 EXPR2)
|
136
172
|
def compile_and(form, lexical_vars, in_tail_pos)
|
137
|
-
|
138
|
-
|
139
|
-
|
173
|
+
if form.count == 3
|
174
|
+
# This is the special form case
|
175
|
+
expr1, expr2 = destructure_form(form, 2)
|
176
|
+
compile_if(Kl::Cons.list([:if, expr1, expr2, false]),
|
177
|
+
lexical_vars, in_tail_pos)
|
178
|
+
else
|
179
|
+
# Partial application falls back to normal application
|
180
|
+
compile_application(form, lexical_vars, in_tail_pos)
|
181
|
+
end
|
140
182
|
end
|
141
183
|
|
142
184
|
# (or EXPR1 EXPR2)
|
143
185
|
def compile_or(form, lexical_vars, in_tail_pos)
|
144
|
-
|
145
|
-
|
146
|
-
|
186
|
+
if form.count == 3
|
187
|
+
expr1, expr2 = destructure_form(form, 2)
|
188
|
+
compile_if(Kl::Cons.list([:if, expr1, true, expr2]),
|
189
|
+
lexical_vars, in_tail_pos)
|
190
|
+
else
|
191
|
+
# Partial application falls back to normal application
|
192
|
+
compile_application(form, lexical_vars, in_tail_pos)
|
193
|
+
end
|
147
194
|
end
|
148
195
|
|
149
196
|
def compile_cond(form, lexical_vars, in_tail_pos)
|
@@ -161,6 +208,26 @@ module Kl
|
|
161
208
|
end
|
162
209
|
end
|
163
210
|
|
211
|
+
# (do EXPR1 EXPR2)
|
212
|
+
# 'do' is not a K Lambda primitive, and is defined in the Shen sources as
|
213
|
+
# a function that receives two arguments, and returns the last one.
|
214
|
+
# 'do' being a function means that the compiler will not see EXPR2 as
|
215
|
+
# being in tail-position, inhibiting TCO.
|
216
|
+
# To work around this, calls to 'do' are compiled to a sequence of
|
217
|
+
# expressions instead of calls to a 'do' function, by doing this, EXPR2
|
218
|
+
# has the potential to be in tail position and optimized as such.
|
219
|
+
def compile_do(form, lexical_vars, in_tail_pos)
|
220
|
+
if form.count == 3
|
221
|
+
expr1, expr2 = destructure_form(form, 2)
|
222
|
+
body1 = compile(expr1, lexical_vars, false)
|
223
|
+
body2 = compile(expr2, lexical_vars, in_tail_pos)
|
224
|
+
"(#{body1}; #{body2})"
|
225
|
+
else
|
226
|
+
# Partial application falls back to normal application
|
227
|
+
compile_application(form, lexical_vars, in_tail_pos)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
164
231
|
# (trap-error EXPR ERR_HANDLER)
|
165
232
|
def compile_trap_error(form, lexical_vars, in_tail_pos)
|
166
233
|
expr, err_handler = destructure_form(form, 2)
|
@@ -178,29 +245,78 @@ module Kl
|
|
178
245
|
"#{catch_clause}; end)"
|
179
246
|
end
|
180
247
|
|
248
|
+
# Inlined version of (cons HD TL)
|
249
|
+
def compile_cons(form, lexical_vars, in_tail_pos)
|
250
|
+
if form.count == 3
|
251
|
+
hd, tl = destructure_form(form, 2)
|
252
|
+
hd_expr = compile(hd, lexical_vars, false)
|
253
|
+
tl_expr = compile(tl, lexical_vars, false)
|
254
|
+
"::Kl::Cons.new(#{hd_expr}, #{tl_expr})"
|
255
|
+
else
|
256
|
+
compile_application(form, lexical_vars, in_tail_pos)
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Inlined version of (hd L)
|
261
|
+
def compile_hd(form, lexical_vars, in_tail_pos)
|
262
|
+
if form.count == 2
|
263
|
+
expr = compile(form.tl.hd, lexical_vars, false)
|
264
|
+
"(#{expr}).hd"
|
265
|
+
else
|
266
|
+
compile_application(form, lexical_vars, in_tail_pos)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# Inlined version of (tl L)
|
271
|
+
def compile_tl(form, lexical_vars, in_tail_pos)
|
272
|
+
if form.count == 2
|
273
|
+
expr = compile(form.tl.hd, lexical_vars, false)
|
274
|
+
"(#{expr}).tl"
|
275
|
+
else
|
276
|
+
compile_application(form, lexical_vars, in_tail_pos)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
# Inlined version of (cons? X)
|
281
|
+
def compile_consp(form, lexical_vars, in_tail_pos)
|
282
|
+
if form.count == 2
|
283
|
+
expr = compile(form.tl.hd, lexical_vars, false)
|
284
|
+
"(#{expr}).kind_of?(::Kl::Cons)"
|
285
|
+
else
|
286
|
+
compile_application(form, lexical_vars, in_tail_pos)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
181
290
|
# Normal function application
|
182
291
|
def compile_application(form, lexical_vars, in_tail_pos)
|
183
292
|
f = form.hd
|
184
293
|
args = form.tl
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
if in_tail_pos
|
193
|
-
"(
|
194
|
-
#{tfn} = #{rator};
|
195
|
-
#{targs} = [#{rands}];
|
196
|
-
@tramp_fn = #{tfn};
|
197
|
-
@tramp_args = #{targs}
|
198
|
-
)"
|
294
|
+
|
295
|
+
if PRIMITIVE_ARITIES[f] == args.count
|
296
|
+
# This is a non-partial primitive application. No need for __apply
|
297
|
+
# or trampolines.
|
298
|
+
send_args = form.map {|f| compile(f, lexical_vars, false)}.join(',')
|
299
|
+
"send(#{send_args})"
|
199
300
|
else
|
200
|
-
|
301
|
+
rator = compile(f, lexical_vars, false)
|
302
|
+
rands = args.map { |arg| compile(arg, lexical_vars, false) }.join(',')
|
303
|
+
|
304
|
+
if in_tail_pos
|
305
|
+
tfn = gen_sym
|
306
|
+
targs = gen_sym
|
307
|
+
|
308
|
+
"(
|
309
|
+
#{tfn} = #{rator};
|
310
|
+
#{targs} = [#{rands}];
|
311
|
+
@tramp_fn = #{tfn};
|
312
|
+
@tramp_args = #{targs}
|
313
|
+
)"
|
314
|
+
else
|
315
|
+
"__apply(#{rator}, [#{rands}])"
|
316
|
+
end
|
201
317
|
end
|
202
318
|
end
|
203
|
-
|
319
|
+
|
204
320
|
# Escape single quotes and backslashes
|
205
321
|
def escape_string(str)
|
206
322
|
new_str = ""
|
data/lib/kl/environment.rb
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'kl/compiler'
|
2
1
|
require 'kl/primitives/booleans'
|
3
2
|
require 'kl/primitives/symbols'
|
4
3
|
require 'kl/primitives/strings'
|
@@ -10,6 +9,8 @@ require 'kl/primitives/vectors'
|
|
10
9
|
require 'kl/primitives/streams'
|
11
10
|
require 'kl/primitives/time'
|
12
11
|
require 'kl/primitives/arithmetic'
|
12
|
+
require 'kl/primitives/extensions'
|
13
|
+
require 'kl/compiler'
|
13
14
|
|
14
15
|
module Kl
|
15
16
|
class Environment
|
@@ -24,52 +25,47 @@ module Kl
|
|
24
25
|
include ::Kl::Primitives::Streams
|
25
26
|
include ::Kl::Primitives::Time
|
26
27
|
include ::Kl::Primitives::Arithmetic
|
28
|
+
include ::Kl::Primitives::Extensions
|
27
29
|
|
28
30
|
def initialize
|
29
31
|
@dump_code = false
|
30
32
|
@tramp_fn = @tramp_args = nil
|
31
|
-
@variables =
|
33
|
+
@variables = Hash.new do |_, k|
|
34
|
+
raise Kl::Error, "variable #{k} has no value"
|
35
|
+
end
|
36
|
+
@functions = Hash.new do |h, k|
|
37
|
+
if respond_to? k
|
38
|
+
fn = method(k).to_proc
|
39
|
+
h[k] = fn
|
40
|
+
else
|
41
|
+
raise Kl::Error, "The function #{k} is undefined"
|
42
|
+
end
|
43
|
+
end
|
32
44
|
@eigenklass = class << self; self; end
|
33
|
-
@arity_cache = Hash.new { |h, k| h[k] = method(k).arity }
|
34
45
|
end
|
35
46
|
|
36
47
|
# Trampoline-aware function application
|
37
48
|
def __apply(fn, args)
|
38
49
|
while fn
|
39
50
|
@tramp_fn = nil
|
40
|
-
if fn.kind_of? Symbol
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
result = method(fn).to_proc.curry.call(*args)
|
48
|
-
else
|
49
|
-
# Uncurrying. Apply fn to its expected number of arguments
|
50
|
-
# and hope that the result is a function that can be applied
|
51
|
-
# to the remainder.
|
52
|
-
fn = __apply(fn, args[0, arity])
|
53
|
-
args = args[arity..-1]
|
54
|
-
next
|
55
|
-
end
|
56
|
-
else
|
57
|
-
raise Kl::Error, "The function #{fn} is undefined"
|
58
|
-
end
|
51
|
+
fn = @functions[fn] if fn.kind_of? Symbol
|
52
|
+
arity = fn.arity
|
53
|
+
if arity == args.size || arity == -1
|
54
|
+
result = fn.call(*args)
|
55
|
+
elsif arity > args.size
|
56
|
+
# Partial application
|
57
|
+
result = fn.curry.call(*args)
|
59
58
|
else
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
# and hope that the result is a function that can be applied
|
68
|
-
# to the remainder.
|
69
|
-
fn = __apply(fn, args[0, arity])
|
70
|
-
args = args[arity..-1]
|
71
|
-
next
|
59
|
+
# Uncurrying. Apply fn to its expected number of arguments
|
60
|
+
# and hope that the result is a function that can be applied
|
61
|
+
# to the remainder.
|
62
|
+
fn = __apply(fn, args[0, arity])
|
63
|
+
unless fn.kind_of?(Proc) || fn.kind_of?(Symbol)
|
64
|
+
raise ::Kl::Error,
|
65
|
+
"The value #{str(fn)} is neither a function nor a symbol."
|
72
66
|
end
|
67
|
+
args = args[arity..-1]
|
68
|
+
next
|
73
69
|
end
|
74
70
|
|
75
71
|
if fn = @tramp_fn
|
@@ -109,6 +105,27 @@ module Kl
|
|
109
105
|
end
|
110
106
|
end
|
111
107
|
|
108
|
+
def method_missing(f, *args)
|
109
|
+
if @functions.has_key?(f)
|
110
|
+
fn = @functions[f]
|
111
|
+
else
|
112
|
+
coerced_f = Kl::Environment.ruby_to_kl(f)
|
113
|
+
if @functions.has_key?(coerced_f)
|
114
|
+
fn = @functions[coerced_f]
|
115
|
+
else
|
116
|
+
super
|
117
|
+
end
|
118
|
+
end
|
119
|
+
coerced_args = args.map { |arg| Kl::Environment.ruby_to_kl(arg)}
|
120
|
+
Kl::Environment.kl_to_ruby(__apply(fn, coerced_args))
|
121
|
+
end
|
122
|
+
|
123
|
+
def respond_to?(f)
|
124
|
+
@functions.has_key?(f) ||
|
125
|
+
@functions.has_key?(Kl::Environment.ruby_to_kl(f)) ||
|
126
|
+
super
|
127
|
+
end
|
128
|
+
|
112
129
|
class << self
|
113
130
|
def load_file(env, path)
|
114
131
|
File.open(path, 'r') do |file|
|
@@ -118,6 +135,28 @@ module Kl
|
|
118
135
|
end
|
119
136
|
end
|
120
137
|
end
|
138
|
+
|
139
|
+
# Coerce Ruby types and conventions to K Lambda
|
140
|
+
def kl_to_ruby(x)
|
141
|
+
if x.kind_of? Kl::Cons
|
142
|
+
x.to_a.map { |y| kl_to_ruby(y) }
|
143
|
+
elsif x.kind_of? Symbol
|
144
|
+
x.to_s.gsub(/-/, '_').to_sym
|
145
|
+
else
|
146
|
+
x
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# Coerce K Lambda types and conventions to Ruby
|
151
|
+
def ruby_to_kl(x)
|
152
|
+
if x.kind_of? Array
|
153
|
+
Kl::Cons.list(x.map {|x| ruby_to_kl(x)})
|
154
|
+
elsif x.kind_of? Symbol
|
155
|
+
x.to_s.gsub(/_/, '-').to_sym
|
156
|
+
else
|
157
|
+
x
|
158
|
+
end
|
159
|
+
end
|
121
160
|
end
|
122
161
|
end
|
123
162
|
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Kl
|
2
|
+
module Primitives
|
3
|
+
# Extensions to the official set of K Lambda primitives.
|
4
|
+
# Unlike the official primitives, these are allowed to be overridden
|
5
|
+
# by the Shen sources later.
|
6
|
+
module Extensions
|
7
|
+
define_method 'do' do |a, b|
|
8
|
+
b
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/kl/reader.rb
CHANGED
@@ -21,20 +21,26 @@ module Kl
|
|
21
21
|
|
22
22
|
def read_list
|
23
23
|
items = []
|
24
|
+
stack = [items]
|
24
25
|
|
25
|
-
|
26
|
+
until stack.empty? do
|
26
27
|
token = @lexer.next
|
27
28
|
raise Kl::Error, 'Unterminated list' if token.nil?
|
28
29
|
case token
|
29
30
|
when Kl::Lexer::OpenParen
|
30
|
-
items
|
31
|
+
items = []
|
32
|
+
stack.push items
|
31
33
|
when Kl::Lexer::CloseParen
|
32
|
-
|
34
|
+
list = Kl::Cons.list(stack.pop)
|
35
|
+
unless stack.empty?
|
36
|
+
items = stack.last
|
37
|
+
items << list
|
38
|
+
end
|
33
39
|
else
|
34
40
|
items << token
|
35
41
|
end
|
36
42
|
end
|
37
|
-
|
43
|
+
list
|
38
44
|
end
|
39
45
|
end
|
40
46
|
end
|
data/lib/shen_ruby/version.rb
CHANGED
data/shen/lib/shen_ruby/shen.rb
CHANGED
@@ -110,6 +110,18 @@ module ShenRuby
|
|
110
110
|
define_method 'set-dump-code' do |val|
|
111
111
|
@dump_code = val
|
112
112
|
end
|
113
|
+
|
114
|
+
# Add a way to evaluate strings, intended for use with Ruby interop.
|
115
|
+
# Returns the result of the last expression evaluated.
|
116
|
+
# Based on the implementation of read-file in reader.shen
|
117
|
+
def eval_string(s)
|
118
|
+
byte_list = Kl::Cons.list(s.bytes.to_a)
|
119
|
+
form = __apply(:compile, [:"shen-<st_input>", byte_list, :"read-error"])
|
120
|
+
result = nil
|
121
|
+
form.each { |f| result = __apply(:eval, [f]) }
|
122
|
+
result
|
123
|
+
end
|
124
|
+
alias_method :"eval-string", :eval_string
|
113
125
|
end
|
114
126
|
|
115
127
|
# Load the rest of the K Lambda files
|
data/spec/kl/environment_spec.rb
CHANGED
@@ -95,9 +95,11 @@ describe Kl::Environment do
|
|
95
95
|
end
|
96
96
|
|
97
97
|
it 'exposes the function for use in Ruby' do
|
98
|
-
pending "a clean way to handle trampolines when calling from outside"
|
99
98
|
eval_str('(defun add7 (X) (+ X 7))')
|
100
99
|
@env.add7(30).should == 37
|
100
|
+
|
101
|
+
eval_str('(defun add14 (X) (add7 (add7 X)))')
|
102
|
+
@env.add14(7).should == 21
|
101
103
|
end
|
102
104
|
|
103
105
|
it 'redefines existing functions' do
|
@@ -112,79 +114,15 @@ describe Kl::Environment do
|
|
112
114
|
(defun return-X () X)) 7)').should == :"return-X"
|
113
115
|
eval_str('(return-X)').should == 7
|
114
116
|
end
|
115
|
-
end
|
116
|
-
|
117
|
-
describe 'evaluation of boolean special forms' do
|
118
|
-
describe "if" do
|
119
|
-
before(:each) do
|
120
|
-
eval_str('(defun one () 1)')
|
121
|
-
eval_str('(defun two () 2)')
|
122
|
-
end
|
123
|
-
|
124
|
-
it 'evaluates and returns the true clause when given true' do
|
125
|
-
@env.should_receive(:one).and_return(1)
|
126
|
-
@env.should_not_receive(:two)
|
127
|
-
eval_str('(if true (one) (two))').should == 1
|
128
|
-
end
|
129
|
-
|
130
|
-
it 'evaluates and returns the false clause when given false' do
|
131
|
-
@env.should_not_receive(:one)
|
132
|
-
@env.should_receive(:two).and_return(2)
|
133
|
-
eval_str('(if false (one) (two))').should == 2
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
describe "and" do
|
138
|
-
before(:each) do
|
139
|
-
eval_str('(defun tr () true)')
|
140
|
-
eval_str('(defun fa () false)')
|
141
|
-
end
|
142
|
-
|
143
|
-
it 'returns false and does not evaluate second form if first is false' do
|
144
|
-
@env.should_not_receive(:tr)
|
145
|
-
eval_str('(and (fa) (tr))').should == false
|
146
|
-
end
|
147
|
-
|
148
|
-
it 'returns false when first is true and second is false' do
|
149
|
-
eval_str('(and (tr) (fa))').should == false
|
150
|
-
end
|
151
|
-
|
152
|
-
it 'returns true when both expressions evaluate to true' do
|
153
|
-
@env.should_receive(:tr).twice.and_return(true)
|
154
|
-
eval_str('(and (tr) (tr))').should == true
|
155
|
-
end
|
156
|
-
|
157
|
-
it 'may be passed as an argument to higher order functions' do
|
158
|
-
eval_str('((lambda Op (Op true true)) and)').should == true
|
159
|
-
end
|
160
|
-
end
|
161
|
-
|
162
|
-
describe "or" do
|
163
|
-
before(:each) do
|
164
|
-
eval_str('(defun tr () true)')
|
165
|
-
eval_str('(defun fa () false)')
|
166
|
-
end
|
167
|
-
|
168
|
-
it 'returns true and does not evaluate second form if first is true' do
|
169
|
-
@env.should_not_receive(:fa)
|
170
|
-
eval_str('(or (tr) (fa))').should == true
|
171
|
-
end
|
172
|
-
|
173
|
-
it 'returns true when first is false and second is true' do
|
174
|
-
eval_str('(or (fa) (tr))').should == true
|
175
|
-
end
|
176
|
-
|
177
|
-
it 'returns false when both expressions evaluate to false' do
|
178
|
-
@env.should_receive(:fa).twice.and_return(false)
|
179
|
-
eval_str('(or (fa) (fa))').should == false
|
180
|
-
end
|
181
|
-
|
182
|
-
it 'may be passed as an argument to higher order functions' do
|
183
|
-
eval_str('((lambda Op (Op false false)) or)').should == false
|
184
|
-
end
|
185
117
|
|
118
|
+
it 'raises an error when attempting to redefine a primitive' do
|
119
|
+
expect {
|
120
|
+
eval_str('(defun + (A B) (* A B))')
|
121
|
+
}.to raise_error(Kl::Error, '+ is primitive and may not be redefined')
|
186
122
|
end
|
123
|
+
end
|
187
124
|
|
125
|
+
describe 'evaluation of boolean special forms' do
|
188
126
|
describe "cond" do
|
189
127
|
before(:each) do
|
190
128
|
eval_str('(defun tr () true)')
|
@@ -236,6 +174,12 @@ describe Kl::Environment do
|
|
236
174
|
eval_str('(set foo 37)')
|
237
175
|
eval_str('(value foo)').should == 37
|
238
176
|
end
|
177
|
+
|
178
|
+
it 'raises an error when fetching an unset value' do
|
179
|
+
expect {
|
180
|
+
eval_str('(value foo)')
|
181
|
+
}.to raise_error(Kl::Error, 'variable foo has no value')
|
182
|
+
end
|
239
183
|
end
|
240
184
|
|
241
185
|
describe "evaluation of freeze" do
|
@@ -283,6 +227,12 @@ describe Kl::Environment do
|
|
283
227
|
eval_str('(defun adder (X) (lambda Y (+ X Y)))')
|
284
228
|
eval_str('(adder 1 2)').should == 3
|
285
229
|
end
|
230
|
+
|
231
|
+
it 'raises an error of too many arguments are given' do
|
232
|
+
expect {
|
233
|
+
eval_str('((lambda X (lambda Y (* X Y))) 6 7 8)')
|
234
|
+
}.to raise_error(Kl::Error, 'The value 42 is neither a function nor a symbol.')
|
235
|
+
end
|
286
236
|
end
|
287
237
|
|
288
238
|
describe 'tail recursion' do
|
@@ -302,5 +252,31 @@ describe Kl::Environment do
|
|
302
252
|
eval_str('(boom)')
|
303
253
|
}.to raise_error(Kl::Error, 'maximum stack depth exceeded')
|
304
254
|
end
|
255
|
+
|
256
|
+
it 'doesn\'t overflow when do is in tail position' do
|
257
|
+
eval_str('(defun factorial-h (X Acc)
|
258
|
+
(if (= X 0)
|
259
|
+
Acc
|
260
|
+
(do dummy (factorial-h (- X 1) (* X Acc)))))')
|
261
|
+
expect {
|
262
|
+
eval_str('(factorial-h 10000 1)')
|
263
|
+
}.to_not raise_error
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
describe 'optimized do expression' do
|
268
|
+
it 'evaluates to the value of the second expression' do
|
269
|
+
eval_str('(do (cn "fi" "rst") (cn "sec" "ond"))').should == "second"
|
270
|
+
end
|
271
|
+
|
272
|
+
it 'evaluates the first expression, then the second' do
|
273
|
+
eval_str('(set temp-value 0)')
|
274
|
+
eval_str('(do (set temp-value 10) (= 10 (value temp-value)))').should == true
|
275
|
+
end
|
276
|
+
|
277
|
+
it 'supports partial application' do
|
278
|
+
eval_str('((do a) b)').should == :b
|
279
|
+
eval_str('(((do) a) b)').should == :b
|
280
|
+
end
|
305
281
|
end
|
306
282
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'Ruby->Shen interop' do
|
4
|
+
def eval_str(str)
|
5
|
+
form = Kl::Reader.new(StringIO.new(str)).next
|
6
|
+
@env.__eval(form)
|
7
|
+
end
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
@env = Kl::Environment.new
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'allows K Lambda functions to be invoked as methods on environment' do
|
14
|
+
eval_str('(defun square (X) (* X X))')
|
15
|
+
@env.respond_to?(:square).should be true
|
16
|
+
@env.square(7).should == 49
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'hides the details of trampolines for tail recursive functions' do
|
20
|
+
eval_str('(defun countdown (X) (if (= X 0) true (countdown (- X 1))))')
|
21
|
+
@env.countdown(10001).should == true
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'coerces underscores to hyphens in function names' do
|
25
|
+
eval_str('(defun typical-function-name (X) X)')
|
26
|
+
@env.respond_to?(:typical_function_name).should be true
|
27
|
+
@env.typical_function_name(37).should == 37
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'raises NoMethodError if the function cannot be found' do
|
31
|
+
expect {
|
32
|
+
@env.undefined_function
|
33
|
+
}.to raise_error(NoMethodError)
|
34
|
+
end
|
35
|
+
|
36
|
+
describe 'when invoking non-primitives' do
|
37
|
+
it 'coerces array arguments to K Lambda lists ' do
|
38
|
+
eval_str('(defun first (X) (hd X))')
|
39
|
+
@env.first([1, 2, 3]).should == 1
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'coerces nested array arguments to K Lambda lists ' do
|
43
|
+
eval_str('(defun caadr (X) (hd (hd (tl X))))')
|
44
|
+
@env.caadr([1, [2, 3]]).should == 2
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'coerces list results to Ruby arrays' do
|
48
|
+
eval_str('(defun a-list () (cons 1 (cons 2 ())))')
|
49
|
+
@env.a_list.should == [1, 2]
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'coerces nested list results to Ruby arrays' do
|
53
|
+
eval_str('(defun a-list () (cons 1 (cons (cons 2 (cons 3 ())) ())))')
|
54
|
+
@env.a_list.should == [1, [2, 3]]
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'coerces underscore to hyphen in symbol arguments' do
|
58
|
+
eval_str('(defun foo (X) (set result X))')
|
59
|
+
@env.foo(:a_symbol)
|
60
|
+
eval_str('(value result)').should == :"a-symbol"
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'coerces hyphen to underscore in results' do
|
64
|
+
eval_str('(defun foo () a-symbol)')
|
65
|
+
@env.foo.should == :a_symbol
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
data/spec/kl/reader_spec.rb
CHANGED
@@ -22,6 +22,12 @@ describe Kl::Reader do
|
|
22
22
|
Kl::Cons.list([3]),
|
23
23
|
Kl::EmptyList.instance])])
|
24
24
|
end
|
25
|
+
|
26
|
+
it 'raises an error on unterminated lists' do
|
27
|
+
expect {
|
28
|
+
reader('(1').next
|
29
|
+
}.to raise_error(Kl::Error, 'Unterminated list')
|
30
|
+
end
|
25
31
|
end
|
26
32
|
|
27
33
|
describe 'Reading booleans' do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shen-ruby
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2013-01-17 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: rspec
|
@@ -41,10 +41,16 @@ files:
|
|
41
41
|
- .rspec
|
42
42
|
- Gemfile
|
43
43
|
- Gemfile.lock
|
44
|
+
- HISTORY.md
|
44
45
|
- MIT_LICENSE.txt
|
45
46
|
- README.md
|
46
47
|
- bin/shen_test_suite.rb
|
47
48
|
- bin/srrepl
|
49
|
+
- k_lambda_spec/atom_spec.rb
|
50
|
+
- k_lambda_spec/primitives/boolean_operations_spec.rb
|
51
|
+
- k_lambda_spec/primitives/generic_functions_spec.rb
|
52
|
+
- k_lambda_spec/spec_helper.rb
|
53
|
+
- k_lambda_spec/support/shared_examples.rb
|
48
54
|
- lib/kl.rb
|
49
55
|
- lib/kl/absvector.rb
|
50
56
|
- lib/kl/compiler.rb
|
@@ -58,6 +64,7 @@ files:
|
|
58
64
|
- lib/kl/primitives/assignments.rb
|
59
65
|
- lib/kl/primitives/booleans.rb
|
60
66
|
- lib/kl/primitives/error_handling.rb
|
67
|
+
- lib/kl/primitives/extensions.rb
|
61
68
|
- lib/kl/primitives/generic_functions.rb
|
62
69
|
- lib/kl/primitives/lists.rb
|
63
70
|
- lib/kl/primitives/streams.rb
|
@@ -66,7 +73,6 @@ files:
|
|
66
73
|
- lib/kl/primitives/time.rb
|
67
74
|
- lib/kl/primitives/vectors.rb
|
68
75
|
- lib/kl/reader.rb
|
69
|
-
- lib/kl/trampoline.rb
|
70
76
|
- lib/shen_ruby.rb
|
71
77
|
- lib/shen_ruby/version.rb
|
72
78
|
- shen-ruby.gemspec
|
@@ -148,6 +154,7 @@ files:
|
|
148
154
|
- shen/release/test_programs/yacc.shen
|
149
155
|
- spec/kl/cons_spec.rb
|
150
156
|
- spec/kl/environment_spec.rb
|
157
|
+
- spec/kl/interop_spec.rb
|
151
158
|
- spec/kl/lexer_spec.rb
|
152
159
|
- spec/kl/primitives/generic_functions_spec.rb
|
153
160
|
- spec/kl/primitives/symbols_spec.rb
|
@@ -182,6 +189,7 @@ summary: ShenRuby is a Ruby port of the Shen programming language
|
|
182
189
|
test_files:
|
183
190
|
- spec/kl/cons_spec.rb
|
184
191
|
- spec/kl/environment_spec.rb
|
192
|
+
- spec/kl/interop_spec.rb
|
185
193
|
- spec/kl/lexer_spec.rb
|
186
194
|
- spec/kl/primitives/generic_functions_spec.rb
|
187
195
|
- spec/kl/primitives/symbols_spec.rb
|
data/lib/kl/trampoline.rb
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
module Kl
|
2
|
-
# Trampolines hold a function and a list of its already-evaluated
|
3
|
-
# arguments. They are used to keep the strack from growing on
|
4
|
-
# tail calls.
|
5
|
-
class Trampoline
|
6
|
-
attr_reader :fn, :args, :f
|
7
|
-
|
8
|
-
def initialize(fn, args, f)
|
9
|
-
@fn = fn
|
10
|
-
@args = args
|
11
|
-
@f = f
|
12
|
-
end
|
13
|
-
end
|
14
|
-
end
|