sol 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/Gemfile +4 -0
- data/LICENSE +339 -0
- data/README.md +29 -0
- data/Rakefile +40 -0
- data/bin/sol +4 -0
- data/lib/sol.rb +7 -0
- data/lib/sol/cli.rb +57 -0
- data/lib/sol/example.sol +9 -0
- data/lib/sol/grammar.tab.rb +456 -0
- data/lib/sol/grammar.y +178 -0
- data/lib/sol/interpreter.rb +187 -0
- data/lib/sol/lexer.rb +163 -0
- data/lib/sol/nodes.rb +79 -0
- data/lib/sol/parser.rb +639 -0
- data/lib/sol/parser.y +639 -0
- data/lib/sol/runtime.rb +13 -0
- data/lib/sol/runtime/bootstrap.rb +156 -0
- data/lib/sol/runtime/class.rb +69 -0
- data/lib/sol/runtime/context.rb +42 -0
- data/lib/sol/runtime/function.rb +42 -0
- data/lib/sol/runtime/object.rb +32 -0
- data/lib/sol/spec/lexer_test.rb +68 -0
- data/lib/sol/spec/parser_test.rb +38 -0
- data/lib/sol/version.rb +5 -0
- data/sol.gemspec +25 -0
- metadata +113 -0
data/lib/sol/grammar.y
ADDED
@@ -0,0 +1,178 @@
|
|
1
|
+
class Parser
|
2
|
+
|
3
|
+
#Declare tokens by the lexer
|
4
|
+
|
5
|
+
token IF
|
6
|
+
token FUNC
|
7
|
+
token NEWLINE
|
8
|
+
token NUMBER
|
9
|
+
token STRING
|
10
|
+
token TRUE FALSE NULL
|
11
|
+
token IDENTIFIER
|
12
|
+
token CONSTANT
|
13
|
+
|
14
|
+
prechigh
|
15
|
+
left "."
|
16
|
+
left "!"
|
17
|
+
left "*" "/"
|
18
|
+
left "+" "-"
|
19
|
+
left ">" ">=" "<" "<="
|
20
|
+
left "==" "!="
|
21
|
+
left "&&"
|
22
|
+
left "||"
|
23
|
+
left "="
|
24
|
+
left ","
|
25
|
+
preclow
|
26
|
+
|
27
|
+
rule
|
28
|
+
|
29
|
+
# All rules are declared in this format:
|
30
|
+
#
|
31
|
+
# RuleName:
|
32
|
+
# OtherRule TOKEN AnotherRule { code to run when this matches }
|
33
|
+
# | OtherRule { ... }
|
34
|
+
# ;
|
35
|
+
#
|
36
|
+
# In the code section (inside the {...} on the right):
|
37
|
+
# - Assign to "result" the value returned by the rule.
|
38
|
+
|
39
|
+
# All parsinng will end in this rule, being the trunk of the AST
|
40
|
+
Root:
|
41
|
+
/* nothing */ { result = Nodes.new([]) }
|
42
|
+
| Expressions { result = val[0] }
|
43
|
+
;
|
44
|
+
|
45
|
+
# Any list of expressions are seperated by line breaks
|
46
|
+
Expressions:
|
47
|
+
Expression { result = Nodes.new(val) }
|
48
|
+
| Expressions Expression { result = val[0] << val[1] }
|
49
|
+
# To ignore trailing line breaks
|
50
|
+
| Expressions Terminator { result = val[0] }
|
51
|
+
| Terminator { result = Nodes.new([]) }
|
52
|
+
;
|
53
|
+
|
54
|
+
# All types of expressions in our language
|
55
|
+
Expression:
|
56
|
+
Literal
|
57
|
+
| Call
|
58
|
+
| Operator
|
59
|
+
| Constant
|
60
|
+
| Assign
|
61
|
+
| Func
|
62
|
+
| If
|
63
|
+
| "(" Expression ")" { result = val[1] }
|
64
|
+
;
|
65
|
+
|
66
|
+
# All tokens that can terminate and expression
|
67
|
+
Terminator:
|
68
|
+
NEWLINE
|
69
|
+
| ";"
|
70
|
+
;
|
71
|
+
|
72
|
+
# All hardcoded value
|
73
|
+
Literal:
|
74
|
+
NUMBER { result = NumberNode.new(val[0]) }
|
75
|
+
| STRING { result = StringNode.new(val[0]) }
|
76
|
+
| TRUE { result = TrueNode.new }
|
77
|
+
| FALSE { result = FalseNode.new }
|
78
|
+
| NULL { result = NullNode.new }
|
79
|
+
;
|
80
|
+
|
81
|
+
# A method call
|
82
|
+
Call:
|
83
|
+
# Method
|
84
|
+
# IDENTIFIER { result = CallNode.new(nil, val[0], []) }
|
85
|
+
# method arguments are optional
|
86
|
+
IDENTIFIER "(" ArgList ")" { result = CallNode.new(nil, val[0], val[2]) }
|
87
|
+
# receiver.method
|
88
|
+
# | Expression "." IDENTIFIER { result = CallNode.new(val[0], val[2], []) }
|
89
|
+
# receiver.method(arguments) arguments are optional
|
90
|
+
| Expression "." IDENTIFIER "(" ArgList ")" { result = CallNode.new(val[0], val[2], val[4]) }
|
91
|
+
;
|
92
|
+
|
93
|
+
ArgList:
|
94
|
+
/* Nothing */ { result = [] }
|
95
|
+
| Expression { result = val }
|
96
|
+
| ArgList "," Expression { result = val[0] << val[2]}
|
97
|
+
;
|
98
|
+
|
99
|
+
Operator:
|
100
|
+
# Binary operators
|
101
|
+
Expression "||" Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
102
|
+
| Expression '&&' Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
103
|
+
| Expression '==' Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
104
|
+
| Expression '!=' Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
105
|
+
| Expression '>' Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
106
|
+
| Expression '>=' Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
107
|
+
| Expression '<' Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
108
|
+
| Expression '<=' Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
109
|
+
| Expression '+' Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
110
|
+
| Expression '-' Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
111
|
+
| Expression '*' Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
112
|
+
| Expression '/' Expression { result = CallNode.new(val[0], val[1], [val[2]]) }
|
113
|
+
;
|
114
|
+
|
115
|
+
Constant:
|
116
|
+
CONSTANT { result = GetConstantNode.new(val[0]) }
|
117
|
+
;
|
118
|
+
|
119
|
+
# Assignment to a variable or constant
|
120
|
+
Assign:
|
121
|
+
IDENTIFIER "=" Expression { result = SetLocalNode.new(val[0], val[2]) }
|
122
|
+
| CONSTANT "=" Expression { result = SetLocalNode.new(val[0], val[2]) }
|
123
|
+
;
|
124
|
+
|
125
|
+
# Method definition
|
126
|
+
Func:
|
127
|
+
# ParamList is optional
|
128
|
+
FUNC IDENTIFIER "(" ParamList ")" Block { result = FuncNode.new(val[1], val[3], val[5]) }
|
129
|
+
;
|
130
|
+
|
131
|
+
ParamList:
|
132
|
+
/* Nothing */
|
133
|
+
| IDENTIFIER { result = [] }
|
134
|
+
| ParamList "," IDENTIFIER { result = val }
|
135
|
+
;
|
136
|
+
|
137
|
+
# If block
|
138
|
+
If:
|
139
|
+
IF Expression Block { result = IfNode.new(val[1], val[2]) }
|
140
|
+
;
|
141
|
+
|
142
|
+
# A block of code all the work was done by the lexer
|
143
|
+
Block:
|
144
|
+
"{" Expressions "}" { result = val[1] }
|
145
|
+
;
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
---- header
|
150
|
+
|
151
|
+
require_relative "lexer"
|
152
|
+
|
153
|
+
require_relative "nodes"
|
154
|
+
|
155
|
+
module Sol
|
156
|
+
|
157
|
+
---- inner
|
158
|
+
|
159
|
+
# This code will be puts as-is in the Parser class
|
160
|
+
def parse(input, show_tokens=true)
|
161
|
+
|
162
|
+
@tokens = Sol::Lexer.new.tokenise(input) # Tokenise the code using our lexer
|
163
|
+
|
164
|
+
puts @tokens.inspect if show_tokens
|
165
|
+
|
166
|
+
do_parse # Kickoff the parsing process
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
def next_token
|
171
|
+
|
172
|
+
@tokens.shift
|
173
|
+
|
174
|
+
end
|
175
|
+
|
176
|
+
---- footer
|
177
|
+
|
178
|
+
end # End the module
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require_relative "parser"
|
2
|
+
require_relative "runtime"
|
3
|
+
|
4
|
+
module Sol
|
5
|
+
|
6
|
+
class Interpreter
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
|
10
|
+
@parser = Sol::Parser.new
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
def eval(input)
|
15
|
+
|
16
|
+
@parser.parse(input).eval(RuntimeModel::Runtime)
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
class Nodes
|
23
|
+
|
24
|
+
# This method is the "interpreter" part of our language. All nodes know how to eval
|
25
|
+
# itself and returns the result of its evaluation by implementing the "eval" method.
|
26
|
+
# The "context" variable is the environment in which the node is evaluated (local variables, current class, etc.).
|
27
|
+
def eval(context)
|
28
|
+
|
29
|
+
return_value = nil
|
30
|
+
|
31
|
+
nodes.each do |node|
|
32
|
+
|
33
|
+
return_value = node.eval(context)
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
# The last value evaluated in a method is the return value. Or null if node
|
38
|
+
return_value || RuntimeModel::Runtime["null"]
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
class NumberNode
|
45
|
+
|
46
|
+
def eval(context)
|
47
|
+
|
48
|
+
# Here we access the Runtime, which we'll see in the next section, to create a new instance of the Number class
|
49
|
+
RuntimeModel::Runtime["Number"].new_with_value(value)
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
class StringNode
|
56
|
+
|
57
|
+
def eval(context)
|
58
|
+
|
59
|
+
RuntimeModel::Runtime["String"].new_with_value(value)
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
class TrueNode
|
66
|
+
|
67
|
+
def eval(context)
|
68
|
+
|
69
|
+
RuntimeModel::Runtime["true"]
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
class FalseNode
|
76
|
+
|
77
|
+
def eval(context)
|
78
|
+
|
79
|
+
RuntimeModel::Runtime["false"]
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
class NullNode
|
86
|
+
|
87
|
+
def eval(context)
|
88
|
+
|
89
|
+
RuntimeModel::Runtime["null"]
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
|
95
|
+
class CallNode
|
96
|
+
|
97
|
+
def eval(context)
|
98
|
+
|
99
|
+
if receiver.nil? && arguments.empty? && RuntimeModel::Runtime.locals[method]
|
100
|
+
|
101
|
+
return context::Runtime.locals[method]
|
102
|
+
|
103
|
+
else
|
104
|
+
|
105
|
+
if receiver
|
106
|
+
|
107
|
+
value = receiver.eval(context)
|
108
|
+
|
109
|
+
else
|
110
|
+
|
111
|
+
value = RuntimeModel::Runtime.current_self # I think this works
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
eval_arguments = arguments.map do |arg|
|
116
|
+
|
117
|
+
arg.eval(context)
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
value.call(method, eval_arguments)
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
end
|
128
|
+
|
129
|
+
class GetConstantNode
|
130
|
+
|
131
|
+
def eval(context)
|
132
|
+
|
133
|
+
context[name]
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
class SetConstantNode
|
140
|
+
|
141
|
+
def eval(context)
|
142
|
+
|
143
|
+
context[name] = value.eval(context)
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
|
149
|
+
class SetLocalNode
|
150
|
+
|
151
|
+
def eval(context)
|
152
|
+
|
153
|
+
context.locals[name] = value.eval(context)
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
class FuncNode
|
160
|
+
|
161
|
+
def eval(context)
|
162
|
+
|
163
|
+
# Defining a method is adding a method to the current class
|
164
|
+
method = RuntimeModel::SolFunction.new(params, body)
|
165
|
+
|
166
|
+
RuntimeModel::Runtime.current_class.runtime_methods[name] = method # I think this works
|
167
|
+
|
168
|
+
end
|
169
|
+
|
170
|
+
end
|
171
|
+
|
172
|
+
class IfNode
|
173
|
+
|
174
|
+
def eval(context)
|
175
|
+
|
176
|
+
# We turn the condition node innto a Ruby value to use Ruby's "if" control structure
|
177
|
+
if condition.eval(context).ruby_value
|
178
|
+
|
179
|
+
body.eval(context)
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
end
|
184
|
+
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
data/lib/sol/lexer.rb
ADDED
@@ -0,0 +1,163 @@
|
|
1
|
+
# encoding=utf-8
|
2
|
+
|
3
|
+
require "readline"
|
4
|
+
|
5
|
+
module Sol
|
6
|
+
|
7
|
+
class Lexer
|
8
|
+
|
9
|
+
attr_reader :KEYWORDS
|
10
|
+
|
11
|
+
IDENTIFIER = /\A([a-zA-Z]\p{WORD}+\w*)/
|
12
|
+
|
13
|
+
NUMBER = /\A([0-9]+)/
|
14
|
+
|
15
|
+
STRING = /\A["'](.*?)["']/
|
16
|
+
|
17
|
+
OPERATOR = /\A(\|\||&&|==|!=|<=|>=)/
|
18
|
+
|
19
|
+
WHITESPACE = /\A([ \t\r\n]+)/
|
20
|
+
|
21
|
+
NEWLINE = /\A([\r\n])+/
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
|
25
|
+
@KEYWORDS = ["func", "if", "true", "false", "null"]
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
# This is how to implement a very simple scanner.
|
30
|
+
# Scan one caracter at the time until you find something to parse.
|
31
|
+
|
32
|
+
def tokenise(input)
|
33
|
+
|
34
|
+
@input = input.chomp # Cleanup code by remove extra line breaks
|
35
|
+
|
36
|
+
@i = 0 # Current character position we're parsing
|
37
|
+
|
38
|
+
@tokens = [] # Collection of all parsed tokens in the form [:TOKEN_TYPE, value]
|
39
|
+
|
40
|
+
while @i < @input.length
|
41
|
+
|
42
|
+
@chunk = @input[@i..-1]
|
43
|
+
|
44
|
+
extract_next_token
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
return @tokens
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
def repl
|
53
|
+
|
54
|
+
loop do
|
55
|
+
|
56
|
+
line = Readline::readline('> ')
|
57
|
+
|
58
|
+
break if line.nil? || line == 'quit'
|
59
|
+
|
60
|
+
Readline::HISTORY.push(line)
|
61
|
+
|
62
|
+
puts "#{tokenise(line)}" # Brackets are for clarity purposes
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def extract_next_token
|
71
|
+
|
72
|
+
return if identifier_token
|
73
|
+
|
74
|
+
return if number_token
|
75
|
+
|
76
|
+
return if string_token
|
77
|
+
|
78
|
+
return if whitespace_token
|
79
|
+
|
80
|
+
return literal_token
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
# Matching if, print, method names, etc.
|
85
|
+
|
86
|
+
def identifier_token
|
87
|
+
|
88
|
+
return false unless identifier = @chunk[IDENTIFIER, 1]
|
89
|
+
|
90
|
+
# Keywords are special identifiers tagged with their own name, 'if' will result
|
91
|
+
# in an [:IF, "if"] token
|
92
|
+
|
93
|
+
if @KEYWORDS.include?(identifier)
|
94
|
+
|
95
|
+
@tokens << [identifier.upcase.to_sym, identifier]
|
96
|
+
|
97
|
+
else
|
98
|
+
|
99
|
+
@tokens << [:IDENTIFIER, identifier]
|
100
|
+
|
101
|
+
end
|
102
|
+
|
103
|
+
@i += identifier.length
|
104
|
+
|
105
|
+
end
|
106
|
+
|
107
|
+
def number_token
|
108
|
+
|
109
|
+
return false unless number = @chunk[NUMBER, 1]
|
110
|
+
|
111
|
+
@tokens << [:NUMBER, number.to_i]
|
112
|
+
|
113
|
+
@i += number.length
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
def string_token
|
118
|
+
|
119
|
+
return false unless string = @chunk[STRING, 1]
|
120
|
+
|
121
|
+
@tokens << [:STRING, string]
|
122
|
+
|
123
|
+
@i += string.length + 2
|
124
|
+
|
125
|
+
end
|
126
|
+
|
127
|
+
# Ignore whitespace
|
128
|
+
|
129
|
+
def whitespace_token
|
130
|
+
|
131
|
+
return false unless whitespace = @chunk[WHITESPACE, 1]
|
132
|
+
|
133
|
+
@i += whitespace.length
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
# We treat all other single characters as a token. Eg.: ( ) , . !
|
138
|
+
|
139
|
+
def literal_token
|
140
|
+
|
141
|
+
value = @chunk[NEWLINE, 1]
|
142
|
+
|
143
|
+
if value
|
144
|
+
|
145
|
+
@tokens << ["\n", "\n"] unless @tokens.last && @tokens.last[0] == "\n"
|
146
|
+
|
147
|
+
return @i + value.length
|
148
|
+
|
149
|
+
end
|
150
|
+
|
151
|
+
value = @chunk[OPERATOR, 1]
|
152
|
+
|
153
|
+
value ||= @chunk[0, 1]
|
154
|
+
|
155
|
+
@tokens << [value, value]
|
156
|
+
|
157
|
+
@i += value.length
|
158
|
+
|
159
|
+
end
|
160
|
+
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|