rus3 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +18 -0
- data/.gitignore +56 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +21 -0
- data/LICENSE +21 -0
- data/README.md +182 -0
- data/Rakefile +20 -0
- data/bin/console +16 -0
- data/bin/setup +8 -0
- data/examples/fact.scm +10 -0
- data/examples/fib.scm +9 -0
- data/examples/iota.scm +13 -0
- data/exe/rus3 +5 -0
- data/lib/rus3.rb +52 -0
- data/lib/rus3/char.rb +6 -0
- data/lib/rus3/error.rb +127 -0
- data/lib/rus3/evaluator.rb +75 -0
- data/lib/rus3/pair.rb +67 -0
- data/lib/rus3/parser.rb +71 -0
- data/lib/rus3/parser/lexer.rb +119 -0
- data/lib/rus3/parser/scheme_parser.rb +322 -0
- data/lib/rus3/port.rb +6 -0
- data/lib/rus3/printer.rb +28 -0
- data/lib/rus3/procedure/control.rb +35 -0
- data/lib/rus3/procedure/list.rb +289 -0
- data/lib/rus3/procedure/predicate.rb +308 -0
- data/lib/rus3/procedure/write.rb +130 -0
- data/lib/rus3/repl.rb +236 -0
- data/lib/rus3/version.rb +5 -0
- data/rus3.gemspec +34 -0
- metadata +77 -0
data/lib/rus3/char.rb
ADDED
data/lib/rus3/error.rb
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rus3
|
4
|
+
class Error < StandardError
|
5
|
+
include EmptyList
|
6
|
+
|
7
|
+
def smart_error_value(obj) # :nodoc:
|
8
|
+
case obj
|
9
|
+
when Array
|
10
|
+
if obj.empty? # an empty list
|
11
|
+
"()"
|
12
|
+
else # a normal proper list
|
13
|
+
list_notation = obj.to_s().gsub(/[\[\],]/, A2L_MAP)
|
14
|
+
"list( %s )" % obj
|
15
|
+
end
|
16
|
+
when Numeric
|
17
|
+
"number(%d)" % obj
|
18
|
+
when Rus3::Pair
|
19
|
+
"pair(%s)" % obj
|
20
|
+
else
|
21
|
+
"%s(%s)" % [obj.class, obj]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
A2L_MAP = { "[" => "(", "," => "", "]" => ")"} # :nodoc:
|
26
|
+
end
|
27
|
+
|
28
|
+
# :stopdoc:
|
29
|
+
|
30
|
+
EMSG = {
|
31
|
+
:pair_required => "pair required: got=%s",
|
32
|
+
:list_required => "proper list required: got=%s",
|
33
|
+
:pair_or_list_required => "pair or proper list required: got=%s",
|
34
|
+
:out_of_range => "argument out of range: got=%s",
|
35
|
+
:unsupported_method => "specified method does not work now.",
|
36
|
+
:wrong_type => "wrong type argument: got=%s, wants=%s",
|
37
|
+
:integer_required => "integer required: got=%s",
|
38
|
+
:real_number_required => "real number required: got=%s",
|
39
|
+
:number_required => "number required: got=%s",
|
40
|
+
:string_required => "string required: got=%s",
|
41
|
+
:scheme_syntax_error => "syntax error as Scheme: got=%s",
|
42
|
+
:cannot_find_file => "cannot find %s",
|
43
|
+
:unsupported_feature => "specified feature (`%s`) does not support for %s"
|
44
|
+
}
|
45
|
+
|
46
|
+
# :startdoc:
|
47
|
+
|
48
|
+
class PairRequiredError < Error
|
49
|
+
def initialize(obj)
|
50
|
+
super(EMSG[:pair_required] % smart_error_value(obj))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
class ListRequiredError < Error
|
55
|
+
def initialize(obj)
|
56
|
+
super(EMSG[:list_required] % smart_error_value(obj))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
class PairOrListRequiredError < Error
|
61
|
+
def initialize(obj)
|
62
|
+
super(EMSG[:pair_or_list_required] % smart_error_value(obj))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class OutOfRangeError < Error
|
67
|
+
def initialize(obj)
|
68
|
+
super(EMSG[:out_of_range] % smart_error_value(obj))
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class UnsupportedMethodError < Error
|
73
|
+
def initialize
|
74
|
+
super(EMSG[:unsupported_method])
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
class WrongTypeError < Error
|
79
|
+
def initialize(obj, expected)
|
80
|
+
emsg = EMSG[:wrong_type] % [obj, expected]
|
81
|
+
super(emsg)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class IntegerRequiredError < Error
|
86
|
+
def initialize(obj)
|
87
|
+
super(EMSG[:integer_required] % smart_error_value(obj))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
class RealNumberRequiredError < Error
|
92
|
+
def initialize(obj)
|
93
|
+
super(EMSG[:real_number_required] % smart_error_value(obj))
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
class NumberRequiredError < Error
|
98
|
+
def initialize(obj)
|
99
|
+
super(EMSG[:number_required] % smart_error_value(obj))
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class StringRequiredError < Error
|
104
|
+
def initialize(obj)
|
105
|
+
super(EMSG[:string_required] % smart_error_value(obj))
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class SchemeSyntaxError < Error
|
110
|
+
def initialize(obj)
|
111
|
+
super(EMSG[:scheme_syntax_error] % smart_error_value(obj))
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class CannotFindFileError < Error
|
116
|
+
def initialize(obj)
|
117
|
+
super(EMSG[:cannot_find_file] % smart_error_value(obj))
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class UnsupportedFeatureError < Error
|
122
|
+
def initialize(feature, obj)
|
123
|
+
super(EMSG[:unsupported_feature] % [feature, obj])
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rus3
|
4
|
+
|
5
|
+
# An evaluator.
|
6
|
+
|
7
|
+
class Evaluator
|
8
|
+
|
9
|
+
# Indicates the version of the evaluator class.
|
10
|
+
VERSION = "0.1.0"
|
11
|
+
|
12
|
+
include EmptyList
|
13
|
+
|
14
|
+
class Environment
|
15
|
+
include Rus3::Procedure::Control
|
16
|
+
include Rus3::Procedure::Write
|
17
|
+
include Rus3::Procedure::List
|
18
|
+
include Rus3::Procedure::Predicate
|
19
|
+
include Rus3::EmptyList
|
20
|
+
|
21
|
+
attr_reader :binding
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@binding = Kernel.binding
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_accessor :verbose
|
30
|
+
|
31
|
+
attr_reader :environment
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
@verbose = false
|
35
|
+
@env = Environment.new
|
36
|
+
define_procs_for_infix_ops
|
37
|
+
end
|
38
|
+
|
39
|
+
def eval(exp)
|
40
|
+
pp exp if @verbose
|
41
|
+
@env.binding.eval(exp)
|
42
|
+
end
|
43
|
+
|
44
|
+
def binding
|
45
|
+
@env.binding
|
46
|
+
end
|
47
|
+
|
48
|
+
def version
|
49
|
+
"Evaluator version: #{VERSION}"
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
INFIX_OPS = {
|
55
|
+
:+ => :add,
|
56
|
+
:- => :subtract,
|
57
|
+
:* => :mul,
|
58
|
+
:/ => :div,
|
59
|
+
:% => :mod,
|
60
|
+
:< => :lt?,
|
61
|
+
:<= => :le?,
|
62
|
+
:> => :gt?,
|
63
|
+
:>= => :ge?,
|
64
|
+
:== => :eqv?,
|
65
|
+
}
|
66
|
+
|
67
|
+
def define_procs_for_infix_ops
|
68
|
+
r = @env.binding.receiver
|
69
|
+
INFIX_OPS.each { |op, proc_name|
|
70
|
+
r.instance_eval("def #{proc_name}(op1, op2); op1 #{op} op2; end")
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
75
|
+
end
|
data/lib/rus3/pair.rb
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rus3
|
4
|
+
|
5
|
+
# A building block to construct a dotted pair.
|
6
|
+
|
7
|
+
class Pair
|
8
|
+
include EmptyList
|
9
|
+
|
10
|
+
# CAR part of the pair.
|
11
|
+
attr_reader :car
|
12
|
+
|
13
|
+
# CDR part of the pair.
|
14
|
+
attr_reader :cdr
|
15
|
+
|
16
|
+
# :call-seq:
|
17
|
+
# new(car, cdr) -> a new Pair object
|
18
|
+
|
19
|
+
def initialize(car = EMPTY_LIST, cdr = EMPTY_LIST)
|
20
|
+
@car = car
|
21
|
+
@cdr = cdr
|
22
|
+
end
|
23
|
+
|
24
|
+
# Replaces the CAR part with the argument.
|
25
|
+
|
26
|
+
def set_car!(obj)
|
27
|
+
@car = obj
|
28
|
+
end
|
29
|
+
|
30
|
+
# Replaces the CDR part with the argument.
|
31
|
+
|
32
|
+
def set_cdr!(obj)
|
33
|
+
@cdr = obj
|
34
|
+
end
|
35
|
+
|
36
|
+
# Compares to an other pair.
|
37
|
+
|
38
|
+
def ==(other)
|
39
|
+
other.instance_of?(Pair) and @car == other.car and @cdr == other.cdr
|
40
|
+
end
|
41
|
+
|
42
|
+
# Converts to an Array, which looks like as follows:
|
43
|
+
#
|
44
|
+
# [CAR, CDR]
|
45
|
+
#
|
46
|
+
# When CAR or CDR part is also a Pair object, converts
|
47
|
+
# recursively.
|
48
|
+
|
49
|
+
def to_a
|
50
|
+
[@car, @cdr].map { |e| Pair === e ? e.to_a : e}
|
51
|
+
end
|
52
|
+
|
53
|
+
# Converts to a String. Normally, uses the dot-pair notaion which
|
54
|
+
# looks like "(CAR . CDR)". If the CDR part is an empty string,
|
55
|
+
# looks like a normal list which has a single element, like
|
56
|
+
# "(CAR)".
|
57
|
+
|
58
|
+
def to_s
|
59
|
+
car_part = null?(@car) ? "()" : @car
|
60
|
+
if null?(@cdr)
|
61
|
+
"(#{car_part})"
|
62
|
+
else
|
63
|
+
"(#{car_part} . #{@cdr})"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
data/lib/rus3/parser.rb
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rus3
|
4
|
+
|
5
|
+
module Parser
|
6
|
+
require "readline"
|
7
|
+
require_relative "parser/lexer"
|
8
|
+
|
9
|
+
# Indicates the version of the parser module.
|
10
|
+
VERSION = "0.1.0"
|
11
|
+
|
12
|
+
# A base class to derived a parser.
|
13
|
+
class Parser
|
14
|
+
|
15
|
+
# Holds a prompt string. It is intended to be set in the REPL
|
16
|
+
# loop.
|
17
|
+
attr_accessor :prompt
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
@prompt = ""
|
21
|
+
end
|
22
|
+
|
23
|
+
# Constructs the version string.
|
24
|
+
|
25
|
+
def version
|
26
|
+
"Parser version #{VERSION}"
|
27
|
+
end
|
28
|
+
|
29
|
+
# Reads an expression from the passed IO instance. Returns nil
|
30
|
+
# when reaches to EOF.
|
31
|
+
|
32
|
+
def read(io = STDIN)
|
33
|
+
exp = Readline::readline(@prompt, true)
|
34
|
+
exp.nil? ? nil : parse(exp)
|
35
|
+
end
|
36
|
+
|
37
|
+
protected
|
38
|
+
|
39
|
+
# Parses the passed expression, then returns the processed
|
40
|
+
# expression to be evaluated by an evaluator. How to process
|
41
|
+
# depends on each derived parser class.
|
42
|
+
|
43
|
+
def parse(exp)
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
require_relative "parser/scheme_parser"
|
50
|
+
|
51
|
+
DEFAULT_PARSER = SchemeParser # :nodoc:
|
52
|
+
|
53
|
+
# :stopdoc:
|
54
|
+
|
55
|
+
class PassthroughParser < Parser
|
56
|
+
PARSER_VERSION = "0.1.0"
|
57
|
+
|
58
|
+
def version
|
59
|
+
super + " (Pass Through Parser version: #{PARSER_VERSION})"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def parse(exp)
|
64
|
+
exp
|
65
|
+
end
|
66
|
+
|
67
|
+
# :startdoc:
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rus3::Parser
|
4
|
+
|
5
|
+
class Lexer < Enumerator
|
6
|
+
|
7
|
+
# Indicates the version of the lexer class
|
8
|
+
LEXER_VERSION = "0.1.0"
|
9
|
+
|
10
|
+
# :stopdoc:
|
11
|
+
|
12
|
+
TYPES = [
|
13
|
+
# delimiters
|
14
|
+
:lparen,
|
15
|
+
:rparen,
|
16
|
+
# value types
|
17
|
+
:boolean,
|
18
|
+
:ident,
|
19
|
+
:string,
|
20
|
+
:number,
|
21
|
+
# operators
|
22
|
+
:op_proc,
|
23
|
+
# keywords
|
24
|
+
:if,
|
25
|
+
:define,
|
26
|
+
# control
|
27
|
+
:illegal,
|
28
|
+
]
|
29
|
+
|
30
|
+
BOOLEAN = /\A#(f|t)\Z/
|
31
|
+
STRING = /\A\"[^\"]*\"\Z/
|
32
|
+
|
33
|
+
# idents
|
34
|
+
EXTENDED = "\\-"
|
35
|
+
IDENT_PAT = "[a-zA-Z_][\\w?!#{EXTENDED}]*"
|
36
|
+
IDENTIFIER = Regexp.new("\\A#{IDENT_PAT}\\Z")
|
37
|
+
|
38
|
+
EXTENDED_REGEXP = Regexp.new("[#{EXTENDED}]")
|
39
|
+
EXTENDED_MAP = { "-" => "_", }
|
40
|
+
|
41
|
+
# operators
|
42
|
+
ARITHMETIC_OPS = /\A[+\-*\/%]\Z/
|
43
|
+
COMPARISON_OPS = /\A[<>]=?\Z/
|
44
|
+
|
45
|
+
# numbers
|
46
|
+
REAL_PAT = "(([1-9][0-9]*)|0)(\.[0-9]+)?"
|
47
|
+
RAT_PAT = "#{REAL_PAT}\\/#{REAL_PAT}"
|
48
|
+
C_REAL_PAT = "(#{REAL_PAT}|#{RAT_PAT})"
|
49
|
+
C_IMAG_PAT = "#{C_REAL_PAT}"
|
50
|
+
COMP_PAT = "#{C_REAL_PAT}(\\+|\\-)#{C_IMAG_PAT}i"
|
51
|
+
|
52
|
+
REAL_NUM = Regexp.new("\\A[+-]?#{REAL_PAT}\\Z")
|
53
|
+
RATIONAL = Regexp.new("\\A[+-]?#{RAT_PAT}\\Z")
|
54
|
+
COMPLEX = Regexp.new("\\A[+-]?#{COMP_PAT}\\Z")
|
55
|
+
PURE_IMAG = Regexp.new("\\A[+-](#{C_IMAG_PAT})?i\\Z")
|
56
|
+
|
57
|
+
KEYWORDS = {
|
58
|
+
"LAMBDA" => :lambda,
|
59
|
+
"IF" => :if,
|
60
|
+
"SET!" => :set!,
|
61
|
+
"DEFINE" => :define,
|
62
|
+
"COND" => :cond,
|
63
|
+
"LET" => :let,
|
64
|
+
"ELSE" => :else, # may use with :cond
|
65
|
+
}
|
66
|
+
|
67
|
+
# :startdoc:
|
68
|
+
|
69
|
+
Token = Struct.new(:type, :literal) # :nodoc:
|
70
|
+
|
71
|
+
class << self
|
72
|
+
|
73
|
+
def new(exp, _ = nil)
|
74
|
+
tokens = tokenize(exp)
|
75
|
+
super(tokens.size) { |y|
|
76
|
+
tokens.each { |tk|
|
77
|
+
y.yield(tk)
|
78
|
+
}
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
S2R_MAP = { "(" => "[ ", ")" => " ]" } # :nodoc:
|
83
|
+
|
84
|
+
def tokenize(exp)
|
85
|
+
source = exp.gsub(/[()]/, S2R_MAP)
|
86
|
+
|
87
|
+
source.split(" ").map { |literal|
|
88
|
+
case literal
|
89
|
+
when "["
|
90
|
+
Token.new(:lparen, literal)
|
91
|
+
when "]"
|
92
|
+
Token.new(:rparen, literal)
|
93
|
+
when BOOLEAN
|
94
|
+
Token.new(:boolean, literal)
|
95
|
+
when IDENTIFIER
|
96
|
+
key = literal.upcase
|
97
|
+
if KEYWORDS.keys.include?(key)
|
98
|
+
Token.new(KEYWORDS[key], literal)
|
99
|
+
else
|
100
|
+
Token.new(:ident, literal.gsub(EXTENDED_REGEXP, EXTENDED_MAP))
|
101
|
+
end
|
102
|
+
when STRING
|
103
|
+
Token.new(:string, literal)
|
104
|
+
when "="
|
105
|
+
Token.new(:op_proc, "==")
|
106
|
+
when ARITHMETIC_OPS, COMPARISON_OPS
|
107
|
+
Token.new(:op_proc, literal)
|
108
|
+
when REAL_NUM, RATIONAL, COMPLEX, PURE_IMAG
|
109
|
+
Token.new(:number, literal)
|
110
|
+
else
|
111
|
+
Token.new(:illegal, literal)
|
112
|
+
end
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
end
|
119
|
+
end
|