turmali 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +0 -4
- data/bin/tml +7 -17
- data/lib/turmali/grammar.y +8 -82
- data/lib/turmali/interpreter.rb +7 -37
- data/lib/turmali/lexer.rb +26 -72
- data/lib/turmali/nodes.rb +0 -42
- data/lib/turmali/runtime/bootstrap.rb +6 -25
- data/lib/turmali/runtime/class.rb +2 -8
- data/lib/turmali/runtime/method.rb +2 -6
- data/lib/turmali/runtime/object.rb +0 -7
- data/lib/turmali/version.rb +2 -2
- metadata +1 -2
- data/CODE_OF_CONDUCT.md +0 -74
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a69583d2a7925678fa9c48d77b797381222ddf1f
|
4
|
+
data.tar.gz: 5a45ddc15b1641e8310116b3efa33d64d39f0218
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 20a56029b2eee2414ea964de2fd6e6fe4f9e9eee2a1af1356941998bc1fce4a73e6aad02a56f3cc373dbbe73988f80815632bb027dca05218db5902f40664972
|
7
|
+
data.tar.gz: 68d49e35f6fb59e1e856773b6b3f407a2d9e4c31ab05c27861b44e80f170d5f4db59491668fc98c8a2034e87b709e22c050cd41413baf88fd4c99d9a3d1cbc76
|
data/README.md
CHANGED
@@ -55,7 +55,3 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/eiffel
|
|
55
55
|
## License
|
56
56
|
|
57
57
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
58
|
-
|
59
|
-
## Code of Conduct
|
60
|
-
|
61
|
-
Everyone interacting in the Turmali project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/eiffelqiu/turmali/blob/master/CODE_OF_CONDUCT.md).
|
data/bin/tml
CHANGED
@@ -1,31 +1,21 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
|
-
# The Turmali language!
|
3
|
-
#
|
4
|
-
# usage:
|
5
|
-
# ./tml example.tml # to eval a file
|
6
|
-
# ./tml # to start the REPL
|
7
|
-
#
|
8
|
-
# on Windows run with: ruby -I. tml [options]
|
9
2
|
|
10
3
|
require "bundler/setup"
|
11
4
|
require "turmali"
|
12
|
-
|
13
5
|
require "readline"
|
14
6
|
|
15
7
|
interpreter = Interpreter.new
|
16
8
|
|
17
|
-
# If a file is given we eval it.
|
18
9
|
if file = ARGV.first
|
19
10
|
interpreter.eval File.read(file)
|
20
|
-
|
21
|
-
# Start the REPL, read-eval-print-loop, or interactive interpreter
|
22
11
|
else
|
23
|
-
puts "Turmali REPL,
|
12
|
+
puts "Turmali (#{Turmali::VERSION}) REPL, Type 'quit' or 'exit' to exit"
|
24
13
|
loop do
|
25
|
-
line = Readline::readline(">> ")
|
14
|
+
line = Readline::readline(">> ")
|
15
|
+
exit if line == 'quit'
|
16
|
+
exit if line == 'exit'
|
26
17
|
Readline::HISTORY.push(line)
|
27
|
-
value = interpreter.eval(line)
|
28
|
-
puts "=> #{value.ruby_value.inspect}"
|
29
|
-
end
|
30
|
-
|
18
|
+
value = interpreter.eval(line)
|
19
|
+
puts "=> #{value.ruby_value.inspect}"
|
20
|
+
end
|
31
21
|
end
|
data/lib/turmali/grammar.y
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
class Parser
|
2
2
|
|
3
|
-
# We need to tell the parser what tokens to expect. So each type of token produced
|
4
|
-
# by our lexer needs to be declared here.
|
5
3
|
token IF
|
6
4
|
token DEF
|
7
5
|
token CLASS
|
@@ -13,9 +11,6 @@ token IDENTIFIER
|
|
13
11
|
token CONSTANT
|
14
12
|
token INDENT DEDENT
|
15
13
|
|
16
|
-
# Here is the Operator Precedence Table. As presented before, it tells the parser in
|
17
|
-
# which order to parse expressions containing operators.
|
18
|
-
# This table is based on the [C and C++ Operator Precedence Table](http://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B#Operator_precedence).
|
19
14
|
prechigh
|
20
15
|
left '.'
|
21
16
|
right '!'
|
@@ -29,41 +24,13 @@ prechigh
|
|
29
24
|
left ','
|
30
25
|
preclow
|
31
26
|
|
32
|
-
|
33
|
-
# All rules are declared using the following format:
|
34
|
-
#
|
35
|
-
# RuleName:
|
36
|
-
# OtherRule TOKEN AnotherRule { result = Node.new }
|
37
|
-
# | OtherRule { ... }
|
38
|
-
# ;
|
39
|
-
#
|
40
|
-
# In the action section (inside the `{...}` on the right), you can do the following:
|
41
|
-
#
|
42
|
-
# * Assign to `result` the value returned by the rule, usually a node for the AST.
|
43
|
-
# * Use `val[index of expression]` to get the `result` of a matched
|
44
|
-
# expressions on the left.
|
27
|
+
|
45
28
|
rule
|
46
|
-
# First, parsers are dumb, we need to explicitly tell it how to handle empty
|
47
|
-
# programs. This is what the first rule does. Note that everything between `/* ... */` is
|
48
|
-
# a comment.
|
49
29
|
Program:
|
50
30
|
/* nothing */ { result = Nodes.new([]) }
|
51
31
|
| Expressions { result = val[0] }
|
52
32
|
;
|
53
33
|
|
54
|
-
# Next, we define what a list of expressions is. Simply put, it's series of expressions separated by a
|
55
|
-
# terminator (a new line or `;` as defined later). But once again, we need to explicitly
|
56
|
-
# define how to handle trailing and orphans line breaks (the last two lines).
|
57
|
-
#
|
58
|
-
# One very powerful trick we'll use to define variable rules like this one
|
59
|
-
# (rules which can match any number of tokens) is *left-recursion*. Which means we reference
|
60
|
-
# the rule itself, directly or indirectly, on the left side **only**. This is true for the current
|
61
|
-
# type of parser we're using (LR). For other types of parsers like ANTLR (LL), it's the opposite,
|
62
|
-
# you can only use right-recursion.
|
63
|
-
#
|
64
|
-
# As you'll see bellow, the `Expressions` rule references `Expressions` itself.
|
65
|
-
# In other words, a list of expressions can be another list of expressions followed by
|
66
|
-
# another expression.
|
67
34
|
Expressions:
|
68
35
|
Expression { result = Nodes.new(val) }
|
69
36
|
| Expressions Terminator Expression { result = val[0] << val[2] }
|
@@ -71,7 +38,6 @@ rule
|
|
71
38
|
| Terminator { result = Nodes.new([]) }
|
72
39
|
;
|
73
40
|
|
74
|
-
# Every type of expression supported by our language is defined here.
|
75
41
|
Expression:
|
76
42
|
Literal
|
77
43
|
| Call
|
@@ -86,20 +52,11 @@ rule
|
|
86
52
|
| '(' Expression ')' { result = val[1] }
|
87
53
|
;
|
88
54
|
|
89
|
-
# Notice how we implement support for parentheses using the previous rule.
|
90
|
-
# `'(' Expression ')'` will force the parsing of `Expression` in its
|
91
|
-
# entirety first. Parentheses will then be discarded leaving only the fully parsed expression.
|
92
|
-
#
|
93
|
-
# Terminators are tokens that can terminate an expression.
|
94
|
-
# When using tokens to define rules, we simply reference them by their type which we defined in
|
95
|
-
# the lexer.
|
96
55
|
Terminator:
|
97
56
|
NEWLINE
|
98
57
|
| ";"
|
99
58
|
;
|
100
|
-
|
101
|
-
# Literals are the hard-coded values inside the program. If you want to add support
|
102
|
-
# for other literal types, such as arrays or hashes, this it where you'd do it.
|
59
|
+
|
103
60
|
Literal:
|
104
61
|
NUMBER { result = NumberNode.new(val[0]) }
|
105
62
|
| STRING { result = StringNode.new(val[0]) }
|
@@ -107,15 +64,7 @@ rule
|
|
107
64
|
| FALSE { result = FalseNode.new }
|
108
65
|
| NIL { result = NilNode.new }
|
109
66
|
;
|
110
|
-
|
111
|
-
# Method calls can take three forms:
|
112
|
-
#
|
113
|
-
# * Without a receiver (`self` is assumed): `method(arguments)`.
|
114
|
-
# * With a receiver: `receiver.method(arguments)`.
|
115
|
-
# * And a hint of syntactic sugar so that we can drop
|
116
|
-
# the `()` if no arguments are given: `receiver.method`.
|
117
|
-
#
|
118
|
-
# Each one of those is handled by the following rule.
|
67
|
+
|
119
68
|
Call:
|
120
69
|
IDENTIFIER Arguments { result = CallNode.new(nil, val[0], val[1]) }
|
121
70
|
| Expression "." IDENTIFIER
|
@@ -133,13 +82,6 @@ rule
|
|
133
82
|
| ArgList "," Expression { result = val[0] << val[2] }
|
134
83
|
;
|
135
84
|
|
136
|
-
|
137
|
-
# In our language, like in Ruby, operators are converted to method calls.
|
138
|
-
# So `1 + 2` will be converted to `1.+(2)`.
|
139
|
-
# `1` is the receiver of the `+` method call, passing `2`
|
140
|
-
# as an argument.
|
141
|
-
# Operators need to be defined individually for the Operator Precedence Table to take
|
142
|
-
# action.
|
143
85
|
Operator:
|
144
86
|
Expression '||' Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
145
87
|
| Expression '&&' Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
@@ -155,7 +97,6 @@ rule
|
|
155
97
|
| Expression '/' Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
156
98
|
;
|
157
99
|
|
158
|
-
# Then we have rules for getting and setting values of constants and local variables.
|
159
100
|
GetConstant:
|
160
101
|
CONSTANT { result = GetConstantNode.new(val[0]) }
|
161
102
|
;
|
@@ -172,20 +113,10 @@ rule
|
|
172
113
|
IDENTIFIER "=" Expression { result = SetLocalNode.new(val[0], val[2]) }
|
173
114
|
;
|
174
115
|
|
175
|
-
# Our language uses indentation to separate blocks of code. But the lexer took care of all
|
176
|
-
# that complexity for us and wrapped all blocks in `INDENT ... DEDENT`. A block
|
177
|
-
# is simply an increment in indentation followed by some code and closing with an equivalent
|
178
|
-
# decrement in indentation.
|
179
|
-
#
|
180
|
-
# If you'd like to use curly brackets or `end` to delimit blocks instead, you'd
|
181
|
-
# simply need to modify this one rule.
|
182
|
-
# You'll also need to remove the indentation logic from the lexer.
|
183
116
|
Block:
|
184
117
|
INDENT Expressions DEDENT { result = val[1] }
|
185
118
|
;
|
186
|
-
|
187
|
-
# The `def` keyword is used for defining methods. Once again, we're introducing
|
188
|
-
# a bit of syntactic sugar here to allow skipping the parentheses when there are no parameters.
|
119
|
+
|
189
120
|
Def:
|
190
121
|
DEF IDENTIFIER Block { result = DefNode.new(val[1], [], val[2]) }
|
191
122
|
| DEF IDENTIFIER
|
@@ -197,24 +128,19 @@ rule
|
|
197
128
|
| IDENTIFIER { result = val }
|
198
129
|
| ParamList "," IDENTIFIER { result = val[0] << val[2] }
|
199
130
|
;
|
200
|
-
|
201
|
-
# Class definition is similar to method definition.
|
202
|
-
# Class names are also constants because they start with a capital letter.
|
131
|
+
|
203
132
|
Class:
|
204
133
|
CLASS CONSTANT Block { result = ClassNode.new(val[1], val[2]) }
|
205
134
|
;
|
206
|
-
|
207
|
-
# Finally, `if` is similar to `class` but receives a *condition*.
|
135
|
+
|
208
136
|
If:
|
209
137
|
IF Expression Block { result = IfNode.new(val[1], val[2]) }
|
210
138
|
;
|
211
139
|
end
|
212
140
|
|
213
|
-
# The final code at the bottom of this Racc file will be put as-is in the generated `Parser` class.
|
214
|
-
# You can put some code at the top (`header`) and some inside the class (`inner`).
|
215
141
|
---- header
|
216
|
-
require "lexer"
|
217
|
-
require "nodes"
|
142
|
+
require "turmali/lexer"
|
143
|
+
require "turmali/nodes"
|
218
144
|
|
219
145
|
---- inner
|
220
146
|
def parse(code, show_tokens=false)
|
data/lib/turmali/interpreter.rb
CHANGED
@@ -5,8 +5,6 @@ require "turmali/runtime/context"
|
|
5
5
|
require "turmali/runtime/class"
|
6
6
|
require "turmali/runtime/bootstrap"
|
7
7
|
|
8
|
-
# First, we create an simple wrapper class to encapsulate the interpretation process.
|
9
|
-
# All this does is parse the code and call `eval` on the node at the top of the AST.
|
10
8
|
class Interpreter
|
11
9
|
def initialize
|
12
10
|
@parser = Parser.new
|
@@ -17,28 +15,16 @@ class Interpreter
|
|
17
15
|
end
|
18
16
|
end
|
19
17
|
|
20
|
-
# The `Nodes` class will always be at the top of the AST. Its only purpose it to
|
21
|
-
# contain other nodes. It correspond to a block of code or a series of expressions.
|
22
|
-
#
|
23
|
-
# The `eval` method of every node is the "interpreter" part of our language.
|
24
|
-
# All nodes know how to evalualte themselves and return the result of their evaluation.
|
25
|
-
# The `context` variable is the `Context` in which the node is evaluated (local
|
26
|
-
# variables, current self and current class).
|
27
18
|
class Nodes
|
28
19
|
def eval(context)
|
29
20
|
return_value = nil
|
30
21
|
nodes.each do |node|
|
31
22
|
return_value = node.eval(context)
|
32
23
|
end
|
33
|
-
return_value || Constants["nil"]
|
24
|
+
return_value || Constants["nil"]
|
34
25
|
end
|
35
26
|
end
|
36
27
|
|
37
|
-
# We're using `Constants` that we created before when bootstrapping the runtime to access
|
38
|
-
# the objects and classes from inside the runtime.
|
39
|
-
#
|
40
|
-
# Next, we implement `eval` on other node types. Think of that `eval` method as how the
|
41
|
-
# node bring itself to life inside the runtime.
|
42
28
|
class NumberNode
|
43
29
|
def eval(context)
|
44
30
|
Constants["Number"].new_with_value(value)
|
@@ -81,9 +67,6 @@ class GetLocalNode
|
|
81
67
|
end
|
82
68
|
end
|
83
69
|
|
84
|
-
# When setting the value of a constant or a local variable, the `value` attribute
|
85
|
-
# is a node, created by the parser. We need to evaluate the node first, to convert
|
86
|
-
# it to an object, before storing it into a variable or constant.
|
87
70
|
class SetConstantNode
|
88
71
|
def eval(context)
|
89
72
|
Constants[name] = value.eval(context)
|
@@ -96,14 +79,12 @@ class SetLocalNode
|
|
96
79
|
end
|
97
80
|
end
|
98
81
|
|
99
|
-
# The `CallNode` for calling a method is a little more complex. It needs to set the receiver
|
100
|
-
# first and then evaluate the arguments before calling the method.
|
101
82
|
class CallNode
|
102
83
|
def eval(context)
|
103
84
|
if receiver
|
104
85
|
value = receiver.eval(context)
|
105
86
|
else
|
106
|
-
value = context.current_self
|
87
|
+
value = context.current_self
|
107
88
|
end
|
108
89
|
|
109
90
|
evaluated_arguments = arguments.map { |arg| arg.eval(context) }
|
@@ -111,7 +92,6 @@ class CallNode
|
|
111
92
|
end
|
112
93
|
end
|
113
94
|
|
114
|
-
# Defining a method, using the `def` keyword, is done by adding a method to the current class.
|
115
95
|
class DefNode
|
116
96
|
def eval(context)
|
117
97
|
method = TurmaliMethod.new(params, body)
|
@@ -119,21 +99,13 @@ class DefNode
|
|
119
99
|
end
|
120
100
|
end
|
121
101
|
|
122
|
-
# Defining a class is done in three steps:
|
123
|
-
#
|
124
|
-
# 1. Reopen or define the class.
|
125
|
-
# 2. Create a special context of evaluation (set `current_self` and `current_class` to the new class).
|
126
|
-
# 3. Evaluate the body of the class inside that context.
|
127
|
-
#
|
128
|
-
# Check back how `DefNode` was implemented, adding methods to `context.current_class`. Here is
|
129
|
-
# where we set the value of `current_class`.
|
130
102
|
class ClassNode
|
131
103
|
def eval(context)
|
132
|
-
turmali_class = Constants[name]
|
104
|
+
turmali_class = Constants[name]
|
133
105
|
|
134
|
-
unless turmali_class
|
106
|
+
unless turmali_class
|
135
107
|
turmali_class = TurmaliClass.new
|
136
|
-
Constants[name] = turmali_class
|
108
|
+
Constants[name] = turmali_class
|
137
109
|
end
|
138
110
|
|
139
111
|
class_context = Context.new(turmali_class, turmali_class)
|
@@ -143,14 +115,12 @@ class ClassNode
|
|
143
115
|
end
|
144
116
|
end
|
145
117
|
|
146
|
-
# Finally, to implement `if` in our language,
|
147
|
-
# we turn the condition node into a Ruby value to use Ruby's `if`.
|
148
118
|
class IfNode
|
149
119
|
def eval(context)
|
150
120
|
if condition.eval(context).ruby_value
|
151
121
|
body.eval(context)
|
152
|
-
else
|
122
|
+
else
|
153
123
|
Constants["nil"]
|
154
124
|
end
|
155
125
|
end
|
156
|
-
end
|
126
|
+
end
|
data/lib/turmali/lexer.rb
CHANGED
@@ -1,72 +1,40 @@
|
|
1
|
-
# Our lexer will be used like so: `Lexer.new.tokenize("code")`,
|
2
|
-
# and will return an array of tokens (a token being a tuple of `[TOKEN_TYPE, TOKEN_VALUE]`).
|
3
1
|
class Lexer
|
4
|
-
|
5
|
-
# It will be used later on in the tokenizing process to disambiguate
|
6
|
-
# an identifier (method name, local variable, etc.) from a keyword.
|
2
|
+
|
7
3
|
KEYWORDS = ["def", "class", "if", "true", "false", "nil"]
|
8
4
|
|
9
5
|
def tokenize(code)
|
10
|
-
code.chomp!
|
11
|
-
tokens = []
|
12
|
-
|
13
|
-
|
14
|
-
# we keep track of the current indentation level we are in, and previous ones in the stack
|
15
|
-
# so that when we dedent, we can check if we're on the correct level.
|
16
|
-
current_indent = 0 # number of spaces in the last indent
|
6
|
+
code.chomp!
|
7
|
+
tokens = []
|
8
|
+
|
9
|
+
current_indent = 0
|
17
10
|
indent_stack = []
|
18
|
-
|
19
|
-
|
20
|
-
# Advance one character at the time until you find something to parse.
|
21
|
-
# We'll use regular expressions to scan from the current position (`i`)
|
22
|
-
# up to the end of the code.
|
23
|
-
i = 0 # Current character position
|
11
|
+
|
12
|
+
i = 0
|
24
13
|
while i < code.size
|
25
14
|
chunk = code[i..-1]
|
26
15
|
|
27
|
-
# Each of the following `if/elsif`s will test the current code chunk with
|
28
|
-
# a regular expression. The order is important as we want to match `if`
|
29
|
-
# as a keyword, and not a method name, we'll need to apply it first.
|
30
|
-
#
|
31
|
-
# First, we'll scan for names: method names and variable names, which we'll call identifiers.
|
32
|
-
# Also scanning for special reserved keywords such as `if`, `def`
|
33
|
-
# and `true`.
|
34
16
|
if identifier = chunk[/\A([a-z]\w*)/, 1]
|
35
|
-
if KEYWORDS.include?(identifier)
|
17
|
+
if KEYWORDS.include?(identifier)
|
36
18
|
tokens << [identifier.upcase.to_sym, identifier]
|
37
19
|
else
|
38
20
|
tokens << [:IDENTIFIER, identifier]
|
39
21
|
end
|
40
|
-
i += identifier.size
|
22
|
+
i += identifier.size
|
41
23
|
|
42
|
-
# Now scanning for constants, names starting with a capital letter.
|
43
|
-
# Which means, class names are constants in our language.
|
44
24
|
elsif constant = chunk[/\A([A-Z]\w*)/, 1]
|
45
25
|
tokens << [:CONSTANT, constant]
|
46
26
|
i += constant.size
|
47
|
-
|
48
|
-
# Next, matching numbers. Our language will only support integers. But to add support for floats,
|
49
|
-
# you'd simply need to add a similar rule and adapt the regular expression accordingly.
|
27
|
+
|
50
28
|
elsif number = chunk[/\A([0-9]+)/, 1]
|
51
29
|
tokens << [:NUMBER, number.to_i]
|
52
30
|
i += number.size
|
53
|
-
|
54
|
-
# Of course, matching strings too. Anything between `"..."`.
|
31
|
+
|
55
32
|
elsif string = chunk[/\A"([^"]*)"/, 1]
|
56
33
|
tokens << [:STRING, string]
|
57
|
-
i += string.size + 2
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
# if true: # 1) The block is created.
|
62
|
-
# line 1
|
63
|
-
# line 2 # 2) New line inside a block, at the same level.
|
64
|
-
# continue # 3) Dedent.
|
65
|
-
#
|
66
|
-
# This `elsif` takes care of the first case. The number of spaces will determine
|
67
|
-
# the indent level.
|
68
|
-
elsif indent = chunk[/\A\:\n( +)/m, 1] # Matches ": <newline> <spaces>"
|
69
|
-
if indent.size <= current_indent # indent should go up when creating a block
|
34
|
+
i += string.size + 2
|
35
|
+
|
36
|
+
elsif indent = chunk[/\A\:\n( +)/m, 1]
|
37
|
+
if indent.size <= current_indent
|
70
38
|
raise "Bad indent level, got #{indent.size} indents, " +
|
71
39
|
"expected > #{current_indent}"
|
72
40
|
end
|
@@ -74,41 +42,29 @@ class Lexer
|
|
74
42
|
indent_stack.push(current_indent)
|
75
43
|
tokens << [:INDENT, indent.size]
|
76
44
|
i += indent.size + 2
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
# * Case 3: Close the current block, if indent level is lower than `current_indent`.
|
83
|
-
elsif indent = chunk[/\A\n( *)/m, 1] # Matches "<newline> <spaces>"
|
84
|
-
if indent.size == current_indent # Case 2
|
85
|
-
tokens << [:NEWLINE, "\n"] # Nothing to do, we're still in the same block
|
86
|
-
elsif indent.size < current_indent # Case 3
|
45
|
+
|
46
|
+
elsif indent = chunk[/\A\n( *)/m, 1]
|
47
|
+
if indent.size == current_indent
|
48
|
+
tokens << [:NEWLINE, "\n"]
|
49
|
+
elsif indent.size < current_indent
|
87
50
|
while indent.size < current_indent
|
88
51
|
indent_stack.pop
|
89
52
|
current_indent = indent_stack.last || 0
|
90
53
|
tokens << [:DEDENT, indent.size]
|
91
54
|
end
|
92
55
|
tokens << [:NEWLINE, "\n"]
|
93
|
-
else
|
94
|
-
raise "Missing ':'"
|
56
|
+
else
|
57
|
+
raise "Missing ':'"
|
95
58
|
end
|
96
59
|
i += indent.size + 1
|
97
60
|
|
98
|
-
# Long operators such as `||`, `&&`, `==`, etc.
|
99
|
-
# will be matched by the following block.
|
100
|
-
# One character long operators are matched by the catch all `else` at the bottom.
|
101
61
|
elsif operator = chunk[/\A(\|\||&&|==|!=|<=|>=)/, 1]
|
102
62
|
tokens << [operator, operator]
|
103
63
|
i += operator.size
|
104
|
-
|
105
|
-
# We're ignoring spaces. Contrary to line breaks, spaces are meaningless in our language.
|
106
|
-
# That's why we don't create tokens for them. They are only used to separate other tokens.
|
64
|
+
|
107
65
|
elsif chunk.match(/\A /)
|
108
66
|
i += 1
|
109
|
-
|
110
|
-
# Finally, catch all single characters, mainly operators.
|
111
|
-
# We treat all other single characters as a token. Eg.: `( ) , . ! + - <`.
|
67
|
+
|
112
68
|
else
|
113
69
|
value = chunk[0,1]
|
114
70
|
tokens << [value, value]
|
@@ -117,13 +73,11 @@ class Lexer
|
|
117
73
|
end
|
118
74
|
|
119
75
|
end
|
120
|
-
|
121
|
-
# Close all open blocks. If the code ends without dedenting, this will take care of
|
122
|
-
# balancing the `INDENT`...`DEDENT`s.
|
76
|
+
|
123
77
|
while indent = indent_stack.pop
|
124
78
|
tokens << [:DEDENT, indent_stack.first || 0]
|
125
79
|
end
|
126
80
|
|
127
81
|
tokens
|
128
82
|
end
|
129
|
-
end
|
83
|
+
end
|
data/lib/turmali/nodes.rb
CHANGED
@@ -1,26 +1,3 @@
|
|
1
|
-
# The first type is responsible for holding a collection of nodes,
|
2
|
-
# each one representing an expression. You can think of it as the internal
|
3
|
-
# representation of a block of code.
|
4
|
-
#
|
5
|
-
# Here we define nodes as Ruby classes that inherit from a `Struct`. This is a
|
6
|
-
# simple way, in Ruby, to create a class that holds some attributes (values).
|
7
|
-
# It is almost equivalent to:
|
8
|
-
#
|
9
|
-
# class Nodes
|
10
|
-
# def initialize(nodes)
|
11
|
-
# @nodes = nodes
|
12
|
-
# end
|
13
|
-
#
|
14
|
-
# def nodes
|
15
|
-
# @nodes
|
16
|
-
# end
|
17
|
-
# end
|
18
|
-
#
|
19
|
-
# n = Nodes.new("this is stored @nodes")
|
20
|
-
# n.nodes # => "this is stored @nodes"
|
21
|
-
#
|
22
|
-
# But Ruby's `Struct` takes care of overriding the `==` operator for us and a bunch of
|
23
|
-
# other things that will make testing easier.
|
24
1
|
class Nodes < Struct.new(:nodes)
|
25
2
|
def <<(node) # Useful method for adding a node on the fly.
|
26
3
|
nodes << node
|
@@ -28,9 +5,6 @@ class Nodes < Struct.new(:nodes)
|
|
28
5
|
end
|
29
6
|
end
|
30
7
|
|
31
|
-
# Literals are static values that have a Ruby representation. For example, a string, a number,
|
32
|
-
# `true`, `false`, `nil`, etc. We define a node for each one of those and store their Ruby
|
33
|
-
# representation inside their `value` attribute.
|
34
8
|
class LiteralNode < Struct.new(:value); end
|
35
9
|
|
36
10
|
class NumberNode < LiteralNode; end
|
@@ -55,34 +29,18 @@ class NilNode < LiteralNode
|
|
55
29
|
end
|
56
30
|
end
|
57
31
|
|
58
|
-
# The node for a method call holds the `receiver`,
|
59
|
-
# the object on which the method is called, the `method` name and its
|
60
|
-
# arguments, which are other nodes.
|
61
32
|
class CallNode < Struct.new(:receiver, :method, :arguments); end
|
62
33
|
|
63
|
-
# Retrieving the value of a constant by its `name` is done by the following node.
|
64
34
|
class GetConstantNode < Struct.new(:name); end
|
65
35
|
|
66
|
-
# And setting its value is done by this one. The `value` will be a node. If we're
|
67
|
-
# storing a number inside a constant, for example, `value` would contain an instance
|
68
|
-
# of `NumberNode`.
|
69
36
|
class SetConstantNode < Struct.new(:name, :value); end
|
70
37
|
|
71
|
-
# Similar to the previous nodes, the next ones are for dealing with local variables.
|
72
38
|
class GetLocalNode < Struct.new(:name); end
|
73
39
|
|
74
40
|
class SetLocalNode < Struct.new(:name, :value); end
|
75
41
|
|
76
|
-
# Each method definition will be stored into the following node. It holds the `name` of the method,
|
77
|
-
# the name of its parameters (`params`) and the `body` to evaluate when the method is called, which
|
78
|
-
# is a tree of node, the root one being a `Nodes` instance.
|
79
42
|
class DefNode < Struct.new(:name, :params, :body); end
|
80
43
|
|
81
|
-
# Class definitions are stored into the following node. Once again, the `name` of the class and
|
82
|
-
# its `body`, a tree of nodes.
|
83
44
|
class ClassNode < Struct.new(:name, :body); end
|
84
45
|
|
85
|
-
# `if` control structures are stored in a node of their own. The `condition` and `body` will also
|
86
|
-
# be nodes that need to be evaluated at some point.
|
87
|
-
# Look at this node if you want to implement other control structures like `while`, `for`, `loop`, etc.
|
88
46
|
class IfNode < Struct.new(:condition, :body); end
|
@@ -1,23 +1,16 @@
|
|
1
1
|
require "turmali/runtime/class"
|
2
|
-
|
3
|
-
# our runtime.
|
4
|
-
# Then, we populate this Hash with the core classes of our language.
|
2
|
+
|
5
3
|
Constants = {}
|
6
4
|
|
7
|
-
Constants["Class"] = TurmaliClass.new
|
8
|
-
Constants["Class"].runtime_class = Constants["Class"]
|
9
|
-
Constants["Object"] = TurmaliClass.new
|
10
|
-
Constants["Number"] = TurmaliClass.new
|
5
|
+
Constants["Class"] = TurmaliClass.new
|
6
|
+
Constants["Class"].runtime_class = Constants["Class"]
|
7
|
+
Constants["Object"] = TurmaliClass.new
|
8
|
+
Constants["Number"] = TurmaliClass.new
|
11
9
|
Constants["String"] = TurmaliClass.new
|
12
10
|
|
13
|
-
# The root context will be the starting point where all our programs will
|
14
|
-
# start their evaluation. This will also set the value of `self` at the root
|
15
|
-
# of our programs.
|
16
11
|
root_self = Constants["Object"].new
|
17
12
|
RootContext = Context.new(root_self)
|
18
13
|
|
19
|
-
# Everything is an object in our language, even `true`, `false` and `nil`. So they need
|
20
|
-
# to have a class too.
|
21
14
|
Constants["TrueClass"] = TurmaliClass.new
|
22
15
|
Constants["FalseClass"] = TurmaliClass.new
|
23
16
|
Constants["NilClass"] = TurmaliClass.new
|
@@ -26,23 +19,11 @@ Constants["true"] = Constants["TrueClass"].new_with_value(true)
|
|
26
19
|
Constants["false"] = Constants["FalseClass"].new_with_value(false)
|
27
20
|
Constants["nil"] = Constants["NilClass"].new_with_value(nil)
|
28
21
|
|
29
|
-
# Now that we have injected all the core classes into the runtime, we can define
|
30
|
-
# methods on those classes.
|
31
|
-
#
|
32
|
-
# The first method we'll define will allow us to do `Object.new` or
|
33
|
-
# `Number.new`. Keep in mind, `Object` or `Number`
|
34
|
-
# are instances of the `Class` class. By defining the `new` method
|
35
|
-
# on `Class`, it will be accessible on all its instances.
|
36
22
|
Constants["Class"].def :new do |receiver, arguments|
|
37
23
|
receiver.new
|
38
24
|
end
|
39
25
|
|
40
|
-
# Next, we'll define the `print` method. Since we want to be able to call it
|
41
|
-
# from everywhere, we'll define it on `Object`.
|
42
|
-
# Remember from the parser's `Call` rule, methods without any receiver will be
|
43
|
-
# sent to `self`. So `print()` is the same as `self.print()`, and
|
44
|
-
# `self` will always be an instance of `Object`.
|
45
26
|
Constants["Object"].def :print do |receiver, arguments|
|
46
27
|
puts arguments.first.ruby_value
|
47
|
-
Constants["nil"]
|
28
|
+
Constants["nil"]
|
48
29
|
end
|
@@ -2,8 +2,7 @@ require "turmali/runtime/object"
|
|
2
2
|
require "turmali/runtime/context"
|
3
3
|
|
4
4
|
class TurmaliClass < TurmaliObject
|
5
|
-
|
6
|
-
|
5
|
+
|
7
6
|
attr_reader :runtime_methods
|
8
7
|
|
9
8
|
def initialize
|
@@ -11,25 +10,20 @@ class TurmaliClass < TurmaliObject
|
|
11
10
|
@runtime_class = Constants["Class"]
|
12
11
|
end
|
13
12
|
|
14
|
-
# Lookup a method
|
15
13
|
def lookup(method_name)
|
16
14
|
method = @runtime_methods[method_name]
|
17
15
|
raise "Method not found: #{method_name}" if method.nil?
|
18
16
|
method
|
19
17
|
end
|
20
18
|
|
21
|
-
# Helper method to define a method on this class from Ruby.
|
22
19
|
def def(name, &block)
|
23
20
|
@runtime_methods[name.to_s] = block
|
24
21
|
end
|
25
22
|
|
26
|
-
# Create a new instance of this class
|
27
23
|
def new
|
28
24
|
TurmaliObject.new(self)
|
29
25
|
end
|
30
|
-
|
31
|
-
# Create an instance of this Turmali class that holds a Ruby value. Like a String,
|
32
|
-
# Number or true.
|
26
|
+
|
33
27
|
def new_with_value(value)
|
34
28
|
TurmaliObject.new(self, value)
|
35
29
|
end
|
@@ -5,16 +5,12 @@ class TurmaliMethod
|
|
5
5
|
end
|
6
6
|
|
7
7
|
def call(receiver, arguments)
|
8
|
-
# Create a context of evaluation in which the method will execute.
|
9
8
|
context = Context.new(receiver)
|
10
|
-
|
11
|
-
# Assign passed arguments to local variables.
|
9
|
+
|
12
10
|
@params.each_with_index do |param, index|
|
13
11
|
context.locals[param] = arguments[index]
|
14
12
|
end
|
15
|
-
|
16
|
-
# The body is a node (created in the parser).
|
17
|
-
# We'll talk in details about the `eval` method in the interpreter chapter.
|
13
|
+
|
18
14
|
@body.eval(context)
|
19
15
|
end
|
20
16
|
end
|
@@ -1,8 +1,4 @@
|
|
1
1
|
class TurmaliObject
|
2
|
-
# Each object has a class (named <code>runtime_class</code> to prevent conflicts
|
3
|
-
# with Ruby's <code>class</code> keyword).
|
4
|
-
# Optionally an object can hold a Ruby value. Eg.: numbers and strings will store their
|
5
|
-
# number or string Ruby equivalent in that variable.
|
6
2
|
attr_accessor :runtime_class, :ruby_value
|
7
3
|
|
8
4
|
def initialize(runtime_class, ruby_value=self)
|
@@ -10,9 +6,6 @@ class TurmaliObject
|
|
10
6
|
@ruby_value = ruby_value
|
11
7
|
end
|
12
8
|
|
13
|
-
# Like a typical Class-based runtime model, we store methods in the class of the
|
14
|
-
# object. When calling a method on an object, we need to first lookup that
|
15
|
-
# method in the class, and then call it.
|
16
9
|
def call(method, arguments=[])
|
17
10
|
@runtime_class.lookup(method).call(self, arguments)
|
18
11
|
end
|
data/lib/turmali/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module Turmali
|
2
|
-
VERSION = "0.0.
|
3
|
-
end
|
2
|
+
VERSION = "0.0.2"
|
3
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: turmali
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eiffel Qiu
|
@@ -64,7 +64,6 @@ files:
|
|
64
64
|
- ".gitignore"
|
65
65
|
- ".rspec"
|
66
66
|
- ".travis.yml"
|
67
|
-
- CODE_OF_CONDUCT.md
|
68
67
|
- Gemfile
|
69
68
|
- LICENSE.txt
|
70
69
|
- README.md
|
data/CODE_OF_CONDUCT.md
DELETED
@@ -1,74 +0,0 @@
|
|
1
|
-
# Contributor Covenant Code of Conduct
|
2
|
-
|
3
|
-
## Our Pledge
|
4
|
-
|
5
|
-
In the interest of fostering an open and welcoming environment, we as
|
6
|
-
contributors and maintainers pledge to making participation in our project and
|
7
|
-
our community a harassment-free experience for everyone, regardless of age, body
|
8
|
-
size, disability, ethnicity, gender identity and expression, level of experience,
|
9
|
-
nationality, personal appearance, race, religion, or sexual identity and
|
10
|
-
orientation.
|
11
|
-
|
12
|
-
## Our Standards
|
13
|
-
|
14
|
-
Examples of behavior that contributes to creating a positive environment
|
15
|
-
include:
|
16
|
-
|
17
|
-
* Using welcoming and inclusive language
|
18
|
-
* Being respectful of differing viewpoints and experiences
|
19
|
-
* Gracefully accepting constructive criticism
|
20
|
-
* Focusing on what is best for the community
|
21
|
-
* Showing empathy towards other community members
|
22
|
-
|
23
|
-
Examples of unacceptable behavior by participants include:
|
24
|
-
|
25
|
-
* The use of sexualized language or imagery and unwelcome sexual attention or
|
26
|
-
advances
|
27
|
-
* Trolling, insulting/derogatory comments, and personal or political attacks
|
28
|
-
* Public or private harassment
|
29
|
-
* Publishing others' private information, such as a physical or electronic
|
30
|
-
address, without explicit permission
|
31
|
-
* Other conduct which could reasonably be considered inappropriate in a
|
32
|
-
professional setting
|
33
|
-
|
34
|
-
## Our Responsibilities
|
35
|
-
|
36
|
-
Project maintainers are responsible for clarifying the standards of acceptable
|
37
|
-
behavior and are expected to take appropriate and fair corrective action in
|
38
|
-
response to any instances of unacceptable behavior.
|
39
|
-
|
40
|
-
Project maintainers have the right and responsibility to remove, edit, or
|
41
|
-
reject comments, commits, code, wiki edits, issues, and other contributions
|
42
|
-
that are not aligned to this Code of Conduct, or to ban temporarily or
|
43
|
-
permanently any contributor for other behaviors that they deem inappropriate,
|
44
|
-
threatening, offensive, or harmful.
|
45
|
-
|
46
|
-
## Scope
|
47
|
-
|
48
|
-
This Code of Conduct applies both within project spaces and in public spaces
|
49
|
-
when an individual is representing the project or its community. Examples of
|
50
|
-
representing a project or community include using an official project e-mail
|
51
|
-
address, posting via an official social media account, or acting as an appointed
|
52
|
-
representative at an online or offline event. Representation of a project may be
|
53
|
-
further defined and clarified by project maintainers.
|
54
|
-
|
55
|
-
## Enforcement
|
56
|
-
|
57
|
-
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
58
|
-
reported by contacting the project team at eiffelqiu@qq.com. All
|
59
|
-
complaints will be reviewed and investigated and will result in a response that
|
60
|
-
is deemed necessary and appropriate to the circumstances. The project team is
|
61
|
-
obligated to maintain confidentiality with regard to the reporter of an incident.
|
62
|
-
Further details of specific enforcement policies may be posted separately.
|
63
|
-
|
64
|
-
Project maintainers who do not follow or enforce the Code of Conduct in good
|
65
|
-
faith may face temporary or permanent repercussions as determined by other
|
66
|
-
members of the project's leadership.
|
67
|
-
|
68
|
-
## Attribution
|
69
|
-
|
70
|
-
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
71
|
-
available at [http://contributor-covenant.org/version/1/4][version]
|
72
|
-
|
73
|
-
[homepage]: http://contributor-covenant.org
|
74
|
-
[version]: http://contributor-covenant.org/version/1/4/
|