bus-scheme 0.7.5 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/COPYING +1 -1
- data/Manifest.txt +12 -0
- data/R5RS.diff +30 -0
- data/README.txt +91 -32
- data/Rakefile +30 -3
- data/bin/bus +1 -1
- data/examples/fib.scm +6 -0
- data/lib/array_extensions.rb +8 -5
- data/lib/bus_scheme.rb +58 -17
- data/lib/cons.rb +43 -3
- data/lib/eval.rb +53 -20
- data/lib/lambda.rb +51 -41
- data/lib/object_extensions.rb +58 -1
- data/lib/parser.rb +93 -64
- data/lib/primitives.rb +63 -43
- data/lib/scheme/core.scm +18 -15
- data/lib/scheme/list.scm +12 -0
- data/lib/scheme/predicates.scm +19 -0
- data/lib/scheme/test.scm +12 -0
- data/lib/stack_frame.rb +57 -0
- data/test/test_core.rb +9 -21
- data/test/test_eval.rb +56 -11
- data/test/test_helper.rb +26 -5
- data/test/test_lambda.rb +83 -21
- data/test/test_list_functions.scm +11 -0
- data/test/test_parser.rb +66 -31
- data/test/test_predicates.scm +24 -0
- data/test/test_primitives.rb +34 -88
- data/test/test_primitives.scm +55 -0
- data/test/test_stack_frame.rb +30 -0
- data/test/test_web.rb +116 -0
- data/test/test_xml.rb +69 -0
- data/test/tracer.scm +4 -0
- data/tutorials/getting_started.html +204 -0
- metadata +21 -6
data/lib/eval.rb
CHANGED
@@ -1,26 +1,59 @@
|
|
1
1
|
module BusScheme
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
2
|
+
module_function # did not know about this until seeing it in Rubinius; handy!
|
3
|
+
SYMBOL_TABLE = {} # top-level scope
|
4
|
+
@@stack = []
|
5
|
+
|
6
|
+
# Parse a string, then eval the result
|
7
|
+
def eval_string(string)
|
8
|
+
eval(parse("(top-level #{string})"))
|
9
|
+
end
|
7
10
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
form
|
17
|
-
end
|
11
|
+
# Eval a form passed in as an array
|
12
|
+
def eval(form)
|
13
|
+
if (form.is_a?(Cons) or form.is_a?(Array)) and form.first
|
14
|
+
apply(form.first, form.rest)
|
15
|
+
elsif form.is_a? Sym
|
16
|
+
self[form.sym]
|
17
|
+
else # well it must be a literal then
|
18
|
+
form
|
18
19
|
end
|
20
|
+
end
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
# Call a function with given args
|
23
|
+
def apply(function_sym, args)
|
24
|
+
args = args.to_a
|
25
|
+
function = eval(function_sym)
|
26
|
+
args.map!{ |arg| eval(arg) } unless function.special_form
|
27
|
+
puts ' ' * stack.length + Cons.new(function_sym, args.sexp).inspect if (@trace ||= false)
|
28
|
+
|
29
|
+
function.call_as(function_sym, *args)
|
30
|
+
end
|
31
|
+
|
32
|
+
# Scoping methods:
|
33
|
+
def current_scope
|
34
|
+
@@stack.empty? ? SYMBOL_TABLE : @@stack.last
|
35
|
+
end
|
36
|
+
|
37
|
+
def in_scope?(sym)
|
38
|
+
current_scope.has_key?(sym) or SYMBOL_TABLE.has_key?(sym)
|
39
|
+
end
|
40
|
+
|
41
|
+
def [](sym)
|
42
|
+
raise EvalError.new("Undefined symbol: #{sym.inspect}") unless in_scope?(sym)
|
43
|
+
current_scope[sym]
|
44
|
+
end
|
45
|
+
|
46
|
+
def []=(sym, val)
|
47
|
+
current_scope[sym] = val
|
48
|
+
end
|
49
|
+
|
50
|
+
# Tracing methods:
|
51
|
+
def stacktrace
|
52
|
+
# TODO: notrace is super-duper-hacky!
|
53
|
+
@@stack.reverse.map{ |frame| frame.trace if frame.respond_to? :trace }.compact
|
54
|
+
end
|
55
|
+
|
56
|
+
def stack
|
57
|
+
@@stack
|
25
58
|
end
|
26
59
|
end
|
data/lib/lambda.rb
CHANGED
@@ -1,55 +1,65 @@
|
|
1
1
|
module BusScheme
|
2
|
-
#
|
3
|
-
class
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
2
|
+
# Lambdas are closures.
|
3
|
+
class Lambda < Cons
|
4
|
+
attr_reader :scope
|
5
|
+
attr_accessor :special_form
|
6
|
+
|
7
|
+
# create new Lambda object
|
8
|
+
def initialize(formals, body)
|
9
|
+
@special_form = false
|
10
|
+
@formals, @body, @enclosing_scope = [formals, body, BusScheme.current_scope]
|
11
|
+
@car = :lambda.sym
|
12
|
+
@cdr = Cons.new(@formals.sexp, @body.sexp)
|
13
|
+
@called_as = nil # avoid warnings
|
8
14
|
end
|
9
15
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
16
|
+
# execute body with args bound to formals
|
17
|
+
def call(*args)
|
18
|
+
locals = if @formals.is_a? Sym # rest args
|
19
|
+
{ @formals => args.to_list }
|
20
|
+
else # regular arg list
|
21
|
+
raise BusScheme::ArgumentError, "Wrong number of args:
|
22
|
+
expected #{@formals.size}, got #{args.size}
|
23
|
+
#{BusScheme.stacktrace.join("\n")}" if @formals.length != args.length
|
24
|
+
@formals.to_a.zip(args).to_hash
|
25
|
+
end
|
17
26
|
|
18
|
-
|
19
|
-
|
27
|
+
@frame = StackFrame.new(locals, @enclosing_scope, @called_as)
|
28
|
+
|
29
|
+
BusScheme.stack.push @frame
|
30
|
+
begin
|
31
|
+
val = @body.map{ |form| BusScheme.eval(form) }.last
|
32
|
+
rescue => e
|
33
|
+
raise e
|
34
|
+
BusScheme.stack.pop
|
35
|
+
end
|
36
|
+
BusScheme.stack.pop
|
37
|
+
return val
|
20
38
|
end
|
21
39
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
else
|
26
|
-
immediate_set symbol, value
|
27
|
-
end
|
40
|
+
def call_as(called_as, *args)
|
41
|
+
@called_as = called_as
|
42
|
+
call(*args)
|
28
43
|
end
|
29
44
|
end
|
30
45
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
attr_reader :scope
|
36
|
-
|
37
|
-
# create new lambda object
|
38
|
-
def initialize(arg_names, body)
|
39
|
-
@arg_names, @body, @enclosing_scope = [arg_names, body, Lambda.scope]
|
40
|
-
end
|
41
|
-
|
42
|
-
# execute lambda with given arg_values
|
43
|
-
def call(*arg_values)
|
44
|
-
raise BusScheme::ArgumentError if @arg_names.length != arg_values.length
|
45
|
-
@scope = RecursiveHash.new(@arg_names.zip(arg_values).to_hash, @enclosing_scope)
|
46
|
-
@@stack << self
|
47
|
-
BusScheme.eval_form(@body.unshift(:begin)).affect { @@stack.pop }
|
46
|
+
class Primitive < Lambda
|
47
|
+
def initialize body
|
48
|
+
@car = @cdr = nil # avoid "Not initialized" warnings
|
49
|
+
@body = body
|
48
50
|
end
|
49
51
|
|
50
|
-
|
51
|
-
|
52
|
-
|
52
|
+
def call(*args)
|
53
|
+
BusScheme.stack.push StackFrame.new({}, BusScheme.current_scope, @called_as)
|
54
|
+
begin
|
55
|
+
val = @body.call(*args)
|
56
|
+
rescue => e
|
57
|
+
BusScheme.stack.pop
|
58
|
+
raise e
|
59
|
+
end
|
60
|
+
BusScheme.stack.pop
|
61
|
+
return val
|
53
62
|
end
|
54
63
|
end
|
55
64
|
end
|
65
|
+
|
data/lib/object_extensions.rb
CHANGED
@@ -1,3 +1,25 @@
|
|
1
|
+
class Sym < String
|
2
|
+
attr_accessor :file, :line
|
3
|
+
|
4
|
+
# TODO: refactor?
|
5
|
+
def special_form
|
6
|
+
BusScheme[self].special_form
|
7
|
+
end
|
8
|
+
|
9
|
+
def inspect
|
10
|
+
self
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def sym
|
18
|
+
self
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
|
1
23
|
class Object
|
2
24
|
# Return self after evaling block
|
3
25
|
# see http://www.ruby-forum.com/topic/131340
|
@@ -6,7 +28,42 @@ class Object
|
|
6
28
|
return self
|
7
29
|
end
|
8
30
|
|
9
|
-
def
|
31
|
+
def sexp(r = false)
|
10
32
|
self
|
11
33
|
end
|
34
|
+
|
35
|
+
def special_form
|
36
|
+
false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module Callable
|
41
|
+
# allows for (mylist 4) => mylist[4]
|
42
|
+
def call_as(sym, *args)
|
43
|
+
self.call(*args)
|
44
|
+
end
|
45
|
+
def call(*args)
|
46
|
+
self.[](*args)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class String
|
51
|
+
include Callable
|
52
|
+
def sym
|
53
|
+
Sym.new(self)
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_html
|
57
|
+
self
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class Symbol
|
62
|
+
def sym
|
63
|
+
Sym.new(self.to_s)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class Hash
|
68
|
+
include Callable
|
12
69
|
end
|
data/lib/parser.rb
CHANGED
@@ -1,77 +1,106 @@
|
|
1
1
|
module BusScheme
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
2
|
+
INVALID_IDENTIFER_BEGIN = ('0' .. '9').to_a + ['+', '-', '.']
|
3
|
+
|
4
|
+
module_function
|
5
|
+
|
6
|
+
# Turn an input string into an S-expression
|
7
|
+
def parse(input)
|
8
|
+
@@lines = 1
|
9
|
+
# TODO: should sexp it as it's being constructed, not after
|
10
|
+
parse_tokens(tokenize(input).flatten).sexp(true)
|
11
|
+
end
|
7
12
|
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
+
# Turn a list of tokens into a properly-nested array
|
14
|
+
def parse_tokens(tokens)
|
15
|
+
token = tokens.shift
|
16
|
+
if token == :'('
|
17
|
+
parse_list(tokens)
|
18
|
+
else
|
19
|
+
raise ParseError unless tokens.empty?
|
20
|
+
token # atom
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Nest a list from a 1-dimensional list of tokens
|
25
|
+
def parse_list(tokens)
|
26
|
+
list = []
|
27
|
+
while element = tokens.shift and element != :')'
|
28
|
+
if element == :'('
|
29
|
+
list << parse_list(tokens)
|
13
30
|
else
|
14
|
-
|
15
|
-
token # atom
|
31
|
+
list << element
|
16
32
|
end
|
17
33
|
end
|
34
|
+
raise IncompleteError unless element == :')'
|
18
35
|
|
19
|
-
|
20
|
-
|
21
|
-
[].affect do |list|
|
22
|
-
while element = tokens.shift and element != :')'
|
23
|
-
if element == :'('
|
24
|
-
list << parse_list(tokens)
|
25
|
-
else
|
26
|
-
list << element
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|
36
|
+
parse_dots_into_cons list
|
37
|
+
end
|
31
38
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
39
|
+
# Parse a "dotted cons" (1 . 2) into (cons 1 2)
|
40
|
+
def parse_dots_into_cons(list)
|
41
|
+
if(list && list.length > 0 && list[1] == :'.')
|
42
|
+
[:cons.sym, list.first, *list[2 .. -1]]
|
43
|
+
else
|
44
|
+
list
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Split an input string into lexically valid tokens
|
49
|
+
def tokenize(input)
|
50
|
+
[].affect do |tokens|
|
51
|
+
while token = pop_token(input)
|
52
|
+
tokens << token
|
38
53
|
end
|
39
54
|
end
|
55
|
+
end
|
40
56
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
57
|
+
# Take a token off the input string and return it
|
58
|
+
def pop_token(input)
|
59
|
+
# can't use ^ since it matches line beginnings in mid-string
|
60
|
+
token = case input
|
61
|
+
when /\A(\s|;.*$)/ # ignore whitespace and comments
|
62
|
+
@@lines += Regexp.last_match[1].count("\n")
|
63
|
+
input[0 .. Regexp.last_match[1].length - 1] = ''
|
64
|
+
return pop_token(input)
|
65
|
+
when /\A(\(|\))/ # parens
|
66
|
+
Regexp.last_match[1].intern
|
67
|
+
# when /\A#([^\)])/
|
68
|
+
when /\A#\(/ # vector
|
69
|
+
input[0 ... 2] = ''
|
70
|
+
return [:'(', :vector.sym, tokenize(input)]
|
71
|
+
when /\A'/ # single-quote
|
72
|
+
input[0 ... 1] = ''
|
73
|
+
return [:'(', :quote.sym,
|
74
|
+
if input[0 ... 1] == '('
|
75
|
+
tokenize(input)
|
76
|
+
else
|
77
|
+
pop_token(input)
|
78
|
+
end,
|
79
|
+
:')']
|
80
|
+
when /\A(-?\+?[0-9]*\.[0-9]+)/ # float
|
81
|
+
Regexp.last_match[1].to_f
|
82
|
+
when /\A(\.)/ # dot (for pair notation), comes after float to pick up any dots that float doesn't accept
|
83
|
+
:'.'
|
84
|
+
when /\A(-?[0-9]+)/ # integer
|
85
|
+
Regexp.last_match[1].to_i
|
86
|
+
when /\A("(.*?)")/m # string
|
87
|
+
Regexp.last_match[2]
|
88
|
+
# Official Scheme valid identifiers:
|
89
|
+
# when /\A([A-Za-z!\$%&\*\.\/:<=>\?@\^_~][A-Za-z0-9!\$%&\*\+\-\.\/:<=>\?@\^_~]*)/ # symbol
|
90
|
+
# when /\A([^-0-9\. \n\)][^ \n\)]*)/
|
91
|
+
when /\A([^ \n\)]+)/ # symbols
|
92
|
+
# puts "#{Regexp.last_match[1]} - #{@@lines}"
|
93
|
+
# cannot begin with a character that may begin a number
|
94
|
+
sym = Regexp.last_match[1].sym
|
95
|
+
sym.file, sym.line = [BusScheme.loaded_files.last, @@lines]
|
96
|
+
raise ParseError, "Invalid identifier: #{sym}" if INVALID_IDENTIFER_BEGIN.include? sym[0 .. 0] and sym.size > 1
|
97
|
+
sym
|
98
|
+
else
|
99
|
+
raise ParseError if input =~ /[^\s ]/
|
100
|
+
end
|
71
101
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
end
|
102
|
+
# Remove the matched part from the string
|
103
|
+
input[0 .. Regexp.last_match[1].length - 1] = '' if token
|
104
|
+
return token
|
76
105
|
end
|
77
106
|
end
|
data/lib/primitives.rb
CHANGED
@@ -1,45 +1,65 @@
|
|
1
1
|
module BusScheme
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
#
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
}
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
}
|
2
|
+
def self.define(identifier, value)
|
3
|
+
# TODO: fix if this turns out to be a good idea
|
4
|
+
value = Primitive.new(value) if value.is_a? Proc
|
5
|
+
BusScheme[identifier.sym] = value
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.special_form(identifier, value)
|
9
|
+
# TODO: fix if this turns out to be a good idea
|
10
|
+
value = Primitive.new(value) if value.is_a? Proc
|
11
|
+
value.special_form = true
|
12
|
+
BusScheme[identifier.sym] = value
|
13
|
+
end
|
14
|
+
|
15
|
+
define '#t', true
|
16
|
+
define '#f', false
|
17
|
+
|
18
|
+
define '+', lambda { |*args| args.inject { |sum, i| sum + i } }
|
19
|
+
define '-', lambda { |x, y| x - y }
|
20
|
+
define '*', lambda { |*args| args.inject { |product, i| product * i } }
|
21
|
+
define '/', lambda { |x, y| x / y }
|
22
|
+
|
23
|
+
define 'concat', lambda { |*args| args.join('') }
|
24
|
+
define 'cons', lambda { |*args| Cons.new(*args) }
|
25
|
+
define 'list', lambda { |*members| members.to_list }
|
26
|
+
define 'vector', lambda { |*members| members.to_a }
|
27
|
+
define 'map', lambda { |fn, list| list.map(lambda { |n| fn.call(n) }).sexp }
|
28
|
+
# TODO: test these
|
29
|
+
define 'now', lambda { Time.now }
|
30
|
+
define 'regex', lambda { |r| Regexp.new(Regexp.escape(r)) }
|
31
|
+
|
32
|
+
define 'read', lambda { gets }
|
33
|
+
define 'write', lambda { |obj| puts obj.inspect; 0 }
|
34
|
+
define 'display', lambda { |obj| puts obj }
|
35
|
+
|
36
|
+
define 'eval', lambda { |code| eval(code) }
|
37
|
+
define 'stacktrace', lambda { BusScheme.stacktrace }
|
38
|
+
define 'trace', lambda { @trace = !@trace }
|
39
|
+
define 'fail', lambda { |message| raise AssertionFailed, "#{message}\n #{BusScheme.stacktrace.join("\n ")}" }
|
40
|
+
|
41
|
+
define 'ruby', lambda { |*code| Kernel.eval code.join('') }
|
42
|
+
define 'send', lambda { |obj, message, *args| obj.send(message.to_sym, *args) }
|
43
|
+
|
44
|
+
define 'load', lambda { |filename| BusScheme.load filename }
|
45
|
+
define 'exit', lambda { exit }
|
46
|
+
define 'quit', BusScheme['exit'.sym]
|
47
|
+
|
48
|
+
# TODO: hacky to coerce everything to sexps... won't work once we start using vectors
|
49
|
+
special_form 'quote', lambda { |arg| arg.sexp }
|
50
|
+
special_form 'if', lambda { |q, yes, *no| eval(eval(q) ? yes : [:begin.sym] + no) }
|
51
|
+
special_form 'begin', lambda { |*args| args.map{ |arg| eval(arg) }.last }
|
52
|
+
special_form 'top-level', BusScheme[:begin.sym]
|
53
|
+
special_form 'begin-notrace', lambda { |*args| args.map{ |arg| eval(arg) }.last }
|
54
|
+
special_form 'lambda', lambda { |args, *form| Lambda.new(args, form) }
|
55
|
+
# TODO: does define always create top-level bindings, or local?
|
56
|
+
special_form 'define', lambda { |sym, value| BusScheme::SYMBOL_TABLE[sym] = eval(value); sym }
|
57
|
+
special_form 'set!', lambda { |sym, value| raise EvalError.new unless BusScheme.in_scope?(sym)
|
58
|
+
BusScheme[sym.sym] = value }
|
59
|
+
|
60
|
+
# TODO: once we have macros, this can be defined in scheme
|
61
|
+
special_form 'and', lambda { |*args| args.all? { |x| eval(x) } }
|
62
|
+
special_form 'or', lambda { |*args| args.any? { |x| eval(x) } }
|
63
|
+
special_form 'let', lambda { |defs, *body| Lambda.new(defs.map{ |d| d.car }, body).call(*defs.map{ |d| eval d.last }) }
|
64
|
+
special_form 'hash', lambda { |*args| args.to_hash } # accepts an alist
|
45
65
|
end
|