malady 0.1.0
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 +9 -0
- data/.rspec +3 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +51 -0
- data/Rakefile +1 -0
- data/bin/console +10 -0
- data/bin/malady +13 -0
- data/bin/setup +7 -0
- data/lib/malady.rb +14 -0
- data/lib/malady/ast.rb +298 -0
- data/lib/malady/compiler.rb +109 -0
- data/lib/malady/evaluator.rb +43 -0
- data/lib/malady/generator.rb +22 -0
- data/lib/malady/parser.rb +114 -0
- data/lib/malady/reader.rb +61 -0
- data/lib/malady/scope.rb +62 -0
- data/lib/malady/version.rb +3 -0
- data/malady.gemspec +23 -0
- metadata +93 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0d14f4e7a9f9b9ce3dc3cb990f414f8dd5e57c80
|
4
|
+
data.tar.gz: e487da725753bca92744a880d4387c2950cfceb1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 7d744a69db79e060449af290b692c3962d2ec1ce98ab1de9b9a232e17150258c10ce2e1d151e3b623f68c108cd140ef2d1f71af1bc719bfcc990a9239e758db9
|
7
|
+
data.tar.gz: 0f62ddf7153ed892fc9798891de11a7290ad7af5ce1ba4d21125edf95b12cd3a32e324ef8fc38c4313fab3dbfa192f2c3f8731c3abea1ec3dd123b331696621d
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2015 Jason Yeo
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
# Malady
|
2
|
+
|
3
|
+
Malady is an implementation of [mal](https://github.com/kanaka/mal) that compiles to [Rubinius](http://rubinius.com) bytecode.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
```bash
|
8
|
+
$ gem install malady
|
9
|
+
```
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
Use the `malady` binary provided in the `bin` directory to execute the malady repl.
|
14
|
+
|
15
|
+
```bash
|
16
|
+
$ malady
|
17
|
+
malady> (+ 42 88)
|
18
|
+
130
|
19
|
+
malady>
|
20
|
+
```
|
21
|
+
|
22
|
+
## Development
|
23
|
+
|
24
|
+
Malady requires [Rubinius](http://rubinius.com).
|
25
|
+
|
26
|
+
Install dependencies with bundle
|
27
|
+
|
28
|
+
```bash
|
29
|
+
$ bundle install
|
30
|
+
```
|
31
|
+
|
32
|
+
To run tests, use rspec
|
33
|
+
|
34
|
+
```bash
|
35
|
+
$ rspec
|
36
|
+
```
|
37
|
+
|
38
|
+
## To Do:
|
39
|
+
|
40
|
+
- [ ] Macros
|
41
|
+
- [ ] Import mal's test cases
|
42
|
+
|
43
|
+
## Contributing
|
44
|
+
|
45
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jsyeo/malady.
|
46
|
+
|
47
|
+
|
48
|
+
## License
|
49
|
+
|
50
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
51
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/console
ADDED
data/bin/malady
ADDED
data/bin/setup
ADDED
data/lib/malady.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
RBX = Rubinius::ToolSets.current::ToolSet
|
2
|
+
|
3
|
+
require 'malady/version'
|
4
|
+
require 'malady/reader'
|
5
|
+
require 'malady/scope'
|
6
|
+
require 'malady/ast'
|
7
|
+
require 'malady/parser'
|
8
|
+
require 'malady/evaluator'
|
9
|
+
require 'malady/generator'
|
10
|
+
require 'malady/compiler'
|
11
|
+
|
12
|
+
module Malady
|
13
|
+
# Your code goes here...
|
14
|
+
end
|
data/lib/malady/ast.rb
ADDED
@@ -0,0 +1,298 @@
|
|
1
|
+
module Malady
|
2
|
+
module AST
|
3
|
+
class Node
|
4
|
+
attr_reader :filename, :line
|
5
|
+
|
6
|
+
def initialize(filename="(script)", line=1, *args)
|
7
|
+
@filename = filename
|
8
|
+
@line = line
|
9
|
+
end
|
10
|
+
|
11
|
+
def pos(g)
|
12
|
+
g.set_line line
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class Program < Node
|
17
|
+
attr_reader :body
|
18
|
+
|
19
|
+
def initialize(filename, line, body)
|
20
|
+
super
|
21
|
+
@body = body
|
22
|
+
end
|
23
|
+
|
24
|
+
def bytecode(g)
|
25
|
+
g.file = (filename || :"(malady)").to_sym
|
26
|
+
pos(g)
|
27
|
+
|
28
|
+
body.each_with_index do |expression, idx|
|
29
|
+
expression.bytecode(g)
|
30
|
+
g.pop unless idx == body.size - 1
|
31
|
+
end
|
32
|
+
|
33
|
+
g.finalize
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class SymbolNode < Node
|
38
|
+
attr_reader :name
|
39
|
+
|
40
|
+
def initialize(filename, line, name)
|
41
|
+
super
|
42
|
+
@name = name
|
43
|
+
end
|
44
|
+
|
45
|
+
def bytecode(g)
|
46
|
+
pos(g)
|
47
|
+
local = g.state.scope.search_local(name)
|
48
|
+
local.get_bytecode(g)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class IntegerNode < Node
|
53
|
+
attr_reader :value
|
54
|
+
|
55
|
+
def initialize(filename, line, value)
|
56
|
+
super
|
57
|
+
@value = value.to_i
|
58
|
+
end
|
59
|
+
|
60
|
+
def bytecode(g)
|
61
|
+
pos(g)
|
62
|
+
g.push_int value
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class BinaryNode < Node
|
67
|
+
attr_reader :lhs, :rhs
|
68
|
+
|
69
|
+
def initialize(filename, line, lhs, rhs)
|
70
|
+
super
|
71
|
+
@lhs = lhs
|
72
|
+
@rhs = rhs
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class AddNode < BinaryNode
|
77
|
+
def bytecode(g)
|
78
|
+
pos(g)
|
79
|
+
lhs.bytecode(g)
|
80
|
+
rhs.bytecode(g)
|
81
|
+
g.send(:+, 1)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class MinusNode < BinaryNode
|
86
|
+
def bytecode(g)
|
87
|
+
pos(g)
|
88
|
+
lhs.bytecode(g)
|
89
|
+
rhs.bytecode(g)
|
90
|
+
g.send(:-, 1)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class DivideNode < BinaryNode
|
95
|
+
def bytecode(g)
|
96
|
+
pos(g)
|
97
|
+
lhs.bytecode(g)
|
98
|
+
rhs.bytecode(g)
|
99
|
+
g.send(:/, 1)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
class MultiplyNode < BinaryNode
|
104
|
+
def bytecode(g)
|
105
|
+
pos(g)
|
106
|
+
lhs.bytecode(g)
|
107
|
+
rhs.bytecode(g)
|
108
|
+
g.send(:*, 1)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class AssignNode < Node
|
113
|
+
attr_reader :name, :value
|
114
|
+
|
115
|
+
def initialize(filename, line, name, value)
|
116
|
+
super
|
117
|
+
@name = name
|
118
|
+
@value = value
|
119
|
+
end
|
120
|
+
|
121
|
+
def bytecode(g)
|
122
|
+
pos(g)
|
123
|
+
local = g.state.scope.new_local(name)
|
124
|
+
value.bytecode(g)
|
125
|
+
local.reference.set_bytecode(g)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class TrueBooleanNode < Node
|
130
|
+
def bytecode(g)
|
131
|
+
pos(g)
|
132
|
+
g.push_true
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class FalseBooleanNode < Node
|
137
|
+
def bytecode(g)
|
138
|
+
pos(g)
|
139
|
+
g.push_false
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
class IfNode < Node
|
144
|
+
attr_reader :condition, :then_branch, :else_branch
|
145
|
+
def initialize(filename, line, condition, then_branch, else_branch)
|
146
|
+
super
|
147
|
+
@condition = condition
|
148
|
+
@then_branch = then_branch
|
149
|
+
@else_branch = else_branch
|
150
|
+
end
|
151
|
+
|
152
|
+
def bytecode(g)
|
153
|
+
pos(g)
|
154
|
+
|
155
|
+
end_label = g.new_label
|
156
|
+
else_label = g.new_label
|
157
|
+
|
158
|
+
condition.bytecode(g)
|
159
|
+
g.goto_if_false else_label
|
160
|
+
then_branch.bytecode(g)
|
161
|
+
g.goto end_label
|
162
|
+
|
163
|
+
else_label.set!
|
164
|
+
else_branch.bytecode(g)
|
165
|
+
|
166
|
+
end_label.set!
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
class LessThanNode < BinaryNode
|
171
|
+
def bytecode(g)
|
172
|
+
pos(g)
|
173
|
+
lhs.bytecode(g)
|
174
|
+
rhs.bytecode(g)
|
175
|
+
g.send(:<, 1)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
class FnNode < Node
|
180
|
+
attr_reader :arguments, :body
|
181
|
+
|
182
|
+
def initialize(filename, line, arguments, body)
|
183
|
+
@filename = filename
|
184
|
+
@line = line
|
185
|
+
@arguments = arguments
|
186
|
+
@body = body
|
187
|
+
end
|
188
|
+
|
189
|
+
def bytecode(g)
|
190
|
+
pos(g)
|
191
|
+
|
192
|
+
# get a new scope
|
193
|
+
scope = Malady::Scope.new
|
194
|
+
|
195
|
+
# nest the scope in the current context
|
196
|
+
state = g.state
|
197
|
+
state.scope.nest_scope scope
|
198
|
+
|
199
|
+
# get a new generator for our block
|
200
|
+
blk = new_block_generator(g, @arguments)
|
201
|
+
|
202
|
+
# push our scope
|
203
|
+
blk.push_state scope
|
204
|
+
|
205
|
+
# setup the state in our block
|
206
|
+
blk.state.push_super state.super
|
207
|
+
blk.state.push_eval state.eval
|
208
|
+
blk.state.push_name blk.name
|
209
|
+
|
210
|
+
# our args are locals in the block
|
211
|
+
@arguments.each do |id|
|
212
|
+
blk.shift_array
|
213
|
+
local = blk.state.scope.new_local(id.to_s)
|
214
|
+
blk.set_local local.slot
|
215
|
+
blk.pop
|
216
|
+
end
|
217
|
+
blk.pop if !@arguments.empty?
|
218
|
+
|
219
|
+
# push the block
|
220
|
+
blk.state.push_block
|
221
|
+
|
222
|
+
# compile the body of the closure in the block
|
223
|
+
body.bytecode(blk)
|
224
|
+
|
225
|
+
# pop the block
|
226
|
+
blk.state.pop_block
|
227
|
+
blk.ret
|
228
|
+
|
229
|
+
# pop the state
|
230
|
+
blk.local_names = blk.state.scope.local_names
|
231
|
+
blk.local_count = blk.state.scope.local_count
|
232
|
+
blk.pop_state
|
233
|
+
blk.close
|
234
|
+
|
235
|
+
# create the block in our current generator
|
236
|
+
g.create_block blk
|
237
|
+
end
|
238
|
+
|
239
|
+
def new_block_generator(g, arguments)
|
240
|
+
blk = g.class.new
|
241
|
+
blk.name = g.state.name || :__block__
|
242
|
+
blk.file = g.file
|
243
|
+
blk.for_block = true
|
244
|
+
|
245
|
+
blk.required_args = arguments.count
|
246
|
+
blk.post_args = arguments.count
|
247
|
+
blk.total_args = arguments.count
|
248
|
+
blk.cast_for_multi_block_arg if !arguments.count.zero?
|
249
|
+
|
250
|
+
blk
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
class LetNode < FnNode
|
255
|
+
attr_reader :bindings, :identifiers, :values
|
256
|
+
|
257
|
+
def initialize(filename, line, bindings, body)
|
258
|
+
@bindings = bindings
|
259
|
+
@identifiers = @bindings.map(&:first)
|
260
|
+
@values = @bindings.map(&:last)
|
261
|
+
super(filename, line, identifiers, body)
|
262
|
+
end
|
263
|
+
|
264
|
+
# A LetNode is basically a closure with its arguments applied to the bindings
|
265
|
+
def bytecode(g)
|
266
|
+
super(g)
|
267
|
+
|
268
|
+
# compile bindings' values
|
269
|
+
@values.each do |val|
|
270
|
+
val.bytecode(g)
|
271
|
+
end
|
272
|
+
|
273
|
+
# send :call to the block
|
274
|
+
g.send :call, @bindings.count
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
class FunctionCallNode < Node
|
279
|
+
attr_reader :function, :arguments
|
280
|
+
|
281
|
+
def initialize(filename, line, function, arguments)
|
282
|
+
super
|
283
|
+
@function = function
|
284
|
+
@arguments = arguments
|
285
|
+
end
|
286
|
+
|
287
|
+
def bytecode(g)
|
288
|
+
function.bytecode(g)
|
289
|
+
|
290
|
+
@arguments.each do |val|
|
291
|
+
val.bytecode(g)
|
292
|
+
end
|
293
|
+
|
294
|
+
g.send :call, @arguments.count
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Malady
|
2
|
+
class Compiler < RBX::Compiler
|
3
|
+
Stages = Hash.new { |h,k| RBX::Compiler::Stages[k] }
|
4
|
+
|
5
|
+
def initialize(from, to)
|
6
|
+
@start = Stages[from].new self, to
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.eval(code, *args)
|
10
|
+
file, line, binding, instance = '(eval)', 1, Object.send(:binding), Object.new
|
11
|
+
args.each do |arg|
|
12
|
+
case arg
|
13
|
+
when String then file = arg
|
14
|
+
when Integer then line = arg
|
15
|
+
when Binding then binding = arg
|
16
|
+
else
|
17
|
+
instance = arg
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
cm = compile_eval(code, binding.variables, file, line)
|
22
|
+
cm.scope = Rubinius::ConstantScope.new(Object)
|
23
|
+
cm.name = :__malady__
|
24
|
+
script = Rubinius::CompiledMethod::Script.new(cm, file, true)
|
25
|
+
be = Rubinius::BlockEnvironment.new
|
26
|
+
|
27
|
+
script.eval_source = code
|
28
|
+
cm.scope.script = script
|
29
|
+
|
30
|
+
be.under_context(binding.variables, cm)
|
31
|
+
be.call_on_instance(instance)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.compile_eval(string, variable_scope, file="(eval)", line=1)
|
35
|
+
if ec = @eval_cache
|
36
|
+
layout = variable_scope.local_layout
|
37
|
+
if code = ec.retrieve([string, layout, line])
|
38
|
+
return code
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
compiler = new :eval, :compiled_code
|
43
|
+
|
44
|
+
parser = compiler.parser
|
45
|
+
parser.root RBX::AST::EvalExpression
|
46
|
+
parser.default_transforms
|
47
|
+
parser.input string, file, line
|
48
|
+
|
49
|
+
compiler.generator.variable_scope = variable_scope
|
50
|
+
|
51
|
+
code = compiler.run
|
52
|
+
|
53
|
+
code.add_metadata :for_eval, true
|
54
|
+
|
55
|
+
if ec and parser.should_cache?
|
56
|
+
ec.set([string.dup, layout, line], code)
|
57
|
+
end
|
58
|
+
|
59
|
+
return code
|
60
|
+
end
|
61
|
+
|
62
|
+
# AST -> Bytecode
|
63
|
+
class Bytecode < RBX::Compiler::Bytecode
|
64
|
+
Stages[:bytecode] = self
|
65
|
+
next_stage RBX::Compiler::Encoder
|
66
|
+
|
67
|
+
def initialize(*)
|
68
|
+
super
|
69
|
+
ensure
|
70
|
+
@processor = Malady::Generator
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# String -> AST
|
75
|
+
class StringParser < RBX::Compiler::Parser
|
76
|
+
Stages[:string] = self
|
77
|
+
next_stage Bytecode
|
78
|
+
|
79
|
+
def initialize(*)
|
80
|
+
super
|
81
|
+
ensure
|
82
|
+
@processor = Malady::Parser.new '(eval)'
|
83
|
+
end
|
84
|
+
|
85
|
+
def create
|
86
|
+
@parser = @processor
|
87
|
+
end
|
88
|
+
|
89
|
+
def input(string, name="(eval)", line=1)
|
90
|
+
@input = string
|
91
|
+
@file = name
|
92
|
+
@line = line
|
93
|
+
end
|
94
|
+
|
95
|
+
def parse
|
96
|
+
create.parse_string(@input)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
class EvalParser < StringParser
|
101
|
+
Stages[:eval] = self
|
102
|
+
next_stage Bytecode
|
103
|
+
|
104
|
+
def should_cache?
|
105
|
+
@output.should_cache?
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Malady
|
2
|
+
module Evaluator
|
3
|
+
module_function
|
4
|
+
def evaluate(env, ast)
|
5
|
+
type = ast.first
|
6
|
+
rest = ast[1..-1]
|
7
|
+
if type == :list
|
8
|
+
evaluated_list = eval_ast(env, ast)
|
9
|
+
fn = evaluated_list[1]
|
10
|
+
args = evaluated_list[2..-1]
|
11
|
+
fn.call(*args)
|
12
|
+
else
|
13
|
+
eval_ast(env, ast)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def eval_with_repl_env(ast)
|
18
|
+
repl_env = {
|
19
|
+
'+' => lambda { |x, y| x + y },
|
20
|
+
'-' => lambda { |x, y| x - y },
|
21
|
+
'/' => lambda { |x, y| Integer(x / y) },
|
22
|
+
'*' => lambda { |x, y| x * y }
|
23
|
+
}
|
24
|
+
evaluate(repl_env, ast)
|
25
|
+
end
|
26
|
+
|
27
|
+
def eval_ast(env, ast)
|
28
|
+
type = ast.first
|
29
|
+
rest = ast[1..-1]
|
30
|
+
case type
|
31
|
+
when :symbol
|
32
|
+
env[ast[1]]
|
33
|
+
when :list
|
34
|
+
result = [:list]
|
35
|
+
result + rest.map { |ast| evaluate(env, ast) }
|
36
|
+
when :integer
|
37
|
+
ast[1]
|
38
|
+
else
|
39
|
+
ast
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Malady
|
2
|
+
class Generator < RBX::Generator
|
3
|
+
def debug!
|
4
|
+
p @detected_locals
|
5
|
+
puts '*****'
|
6
|
+
ip = 0
|
7
|
+
while instruction = stream[ip]
|
8
|
+
instruct = RBX::InstructionSet[instruction]
|
9
|
+
ip += instruct.size
|
10
|
+
puts "#{instruct.name}"
|
11
|
+
end
|
12
|
+
puts '**end**'
|
13
|
+
end
|
14
|
+
|
15
|
+
def finalize
|
16
|
+
self.local_names = state.scope.local_names
|
17
|
+
self.local_count = state.scope.local_count
|
18
|
+
pop_state
|
19
|
+
close
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module Malady
|
2
|
+
class Parser
|
3
|
+
attr_reader :filename
|
4
|
+
|
5
|
+
def initialize(filename)
|
6
|
+
@filename = filename
|
7
|
+
@line = 0
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse_string(string)
|
11
|
+
sexp = Reader.read_str(string)
|
12
|
+
program_body = [parse(sexp)]
|
13
|
+
Malady::AST::Program.new @filename, @line, program_body
|
14
|
+
end
|
15
|
+
|
16
|
+
def parse(sexp)
|
17
|
+
type, *rest = sexp
|
18
|
+
if type == :list
|
19
|
+
symbol = rest.first
|
20
|
+
case symbol[1]
|
21
|
+
when 'def!'
|
22
|
+
parse_def(sexp)
|
23
|
+
when 'let*'
|
24
|
+
parse_let(sexp)
|
25
|
+
when 'if'
|
26
|
+
parse_if(sexp)
|
27
|
+
when 'fn*'
|
28
|
+
parse_fn(sexp)
|
29
|
+
else
|
30
|
+
fn, *args = parse_sexp(sexp)
|
31
|
+
apply(fn, args)
|
32
|
+
end
|
33
|
+
else
|
34
|
+
parse_sexp(sexp)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_sexp(sexp)
|
39
|
+
type, *rest = sexp
|
40
|
+
case type
|
41
|
+
when :boolean
|
42
|
+
boolean = sexp.last
|
43
|
+
if boolean == :true
|
44
|
+
Malady::AST::TrueBooleanNode.new(@filename, @line)
|
45
|
+
else
|
46
|
+
Malady::AST::FalseBooleanNode.new(@filename, @line)
|
47
|
+
end
|
48
|
+
when :symbol
|
49
|
+
name = sexp.last
|
50
|
+
builtins.fetch(name, Malady::AST::SymbolNode.new(@filename, @line, name))
|
51
|
+
when :integer
|
52
|
+
Malady::AST::IntegerNode.new @filename, @line, sexp[1]
|
53
|
+
when :list
|
54
|
+
rest.map { |sexp| parse(sexp) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def parse_def(sexp)
|
59
|
+
# [:list, [:symbol, 'def!'], [:symbol, 'blah'], sexp]
|
60
|
+
name = sexp[2][1]
|
61
|
+
value = sexp[3]
|
62
|
+
Malady::AST::AssignNode.new(@filename, @line, name, parse(value))
|
63
|
+
end
|
64
|
+
|
65
|
+
def parse_let(sexp)
|
66
|
+
# [:list, [:symbol, 'let'], [:list, [:symbol, 'c'], sexp], sexp]
|
67
|
+
bindings = sexp[2][1..-1].each_slice(2).to_a
|
68
|
+
body = sexp[3]
|
69
|
+
parsed_bindings = bindings.map { |s, val| [s[1], parse(val)] }
|
70
|
+
Malady::AST::LetNode.new(@filename, @line, parsed_bindings, parse(body))
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_if(sexp)
|
74
|
+
# [:list, [:symbol, 'if'], condition, then_branch, else_branch]
|
75
|
+
_, _, condition, then_branch, else_branch = sexp
|
76
|
+
Malady::AST::IfNode.new(@filename, @line, parse(condition), parse(then_branch), parse(else_branch))
|
77
|
+
end
|
78
|
+
|
79
|
+
def parse_fn(sexp)
|
80
|
+
# [:list, [:symbol, 'fn*'], [:list, [:symbol, 'x']], body]
|
81
|
+
_, _, (_, *arguments), body = sexp
|
82
|
+
arguments = arguments.map(&:last)
|
83
|
+
Malady::AST::FnNode.new(@filename, @line, arguments, parse(body))
|
84
|
+
end
|
85
|
+
|
86
|
+
def apply(fn, args)
|
87
|
+
if builtins.values.include? fn
|
88
|
+
fn.new(@filename, @line, *args)
|
89
|
+
else
|
90
|
+
Malady::AST::FunctionCallNode.new(@filename, @line, fn, args)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def builtins
|
95
|
+
{
|
96
|
+
'+' => Malady::AST::AddNode,
|
97
|
+
'-' => Malady::AST::MinusNode,
|
98
|
+
'/' => Malady::AST::DivideNode,
|
99
|
+
'*' => Malady::AST::MultiplyNode,
|
100
|
+
'<' => Malady::AST::LessThanNode,
|
101
|
+
}
|
102
|
+
end
|
103
|
+
|
104
|
+
# the following methods are needed to conform with RBX's parser interface
|
105
|
+
def pre_exe
|
106
|
+
[]
|
107
|
+
end
|
108
|
+
|
109
|
+
def on_error(t, val, vstack)
|
110
|
+
raise ParseError, sprintf("\nparse error on value %s (%s) #{@filename}:#{@line}",
|
111
|
+
val.inspect, token_to_str(t) || '?')
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Malady
|
2
|
+
module Reader
|
3
|
+
module_function
|
4
|
+
def parse_string(string)
|
5
|
+
read_str(string)
|
6
|
+
end
|
7
|
+
|
8
|
+
def read_str(string)
|
9
|
+
tokens = tokenizer(string)
|
10
|
+
read_form(tokens)
|
11
|
+
end
|
12
|
+
|
13
|
+
def read_form(tokens)
|
14
|
+
if tokens.first =~ /(\(|\[)/
|
15
|
+
read_list(tokens)
|
16
|
+
else
|
17
|
+
read_atom(tokens.shift)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def read_list(tokens)
|
22
|
+
raise 'Reader error: read_list called on non-list' if tokens.shift !~ /(\(|\[)/
|
23
|
+
list = [:list]
|
24
|
+
|
25
|
+
while tokens.first !~ /(\)|\])/
|
26
|
+
raise 'Reader error: Unmatched parens' if tokens.empty?
|
27
|
+
list << read_form(tokens)
|
28
|
+
end
|
29
|
+
|
30
|
+
tokens.shift # pop our closing paren
|
31
|
+
|
32
|
+
list
|
33
|
+
end
|
34
|
+
|
35
|
+
def read_atom(token)
|
36
|
+
case token
|
37
|
+
when /^-?\d+$/
|
38
|
+
[:integer, token.to_i]
|
39
|
+
when 'true'
|
40
|
+
[:boolean, :true]
|
41
|
+
when 'false'
|
42
|
+
[:boolean, :false]
|
43
|
+
when /^\D+$/
|
44
|
+
[:symbol, token]
|
45
|
+
else
|
46
|
+
raise 'Reader error: Unknown token'
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def tokenizer(string)
|
51
|
+
pos = 0
|
52
|
+
re = /[\s,]*(~@|[\[\]{}()'`~^@]|"(?:\\.|[^\\"])*"|;.*|[^\s\[\]{}('"`,;)]*)/
|
53
|
+
result = []
|
54
|
+
while (m = re.match(string, pos)) && pos < string.size
|
55
|
+
result << m.to_s.strip
|
56
|
+
pos = m.end(0)
|
57
|
+
end
|
58
|
+
result
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/malady/scope.rb
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
module Malady
|
2
|
+
class Scope
|
3
|
+
include RBX::Compiler::LocalVariables
|
4
|
+
|
5
|
+
attr_accessor :parent
|
6
|
+
|
7
|
+
def nest_scope(scope)
|
8
|
+
scope.parent = self
|
9
|
+
end
|
10
|
+
|
11
|
+
# A nested scope is looking up a local variable. If the variable exists
|
12
|
+
# in our local variables hash, return a nested reference to it. If it
|
13
|
+
# exists in an enclosing scope, increment the depth of the reference
|
14
|
+
# when it passes through this nested scope (i.e. the depth of a
|
15
|
+
# reference is a function of the nested scopes it passes through from
|
16
|
+
# the scope it is defined in to the scope it is used in).
|
17
|
+
def search_local(name)
|
18
|
+
if variable = variables[name]
|
19
|
+
variable.nested_reference
|
20
|
+
elsif block_local?(name)
|
21
|
+
new_local name
|
22
|
+
elsif reference = @parent.search_local(name)
|
23
|
+
reference.depth += 1
|
24
|
+
reference
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def block_local?(name)
|
29
|
+
@locals.include?(name) if @locals
|
30
|
+
end
|
31
|
+
|
32
|
+
def new_local(name)
|
33
|
+
variable = RBX::Compiler::LocalVariable.new allocate_slot
|
34
|
+
variables[name] = variable
|
35
|
+
end
|
36
|
+
|
37
|
+
def new_nested_local(name)
|
38
|
+
new_local(name).nested_reference
|
39
|
+
end
|
40
|
+
|
41
|
+
# If the local variable exists in this scope, set the local variable
|
42
|
+
# node attribute to a reference to the local variable. If the variable
|
43
|
+
# exists in an enclosing scope, set the local variable node attribute to
|
44
|
+
# a nested reference to the local variable. Otherwise, create a local
|
45
|
+
# variable in this scope and set the local variable node attribute.
|
46
|
+
def assign_local_reference(var)
|
47
|
+
if variable = variables[var.name]
|
48
|
+
var.variable = variable.reference
|
49
|
+
elsif block_local?(var.name)
|
50
|
+
variable = new_local var.name
|
51
|
+
var.variable = variable.reference
|
52
|
+
elsif reference = @parent.search_local(var.name)
|
53
|
+
reference.depth += 1
|
54
|
+
var.variable = reference
|
55
|
+
else
|
56
|
+
variable = new_local var.name
|
57
|
+
var.variable = variable.reference
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
data/malady.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'malady/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "malady"
|
8
|
+
spec.version = Malady::VERSION
|
9
|
+
spec.authors = ["Jason Yeo"]
|
10
|
+
spec.email = ["jasonyeo88@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = %q{An implementation of lisp that compiles to rubinius bytecode.}
|
13
|
+
spec.homepage = "https://github.com/jsyeo/malady"
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
17
|
+
spec.bindir = "exe"
|
18
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
22
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: malady
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jason Yeo
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-01-26 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '1.10'
|
19
|
+
name: bundler
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '10.0'
|
33
|
+
name: rake
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
description:
|
42
|
+
email:
|
43
|
+
- jasonyeo88@gmail.com
|
44
|
+
executables: []
|
45
|
+
extensions: []
|
46
|
+
extra_rdoc_files: []
|
47
|
+
files:
|
48
|
+
- ".gitignore"
|
49
|
+
- ".rspec"
|
50
|
+
- ".travis.yml"
|
51
|
+
- Gemfile
|
52
|
+
- LICENSE.txt
|
53
|
+
- README.md
|
54
|
+
- Rakefile
|
55
|
+
- bin/console
|
56
|
+
- bin/malady
|
57
|
+
- bin/setup
|
58
|
+
- lib/malady.rb
|
59
|
+
- lib/malady/ast.rb
|
60
|
+
- lib/malady/compiler.rb
|
61
|
+
- lib/malady/evaluator.rb
|
62
|
+
- lib/malady/generator.rb
|
63
|
+
- lib/malady/parser.rb
|
64
|
+
- lib/malady/reader.rb
|
65
|
+
- lib/malady/scope.rb
|
66
|
+
- lib/malady/version.rb
|
67
|
+
- malady.gemspec
|
68
|
+
homepage: https://github.com/jsyeo/malady
|
69
|
+
licenses:
|
70
|
+
- MIT
|
71
|
+
metadata: {}
|
72
|
+
post_install_message:
|
73
|
+
rdoc_options: []
|
74
|
+
require_paths:
|
75
|
+
- lib
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
requirements:
|
83
|
+
- - ">="
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
requirements: []
|
87
|
+
rubyforge_project:
|
88
|
+
rubygems_version: 2.5.1
|
89
|
+
signing_key:
|
90
|
+
specification_version: 4
|
91
|
+
summary: An implementation of lisp that compiles to rubinius bytecode.
|
92
|
+
test_files: []
|
93
|
+
has_rdoc:
|