bus-scheme 0.7.5 → 0.7.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|