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/runtime.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
=begin
|
2
|
+
require_relative "runtime/object"
|
3
|
+
require_relative "runtime/class"
|
4
|
+
require_relative "runtime/method"
|
5
|
+
require_relative "runtime/context"
|
6
|
+
require_relative "runtime/bootstrap"
|
7
|
+
=end
|
8
|
+
|
9
|
+
Dir[File.dirname(__FILE__) + "/runtime/*.rb"].each { |file| require file }
|
10
|
+
|
11
|
+
module Sol
|
12
|
+
|
13
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require_relative "class"
|
2
|
+
require_relative "context"
|
3
|
+
|
4
|
+
module Sol
|
5
|
+
|
6
|
+
module RuntimeModel
|
7
|
+
|
8
|
+
# Bootstrap the runtime. This is where we assemble all the classes and objects together to form the runtime
|
9
|
+
# Whats happening in the runtime
|
10
|
+
sol_class = SolClass.new
|
11
|
+
|
12
|
+
sol_class.runtime_class = sol_class
|
13
|
+
|
14
|
+
object_class = SolClass.new
|
15
|
+
|
16
|
+
object_class.runtime_class = sol_class
|
17
|
+
|
18
|
+
Runtime = Context.new(object_class.new)
|
19
|
+
|
20
|
+
Runtime["Class"] = sol_class
|
21
|
+
|
22
|
+
Runtime["Object"] = object_class
|
23
|
+
|
24
|
+
Runtime["Number"] = SolClass.new
|
25
|
+
|
26
|
+
Runtime["String"] = SolClass.new
|
27
|
+
|
28
|
+
# Everything is an object in our language, even true, false and null. So they nneed to have a class too.
|
29
|
+
Runtime["TrueClass"] = SolClass.new
|
30
|
+
|
31
|
+
Runtime["FalseClass"] = SolClass.new
|
32
|
+
|
33
|
+
Runtime["NullClass"] = SolClass.new
|
34
|
+
|
35
|
+
Runtime["true"] = Runtime["TrueClass"].new_with_value(true)
|
36
|
+
|
37
|
+
Runtime["false"] = Runtime["FalseClass"].new_with_value(false)
|
38
|
+
|
39
|
+
Runtime["null"] = Runtime["TrueClass"].new_with_value(nil)
|
40
|
+
|
41
|
+
# Add a few core methods to the runtime
|
42
|
+
|
43
|
+
# Print an object to the console, e.g print("hello, world")
|
44
|
+
Runtime["Object"].runtime_methods["print"] = proc do |receiver, arguments|
|
45
|
+
|
46
|
+
puts arguments.first.ruby_value
|
47
|
+
|
48
|
+
Runtime["Null"]
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
# Exit
|
53
|
+
Runtime["Object"].runtime_methods["exit"] = proc do |receiver|
|
54
|
+
|
55
|
+
Runtime["Null"]
|
56
|
+
|
57
|
+
exit(0)
|
58
|
+
|
59
|
+
end
|
60
|
+
|
61
|
+
# Quit
|
62
|
+
Runtime["Object"].runtime_methods["quit"] = proc do |receiver|
|
63
|
+
|
64
|
+
Runtime["Null"]
|
65
|
+
|
66
|
+
exit(0)
|
67
|
+
|
68
|
+
end
|
69
|
+
|
70
|
+
# Or (||)
|
71
|
+
Runtime["Number"].runtime_methods["||"] = proc do |receiver, arguments|
|
72
|
+
|
73
|
+
Runtime["Number"].new_with_value(receiver.ruby_value || arguments.first.ruby_value)
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
# And (&&)
|
78
|
+
Runtime["Number"].runtime_methods["&&"] = proc do |receiver, arguments|
|
79
|
+
|
80
|
+
Runtime["Number"].new_with_value(receiver.ruby_value && arguments.first.ruby_value)
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
# Equal to (==)
|
85
|
+
Runtime["Number"].runtime_methods["=="] = proc do |receiver, arguments|
|
86
|
+
|
87
|
+
Runtime["Number"].new_with_value(receiver.ruby_value == arguments.first.ruby_value)
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
# Does not equal to (!=)
|
92
|
+
Runtime["Number"].runtime_methods["!="] = proc do |receiver, arguments|
|
93
|
+
|
94
|
+
Runtime["Number"].new_with_value(receiver.ruby_value != arguments.first.ruby_value)
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
# Greater than (>)
|
99
|
+
Runtime["Number"].runtime_methods[">"] = proc do |receiver, arguments|
|
100
|
+
|
101
|
+
Runtime["Number"].new_with_value(receiver.ruby_value > arguments.first.ruby_value)
|
102
|
+
|
103
|
+
end
|
104
|
+
|
105
|
+
# Greater than or equal to (>=)
|
106
|
+
Runtime["Number"].runtime_methods[">="] = proc do |receiver, arguments|
|
107
|
+
|
108
|
+
Runtime["Number"].new_with_value(receiver.ruby_value >= arguments.first.ruby_value)
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
# Less than (<)
|
113
|
+
Runtime["Number"].runtime_methods["<"] = proc do |receiver, arguments|
|
114
|
+
|
115
|
+
Runtime["Number"].new_with_value(receiver.ruby_value < arguments.first.ruby_value)
|
116
|
+
|
117
|
+
end
|
118
|
+
|
119
|
+
# Less than or equal to (>=)
|
120
|
+
Runtime["Number"].runtime_methods["<="] = proc do |receiver, arguments|
|
121
|
+
|
122
|
+
Runtime["Number"].new_with_value(receiver.ruby_value <= arguments.first.ruby_value)
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
# Add
|
127
|
+
Runtime["Number"].runtime_methods["+"] = proc do |receiver, arguments|
|
128
|
+
|
129
|
+
Runtime["Number"].new_with_value(receiver.ruby_value + arguments.first.ruby_value)
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
# Minus
|
134
|
+
Runtime["Number"].runtime_methods["-"] = proc do |receiver, arguments|
|
135
|
+
|
136
|
+
Runtime["Number"].new_with_value(receiver.ruby_value - arguments.first.ruby_value)
|
137
|
+
|
138
|
+
end
|
139
|
+
|
140
|
+
# Times
|
141
|
+
Runtime["Number"].runtime_methods["*"] = proc do |receiver, arguments|
|
142
|
+
|
143
|
+
Runtime["Number"].new_with_value(receiver.ruby_value * arguments.first.ruby_value)
|
144
|
+
|
145
|
+
end
|
146
|
+
|
147
|
+
# Divide
|
148
|
+
Runtime["Number"].runtime_methods["/"] = proc do |receiver, arguments|
|
149
|
+
|
150
|
+
Runtime["Number"].new_with_value(receiver.ruby_value / arguments.first.ruby_value)
|
151
|
+
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
|
156
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
require_relative "object"
|
2
|
+
|
3
|
+
module Sol
|
4
|
+
|
5
|
+
module RuntimeModel
|
6
|
+
|
7
|
+
# Represents a Sol class in the Ruby world. Classes are objects in Sol so they inherit from SolObject
|
8
|
+
class SolClass < SolObject
|
9
|
+
|
10
|
+
attr_reader :runtime_methods
|
11
|
+
|
12
|
+
# Create a new class. Number is an instance of Class for example
|
13
|
+
def initialize
|
14
|
+
|
15
|
+
@runtime_methods = {}
|
16
|
+
|
17
|
+
# Check if we're bootstrapping (launching the runtime). During this process the
|
18
|
+
# runtime is not fully initialised and core classes do not yet exists, so we defer
|
19
|
+
# using those once the language is bootstrapped.
|
20
|
+
# This solves the chicken-or-the-egg problem with the Class class. We can
|
21
|
+
# initialise Class then set Class.class = Class.
|
22
|
+
if defined?(Runtime) # RuntimeModel is a temporary name
|
23
|
+
|
24
|
+
runtime_class = Runtime["Class"]
|
25
|
+
|
26
|
+
else
|
27
|
+
|
28
|
+
runtime_class = nil
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
super(runtime_class)
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
# Lookup a method
|
37
|
+
def lookup(method_name)
|
38
|
+
|
39
|
+
method = @runtime_methods[method_name]
|
40
|
+
|
41
|
+
unless method
|
42
|
+
|
43
|
+
raise "Method not found: #{method_name}"
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
return method
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
# Create a new instance of the class
|
52
|
+
def new
|
53
|
+
|
54
|
+
SolObject.new(self)
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
# Create an instance of this Sol class that holds a Ruby value
|
59
|
+
def new_with_value(value)
|
60
|
+
|
61
|
+
SolObject.new(self, value)
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Sol
|
2
|
+
|
3
|
+
module RuntimeModel
|
4
|
+
|
5
|
+
class Context
|
6
|
+
|
7
|
+
attr_reader :locals, :current_self, :current_class
|
8
|
+
|
9
|
+
# We store constants as class variable (class variables start with @@ and instance
|
10
|
+
# variables start with @ in Ruby) since they are globally accessible. If you want to
|
11
|
+
# implement namespacing of constants, you could store it in the instance of this class.
|
12
|
+
@@constants = {}
|
13
|
+
|
14
|
+
def initialize(current_self, current_class=current_self.runtime_class)
|
15
|
+
|
16
|
+
@locals = {}
|
17
|
+
|
18
|
+
@current_self = current_self
|
19
|
+
|
20
|
+
@current_class = current_class
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
# Shortcuts to access constants, Runtime[...] instead of Runtime.constants[...]
|
25
|
+
|
26
|
+
def [](name)
|
27
|
+
|
28
|
+
@@constants[name]
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
def []=(name, value)
|
33
|
+
|
34
|
+
@@constants[name] = value
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Sol
|
2
|
+
|
3
|
+
module RuntimeModel
|
4
|
+
|
5
|
+
require_relative "object"
|
6
|
+
|
7
|
+
# Represents a method defined in the runtime
|
8
|
+
class SolFunction < SolObject
|
9
|
+
|
10
|
+
def initialize(params, body)
|
11
|
+
|
12
|
+
@params = params
|
13
|
+
|
14
|
+
@body = body
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(reciever, arguments)
|
19
|
+
|
20
|
+
# Create a context of evaluation in which the method will execute
|
21
|
+
context = Context.new(reciever)
|
22
|
+
|
23
|
+
if @params.class == "NullClass"
|
24
|
+
|
25
|
+
# Assign arguments to local variables
|
26
|
+
@params.to_enum.each_with_index do |param, index|
|
27
|
+
|
28
|
+
context.locals[param] = arguments[index]
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
return @body.eval(context)
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Sol
|
2
|
+
|
3
|
+
module RuntimeModel
|
4
|
+
|
5
|
+
# Represents an Sol object instance in the Ruby world
|
6
|
+
class SolObject
|
7
|
+
|
8
|
+
attr_accessor :runtime_class, :ruby_value
|
9
|
+
|
10
|
+
# Each object have a class (named runtime_class to prevents errors with Ruby's class method).
|
11
|
+
# Optionally an object can hold a Ruby value (e.g strings and numbers)
|
12
|
+
def initialize(runtime_class, ruby_value=self)
|
13
|
+
|
14
|
+
@runtime_class = runtime_class
|
15
|
+
|
16
|
+
@ruby_value = ruby_value
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
# Call a method on the object
|
21
|
+
def call(method, arguments=[])
|
22
|
+
|
23
|
+
# Like a typical Class-based runtime model, we store methods in the class of the object
|
24
|
+
@runtime_class.lookup(method).call(self, arguments)
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require "minitest/spec"
|
2
|
+
require "minitest/autorun"
|
3
|
+
|
4
|
+
require_relative "../lexer" # Reqiure lexer in above folder
|
5
|
+
|
6
|
+
describe "lexer" do
|
7
|
+
|
8
|
+
before do
|
9
|
+
|
10
|
+
@lexer = Sol::Lexer.new
|
11
|
+
|
12
|
+
@literals = ["(", ")", ".", "!", "+", "-", "*", "/"]
|
13
|
+
|
14
|
+
@operators = ["+", "-", "*", "/", "&&", "==", "!=", "<=", ">="]
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
it "can accept all keywords" do
|
19
|
+
|
20
|
+
@lexer.KEYWORDS.each do |keyword|
|
21
|
+
|
22
|
+
assert_equal [[keyword.upcase.to_sym, keyword]], @lexer.tokenise(keyword)
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
it "can accept numbers" do
|
29
|
+
|
30
|
+
assert_equal [[:NUMBER, 123]], @lexer.tokenise("123")
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
it "can accept double qouted strings" do
|
35
|
+
|
36
|
+
# Escaped strings are the best
|
37
|
+
|
38
|
+
assert_equal [[:STRING, "hello"]], @lexer.tokenise("\"hello\"")
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
it "can accept single qouted strings" do
|
43
|
+
|
44
|
+
assert_equal [[:STRING, 'hello']], @lexer.tokenise("'hello'")
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
it "can accept operators" do
|
49
|
+
|
50
|
+
@operators.each do |operator|
|
51
|
+
|
52
|
+
assert_equal [[operator, operator]], @lexer.tokenise(operator)
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
it "can accept literals" do
|
59
|
+
|
60
|
+
@literals.each do |literal|
|
61
|
+
|
62
|
+
assert_equal [[literal, literal]], @lexer.tokenise(literal)
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require "minitest/autorun"
|
2
|
+
require "minitest/spec"
|
3
|
+
|
4
|
+
require_relative "../parser.rb" # Reqiure parser in above folder
|
5
|
+
|
6
|
+
describe "parser" do
|
7
|
+
|
8
|
+
before do
|
9
|
+
|
10
|
+
@parser = Sol::Parser.new
|
11
|
+
|
12
|
+
@operators = ["||", "&&", "==", "!=", ">", ">=", "<", "<=", "+", "-", "*", "/"]
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
it "can do operations" do
|
17
|
+
|
18
|
+
@operators.each do |operator|
|
19
|
+
|
20
|
+
@parser.parse "1#{operator}1", show_tokens = false
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
26
|
+
it "can do functions" do
|
27
|
+
|
28
|
+
@parser.parse "func hello() {1 + 1}", show_tokens = false
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
it "can do if statements" do
|
33
|
+
|
34
|
+
@parser.parse "if 1 == 1 { 1 + 1 }", show_tokens = false
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|