ruspea_lang 0.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +47 -0
- data/LICENSE.txt +21 -0
- data/README.md +183 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/exe/ruspea +8 -0
- data/lib/example.rb +10 -0
- data/lib/example.rsp +23 -0
- data/lib/language/standard.rsp +77 -0
- data/lib/ruspea.rb +9 -0
- data/lib/ruspea/code.rb +19 -0
- data/lib/ruspea/error/arity.rb +7 -0
- data/lib/ruspea/error/resolution.rb +7 -0
- data/lib/ruspea/error/standard.rb +3 -0
- data/lib/ruspea/error/syntax.rb +4 -0
- data/lib/ruspea/interpreter/evaler.rb +67 -0
- data/lib/ruspea/interpreter/form.rb +27 -0
- data/lib/ruspea/interpreter/reader.rb +127 -0
- data/lib/ruspea/language/core.rb +185 -0
- data/lib/ruspea/language/ruby.rb +36 -0
- data/lib/ruspea/printer.rb +54 -0
- data/lib/ruspea/repl/loop.rb +85 -0
- data/lib/ruspea/repl/repl.rsp +2 -0
- data/lib/ruspea/runtime/env.rb +119 -0
- data/lib/ruspea/runtime/fn.rb +23 -0
- data/lib/ruspea/runtime/list.rb +67 -0
- data/lib/ruspea/runtime/lm.rb +74 -0
- data/lib/ruspea/runtime/nill.rb +54 -0
- data/lib/ruspea/runtime/sym.rb +28 -0
- data/lib/ruspea/version.rb +3 -0
- data/lib/ruspea_lang.rb +4 -0
- data/ruspea.gemspec +31 -0
- metadata +137 -0
data/lib/ruspea.rb
ADDED
data/lib/ruspea/code.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Ruspea
|
2
|
+
class Code
|
3
|
+
def initialize
|
4
|
+
@reader = Ruspea::Interpreter::Reader.new
|
5
|
+
@evaler = Ruspea::Interpreter::Evaler.new
|
6
|
+
@printer = Ruspea::Printer.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def load(file_path)
|
10
|
+
raise "#{file_path} is not a file" if !File.exists?(file_path)
|
11
|
+
exec(File.read(file_path))
|
12
|
+
end
|
13
|
+
|
14
|
+
def run(code, env: Ruspea::Language::Core.new)
|
15
|
+
_, forms = @reader.call(code)
|
16
|
+
@evaler.call(forms, context: env)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Ruspea::Interpreter
|
2
|
+
class Evaler
|
3
|
+
include Ruspea::Runtime
|
4
|
+
include Ruspea::Error
|
5
|
+
|
6
|
+
def call(value, context: Env::Empty.instance)
|
7
|
+
return call(value.value, context: context) if value.is_a?(Form)
|
8
|
+
|
9
|
+
case value
|
10
|
+
when Sym
|
11
|
+
context.lookup value
|
12
|
+
when Numeric
|
13
|
+
value
|
14
|
+
when String
|
15
|
+
value
|
16
|
+
when NilClass
|
17
|
+
nil
|
18
|
+
when TrueClass
|
19
|
+
true
|
20
|
+
when FalseClass
|
21
|
+
false
|
22
|
+
when Array
|
23
|
+
value.map { |f|
|
24
|
+
call(f, context: context)
|
25
|
+
}
|
26
|
+
when List
|
27
|
+
fn = fn_from_invocation(value, context)
|
28
|
+
# TODO: raise if ! respond_to?(:call)
|
29
|
+
|
30
|
+
arguments =
|
31
|
+
if fn.respond_to?(:arity)
|
32
|
+
if fn.arity == 1 && value.tail.count > 1
|
33
|
+
[value.tail]
|
34
|
+
else
|
35
|
+
value.tail.to_a
|
36
|
+
end
|
37
|
+
else
|
38
|
+
if fn.is_a? Fn
|
39
|
+
value.tail.to_a
|
40
|
+
else
|
41
|
+
[value.tail.to_a]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
fn.call(*arguments, context: context, evaler: self)
|
46
|
+
else
|
47
|
+
raise "Unrecognized expression"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def fn_from_invocation(form, context)
|
54
|
+
name =
|
55
|
+
if form.head.is_a?(Ruspea::Interpreter::Form)
|
56
|
+
form.head.value
|
57
|
+
else
|
58
|
+
form.head
|
59
|
+
end
|
60
|
+
context
|
61
|
+
.lookup(name)
|
62
|
+
.tap { |fn|
|
63
|
+
raise Resolution.new(name) if fn.nil?
|
64
|
+
}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Ruspea::Interpreter
|
2
|
+
class Form
|
3
|
+
attr_reader :value, :meta
|
4
|
+
|
5
|
+
def initialize(value, meta = {closed: true})
|
6
|
+
@value = value
|
7
|
+
@meta = meta
|
8
|
+
end
|
9
|
+
|
10
|
+
def eq?(other)
|
11
|
+
self == other
|
12
|
+
end
|
13
|
+
|
14
|
+
def eql?(other)
|
15
|
+
self == other
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
return false if self.class != other.class
|
20
|
+
value == other.value && meta == other.meta
|
21
|
+
end
|
22
|
+
|
23
|
+
def inspect
|
24
|
+
"Form< #{value.inspect} >"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Ruspea::Interpreter
|
2
|
+
class Reader
|
3
|
+
include Ruspea::Runtime
|
4
|
+
include Ruspea::Error
|
5
|
+
|
6
|
+
def call(source, forms = [])
|
7
|
+
return [source, forms] if source.empty?
|
8
|
+
|
9
|
+
code = to_list(source)
|
10
|
+
|
11
|
+
rest, new_form =
|
12
|
+
case code.head
|
13
|
+
when HIFEN
|
14
|
+
if DIGIT.match?(code.tail.head)
|
15
|
+
numberify(code.tail, code.head)
|
16
|
+
else
|
17
|
+
symbolize(code)
|
18
|
+
end
|
19
|
+
when DELIMITERS
|
20
|
+
# Closing lists, arrays, etc...
|
21
|
+
return [code, forms]
|
22
|
+
when SEPARATOR
|
23
|
+
# ignore separators
|
24
|
+
[code.tail, nil]
|
25
|
+
when STRING
|
26
|
+
stringfy(code.tail)
|
27
|
+
when DIGIT
|
28
|
+
numberify(code.tail, code.head)
|
29
|
+
when LIST_OPEN
|
30
|
+
listify(code.tail)
|
31
|
+
when ARRAY_OPEN
|
32
|
+
arraify(code.tail)
|
33
|
+
when QUOTE
|
34
|
+
quotify(code.tail)
|
35
|
+
else
|
36
|
+
symbolize(code)
|
37
|
+
end
|
38
|
+
|
39
|
+
new_forms = new_form.nil? ? forms : forms + [new_form]
|
40
|
+
call(rest, new_forms)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
HIFEN = /\A-/
|
46
|
+
SEPARATOR = /\A[,\s]/
|
47
|
+
STRING = /\A"/
|
48
|
+
DIGIT = /\A\d/
|
49
|
+
NUMERIC = /\A[\d_]/
|
50
|
+
QUOTE = /\A'/
|
51
|
+
LIST_OPEN = /\A\(/
|
52
|
+
LIST_CLOSE = /\A\)/
|
53
|
+
ARRAY_OPEN = /\A\[/
|
54
|
+
ARRAY_CLOSE = /\A\]/
|
55
|
+
DELIMITERS = Regexp.union(LIST_CLOSE, ARRAY_CLOSE)
|
56
|
+
ENDER = Regexp.union(SEPARATOR, DELIMITERS)
|
57
|
+
|
58
|
+
def to_list(source)
|
59
|
+
return source if source.is_a?(Ruspea::Runtime::List)
|
60
|
+
|
61
|
+
Ruspea::Runtime::List.create(*source.chars)
|
62
|
+
end
|
63
|
+
|
64
|
+
def stringfy(code, string = "")
|
65
|
+
finished = code.head == "\"" || code.empty?
|
66
|
+
return [code.tail, Form.new(string, closed: !code.empty?)] if finished
|
67
|
+
|
68
|
+
stringfy(code.tail, string + code.head)
|
69
|
+
end
|
70
|
+
|
71
|
+
def numberify(code, number = "")
|
72
|
+
return floatfy(code.tail, number + code.head) if code.head == "."
|
73
|
+
|
74
|
+
finished = code.empty? || !NUMERIC.match?(code.head)
|
75
|
+
return [code, Form.new(Integer(number))] if finished
|
76
|
+
|
77
|
+
numberify(code.tail, number + code.head)
|
78
|
+
end
|
79
|
+
|
80
|
+
def floatfy(code, number)
|
81
|
+
finished = code.empty? || !NUMERIC.match?(code.head)
|
82
|
+
return [code.tail, Form.new(Float(number))] if finished
|
83
|
+
|
84
|
+
floatfy(code.tail, number + code.head)
|
85
|
+
end
|
86
|
+
|
87
|
+
def listify(code)
|
88
|
+
more_code, forms = call(code)
|
89
|
+
closed = LIST_CLOSE.match?(more_code.head)
|
90
|
+
form = Form.new(List.create(*forms), closed: closed)
|
91
|
+
|
92
|
+
[more_code.tail, form]
|
93
|
+
end
|
94
|
+
|
95
|
+
def arraify(code)
|
96
|
+
more_code, forms = call(code)
|
97
|
+
form = Form.new(forms, closed: ARRAY_CLOSE.match?(more_code.head))
|
98
|
+
|
99
|
+
[more_code.tail, form]
|
100
|
+
end
|
101
|
+
|
102
|
+
def quotify(code)
|
103
|
+
more_code, content = call(code)
|
104
|
+
|
105
|
+
invocation = List.create(
|
106
|
+
Form.new(Sym.new("quote")),
|
107
|
+
*content
|
108
|
+
)
|
109
|
+
|
110
|
+
[more_code, Form.new(invocation)]
|
111
|
+
end
|
112
|
+
|
113
|
+
def symbolize(code, word = "")
|
114
|
+
finished = code.empty? || ENDER.match?(code.head)
|
115
|
+
return [code, Form.new(read_word(word))] if finished
|
116
|
+
|
117
|
+
symbolize(code.tail, word + code.head)
|
118
|
+
end
|
119
|
+
|
120
|
+
def read_word(word)
|
121
|
+
return word == "true" if word == "true" || word == "false"
|
122
|
+
return nil if word == "nil"
|
123
|
+
|
124
|
+
Sym.new(word)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
module Ruspea::Language
|
2
|
+
class Core < Ruspea::Runtime::Env
|
3
|
+
include Ruspea::Runtime
|
4
|
+
|
5
|
+
def initialize(*args)
|
6
|
+
super(*args)
|
7
|
+
|
8
|
+
@ruby = Ruby.new
|
9
|
+
|
10
|
+
define Sym.new("quote"), fn_quote
|
11
|
+
define Sym.new("def"), fn_def
|
12
|
+
define Sym.new("fn"), fn_fn
|
13
|
+
define Sym.new("cond"), fn_cond
|
14
|
+
|
15
|
+
define Sym.new("::"), fn_constantize
|
16
|
+
define Sym.new("."), fn_dot
|
17
|
+
define Sym.new("eval"), fn_eval
|
18
|
+
|
19
|
+
load_standard
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
attr_reader :ruby
|
25
|
+
|
26
|
+
def load_standard
|
27
|
+
reader = Ruspea::Interpreter::Reader.new
|
28
|
+
evaler = Ruspea::Interpreter::Evaler.new
|
29
|
+
Dir.glob(
|
30
|
+
Ruspea.root.join("language/*.rsp").to_s
|
31
|
+
).each do |file|
|
32
|
+
_, forms = reader.call(
|
33
|
+
File.read(file))
|
34
|
+
evaler.call(forms, context: self)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def fn_quote
|
39
|
+
Lm.new(
|
40
|
+
params: [Sym.new("expression")],
|
41
|
+
body: ->(env, _) {
|
42
|
+
env.call(Sym.new("expression"))
|
43
|
+
}
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def fn_def
|
48
|
+
Lm.new(
|
49
|
+
params: [Sym.new("sym"), Sym.new("val")],
|
50
|
+
body: ->(env, evaler) {
|
51
|
+
caller_context = env.call(Sym.new("%ctx"))
|
52
|
+
sym = env.call(Sym.new("sym"))
|
53
|
+
value_form = env.call(Sym.new("val"))
|
54
|
+
value = evaler.call(value_form, context: env)
|
55
|
+
|
56
|
+
caller_context.call(sym, value)
|
57
|
+
}
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
def fn_fn
|
62
|
+
Lm.new(
|
63
|
+
params: [Sym.new("declaration")],
|
64
|
+
body: ->(env, evaler) {
|
65
|
+
declaration = env.call(Sym.new("declaration"))
|
66
|
+
caller_context = env.call(Sym.new("%ctx"))
|
67
|
+
|
68
|
+
params = declaration.head.value.map { |arg| arg.value }
|
69
|
+
Lm.new(
|
70
|
+
params: params,
|
71
|
+
body: declaration.tail,
|
72
|
+
closure: caller_context
|
73
|
+
)
|
74
|
+
}
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
def fn_cond
|
79
|
+
Lm.new(
|
80
|
+
params: [Sym.new("tuples")],
|
81
|
+
body: ->(env, evaler) {
|
82
|
+
conditions = env.lookup(Sym.new("tuples"))
|
83
|
+
find_true(conditions, evaler, env)
|
84
|
+
}
|
85
|
+
)
|
86
|
+
end
|
87
|
+
|
88
|
+
def find_true(conditions, evaler, context)
|
89
|
+
return nil if conditions.empty?
|
90
|
+
tuple = conditions.head.value
|
91
|
+
|
92
|
+
raise "NOPE: what is the expression?" if tuple.tail.empty?
|
93
|
+
|
94
|
+
if evaler.call(tuple.head, context: context) == true
|
95
|
+
tuple.tail.to_a.reduce(nil) { |result, form|
|
96
|
+
evaler.call(form, context: context)
|
97
|
+
}
|
98
|
+
else
|
99
|
+
find_true(conditions.tail, evaler, context)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def fn_dot
|
104
|
+
Lm.new(
|
105
|
+
params: [Sym.new("path")],
|
106
|
+
body: ->(env, evaler) {
|
107
|
+
context = env.lookup(Sym.new("%ctx"))
|
108
|
+
path = env.lookup(Sym.new("path"))
|
109
|
+
|
110
|
+
target =
|
111
|
+
begin
|
112
|
+
evaler.call(path.head, context: context)
|
113
|
+
rescue Ruspea::Error::Resolution
|
114
|
+
# if form is a symbol not solvable in the current context,
|
115
|
+
# then we use it as the name of the "contantizable" thing.
|
116
|
+
path.head.value.to_s
|
117
|
+
end
|
118
|
+
|
119
|
+
method = path.tail.head.value.to_s
|
120
|
+
|
121
|
+
has_args = !path.tail.tail.empty?
|
122
|
+
if has_args
|
123
|
+
args = path.tail.tail
|
124
|
+
.to_a
|
125
|
+
.map { |form|
|
126
|
+
evaler.call(form, context: context) }
|
127
|
+
target.send(method, *args)
|
128
|
+
else
|
129
|
+
target.send(method)
|
130
|
+
end
|
131
|
+
}
|
132
|
+
)
|
133
|
+
end
|
134
|
+
|
135
|
+
def fn_constantize
|
136
|
+
Lm.new(
|
137
|
+
params: [Sym.new("path")],
|
138
|
+
body: ->(env, evaler) {
|
139
|
+
path = env.lookup(Sym.new("path"))
|
140
|
+
Array(path)
|
141
|
+
.map { |target| target.to_s }
|
142
|
+
.reduce(nil) { |value, component|
|
143
|
+
value = ruby.constantize(component)
|
144
|
+
}
|
145
|
+
}
|
146
|
+
)
|
147
|
+
end
|
148
|
+
|
149
|
+
def fn_eval
|
150
|
+
fn = Fn.new
|
151
|
+
|
152
|
+
fn.add(
|
153
|
+
Lm.new(
|
154
|
+
params: [Sym.new("forms")],
|
155
|
+
body: ->(env, evaler) {
|
156
|
+
forms = evaler.call(
|
157
|
+
env.lookup(Sym.new("forms")), context: env)
|
158
|
+
forms.reduce(nil) { |_, form|
|
159
|
+
evaler.call(form, context: env)
|
160
|
+
}
|
161
|
+
}
|
162
|
+
)
|
163
|
+
)
|
164
|
+
|
165
|
+
fn.add(
|
166
|
+
Lm.new(
|
167
|
+
params: [Sym.new("forms"), Sym.new("context")],
|
168
|
+
body: ->(env, evaler) {
|
169
|
+
context = evaler.call(
|
170
|
+
env.lookup(Sym.new("context")), context: env)
|
171
|
+
|
172
|
+
forms = evaler.call(
|
173
|
+
env.lookup(Sym.new("forms")), context: env)
|
174
|
+
|
175
|
+
forms.reduce(nil) { |_, form|
|
176
|
+
evaler.call(form, context: context)
|
177
|
+
}
|
178
|
+
}
|
179
|
+
)
|
180
|
+
)
|
181
|
+
|
182
|
+
fn
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|