toylang 0.2.0
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/.rspec +3 -0
- data/.rubocop.yml +106 -0
- data/CHANGELOG.md +10 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +154 -0
- data/Rakefile +18 -0
- data/bin/toylang +33 -0
- data/lib/toylang/ast/nodes.rb +45 -0
- data/lib/toylang/grammar/grammar.y +171 -0
- data/lib/toylang/interpreter/evaluation.rb +131 -0
- data/lib/toylang/interpreter/interpreter.rb +14 -0
- data/lib/toylang/lexer/lexer.rb +98 -0
- data/lib/toylang/monkey_patching/string.rb +19 -0
- data/lib/toylang/parser/parser.rb +922 -0
- data/lib/toylang/runtime/bootstrap.rb +114 -0
- data/lib/toylang/runtime/class.rb +39 -0
- data/lib/toylang/runtime/context.rb +20 -0
- data/lib/toylang/runtime/method.rb +18 -0
- data/lib/toylang/runtime/object.rb +18 -0
- data/lib/toylang/version.rb +5 -0
- data/lib/toylang.rb +19 -0
- data/toylang.gemspec +44 -0
- metadata +197 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../parser/parser'
|
|
4
|
+
require_relative '../runtime/object'
|
|
5
|
+
require_relative '../runtime/class'
|
|
6
|
+
require_relative '../runtime/method'
|
|
7
|
+
require_relative '../runtime/context'
|
|
8
|
+
require_relative '../runtime/bootstrap'
|
|
9
|
+
|
|
10
|
+
class Nodes
|
|
11
|
+
def eval(context)
|
|
12
|
+
return_value = nil
|
|
13
|
+
nodes.each do |node|
|
|
14
|
+
return_value = node.eval(context)
|
|
15
|
+
end
|
|
16
|
+
return_value || Runtime['nil']
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class NumberNode
|
|
21
|
+
def eval(_context)
|
|
22
|
+
Runtime['Number'].new_with_value(value)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class StringNode
|
|
27
|
+
def eval(_context)
|
|
28
|
+
Runtime['String'].new_with_value(value)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class TrueNode
|
|
33
|
+
def eval(_context)
|
|
34
|
+
Runtime['true']
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class FalseNode
|
|
39
|
+
def eval(_context)
|
|
40
|
+
Runtime['false']
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
class NilNode
|
|
45
|
+
def eval(_context)
|
|
46
|
+
Runtime['nil']
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
class GetConstantNode
|
|
51
|
+
def eval(context)
|
|
52
|
+
context[name]
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
class GetLocalNode
|
|
57
|
+
def eval(context)
|
|
58
|
+
context.locals[name]
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class SetConstantNode
|
|
63
|
+
def eval(context)
|
|
64
|
+
raise "Unable to re-assigne constant: #{name}" unless context[name].nil?
|
|
65
|
+
|
|
66
|
+
context[name] = value.eval(context)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class SetLocalNode
|
|
71
|
+
def eval(context)
|
|
72
|
+
context.locals[name] = value.eval(context)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
class CallNode
|
|
77
|
+
def eval(context)
|
|
78
|
+
if receiver.nil? && context.locals[call_method] && arguments.empty?
|
|
79
|
+
context.locals[call_method]
|
|
80
|
+
else
|
|
81
|
+
value = if receiver
|
|
82
|
+
receiver.eval(context)
|
|
83
|
+
else
|
|
84
|
+
context.current_self
|
|
85
|
+
end
|
|
86
|
+
eval_arguments = arguments.map { |arg| arg.eval(context) }
|
|
87
|
+
value.call(call_method, eval_arguments)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
class DefNode
|
|
93
|
+
def eval(context)
|
|
94
|
+
method = ToyLangMethod.new(params, body)
|
|
95
|
+
context.current_class.runtime_methods[name] = method
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
class ClassNode
|
|
100
|
+
def eval(context)
|
|
101
|
+
toylang_class = context[name]
|
|
102
|
+
|
|
103
|
+
unless toylang_class
|
|
104
|
+
toylang_class = ToyLangClass.new(Runtime[superclass || 'Object'])
|
|
105
|
+
context[name] = toylang_class
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
class_context = Context.new(toylang_class, toylang_class)
|
|
109
|
+
body.eval(class_context)
|
|
110
|
+
|
|
111
|
+
toylang_class
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
class IfElseNode
|
|
116
|
+
def eval(context)
|
|
117
|
+
if condition.eval(context).ruby_value
|
|
118
|
+
body.eval(context)
|
|
119
|
+
elsif else_body.nil?
|
|
120
|
+
Runtime['nil']
|
|
121
|
+
else
|
|
122
|
+
else_body.eval(context)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
class WhileNode
|
|
128
|
+
def eval(context)
|
|
129
|
+
body.eval(context) while condition.eval(context).ruby_value
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Lexer
|
|
4
|
+
KEYWORDS = %w[def class if else true false nil while].freeze
|
|
5
|
+
|
|
6
|
+
def tokenize(code)
|
|
7
|
+
code = code.chomp.gsub(/^$\n/, '')
|
|
8
|
+
tokens = []
|
|
9
|
+
parse_indent = true
|
|
10
|
+
|
|
11
|
+
current_indent = 0
|
|
12
|
+
indent_stack = []
|
|
13
|
+
|
|
14
|
+
position = 0
|
|
15
|
+
while position < code.size
|
|
16
|
+
chunk = code[position..-1]
|
|
17
|
+
|
|
18
|
+
if chunk.start_with?('#')
|
|
19
|
+
next_line = chunk.index("\n")
|
|
20
|
+
offset = if next_line.nil?
|
|
21
|
+
chunk.size
|
|
22
|
+
else
|
|
23
|
+
next_line
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
elsif (identifier = chunk[/\A([a-z]\w*)/, 1])
|
|
27
|
+
token, offset = tokenize_identifier(identifier)
|
|
28
|
+
tokens << token
|
|
29
|
+
|
|
30
|
+
elsif (constant = chunk[/\A([A-Z]\w*)/, 1])
|
|
31
|
+
tokens << [:CONSTANT, constant]
|
|
32
|
+
offset = constant.size
|
|
33
|
+
|
|
34
|
+
elsif (number = chunk[/\A(([0-9]*[.])?[0-9]+)/, 1])
|
|
35
|
+
tokens << [:NUMBER, number.to_numeric]
|
|
36
|
+
offset = number.size
|
|
37
|
+
|
|
38
|
+
elsif (string = chunk[/\A"([^"]*)"/, 1])
|
|
39
|
+
tokens << [:STRING, string]
|
|
40
|
+
offset = string.size + 2
|
|
41
|
+
|
|
42
|
+
# Matches ": <newline> <spaces>"
|
|
43
|
+
elsif parse_indent && (indent = chunk[/\A:\n( +)/m, 1])
|
|
44
|
+
if indent.size <= current_indent
|
|
45
|
+
raise "Bad indent level, got #{indent.size} indents, " \
|
|
46
|
+
"expected > #{current_indent}"
|
|
47
|
+
end
|
|
48
|
+
current_indent = indent.size
|
|
49
|
+
indent_stack.push(current_indent)
|
|
50
|
+
tokens << [:INDENT, indent.size]
|
|
51
|
+
offset = indent.size + 2
|
|
52
|
+
|
|
53
|
+
elsif parse_indent && (indent = chunk[/\A\n( *)/m, 1])
|
|
54
|
+
if indent.size == current_indent
|
|
55
|
+
tokens << [:NEWLINE, "\n"]
|
|
56
|
+
elsif indent.size < current_indent
|
|
57
|
+
while indent.size < current_indent
|
|
58
|
+
indent_stack.pop
|
|
59
|
+
current_indent = indent_stack.last || 0
|
|
60
|
+
tokens << [:DEDENT, indent.size]
|
|
61
|
+
end
|
|
62
|
+
tokens << [:NEWLINE, "\n"]
|
|
63
|
+
else
|
|
64
|
+
raise "Missing ':'"
|
|
65
|
+
end
|
|
66
|
+
offset = indent.size + 1
|
|
67
|
+
|
|
68
|
+
elsif (operator = chunk[
|
|
69
|
+
%r{\A(\|\||&&|==|!=|<=|>=|\+=|-=|\*=|/=|%=|\*\*=|&=|\|=|\^=|<<|>>|<<=|>>=|&&=|\|\|=|\*\*)}, 1])
|
|
70
|
+
tokens << [operator, operator]
|
|
71
|
+
offset = operator.size
|
|
72
|
+
|
|
73
|
+
elsif chunk.start_with?(' ')
|
|
74
|
+
offset = 1
|
|
75
|
+
|
|
76
|
+
else
|
|
77
|
+
value = chunk[0, 1]
|
|
78
|
+
tokens << [value, value]
|
|
79
|
+
offset = 1
|
|
80
|
+
end
|
|
81
|
+
position += offset
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Close all open blocks. If the code ends without dedenting, this will take care of
|
|
85
|
+
tokens << [:DEDENT, indent_stack.first || 0] while indent_stack.pop
|
|
86
|
+
tokens
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def tokenize_identifier(identifier)
|
|
90
|
+
token = if KEYWORDS.include?(identifier)
|
|
91
|
+
# keywords will generate [:IF, "if"]
|
|
92
|
+
[identifier.upcase.to_sym, identifier]
|
|
93
|
+
else
|
|
94
|
+
[:IDENTIFIER, identifier]
|
|
95
|
+
end
|
|
96
|
+
[token, identifier.size]
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class String
|
|
4
|
+
def numeric_string?
|
|
5
|
+
!Float(self).nil?
|
|
6
|
+
rescue StandardError
|
|
7
|
+
false
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def to_numeric
|
|
11
|
+
return unless numeric_string?
|
|
12
|
+
|
|
13
|
+
if index('.')
|
|
14
|
+
to_f
|
|
15
|
+
else
|
|
16
|
+
to_i
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|