forthic 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,328 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'forthic_error'
4
+ require_relative 'tokenizer'
5
+ require_relative 'token'
6
+ require_relative 'code_location'
7
+ require_relative 'positioned_string'
8
+ require_relative 'words/word'
9
+ require_relative 'words/push_value_word'
10
+ require_relative 'words/start_module_word'
11
+ require_relative 'words/end_module_word'
12
+ require_relative 'words/end_array_word'
13
+ require_relative 'words/definition_word'
14
+ require_relative 'global_module'
15
+
16
+ module Forthic
17
+ class Interpreter
18
+ attr_accessor :stack, :global_module, :app_module, :module_stack, :registered_modules,
19
+ :is_compiling, :should_stop, :is_memo_definition, :cur_definition,
20
+ :screens, :default_module_flags, :module_flags, :string_location,
21
+ :word_counts, :is_profiling, :start_profile_time, :timestamps
22
+
23
+ def initialize
24
+ @stack = []
25
+ @registered_modules = {}
26
+ @is_compiling = false
27
+ @should_stop = false
28
+ @is_memo_definition = false
29
+ @cur_definition = nil
30
+ @screens = {}
31
+ @default_module_flags = {}
32
+ @module_flags = {}
33
+ @string_location = nil
34
+
35
+ @global_module = GlobalModule.new(self)
36
+ @app_module = ForthicModule.new("", self)
37
+ @module_stack = [@app_module]
38
+
39
+ @word_counts = {}
40
+ @is_profiling = false
41
+ @start_profile_time = nil
42
+ @timestamps = []
43
+ end
44
+
45
+ def halt
46
+ @should_stop = true
47
+ end
48
+
49
+ # @return [ForthicModule]
50
+ def get_app_module
51
+ @app_module
52
+ end
53
+
54
+ # @return [CodeLocation, nil]
55
+ def get_string_location
56
+ @string_location
57
+ end
58
+
59
+ # @param [String] module_id
60
+ # @param [Hash] flags
61
+ def set_flags(module_id, flags)
62
+ @default_module_flags[module_id] = flags
63
+ @module_flags[module_id] = flags
64
+ end
65
+
66
+ # @param [String] module_id
67
+ # @return [Hash]
68
+ def get_flags(module_id)
69
+ module_flags = @module_flags[module_id] || {}
70
+ result = module_flags.dup
71
+ @module_flags[module_id] = @default_module_flags[module_id].dup
72
+ result
73
+ end
74
+
75
+ # @param [String] module_id
76
+ # @param [Hash] flags
77
+ def modify_flags(module_id, flags)
78
+ module_flags = @module_flags[module_id] || {}
79
+ @module_flags[module_id] = module_flags.merge(flags)
80
+ end
81
+
82
+ def reset
83
+ @stack = []
84
+ @app_module.variables = {}
85
+ @module_stack = [@app_module]
86
+ @is_compiling = false
87
+ @is_memo_definition = false
88
+ @cur_definition = nil
89
+ @string_location = nil
90
+ end
91
+
92
+ # @param [String] screen_name
93
+ # @return [String]
94
+ def get_screen_forthic(screen_name)
95
+ screen = @screens[screen_name]
96
+ raise ForthicError.new("interpreter-199", "Unable to find screen \"#{screen_name}\"", "Hmmm...something went wrong. Please file a ticket if this continues to happen") unless screen
97
+ screen
98
+ end
99
+
100
+ # @param [String] string
101
+ # @param [CodeLocation, nil] reference_location
102
+ # @return [Boolean]
103
+ def run(string, reference_location = nil)
104
+ tokenizer = Tokenizer.new(string, reference_location)
105
+ run_with_tokenizer(tokenizer)
106
+ end
107
+
108
+ # @param [Tokenizer] tokenizer
109
+ # @return [Boolean]
110
+ def run_with_tokenizer(tokenizer)
111
+ token = nil
112
+ loop do
113
+ token = tokenizer.next_token
114
+ handle_token(token)
115
+ break if token.type == TokenType::EOS || @should_stop
116
+ next if [TokenType::START_DEF, TokenType::END_DEF, TokenType::COMMENT].include?(token.type) || @is_compiling
117
+ end
118
+ true
119
+ # rescue => e
120
+ # error = ForthicError.new("interpreter-213", "Ran into an error executing this '#{token.string}'", "If there is an unknown error in the stack details, please file a ticket so we can resolve it.", token.location)
121
+ # error.set_caught_error(e)
122
+ # raise error
123
+ end
124
+
125
+ # @return [ForthicModule]
126
+ def cur_module
127
+ @module_stack.last
128
+ end
129
+
130
+ # @param [String] name
131
+ # @return [ForthicModule]
132
+ def find_module(name)
133
+ result = @registered_modules[name]
134
+ raise ForthicError.new("interpreter-236", "Couldn't find '#{name}' module", "This is most likely a typo in your Forthic code. Please check to see if '#{name}' is properly spelled and that you have permission to access it") unless result
135
+ result
136
+ end
137
+
138
+ # @param [Object] val
139
+ def stack_push(val)
140
+ @stack.push(val)
141
+ end
142
+
143
+ # @return [Object]
144
+ def stack_pop
145
+ raise ForthicError.new("interpreter-251", "Stack underflow", "This happens when we expect something to be on the stack, but it's empty. This is caused by a logical error in the Forthic and can be resolved through debugging.") if @stack.empty?
146
+ result = @stack.pop
147
+ @string_location = result.is_a?(PositionedString) ? result.location : nil
148
+ result.is_a?(PositionedString) ? result.value_of : result
149
+ end
150
+
151
+ # @param [ForthicModule] mod
152
+ def module_stack_push(mod)
153
+ @module_stack.push(mod)
154
+ end
155
+
156
+ def module_stack_pop
157
+ @module_stack.pop
158
+ end
159
+
160
+ # @param [ForthicModule] mod
161
+ def register_module(mod)
162
+ @registered_modules[mod.name] = mod
163
+ end
164
+
165
+ # @param [ForthicModule] mod
166
+ def run_module_code(mod)
167
+ module_stack_push(mod)
168
+ run(mod.forthic_code)
169
+ module_stack_pop
170
+ rescue => e
171
+ error = ForthicError.new("interpreter-278", "Something went wrong when running the module #{mod.name}", "TODO: File a ticket")
172
+ error.set_caught_error(e)
173
+ raise error
174
+ end
175
+
176
+ # @param [String] name
177
+ # @return [Word, nil]
178
+ def find_word(name)
179
+ result = nil
180
+ @module_stack.reverse_each do |m|
181
+ result = m.find_word(name)
182
+ break if result
183
+ end
184
+ result ||= @global_module.find_word(name)
185
+ result
186
+ end
187
+
188
+ def start_profiling
189
+ @is_profiling = true
190
+ @timestamps = []
191
+ @start_profile_time = Time.now
192
+ add_timestamp("START")
193
+ @word_counts = {}
194
+ end
195
+
196
+ # @param [Word] word
197
+ def count_word(word)
198
+ return unless @is_profiling
199
+ @word_counts[word.name] ||= 0
200
+ @word_counts[word.name] += 1
201
+ end
202
+
203
+ def stop_profiling
204
+ add_timestamp("END")
205
+ @is_profiling = false
206
+ end
207
+
208
+ # @param [String] label
209
+ def add_timestamp(label)
210
+ return unless @is_profiling
211
+ timestamp = { label: label, time_ms: (Time.now - @start_profile_time) * 1000 }
212
+ @timestamps.push(timestamp)
213
+ end
214
+
215
+ # @return [Array<Hash>]
216
+ def word_histogram
217
+ @word_counts.map { |name, count| { word: name, count: count } }.sort_by { |item| -item[:count] }
218
+ end
219
+
220
+ # @return [Array<Hash>]
221
+ def profile_timestamps
222
+ @timestamps
223
+ end
224
+
225
+ # @param [Token] token
226
+ def handle_token(token)
227
+ case token.type
228
+ when TokenType::STRING then handle_string_token(token)
229
+ when TokenType::COMMENT then handle_comment_token(token)
230
+ when TokenType::START_ARRAY then handle_start_array_token(token)
231
+ when TokenType::END_ARRAY then handle_end_array_token(token)
232
+ when TokenType::START_MODULE then handle_start_module_token(token)
233
+ when TokenType::END_MODULE then handle_end_module_token(token)
234
+ when TokenType::START_DEF then handle_start_definition_token(token)
235
+ when TokenType::START_MEMO then handle_start_memo_token(token)
236
+ when TokenType::END_DEF then handle_end_definition_token(token)
237
+ when TokenType::WORD then handle_word_token(token)
238
+ when TokenType::EOS then return
239
+ else
240
+ raise ForthicError.new("interpreter-362", "Hmmm...the interpreter doesn't know what to make of '#{token.string}'", "This is most likely caused by a typo in the Forthic code and can be resolved by debugging.", token.location)
241
+ end
242
+ end
243
+
244
+ # @param [Token] token
245
+ def handle_string_token(token)
246
+ value = PositionedString.new(token.string, token.location)
247
+ handle_word(PushValueWord.new("<string>", value))
248
+ end
249
+
250
+ # @param [Token] token
251
+ def handle_start_module_token(token)
252
+ word = StartModuleWord.new(token.string)
253
+ @cur_definition.add_word(word) if @is_compiling
254
+ count_word(word)
255
+ word.execute(self)
256
+ end
257
+
258
+ # @param [Token] _token
259
+ def handle_end_module_token(_token)
260
+ word = EndModuleWord.new
261
+ @cur_definition.add_word(word) if @is_compiling
262
+ count_word(word)
263
+ word.execute(self)
264
+ end
265
+
266
+ # @param [Token] token
267
+ def handle_start_array_token(token)
268
+ handle_word(PushValueWord.new("<start_array_token>", token))
269
+ end
270
+
271
+ # @param [Token] _token
272
+ def handle_end_array_token(_token)
273
+ handle_word(EndArrayWord.new)
274
+ end
275
+
276
+ # @param [Token] _token
277
+ def handle_comment_token(_token)
278
+ # Handle comment token (no-op)
279
+ end
280
+
281
+ # @param [Token] token
282
+ def handle_start_definition_token(token)
283
+ raise ForthicError.new("interpreter-407", "A definition was started while an existing definition was not ended", "This is probably caused by a missing semicolon. To resolve, ensure that all word definitions end with semicolons.", token.location) if @is_compiling
284
+ @cur_definition = DefinitionWord.new(token.string)
285
+ @is_compiling = true
286
+ @is_memo_definition = false
287
+ end
288
+
289
+ # @param [Token] token
290
+ def handle_start_memo_token(token)
291
+ raise ForthicError.new("interpreter-420", "A memo definition was started while an existing definition was not ended", "This is probably caused by a missing semicolon. To resolve, ensure that all word definitions end with semicolons.", token.location) if @is_compiling
292
+ @cur_definition = DefinitionWord.new(token.string)
293
+ @is_compiling = true
294
+ @is_memo_definition = true
295
+ end
296
+
297
+ # @param [Token] token
298
+ def handle_end_definition_token(token)
299
+ raise ForthicError.new("interpreter-433", "A definition was ended when one hadn't been started yet", "This is probably caused by an extra semicolon. To resolve, ensure that there are no spurious semicolons in the Forthic code.", token.location) unless @is_compiling
300
+ raise ForthicError.new("interpreter-440", "Cannot finish definition because there is no current definition", "Please file a ticket", token.location) unless @cur_definition
301
+ if @is_memo_definition
302
+ cur_module.add_memo_words(@cur_definition)
303
+ else
304
+ cur_module.add_word(@cur_definition)
305
+ end
306
+ @is_compiling = false
307
+ end
308
+
309
+ # @param [Token] token
310
+ def handle_word_token(token)
311
+ word = find_word(token.string)
312
+ raise ForthicError.new("interpreter-458", "Could not find word: #{token.string}", "Check to see if you have a typo in your word or the definition of that word", token.location) unless word
313
+ handle_word(word, token.location)
314
+ end
315
+
316
+ # @param [Word] word
317
+ # @param [CodeLocation, nil] location
318
+ def handle_word(word, location = nil)
319
+ if @is_compiling
320
+ word.set_location(location)
321
+ @cur_definition.add_word(word)
322
+ else
323
+ count_word(word)
324
+ word.execute(self)
325
+ end
326
+ end
327
+ end
328
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Forthic
4
+ class PositionedString
5
+ attr_accessor :string, :location
6
+
7
+ # @string [String] the string value
8
+ # @location [CodeLocation] the location of the string in the code
9
+ def initialize(string, location)
10
+ @string = string
11
+ @location = location
12
+ end
13
+
14
+ # @return [String]
15
+ def value_of
16
+ @string
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Forthic
4
+ module TokenType
5
+ STRING = 1
6
+ COMMENT = 2
7
+ START_ARRAY = 3
8
+ END_ARRAY = 4
9
+ START_MODULE = 5
10
+ END_MODULE = 6
11
+ START_DEF = 7
12
+ END_DEF = 8
13
+ START_MEMO = 9
14
+ WORD = 10
15
+ EOS = 11
16
+ end
17
+ end
18
+
19
+
20
+ module Forthic
21
+ class Token
22
+ attr_reader :type, :value, :location
23
+
24
+ # @param [TokenType] type
25
+ # @param [String] value
26
+ # @param [CodeLocation] location
27
+ def initialize(type, value, location)
28
+ @type = type
29
+ @value = value
30
+ @location = location
31
+ end
32
+
33
+ # @return [String]
34
+ def string
35
+ @value
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Forthic
4
+ class Tokenizer
5
+ attr_accessor :reference_location, :line, :column, :input_string, :input_pos,
6
+ :whitespace, :quote_chars, :token_start_pos, :token_end_pos,
7
+ :token_line, :token_column, :token_string
8
+
9
+ # @param [String] string
10
+ # @param [CodeLocation, nil] reference_location
11
+ def initialize(string, reference_location = nil)
12
+ reference_location ||= CodeLocation.new(screen_name: "<ad-hoc>")
13
+ @reference_location = reference_location
14
+ @line = reference_location.line
15
+ @column = reference_location.column
16
+ @input_string = unescape_string(string)
17
+ @input_pos = 0
18
+ @whitespace = [" ", "\t", "\n", "\r", "(", ")", ","]
19
+ @quote_chars = ['"', "'"]
20
+
21
+ # Token info
22
+ @token_start_pos = 0
23
+ @token_end_pos = 0
24
+ @token_line = 0
25
+ @token_column = 0
26
+ @token_string = ""
27
+ end
28
+
29
+ # @return [Token]
30
+ def next_token
31
+ clear_token_string
32
+ transition_from_START
33
+ end
34
+
35
+ # @param [String] string
36
+ # @return [String]
37
+ def unescape_string(string)
38
+ string
39
+ end
40
+
41
+ def clear_token_string
42
+ @token_string = ""
43
+ end
44
+
45
+ def note_start_token
46
+ @token_start_pos = @input_pos + @reference_location.start_pos
47
+ @token_line = @line
48
+ @token_column = @column
49
+ end
50
+
51
+ # @param [String] char
52
+ # @return [Boolean]
53
+ def is_whitespace(char)
54
+ @whitespace.include?(char)
55
+ end
56
+
57
+ # @param [String] char
58
+ # @return [Boolean]
59
+ def is_quote(char)
60
+ @quote_chars.include?(char)
61
+ end
62
+
63
+ # @param [Integer] index
64
+ # @param [String] char
65
+ # @return [Boolean]
66
+ def is_triple_quote(index, char)
67
+ return false unless is_quote(char)
68
+ return false if index + 2 >= @input_string.length
69
+ @input_string[index + 1] == char && @input_string[index + 2] == char
70
+ end
71
+
72
+ # @param [Integer] index
73
+ # @return [Boolean]
74
+ def is_start_memo(index)
75
+ return false if index + 1 >= @input_string.length
76
+ @input_string[index] == "@" && @input_string[index + 1] == ":"
77
+ end
78
+
79
+ # @param [Integer] num_chars
80
+ def advance_position(num_chars)
81
+ if num_chars >= 0
82
+ num_chars.times do
83
+ if @input_string[@input_pos] == "\n"
84
+ @line += 1
85
+ @column = 1
86
+ else
87
+ @column += 1
88
+ end
89
+ @input_pos += 1
90
+ end
91
+ else
92
+ (-num_chars).times do
93
+ @input_pos -= 1
94
+ raise Forthic::Error, "Invalid position" if @input_pos < 0 || @column < 0
95
+ if @input_string[@input_pos] == "\n"
96
+ @line -= 1
97
+ @column = 1
98
+ else
99
+ @column -= 1
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ # @return [CodeLocation]
106
+ def get_token_location
107
+ CodeLocation.new(
108
+ screen_name: @reference_location.screen_name,
109
+ line: @token_line,
110
+ column: @token_column,
111
+ start_pos: @token_start_pos,
112
+ end_pos: @token_start_pos + @token_string.length
113
+ )
114
+ end
115
+
116
+ # @return [Token]
117
+ def transition_from_START
118
+ while @input_pos < @input_string.length
119
+ char = @input_string[@input_pos]
120
+ note_start_token
121
+ advance_position(1)
122
+
123
+ next if is_whitespace(char)
124
+ case char
125
+ when "#"
126
+ return transition_from_COMMENT
127
+ when ":"
128
+ return transition_from_START_DEFINITION
129
+ when ";"
130
+ @token_string = char
131
+ return Token.new(TokenType::END_DEF, char, get_token_location)
132
+ when "["
133
+ @token_string = char
134
+ return Token.new(TokenType::START_ARRAY, char, get_token_location)
135
+ when "]"
136
+ @token_string = char
137
+ return Token.new(TokenType::END_ARRAY, char, get_token_location)
138
+ when "{"
139
+ return transition_from_GATHER_MODULE
140
+ when "}"
141
+ @token_string = char
142
+ return Token.new(TokenType::END_MODULE, char, get_token_location)
143
+ else
144
+ if is_start_memo(@input_pos - 1)
145
+ advance_position(1)
146
+ return transition_from_START_MEMO
147
+ elsif is_triple_quote(@input_pos - 1, char)
148
+ advance_position(2)
149
+ return transition_from_GATHER_TRIPLE_QUOTE_STRING(char)
150
+ elsif is_quote(char)
151
+ return transition_from_GATHER_STRING(char)
152
+ else
153
+ advance_position(-1)
154
+ return transition_from_GATHER_WORD
155
+ end
156
+ end
157
+ end
158
+ Token.new(TokenType::EOS, "", get_token_location)
159
+ end
160
+
161
+ # @return [Token]
162
+ def transition_from_COMMENT
163
+ note_start_token
164
+ while @input_pos < @input_string.length
165
+ char = @input_string[@input_pos]
166
+ @token_string += char
167
+ advance_position(1)
168
+ break if char == "\n"
169
+ end
170
+ Token.new(TokenType::COMMENT, @token_string, get_token_location)
171
+ end
172
+
173
+ # @return [Token]
174
+ def transition_from_START_DEFINITION
175
+ while @input_pos < @input_string.length
176
+ char = @input_string[@input_pos]
177
+ advance_position(1)
178
+ next if is_whitespace(char)
179
+ if is_quote(char)
180
+ raise Forthic::Error, "Definition names can't have quotes in them"
181
+ else
182
+ advance_position(-1)
183
+ return transition_from_GATHER_DEFINITION_NAME
184
+ end
185
+ end
186
+ raise Forthic::Error, "Got EOS in START_DEFINITION"
187
+ end
188
+
189
+ # @return [Token]
190
+ def transition_from_START_MEMO
191
+ while @input_pos < @input_string.length
192
+ char = @input_string[@input_pos]
193
+ advance_position(1)
194
+ next if is_whitespace(char)
195
+ if is_quote(char)
196
+ raise Forthic::Error, "Definitions shouldn't have quotes in them"
197
+ else
198
+ advance_position(-1)
199
+ return transition_from_GATHER_MEMO_NAME
200
+ end
201
+ end
202
+ raise Forthic::Error, "Got EOS in START_MEMO"
203
+ end
204
+
205
+ def gather_definition_name
206
+ while @input_pos < @input_string.length
207
+ char = @input_string[@input_pos]
208
+ advance_position(1)
209
+ break if is_whitespace(char)
210
+ if is_quote(char)
211
+ raise Forthic::Error, "Definition names can't have quotes in them"
212
+ elsif ["[", "]", "{", "}"].include?(char)
213
+ raise Forthic::Error, "Definitions can't have '#{char}' in them"
214
+ else
215
+ @token_string += char
216
+ end
217
+ end
218
+ end
219
+
220
+ # @return [Token]
221
+ def transition_from_GATHER_DEFINITION_NAME
222
+ note_start_token
223
+ gather_definition_name
224
+ Token.new(TokenType::START_DEF, @token_string, get_token_location)
225
+ end
226
+
227
+ # @return [Token]
228
+ def transition_from_GATHER_MEMO_NAME
229
+ note_start_token
230
+ gather_definition_name
231
+ Token.new(TokenType::START_MEMO, @token_string, get_token_location)
232
+ end
233
+
234
+ # @return [Token]
235
+ def transition_from_GATHER_MODULE
236
+ note_start_token
237
+ while @input_pos < @input_string.length
238
+ char = @input_string[@input_pos]
239
+ advance_position(1)
240
+ break if is_whitespace(char)
241
+ if char == "}"
242
+ advance_position(-1)
243
+ break
244
+ else
245
+ @token_string += char
246
+ end
247
+ end
248
+ Token.new(TokenType::START_MODULE, @token_string, get_token_location)
249
+ end
250
+
251
+ # @param [String] delim
252
+ # @return [Token]
253
+ def transition_from_GATHER_TRIPLE_QUOTE_STRING(delim)
254
+ note_start_token
255
+ string_delimiter = delim
256
+
257
+ while @input_pos < @input_string.length
258
+ char = @input_string[@input_pos]
259
+ if char == string_delimiter && is_triple_quote(@input_pos, char)
260
+ advance_position(3)
261
+ return Token.new(TokenType::STRING, @token_string, get_token_location)
262
+ else
263
+ advance_position(1)
264
+ @token_string += char
265
+ end
266
+ end
267
+ raise Forthic::Error, "Unterminated string: #{delim * 3}#{@token_string}"
268
+ end
269
+
270
+ # @param [String] delim
271
+ # @return [Token]
272
+ def transition_from_GATHER_STRING(delim)
273
+ note_start_token
274
+ string_delimiter = delim
275
+
276
+ while @input_pos < @input_string.length
277
+ char = @input_string[@input_pos]
278
+ advance_position(1)
279
+ if char == string_delimiter
280
+ return Token.new(TokenType::STRING, @token_string, get_token_location)
281
+ else
282
+ @token_string += char
283
+ end
284
+ end
285
+ raise Forthic::Error, "Unterminated string: #{delim}#{@token_string}"
286
+ end
287
+
288
+ # @return [Token]
289
+ def transition_from_GATHER_WORD
290
+ note_start_token
291
+ while @input_pos < @input_string.length
292
+ char = @input_string[@input_pos]
293
+ advance_position(1)
294
+ break if is_whitespace(char)
295
+ if [";", "[", "]", "{", "}", "#"].include?(char)
296
+ advance_position(-1)
297
+ break
298
+ else
299
+ @token_string += char
300
+ end
301
+ end
302
+ Token.new(TokenType::WORD, @token_string, get_token_location)
303
+ end
304
+ end
305
+ end