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.
@@ -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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../parser/parser'
4
+ require_relative '../runtime/bootstrap'
5
+
6
+ class Interpreter
7
+ def initialize
8
+ @parser = Parser.new
9
+ end
10
+
11
+ def eval(code)
12
+ @parser.parse(code).eval(Runtime)
13
+ end
14
+ 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