sol 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +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
|