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 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 begins 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 leading to the eventual 1.0.0 release.
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 and, to a lesser extent, Rubnius in 1.9 mode. It it not yet passing the Shen Test Suite under JRuby.
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.1.0 is the current release. To install it as gem, use the following command:
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 Shen.... Completed in 18.61 seconds.
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.1.0 ported by Greg Spurrier
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 20 seconds on a 2.66 GHz MacBook Pro). This will be addressed in future releases.
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 MRI, JRuby, and Rubinius
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
- The "Qi interpreter - chapter 13" test case in the Shen Test Suite and some of the benchmarks are currently failing with stack overflow errors.
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
- puts "Ruby exception: #{e.message}"
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,7 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Primitives for Generic Functions' do
4
+ describe 'freeze' do
5
+ # Should the frozen result be re-evaluated on each thaw?
6
+ end
7
+ 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
- "(@eigenklass.send(:define_method, #{fn_name}, ::Kernel.lambda { |#{fn_args}| #{fn_body}}); @arity_cache[#{fn_name}] = #{fn_arity}; #{fn_name})"
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
- expr1, expr2 = destructure_form(form, 2)
138
- compile_if(Kl::Cons.list([:if, expr1, expr2, false]),
139
- lexical_vars, in_tail_pos)
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
- expr1, expr2 = destructure_form(form, 2)
145
- compile_if(Kl::Cons.list([:if, expr1, true, expr2]),
146
- lexical_vars, in_tail_pos)
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
- rator = compile(f, lexical_vars, false)
187
- rands = args.map { |arg| compile(arg, lexical_vars, false) }.join(',')
188
-
189
- tfn = gen_sym
190
- targs = gen_sym
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
- "__apply(#{rator}, [#{rands}])"
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 = ""
@@ -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
- if respond_to? fn
42
- arity = @arity_cache[fn]
43
- if arity == args.size || arity == -1
44
- result = send(fn, *args)
45
- elsif arity > args.size
46
- # Partial application
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
- arity = fn.arity
61
- if arity == args.size || arity == -1
62
- result = fn.call(*args)
63
- elsif arity > args.size
64
- result = fn.curry.call(*args)
65
- else
66
- # Uncurrying. Apply fn to its expected number of arguments
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
@@ -7,11 +7,7 @@ module Kl
7
7
  end
8
8
 
9
9
  def value(sym)
10
- if @variables.has_key?(sym)
11
- @variables[sym]
12
- else
13
- raise Kl::Error, "variable #{sym} has no value"
14
- end
10
+ @variables[sym]
15
11
  end
16
12
  end
17
13
  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
- loop do
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 << read_list
31
+ items = []
32
+ stack.push items
31
33
  when Kl::Lexer::CloseParen
32
- break
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
- Kl::Cons.list(items)
43
+ list
38
44
  end
39
45
  end
40
46
  end
@@ -1,3 +1,3 @@
1
1
  module ShenRuby
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -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
@@ -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
@@ -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.1.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: 2012-12-31 00:00:00.000000000 Z
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