turmali 0.0.1 → 0.0.2
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 +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/
|