forthic 0.1.0 → 0.3.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 +4 -4
- data/README.md +314 -14
- data/Rakefile +37 -8
- data/lib/forthic/decorators/docs.rb +69 -0
- data/lib/forthic/decorators/word.rb +331 -0
- data/lib/forthic/errors.rb +270 -0
- data/lib/forthic/grpc/client.rb +223 -0
- data/lib/forthic/grpc/errors.rb +149 -0
- data/lib/forthic/grpc/forthic_runtime_pb.rb +32 -0
- data/lib/forthic/grpc/forthic_runtime_services_pb.rb +31 -0
- data/lib/forthic/grpc/remote_module.rb +120 -0
- data/lib/forthic/grpc/remote_runtime_module.rb +148 -0
- data/lib/forthic/grpc/remote_word.rb +91 -0
- data/lib/forthic/grpc/runtime_manager.rb +60 -0
- data/lib/forthic/grpc/serializer.rb +184 -0
- data/lib/forthic/grpc/server.rb +361 -0
- data/lib/forthic/interpreter.rb +682 -133
- data/lib/forthic/literals.rb +170 -0
- data/lib/forthic/module.rb +383 -0
- data/lib/forthic/modules/standard/array_module.rb +940 -0
- data/lib/forthic/modules/standard/boolean_module.rb +176 -0
- data/lib/forthic/modules/standard/core_module.rb +362 -0
- data/lib/forthic/modules/standard/datetime_module.rb +349 -0
- data/lib/forthic/modules/standard/json_module.rb +55 -0
- data/lib/forthic/modules/standard/math_module.rb +365 -0
- data/lib/forthic/modules/standard/record_module.rb +203 -0
- data/lib/forthic/modules/standard/string_module.rb +170 -0
- data/lib/forthic/tokenizer.rb +225 -78
- data/lib/forthic/utils.rb +35 -0
- data/lib/forthic/websocket/handler.rb +548 -0
- data/lib/forthic/websocket/serializer.rb +160 -0
- data/lib/forthic/word_options.rb +141 -0
- data/lib/forthic.rb +30 -20
- data/protos/README.md +43 -0
- data/protos/v1/forthic_runtime.proto +200 -0
- metadata +76 -39
- data/.standard.yml +0 -3
- data/CHANGELOG.md +0 -5
- data/Guardfile +0 -42
- data/lib/forthic/code_location.rb +0 -20
- data/lib/forthic/forthic_error.rb +0 -51
- data/lib/forthic/forthic_module.rb +0 -145
- data/lib/forthic/global_module.rb +0 -2341
- data/lib/forthic/positioned_string.rb +0 -19
- data/lib/forthic/token.rb +0 -38
- data/lib/forthic/variable.rb +0 -34
- data/lib/forthic/version.rb +0 -5
- data/lib/forthic/words/definition_word.rb +0 -40
- data/lib/forthic/words/end_array_word.rb +0 -28
- data/lib/forthic/words/end_module_word.rb +0 -16
- data/lib/forthic/words/imported_word.rb +0 -27
- data/lib/forthic/words/map_word.rb +0 -169
- data/lib/forthic/words/module_memo_bang_at_word.rb +0 -22
- data/lib/forthic/words/module_memo_bang_word.rb +0 -21
- data/lib/forthic/words/module_memo_word.rb +0 -35
- data/lib/forthic/words/module_word.rb +0 -21
- data/lib/forthic/words/push_value_word.rb +0 -21
- data/lib/forthic/words/start_module_word.rb +0 -31
- data/lib/forthic/words/word.rb +0 -30
- data/sig/forthic.rbs +0 -4
data/lib/forthic/interpreter.rb
CHANGED
|
@@ -1,154 +1,357 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative 'forthic_error'
|
|
4
3
|
require_relative 'tokenizer'
|
|
5
|
-
require_relative '
|
|
6
|
-
require_relative '
|
|
7
|
-
require_relative '
|
|
8
|
-
|
|
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'
|
|
4
|
+
require_relative 'module'
|
|
5
|
+
require_relative 'errors'
|
|
6
|
+
require_relative 'literals'
|
|
7
|
+
require 'time'
|
|
15
8
|
|
|
16
9
|
module Forthic
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
10
|
+
# Timestamp - Struct for profiling timestamps
|
|
11
|
+
Timestamp = Struct.new(:label, :time_ms, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
# StartModuleWord - Handles module creation and switching
|
|
14
|
+
#
|
|
15
|
+
# Pushes a module onto the module stack, creating it if necessary.
|
|
16
|
+
# An empty name refers to the app module.
|
|
17
|
+
class StartModuleWord < Word
|
|
18
|
+
def execute(interp)
|
|
19
|
+
# The app module is the only module with a blank name
|
|
20
|
+
if @name == ""
|
|
21
|
+
interp.module_stack_push(interp.get_app_module)
|
|
22
|
+
return
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# If the module is used by the current module, push it onto the stack, otherwise
|
|
26
|
+
# create a new module.
|
|
27
|
+
mod = interp.cur_module.find_module(@name)
|
|
28
|
+
unless mod
|
|
29
|
+
mod = Module.new(@name)
|
|
30
|
+
interp.cur_module.register_module(mod.name, mod.name, mod)
|
|
31
|
+
|
|
32
|
+
# If we're at the app module, also register with interpreter
|
|
33
|
+
if interp.cur_module.name == ""
|
|
34
|
+
interp.register_module(mod)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
interp.module_stack_push(mod)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# EndModuleWord - Pops the current module from the module stack
|
|
42
|
+
#
|
|
43
|
+
# Completes module context and returns to the previous module.
|
|
44
|
+
class EndModuleWord < Word
|
|
45
|
+
def initialize
|
|
46
|
+
super("}")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def execute(interp)
|
|
50
|
+
interp.module_stack_pop
|
|
51
|
+
end
|
|
52
|
+
end
|
|
22
53
|
|
|
54
|
+
# EndArrayWord - Collects items from stack into an array
|
|
55
|
+
#
|
|
56
|
+
# Pops items from the stack until a START_ARRAY token is found,
|
|
57
|
+
# then pushes them as a single array in the correct order.
|
|
58
|
+
class EndArrayWord < Word
|
|
23
59
|
def initialize
|
|
24
|
-
|
|
60
|
+
super("]")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def execute(interp)
|
|
64
|
+
items = []
|
|
65
|
+
item = interp.stack_pop
|
|
66
|
+
# NOTE: This won't infinite loop because interp.stack_pop will eventually fail
|
|
67
|
+
loop do
|
|
68
|
+
break if item.is_a?(Token) && item.type == TokenType::START_ARRAY
|
|
69
|
+
items << item
|
|
70
|
+
item = interp.stack_pop
|
|
71
|
+
end
|
|
72
|
+
items.reverse!
|
|
73
|
+
interp.stack_push(items)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Stack - Wrapper for the interpreter's data stack
|
|
78
|
+
#
|
|
79
|
+
# Provides stack operations with support for array indexing.
|
|
80
|
+
# Handles PositionedString unwrapping and provides JSON serialization.
|
|
81
|
+
# Items can be accessed with bracket notation (e.g., stack[0]).
|
|
82
|
+
class Stack
|
|
83
|
+
def initialize(items = [])
|
|
84
|
+
@items = items
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Array-like access methods
|
|
88
|
+
def [](index)
|
|
89
|
+
@items[index]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def []=(index, value)
|
|
93
|
+
@items[index] = value
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def length
|
|
97
|
+
@items.length
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
alias size length
|
|
101
|
+
|
|
102
|
+
def get_items
|
|
103
|
+
@items.map do |item|
|
|
104
|
+
if item.is_a?(PositionedString)
|
|
105
|
+
item.value
|
|
106
|
+
else
|
|
107
|
+
item
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def get_raw_items
|
|
113
|
+
@items
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def set_raw_items(items)
|
|
117
|
+
@items = items
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def to_json(*args)
|
|
121
|
+
@items.to_json(*args)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def pop
|
|
125
|
+
@items.pop
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def push(item)
|
|
129
|
+
@items.push(item)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Duplicate stack with a shallow copy of items
|
|
133
|
+
def dup
|
|
134
|
+
Stack.new(@items.dup)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Interpreter - Base Forthic interpreter
|
|
139
|
+
#
|
|
140
|
+
# Core interpreter that tokenizes and executes Forthic code.
|
|
141
|
+
# Manages the data stack, module stack, and execution context.
|
|
142
|
+
#
|
|
143
|
+
# Features:
|
|
144
|
+
# - Stack-based execution model
|
|
145
|
+
# - Module system with imports and namespacing
|
|
146
|
+
# - Literal handlers for parsing values (numbers, dates, booleans, etc.)
|
|
147
|
+
# - Error handling with recovery attempts
|
|
148
|
+
# - Profiling and performance tracking
|
|
149
|
+
# - Streaming execution support
|
|
150
|
+
#
|
|
151
|
+
# Note: This is the base interpreter without standard library modules.
|
|
152
|
+
# Use StandardInterpreter for a full-featured interpreter with stdlib.
|
|
153
|
+
class Interpreter
|
|
154
|
+
attr_reader :timezone, :stack
|
|
155
|
+
attr_accessor :handle_error, :max_attempts, :on_word_execute
|
|
156
|
+
|
|
157
|
+
def initialize(modules = [], timezone = "UTC")
|
|
158
|
+
@timezone = timezone
|
|
159
|
+
@stack = Stack.new
|
|
160
|
+
|
|
161
|
+
@tokenizer_stack = []
|
|
162
|
+
@max_attempts = 3
|
|
163
|
+
@handle_error = nil
|
|
164
|
+
|
|
165
|
+
@app_module = Module.new("")
|
|
166
|
+
@app_module.set_interp(self)
|
|
167
|
+
@module_stack = [@app_module]
|
|
25
168
|
@registered_modules = {}
|
|
26
169
|
@is_compiling = false
|
|
27
|
-
@should_stop = false
|
|
28
170
|
@is_memo_definition = false
|
|
29
171
|
@cur_definition = nil
|
|
30
|
-
@screens = {}
|
|
31
|
-
@default_module_flags = {}
|
|
32
|
-
@module_flags = {}
|
|
33
|
-
@string_location = nil
|
|
34
172
|
|
|
35
|
-
|
|
36
|
-
@
|
|
37
|
-
@
|
|
173
|
+
# Debug support
|
|
174
|
+
@string_location = nil
|
|
175
|
+
@previous_token = nil
|
|
38
176
|
|
|
177
|
+
# Profiling support
|
|
39
178
|
@word_counts = {}
|
|
40
179
|
@is_profiling = false
|
|
41
180
|
@start_profile_time = nil
|
|
42
181
|
@timestamps = []
|
|
182
|
+
|
|
183
|
+
# Streaming support
|
|
184
|
+
@streaming_token_index = 0
|
|
185
|
+
@stream = false
|
|
186
|
+
@previous_delta_length = 0
|
|
187
|
+
|
|
188
|
+
# Progress callback for streaming execution
|
|
189
|
+
@on_word_execute = nil
|
|
190
|
+
@word_execution_count = 0
|
|
191
|
+
@total_words_estimate = 0
|
|
192
|
+
|
|
193
|
+
# Literal handlers
|
|
194
|
+
@literal_handlers = []
|
|
195
|
+
register_standard_literals
|
|
196
|
+
|
|
197
|
+
# If modules are provided, import them unprefixed as a convenience
|
|
198
|
+
import_modules(modules)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def get_timezone
|
|
202
|
+
@timezone
|
|
43
203
|
end
|
|
44
204
|
|
|
45
|
-
def
|
|
46
|
-
@
|
|
205
|
+
def set_timezone(timezone)
|
|
206
|
+
@timezone = timezone
|
|
47
207
|
end
|
|
48
208
|
|
|
49
|
-
# @return [ForthicModule]
|
|
50
209
|
def get_app_module
|
|
51
210
|
@app_module
|
|
52
211
|
end
|
|
53
212
|
|
|
54
|
-
|
|
213
|
+
def get_top_input_string
|
|
214
|
+
return "" if @tokenizer_stack.empty?
|
|
215
|
+
@tokenizer_stack[0].get_input_string
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def get_tokenizer
|
|
219
|
+
@tokenizer_stack.last
|
|
220
|
+
end
|
|
221
|
+
|
|
55
222
|
def get_string_location
|
|
56
223
|
@string_location
|
|
57
224
|
end
|
|
58
225
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def set_flags(module_id, flags)
|
|
62
|
-
@default_module_flags[module_id] = flags
|
|
63
|
-
@module_flags[module_id] = flags
|
|
226
|
+
def set_max_attempts(max_attempts)
|
|
227
|
+
@max_attempts = max_attempts
|
|
64
228
|
end
|
|
65
229
|
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
230
|
+
def set_error_handler(handle_error)
|
|
231
|
+
@handle_error = handle_error
|
|
73
232
|
end
|
|
74
233
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
234
|
+
def get_max_attempts
|
|
235
|
+
@max_attempts
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def get_error_handler
|
|
239
|
+
@handle_error
|
|
80
240
|
end
|
|
81
241
|
|
|
82
242
|
def reset
|
|
83
|
-
@stack =
|
|
243
|
+
@stack = Stack.new
|
|
84
244
|
@app_module.variables = {}
|
|
245
|
+
|
|
85
246
|
@module_stack = [@app_module]
|
|
86
247
|
@is_compiling = false
|
|
87
248
|
@is_memo_definition = false
|
|
88
249
|
@cur_definition = nil
|
|
250
|
+
|
|
251
|
+
# Debug support
|
|
89
252
|
@string_location = nil
|
|
90
253
|
end
|
|
91
254
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
255
|
+
def run(string, reference_location = nil)
|
|
256
|
+
@tokenizer_stack.push(Tokenizer.new(string, reference_location))
|
|
257
|
+
|
|
258
|
+
if @handle_error
|
|
259
|
+
execute_with_recovery
|
|
260
|
+
else
|
|
261
|
+
run_with_tokenizer(@tokenizer_stack.last)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
@tokenizer_stack.pop
|
|
265
|
+
true
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
def execute_with_recovery(num_attempts = 0)
|
|
269
|
+
num_attempts += 1
|
|
270
|
+
if num_attempts > @max_attempts
|
|
271
|
+
raise TooManyAttemptsError.new(
|
|
272
|
+
get_top_input_string,
|
|
273
|
+
num_attempts,
|
|
274
|
+
@max_attempts
|
|
275
|
+
)
|
|
276
|
+
end
|
|
277
|
+
continue_execution
|
|
278
|
+
num_attempts
|
|
279
|
+
rescue => e
|
|
280
|
+
raise unless @handle_error
|
|
281
|
+
@handle_error.call(e, self)
|
|
282
|
+
execute_with_recovery(num_attempts)
|
|
98
283
|
end
|
|
99
284
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
# @return [Boolean]
|
|
103
|
-
def run(string, reference_location = nil)
|
|
104
|
-
tokenizer = Tokenizer.new(string, reference_location)
|
|
105
|
-
run_with_tokenizer(tokenizer)
|
|
285
|
+
def continue_execution
|
|
286
|
+
run_with_tokenizer(@tokenizer_stack.last)
|
|
106
287
|
end
|
|
107
288
|
|
|
108
|
-
# @param [Tokenizer] tokenizer
|
|
109
|
-
# @return [Boolean]
|
|
110
289
|
def run_with_tokenizer(tokenizer)
|
|
111
290
|
token = nil
|
|
112
291
|
loop do
|
|
292
|
+
@previous_token = token
|
|
113
293
|
token = tokenizer.next_token
|
|
114
294
|
handle_token(token)
|
|
115
|
-
break if token.type == TokenType::EOS
|
|
116
|
-
next if [TokenType::START_DEF, TokenType::END_DEF, TokenType::COMMENT].include?(token.type) || @is_compiling
|
|
295
|
+
break if token.type == TokenType::EOS
|
|
117
296
|
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
|
|
297
|
+
true # Done executing
|
|
123
298
|
end
|
|
124
299
|
|
|
125
|
-
# @return [ForthicModule]
|
|
126
300
|
def cur_module
|
|
127
301
|
@module_stack.last
|
|
128
302
|
end
|
|
129
303
|
|
|
130
|
-
# @param [String] name
|
|
131
|
-
# @return [ForthicModule]
|
|
132
304
|
def find_module(name)
|
|
133
305
|
result = @registered_modules[name]
|
|
134
|
-
|
|
306
|
+
unless result
|
|
307
|
+
raise UnknownModuleError.new(
|
|
308
|
+
get_top_input_string,
|
|
309
|
+
name,
|
|
310
|
+
location: @string_location
|
|
311
|
+
)
|
|
312
|
+
end
|
|
313
|
+
result
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def stack_peek
|
|
317
|
+
top = @stack[@stack.length - 1]
|
|
318
|
+
result = top
|
|
319
|
+
result = top.value if top.is_a?(PositionedString)
|
|
135
320
|
result
|
|
136
321
|
end
|
|
137
322
|
|
|
138
|
-
# @param [Object] val
|
|
139
323
|
def stack_push(val)
|
|
140
324
|
@stack.push(val)
|
|
141
325
|
end
|
|
142
326
|
|
|
143
|
-
# @return [Object]
|
|
144
327
|
def stack_pop
|
|
145
|
-
|
|
328
|
+
if @stack.length.zero?
|
|
329
|
+
tokenizer = @tokenizer_stack.length > 0 ? get_tokenizer : nil
|
|
330
|
+
raise StackUnderflowError.new(
|
|
331
|
+
get_top_input_string,
|
|
332
|
+
location: tokenizer&.get_token_location
|
|
333
|
+
)
|
|
334
|
+
end
|
|
146
335
|
result = @stack.pop
|
|
147
|
-
|
|
148
|
-
|
|
336
|
+
|
|
337
|
+
# If we have a PositionedString, we need to record the location
|
|
338
|
+
@string_location = nil
|
|
339
|
+
if result.is_a?(PositionedString)
|
|
340
|
+
positioned_string = result
|
|
341
|
+
result = positioned_string.value
|
|
342
|
+
@string_location = positioned_string.location
|
|
343
|
+
end
|
|
344
|
+
result
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def get_stack
|
|
348
|
+
@stack
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def set_stack(stack)
|
|
352
|
+
@stack = stack
|
|
149
353
|
end
|
|
150
354
|
|
|
151
|
-
# @param [ForthicModule] mod
|
|
152
355
|
def module_stack_push(mod)
|
|
153
356
|
@module_stack.push(mod)
|
|
154
357
|
end
|
|
@@ -157,47 +360,147 @@ module Forthic
|
|
|
157
360
|
@module_stack.pop
|
|
158
361
|
end
|
|
159
362
|
|
|
160
|
-
# @param [ForthicModule] mod
|
|
161
363
|
def register_module(mod)
|
|
162
364
|
@registered_modules[mod.name] = mod
|
|
365
|
+
mod.set_interp(self)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# If names is an array of strings, import each module without a prefix (empty string)
|
|
369
|
+
# If names is an array of arrays, import each module using the first element as the
|
|
370
|
+
# module name and the second element as the prefix
|
|
371
|
+
def use_modules(names)
|
|
372
|
+
names.each do |name|
|
|
373
|
+
module_name = name
|
|
374
|
+
prefix = "" # Default to empty prefix (no prefix)
|
|
375
|
+
if name.is_a?(Array)
|
|
376
|
+
module_name = name[0]
|
|
377
|
+
prefix = name[1] # Allow explicit prefix specification
|
|
378
|
+
end
|
|
379
|
+
mod = find_module(module_name)
|
|
380
|
+
get_app_module.import_module(prefix, mod, self)
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
# A convenience method to register and use a module
|
|
385
|
+
def import_module(mod, prefix = "")
|
|
386
|
+
register_module(mod)
|
|
387
|
+
use_modules([[mod.name, prefix]])
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def import_modules(modules)
|
|
391
|
+
modules.each do |mod|
|
|
392
|
+
import_module(mod)
|
|
393
|
+
end
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Transforms simple module names to unprefixed imports: "math" -> ["math", ""]
|
|
397
|
+
# Preserves explicit prefix specifications: ["math", "m"] -> ["math", "m"]
|
|
398
|
+
def use_modules_unprefixed(names)
|
|
399
|
+
unprefixed = names.map do |name|
|
|
400
|
+
name.is_a?(Array) ? name : [name, ""]
|
|
401
|
+
end
|
|
402
|
+
use_modules(unprefixed)
|
|
163
403
|
end
|
|
164
404
|
|
|
165
|
-
# @param [ForthicModule] mod
|
|
166
405
|
def run_module_code(mod)
|
|
167
406
|
module_stack_push(mod)
|
|
168
|
-
|
|
407
|
+
begin
|
|
408
|
+
# Set source to module name when running module code
|
|
409
|
+
module_location = CodeLocation.new(source: mod.name)
|
|
410
|
+
run(mod.forthic_code, module_location)
|
|
411
|
+
rescue => e
|
|
412
|
+
raise ModuleError.new(
|
|
413
|
+
get_top_input_string,
|
|
414
|
+
mod.name,
|
|
415
|
+
e,
|
|
416
|
+
location: @string_location,
|
|
417
|
+
cause: e
|
|
418
|
+
)
|
|
419
|
+
end
|
|
420
|
+
|
|
169
421
|
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
422
|
end
|
|
175
423
|
|
|
176
|
-
#
|
|
177
|
-
#
|
|
424
|
+
# ======================
|
|
425
|
+
# Literal Handlers
|
|
426
|
+
|
|
427
|
+
# Register standard literal handlers
|
|
428
|
+
# Order matters: more specific handlers first
|
|
429
|
+
def register_standard_literals
|
|
430
|
+
@literal_handlers = [
|
|
431
|
+
method(:to_bool).to_proc, # TRUE, FALSE
|
|
432
|
+
method(:to_float).to_proc, # 3.14
|
|
433
|
+
Forthic.to_zoned_datetime(@timezone), # 2020-06-05T10:15:00Z
|
|
434
|
+
Forthic.to_literal_date(@timezone), # 2020-06-05, YYYY-MM-DD
|
|
435
|
+
method(:to_time).to_proc, # 9:00, 11:30 PM
|
|
436
|
+
method(:to_int).to_proc, # 42
|
|
437
|
+
]
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# Register a custom literal handler
|
|
441
|
+
# Handlers are checked in registration order
|
|
442
|
+
def register_literal_handler(handler)
|
|
443
|
+
@literal_handlers << handler
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
# Unregister a literal handler
|
|
447
|
+
def unregister_literal_handler(handler)
|
|
448
|
+
@literal_handlers.delete(handler)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# Try to parse string as a literal value
|
|
452
|
+
# Returns PushValueWord if successful, nil otherwise
|
|
453
|
+
def find_literal_word(name)
|
|
454
|
+
@literal_handlers.each do |handler|
|
|
455
|
+
value = handler.call(name)
|
|
456
|
+
return PushValueWord.new(name, value) unless value.nil?
|
|
457
|
+
end
|
|
458
|
+
nil
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
# ======================
|
|
462
|
+
# Find Word
|
|
463
|
+
|
|
178
464
|
def find_word(name)
|
|
465
|
+
# 1. Check module stack (dictionary words + variables)
|
|
179
466
|
result = nil
|
|
180
|
-
@module_stack.
|
|
467
|
+
(@module_stack.length - 1).downto(0) do |i|
|
|
468
|
+
m = @module_stack[i]
|
|
181
469
|
result = m.find_word(name)
|
|
182
470
|
break if result
|
|
183
471
|
end
|
|
184
|
-
|
|
472
|
+
|
|
473
|
+
# 2. Check literal handlers as fallback
|
|
474
|
+
result = find_literal_word(name) unless result
|
|
475
|
+
|
|
476
|
+
# 3. Throw error if still not found
|
|
477
|
+
unless result
|
|
478
|
+
raise UnknownWordError.new(
|
|
479
|
+
get_top_input_string,
|
|
480
|
+
name,
|
|
481
|
+
location: get_string_location
|
|
482
|
+
)
|
|
483
|
+
end
|
|
484
|
+
|
|
185
485
|
result
|
|
186
486
|
end
|
|
187
487
|
|
|
488
|
+
# ======================
|
|
489
|
+
# Profiling
|
|
490
|
+
|
|
188
491
|
def start_profiling
|
|
189
492
|
@is_profiling = true
|
|
190
493
|
@timestamps = []
|
|
191
|
-
@start_profile_time = Time.now
|
|
494
|
+
@start_profile_time = Time.now.to_f * 1000 # milliseconds
|
|
192
495
|
add_timestamp("START")
|
|
193
496
|
@word_counts = {}
|
|
194
497
|
end
|
|
195
498
|
|
|
196
|
-
# @param [Word] word
|
|
197
499
|
def count_word(word)
|
|
198
500
|
return unless @is_profiling
|
|
199
|
-
|
|
200
|
-
@word_counts[
|
|
501
|
+
name = word.name
|
|
502
|
+
@word_counts[name] ||= 0
|
|
503
|
+
@word_counts[name] += 1
|
|
201
504
|
end
|
|
202
505
|
|
|
203
506
|
def stop_profiling
|
|
@@ -205,99 +508,141 @@ module Forthic
|
|
|
205
508
|
@is_profiling = false
|
|
206
509
|
end
|
|
207
510
|
|
|
208
|
-
# @param [String] label
|
|
209
511
|
def add_timestamp(label)
|
|
210
512
|
return unless @is_profiling
|
|
211
|
-
timestamp =
|
|
212
|
-
|
|
513
|
+
timestamp = Timestamp.new(
|
|
514
|
+
label: label,
|
|
515
|
+
time_ms: (Time.now.to_f * 1000) - @start_profile_time
|
|
516
|
+
)
|
|
517
|
+
@timestamps << timestamp
|
|
213
518
|
end
|
|
214
519
|
|
|
215
|
-
# @return [Array<Hash>]
|
|
216
520
|
def word_histogram
|
|
217
|
-
|
|
521
|
+
items = []
|
|
522
|
+
@word_counts.each do |name, count|
|
|
523
|
+
items << { word: name, count: count }
|
|
524
|
+
end
|
|
525
|
+
items.sort_by { |item| -item[:count] }
|
|
218
526
|
end
|
|
219
527
|
|
|
220
|
-
# @return [Array<Hash>]
|
|
221
528
|
def profile_timestamps
|
|
222
529
|
@timestamps
|
|
223
530
|
end
|
|
224
531
|
|
|
225
|
-
#
|
|
532
|
+
# ======================
|
|
533
|
+
# Handle tokens
|
|
534
|
+
|
|
226
535
|
def handle_token(token)
|
|
227
536
|
case token.type
|
|
228
|
-
when TokenType::STRING
|
|
229
|
-
|
|
230
|
-
when TokenType::
|
|
231
|
-
|
|
232
|
-
when TokenType::
|
|
233
|
-
|
|
234
|
-
when TokenType::
|
|
235
|
-
|
|
236
|
-
when TokenType::
|
|
237
|
-
|
|
238
|
-
when TokenType::
|
|
537
|
+
when TokenType::STRING
|
|
538
|
+
handle_string_token(token)
|
|
539
|
+
when TokenType::COMMENT
|
|
540
|
+
handle_comment_token(token)
|
|
541
|
+
when TokenType::START_ARRAY
|
|
542
|
+
handle_start_array_token(token)
|
|
543
|
+
when TokenType::END_ARRAY
|
|
544
|
+
handle_end_array_token(token)
|
|
545
|
+
when TokenType::START_MODULE
|
|
546
|
+
handle_start_module_token(token)
|
|
547
|
+
when TokenType::END_MODULE
|
|
548
|
+
handle_end_module_token(token)
|
|
549
|
+
when TokenType::START_DEF
|
|
550
|
+
handle_start_definition_token(token)
|
|
551
|
+
when TokenType::START_MEMO
|
|
552
|
+
handle_start_memo_token(token)
|
|
553
|
+
when TokenType::END_DEF
|
|
554
|
+
handle_end_definition_token(token)
|
|
555
|
+
when TokenType::DOT_SYMBOL
|
|
556
|
+
handle_dot_symbol_token(token)
|
|
557
|
+
when TokenType::WORD
|
|
558
|
+
handle_word_token(token)
|
|
559
|
+
when TokenType::EOS
|
|
560
|
+
if @is_compiling
|
|
561
|
+
raise MissingSemicolonError.new(
|
|
562
|
+
get_top_input_string,
|
|
563
|
+
location: @previous_token&.location
|
|
564
|
+
)
|
|
565
|
+
end
|
|
239
566
|
else
|
|
240
|
-
raise
|
|
567
|
+
raise UnknownTokenError.new(
|
|
568
|
+
get_top_input_string,
|
|
569
|
+
token.string,
|
|
570
|
+
location: @string_location
|
|
571
|
+
)
|
|
241
572
|
end
|
|
242
573
|
end
|
|
243
574
|
|
|
244
|
-
# @param [Token] token
|
|
245
575
|
def handle_string_token(token)
|
|
246
576
|
value = PositionedString.new(token.string, token.location)
|
|
247
577
|
handle_word(PushValueWord.new("<string>", value))
|
|
248
578
|
end
|
|
249
579
|
|
|
250
|
-
|
|
580
|
+
def handle_dot_symbol_token(token)
|
|
581
|
+
value = PositionedString.new(token.string, token.location)
|
|
582
|
+
handle_word(PushValueWord.new("<dot-symbol>", value))
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
# Start/end module tokens are treated as IMMEDIATE words *and* are also compiled
|
|
251
586
|
def handle_start_module_token(token)
|
|
252
587
|
word = StartModuleWord.new(token.string)
|
|
588
|
+
|
|
253
589
|
@cur_definition.add_word(word) if @is_compiling
|
|
254
|
-
count_word(word)
|
|
590
|
+
count_word(word) # For profiling
|
|
255
591
|
word.execute(self)
|
|
256
592
|
end
|
|
257
593
|
|
|
258
|
-
# @param [Token] _token
|
|
259
594
|
def handle_end_module_token(_token)
|
|
260
595
|
word = EndModuleWord.new
|
|
596
|
+
|
|
261
597
|
@cur_definition.add_word(word) if @is_compiling
|
|
262
598
|
count_word(word)
|
|
263
599
|
word.execute(self)
|
|
264
600
|
end
|
|
265
601
|
|
|
266
|
-
# @param [Token] token
|
|
267
602
|
def handle_start_array_token(token)
|
|
268
603
|
handle_word(PushValueWord.new("<start_array_token>", token))
|
|
269
604
|
end
|
|
270
605
|
|
|
271
|
-
# @param [Token] _token
|
|
272
606
|
def handle_end_array_token(_token)
|
|
273
607
|
handle_word(EndArrayWord.new)
|
|
274
608
|
end
|
|
275
609
|
|
|
276
|
-
# @param [Token] _token
|
|
277
610
|
def handle_comment_token(_token)
|
|
278
|
-
#
|
|
611
|
+
# Comment handling (currently no-op)
|
|
279
612
|
end
|
|
280
613
|
|
|
281
|
-
# @param [Token] token
|
|
282
614
|
def handle_start_definition_token(token)
|
|
283
|
-
|
|
615
|
+
if @is_compiling
|
|
616
|
+
raise MissingSemicolonError.new(
|
|
617
|
+
get_top_input_string,
|
|
618
|
+
location: @previous_token&.location
|
|
619
|
+
)
|
|
620
|
+
end
|
|
284
621
|
@cur_definition = DefinitionWord.new(token.string)
|
|
285
622
|
@is_compiling = true
|
|
286
623
|
@is_memo_definition = false
|
|
287
624
|
end
|
|
288
625
|
|
|
289
|
-
# @param [Token] token
|
|
290
626
|
def handle_start_memo_token(token)
|
|
291
|
-
|
|
627
|
+
if @is_compiling
|
|
628
|
+
raise MissingSemicolonError.new(
|
|
629
|
+
get_top_input_string,
|
|
630
|
+
location: @previous_token&.location
|
|
631
|
+
)
|
|
632
|
+
end
|
|
292
633
|
@cur_definition = DefinitionWord.new(token.string)
|
|
293
634
|
@is_compiling = true
|
|
294
635
|
@is_memo_definition = true
|
|
295
636
|
end
|
|
296
637
|
|
|
297
|
-
# @param [Token] token
|
|
298
638
|
def handle_end_definition_token(token)
|
|
299
|
-
|
|
300
|
-
|
|
639
|
+
unless @is_compiling && @cur_definition
|
|
640
|
+
raise ExtraSemicolonError.new(
|
|
641
|
+
get_top_input_string,
|
|
642
|
+
location: token.location
|
|
643
|
+
)
|
|
644
|
+
end
|
|
645
|
+
|
|
301
646
|
if @is_memo_definition
|
|
302
647
|
cur_module.add_memo_words(@cur_definition)
|
|
303
648
|
else
|
|
@@ -306,23 +651,227 @@ module Forthic
|
|
|
306
651
|
@is_compiling = false
|
|
307
652
|
end
|
|
308
653
|
|
|
309
|
-
# @param [Token] token
|
|
310
654
|
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
|
|
655
|
+
word = find_word(token.string) # Throws UnknownWordError if not found
|
|
313
656
|
handle_word(word, token.location)
|
|
314
657
|
end
|
|
315
658
|
|
|
316
|
-
# @param [Word] word
|
|
317
|
-
# @param [CodeLocation, nil] location
|
|
318
659
|
def handle_word(word, location = nil)
|
|
319
660
|
if @is_compiling
|
|
320
661
|
word.set_location(location)
|
|
321
662
|
@cur_definition.add_word(word)
|
|
322
663
|
else
|
|
323
664
|
count_word(word)
|
|
665
|
+
|
|
666
|
+
# Notify progress callback if set (for streaming execution)
|
|
667
|
+
if @on_word_execute && word.is_a?(DefinitionWord)
|
|
668
|
+
@word_execution_count += 1
|
|
669
|
+
@on_word_execute.call(word.name, @word_execution_count, @total_words_estimate)
|
|
670
|
+
end
|
|
671
|
+
|
|
324
672
|
word.execute(self)
|
|
325
673
|
end
|
|
326
674
|
end
|
|
675
|
+
|
|
676
|
+
# Estimate the number of words in code (for progress tracking)
|
|
677
|
+
def estimate_word_count(code)
|
|
678
|
+
# Simple heuristic: count uppercase words (likely Forthic words)
|
|
679
|
+
code.scan(/[A-Z][A-Z0-9_-]*/).length
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# Set up progress tracking for streaming execution
|
|
683
|
+
def setup_progress_tracking(code)
|
|
684
|
+
@word_execution_count = 0
|
|
685
|
+
@total_words_estimate = estimate_word_count(code)
|
|
686
|
+
end
|
|
687
|
+
|
|
688
|
+
# Reset progress tracking
|
|
689
|
+
def reset_progress_tracking
|
|
690
|
+
@word_execution_count = 0
|
|
691
|
+
@total_words_estimate = 0
|
|
692
|
+
@on_word_execute = nil
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
# Streaming execution support
|
|
696
|
+
def streaming_run(code_stream, done, reference_location = nil)
|
|
697
|
+
# Create a new Tokenizer for the full string
|
|
698
|
+
tokenizer = Tokenizer.new(code_stream, reference_location, done ? false : true)
|
|
699
|
+
tokens = []
|
|
700
|
+
eos_found = false
|
|
701
|
+
|
|
702
|
+
@tokenizer_stack.push(tokenizer)
|
|
703
|
+
|
|
704
|
+
# Gather tokens from the beginning
|
|
705
|
+
loop do
|
|
706
|
+
token = tokenizer.next_token
|
|
707
|
+
break unless token
|
|
708
|
+
|
|
709
|
+
# If we hit an EOS token then push it and break
|
|
710
|
+
if token.type == TokenType::EOS
|
|
711
|
+
tokens << token
|
|
712
|
+
eos_found = true
|
|
713
|
+
break
|
|
714
|
+
end
|
|
715
|
+
|
|
716
|
+
tokens << token
|
|
717
|
+
end
|
|
718
|
+
|
|
719
|
+
delta = eos_found ? nil : tokenizer.get_string_delta
|
|
720
|
+
|
|
721
|
+
new_stop = find_last_word_or_eos(tokens)
|
|
722
|
+
|
|
723
|
+
new_stop -= 1 if eos_found && !done
|
|
724
|
+
new_stop += 1 if !eos_found && !done
|
|
725
|
+
|
|
726
|
+
# Execute only tokens we have not executed previously
|
|
727
|
+
(@streaming_token_index...new_stop).each do |i|
|
|
728
|
+
token = tokens[i]
|
|
729
|
+
next unless token
|
|
730
|
+
|
|
731
|
+
handle_token(token)
|
|
732
|
+
|
|
733
|
+
if @stream && (token.type != TokenType::WORD || token.string != "START-LOG")
|
|
734
|
+
yield token.string
|
|
735
|
+
end
|
|
736
|
+
@previous_token = token
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
# Done with this tokenizer
|
|
740
|
+
@tokenizer_stack.pop
|
|
741
|
+
|
|
742
|
+
if @stream && !eos_found && delta
|
|
743
|
+
# Yield string delta if we're streaming and tokenizer has a delta
|
|
744
|
+
new_portion = delta[@previous_delta_length..-1]
|
|
745
|
+
|
|
746
|
+
yield({ string_delta: new_portion }) if new_portion && !new_portion.empty?
|
|
747
|
+
@previous_delta_length = delta.length
|
|
748
|
+
end
|
|
749
|
+
|
|
750
|
+
if done
|
|
751
|
+
end_stream
|
|
752
|
+
return
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# Update our pointer and reset if done
|
|
756
|
+
@streaming_token_index = new_stop
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def start_stream
|
|
760
|
+
@stream = true
|
|
761
|
+
@previous_delta_length = 0
|
|
762
|
+
@streaming_token_index = 0
|
|
763
|
+
end
|
|
764
|
+
|
|
765
|
+
def end_stream
|
|
766
|
+
@stream = false
|
|
767
|
+
@previous_delta_length = 0
|
|
768
|
+
@streaming_token_index = 0
|
|
769
|
+
end
|
|
770
|
+
|
|
771
|
+
private
|
|
772
|
+
|
|
773
|
+
def find_last_word_or_eos(tokens)
|
|
774
|
+
tokens.rindex { |token| token.type == TokenType::WORD || token.type == TokenType::EOS } || -1
|
|
775
|
+
end
|
|
776
|
+
|
|
777
|
+
# Wrapper methods for literal handlers to match expected interface
|
|
778
|
+
def to_bool(str)
|
|
779
|
+
Forthic.to_bool(str)
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def to_float(str)
|
|
783
|
+
Forthic.to_float(str)
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def to_int(str)
|
|
787
|
+
Forthic.to_int(str)
|
|
788
|
+
end
|
|
789
|
+
|
|
790
|
+
def to_time(str)
|
|
791
|
+
Forthic.to_time(str)
|
|
792
|
+
end
|
|
793
|
+
end
|
|
794
|
+
|
|
795
|
+
# Duplicate an interpreter
|
|
796
|
+
#
|
|
797
|
+
# @param interp [Interpreter] The interpreter to duplicate
|
|
798
|
+
# @return [Interpreter] A new interpreter with copied state
|
|
799
|
+
def self.dup_interpreter(interp)
|
|
800
|
+
# Create new interpreter of the same type as the source
|
|
801
|
+
result_interp = interp.class.new([], interp.get_timezone)
|
|
802
|
+
|
|
803
|
+
# Use copy() instead of dup() to preserve module_prefixes
|
|
804
|
+
result_interp.instance_variable_set(:@app_module, interp.instance_variable_get(:@app_module).copy(result_interp))
|
|
805
|
+
result_interp.instance_variable_set(:@module_stack, [result_interp.instance_variable_get(:@app_module)])
|
|
806
|
+
|
|
807
|
+
# Use Stack.dup() method
|
|
808
|
+
result_interp.instance_variable_set(:@stack, interp.instance_variable_get(:@stack).dup)
|
|
809
|
+
|
|
810
|
+
# Share registered modules reference (modules are shared, not copied)
|
|
811
|
+
result_interp.instance_variable_set(:@registered_modules, interp.instance_variable_get(:@registered_modules))
|
|
812
|
+
|
|
813
|
+
# Copy error handler if present
|
|
814
|
+
handle_error = interp.instance_variable_get(:@handle_error)
|
|
815
|
+
result_interp.instance_variable_set(:@handle_error, handle_error) if handle_error
|
|
816
|
+
|
|
817
|
+
result_interp
|
|
818
|
+
end
|
|
819
|
+
|
|
820
|
+
# StandardInterpreter - Full-featured interpreter with standard library
|
|
821
|
+
#
|
|
822
|
+
# Extends Interpreter and automatically imports standard modules:
|
|
823
|
+
# - CoreModule: Stack operations, variables, module system, control flow
|
|
824
|
+
# - ArrayModule: Array/collection operations
|
|
825
|
+
# - RecordModule: Record/hash operations
|
|
826
|
+
# - StringModule: String operations
|
|
827
|
+
# - MathModule: Mathematical operations
|
|
828
|
+
# - BooleanModule: Boolean logic
|
|
829
|
+
# - JsonModule: JSON operations
|
|
830
|
+
# - DateTimeModule: DateTime operations
|
|
831
|
+
#
|
|
832
|
+
# For most use cases, use this class. Use Interpreter if you need
|
|
833
|
+
# full control over which modules are loaded.
|
|
834
|
+
class StandardInterpreter < Interpreter
|
|
835
|
+
def initialize(modules = [], timezone = "UTC")
|
|
836
|
+
# Don't pass modules to super - we'll import them after stdlib
|
|
837
|
+
super([], timezone)
|
|
838
|
+
|
|
839
|
+
# Import standard library modules FIRST (checked last during lookup)
|
|
840
|
+
# This allows user modules to shadow stdlib words
|
|
841
|
+
import_standard_library
|
|
842
|
+
|
|
843
|
+
# Import user modules AFTER stdlib (checked first during lookup)
|
|
844
|
+
import_modules(modules)
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
private
|
|
848
|
+
|
|
849
|
+
def import_standard_library
|
|
850
|
+
require_relative 'modules/standard/core_module'
|
|
851
|
+
require_relative 'modules/standard/array_module'
|
|
852
|
+
require_relative 'modules/standard/record_module'
|
|
853
|
+
require_relative 'modules/standard/string_module'
|
|
854
|
+
require_relative 'modules/standard/math_module'
|
|
855
|
+
require_relative 'modules/standard/boolean_module'
|
|
856
|
+
require_relative 'modules/standard/json_module'
|
|
857
|
+
require_relative 'modules/standard/datetime_module'
|
|
858
|
+
|
|
859
|
+
stdlib = [
|
|
860
|
+
Modules::CoreModule.new,
|
|
861
|
+
Modules::ArrayModule.new,
|
|
862
|
+
Modules::RecordModule.new,
|
|
863
|
+
Modules::StringModule.new,
|
|
864
|
+
Modules::MathModule.new,
|
|
865
|
+
Modules::BooleanModule.new,
|
|
866
|
+
Modules::JsonModule.new,
|
|
867
|
+
Modules::DateTimeModule.new,
|
|
868
|
+
]
|
|
869
|
+
|
|
870
|
+
# Import unprefixed at the BOTTOM of module stack
|
|
871
|
+
# This ensures they're checked LAST during find_word()
|
|
872
|
+
stdlib.each do |mod|
|
|
873
|
+
import_module(mod, "")
|
|
874
|
+
end
|
|
875
|
+
end
|
|
327
876
|
end
|
|
328
|
-
end
|
|
877
|
+
end
|