Module: BusScheme
- Defined in:
- lib/cons.rb,
lib/eval.rb,
lib/lambda.rb,
lib/parser.rb,
lib/bus_scheme.rb,
lib/primitives.rb,
lib/stack_frame.rb
Defined Under Namespace
Classes: ArgumentError, AssertionFailed, BusSchemeError, Cons, EvalError, IncompleteError, Lambda, LoadError, ParseError, Primitive, StackFrame
Constant Summary collapse
- SYMBOL_TABLE =
top-level scope
{}
- INVALID_IDENTIFER_BEGIN =
('0' .. '9').to_a + ['+', '-', '.']
- VERSION =
"0.7.6"- PROMPT =
'> '- INCOMPLETE_PROMPT =
' ... '- @@stack =
[]
Class Method Summary collapse
- .[](sym) ⇒ Object
- .[]=(sym, val) ⇒ Object
- .add_load_path(filename, load_path = ) ⇒ Object
-
.apply(function_sym, args) ⇒ Object
Call a function with given args.
- .cons(car, cdr = nil) ⇒ Object
-
.current_scope ⇒ Object
Scoping methods:.
- .define(identifier, value) ⇒ Object
-
.eval(form) ⇒ Object
Eval a form passed in as an array.
-
.eval_string(string) ⇒ Object
Parse a string, then eval the result.
- .in_scope?(sym) ⇒ Boolean
-
.load(filename) ⇒ Object
Load a file if on the load path or absolute.
-
.loaded_files ⇒ Object
For stack traces.
-
.parse(input) ⇒ Object
Turn an input string into an S-expression.
-
.parse_dots_into_cons(list) ⇒ Object
Parse a “dotted cons” (1 . 2) into (cons 1 2).
-
.parse_list(tokens) ⇒ Object
Nest a list from a 1-dimensional list of tokens.
-
.parse_tokens(tokens) ⇒ Object
Turn a list of tokens into a properly-nested array.
-
.pop_token(input) ⇒ Object
Take a token off the input string and return it.
-
.repl ⇒ Object
Read-Eval-Print-Loop.
- .special_form(identifier, value) ⇒ Object
- .stack ⇒ Object
-
.stacktrace ⇒ Object
Tracing methods:.
-
.tokenize(input) ⇒ Object
Split an input string into lexically valid tokens.
Class Method Details
.[](sym) ⇒ Object
41 42 43 44 |
# File 'lib/eval.rb', line 41 def [](sym) raise EvalError.new("Undefined symbol: #{sym.inspect}") unless in_scope?(sym) current_scope[sym] end |
.[]=(sym, val) ⇒ Object
46 47 48 |
# File 'lib/eval.rb', line 46 def []=(sym, val) current_scope[sym] = val end |
.add_load_path(filename, load_path = ) ⇒ Object
75 76 77 78 79 80 |
# File 'lib/bus_scheme.rb', line 75 def self.add_load_path(filename, load_path = BusScheme['load-path'.sym]) return filename if filename.match(/^\//) or File.exist? filename raise LoadError, "File not found: #{filename}" if load_path.empty? return load_path.car + '/' + filename if File.exist? load_path.car + '/' + filename return add_load_path(filename, load_path.cdr) end |
.apply(function_sym, args) ⇒ Object
Call a function with given args
23 24 25 26 27 28 29 30 |
# File 'lib/eval.rb', line 23 def apply(function_sym, args) args = args.to_a function = eval(function_sym) args.map!{ |arg| eval(arg) } unless function.special_form puts ' ' * stack.length + Cons.new(function_sym, args.sexp).inspect if (@trace ||= false) function.call_as(function_sym, *args) end |
.cons(car, cdr = nil) ⇒ Object
77 78 79 |
# File 'lib/cons.rb', line 77 def cons(car, cdr = nil) Cons.new(car, cdr) end |
.current_scope ⇒ Object
Scoping methods:
33 34 35 |
# File 'lib/eval.rb', line 33 def current_scope @@stack.empty? ? SYMBOL_TABLE : @@stack.last end |
.define(identifier, value) ⇒ Object
2 3 4 5 6 |
# File 'lib/primitives.rb', line 2 def self.define(identifier, value) # TODO: fix if this turns out to be a good idea value = Primitive.new(value) if value.is_a? Proc BusScheme[identifier.sym] = value end |
.eval(form) ⇒ Object
Eval a form passed in as an array
12 13 14 15 16 17 18 19 20 |
# File 'lib/eval.rb', line 12 def eval(form) if (form.is_a?(Cons) or form.is_a?(Array)) and form.first apply(form.first, form.rest) elsif form.is_a? Sym self[form.sym] else # well it must be a literal then form end end |
.eval_string(string) ⇒ Object
Parse a string, then eval the result
7 8 9 |
# File 'lib/eval.rb', line 7 def eval_string(string) eval(parse("(top-level #{string})")) end |
.in_scope?(sym) ⇒ Boolean
37 38 39 |
# File 'lib/eval.rb', line 37 def in_scope?(sym) current_scope.has_key?(sym) or SYMBOL_TABLE.has_key?(sym) end |
.load(filename) ⇒ Object
Load a file if on the load path or absolute
64 65 66 67 68 69 70 71 72 73 |
# File 'lib/bus_scheme.rb', line 64 def self.load(filename) begin loaded_files.push filename eval_string File.read(add_load_path(filename)) loaded_files.pop rescue loaded_files.pop raise end end |
.loaded_files ⇒ Object
For stack traces
83 84 85 |
# File 'lib/bus_scheme.rb', line 83 def self.loaded_files (@loaded_files ||= ["(eval)"]) end |
.parse(input) ⇒ Object
Turn an input string into an S-expression
7 8 9 10 11 |
# File 'lib/parser.rb', line 7 def parse(input) @@lines = 1 # TODO: should sexp it as it's being constructed, not after parse_tokens(tokenize(input).flatten).sexp(true) end |
.parse_dots_into_cons(list) ⇒ Object
Parse a “dotted cons” (1 . 2) into (cons 1 2)
40 41 42 43 44 45 46 |
# File 'lib/parser.rb', line 40 def parse_dots_into_cons(list) if(list && list.length > 0 && list[1] == :'.') [:cons.sym, list.first, *list[2 .. -1]] else list end end |
.parse_list(tokens) ⇒ Object
Nest a list from a 1-dimensional list of tokens
25 26 27 28 29 30 31 32 33 34 35 36 37 |
# File 'lib/parser.rb', line 25 def parse_list(tokens) list = [] while element = tokens.shift and element != :')' if element == :'(' list << parse_list(tokens) else list << element end end raise IncompleteError unless element == :')' parse_dots_into_cons list end |
.parse_tokens(tokens) ⇒ Object
Turn a list of tokens into a properly-nested array
14 15 16 17 18 19 20 21 22 |
# File 'lib/parser.rb', line 14 def parse_tokens(tokens) token = tokens.shift if token == :'(' parse_list(tokens) else raise ParseError unless tokens.empty? token # atom end end |
.pop_token(input) ⇒ Object
Take a token off the input string and return it
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/parser.rb', line 58 def pop_token(input) # can't use ^ since it matches line beginnings in mid-string token = case input when /\A(\s|;.*$)/ # ignore whitespace and comments @@lines += Regexp.last_match[1].count("\n") input[0 .. Regexp.last_match[1].length - 1] = '' return pop_token(input) when /\A(\(|\))/ # parens Regexp.last_match[1].intern # when /\A#([^\)])/ when /\A#\(/ # vector input[0 ... 2] = '' return [:'(', :vector.sym, tokenize(input)] when /\A'/ # single-quote input[0 ... 1] = '' return [:'(', :quote.sym, if input[0 ... 1] == '(' tokenize(input) else pop_token(input) end, :')'] when /\A(-?\+?[0-9]*\.[0-9]+)/ # float Regexp.last_match[1].to_f when /\A(\.)/ # dot (for pair notation), comes after float to pick up any dots that float doesn't accept :'.' when /\A(-?[0-9]+)/ # integer Regexp.last_match[1].to_i when /\A("(.*?)")/m # string Regexp.last_match[2] # Official Scheme valid identifiers: # when /\A([A-Za-z!\$%&\*\.\/:<=>\?@\^_~][A-Za-z0-9!\$%&\*\+\-\.\/:<=>\?@\^_~]*)/ # symbol # when /\A([^-0-9\. \n\)][^ \n\)]*)/ when /\A([^ \n\)]+)/ # symbols # puts "#{Regexp.last_match[1]} - #{@@lines}" # cannot begin with a character that may begin a number sym = Regexp.last_match[1].sym sym.file, sym.line = [BusScheme.loaded_files.last, @@lines] raise ParseError, "Invalid identifier: #{sym}" if INVALID_IDENTIFER_BEGIN.include? sym[0 .. 0] and sym.size > 1 sym else raise ParseError if input =~ /[^\s ]/ end # Remove the matched part from the string input[0 .. Regexp.last_match[1].length - 1] = '' if token return token end |
.repl ⇒ Object
Read-Eval-Print-Loop
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
# File 'lib/bus_scheme.rb', line 39 def self.repl loop do puts begin input = Readline.readline(PROMPT) exit if input.nil? # only Ctrl-D produces nil here it seems begin # allow for multiline input result = BusScheme.eval_string(input).inspect rescue IncompleteError input += "\n" + Readline.readline(INCOMPLETE_PROMPT) retry end Readline::HISTORY.push(input) result rescue Interrupt 'Type "(quit)" or press Ctrl-D to leave Bus Scheme.' rescue BusSchemeError => e "Error: #{e}" rescue StandardError => e "You found a bug in Bus Scheme!\n" + "#{e.class}: #{e}\n#{e.backtrace.join("\n")}" end end end |
.special_form(identifier, value) ⇒ Object
8 9 10 11 12 13 |
# File 'lib/primitives.rb', line 8 def self.special_form(identifier, value) # TODO: fix if this turns out to be a good idea value = Primitive.new(value) if value.is_a? Proc value.special_form = true BusScheme[identifier.sym] = value end |
.stack ⇒ Object
56 57 58 |
# File 'lib/eval.rb', line 56 def stack @@stack end |
.stacktrace ⇒ Object
Tracing methods:
51 52 53 54 |
# File 'lib/eval.rb', line 51 def stacktrace # TODO: notrace is super-duper-hacky! @@stack.reverse.map{ |frame| frame.trace if frame.respond_to? :trace }.compact end |
.tokenize(input) ⇒ Object
Split an input string into lexically valid tokens
49 50 51 52 53 54 55 |
# File 'lib/parser.rb', line 49 def tokenize(input) [].affect do |tokens| while token = pop_token(input) tokens << token end end end |