forthic 0.2.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 +36 -7
- 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 +694 -245
- 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 +224 -77
- 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 +72 -39
- data/.standard.yml +0 -3
- data/CHANGELOG.md +0 -11
- data/CLAUDE.md +0 -74
- data/Guardfile +0 -42
- data/lib/forthic/code_location.rb +0 -20
- data/lib/forthic/forthic_error.rb +0 -50
- data/lib/forthic/forthic_module.rb +0 -146
- data/lib/forthic/global_module.rb +0 -2328
- data/lib/forthic/positioned_string.rb +0 -19
- data/lib/forthic/token.rb +0 -37
- data/lib/forthic/variable.rb +0 -34
- data/lib/forthic/version.rb +0 -5
- data/lib/forthic/words/definition_word.rb +0 -38
- 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,428 +1,877 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
5
|
-
require_relative
|
|
6
|
-
require_relative
|
|
7
|
-
|
|
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"
|
|
3
|
+
require_relative 'tokenizer'
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
30
39
|
end
|
|
31
40
|
|
|
32
|
-
#
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
36
48
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
@module_stack = [app_module]
|
|
40
|
-
@is_compiling = false
|
|
41
|
-
@should_stop = false
|
|
42
|
-
@is_memo_definition = false
|
|
43
|
-
@cur_definition = nil
|
|
44
|
-
@string_location = nil
|
|
49
|
+
def execute(interp)
|
|
50
|
+
interp.module_stack_pop
|
|
45
51
|
end
|
|
52
|
+
end
|
|
46
53
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
59
|
+
def initialize
|
|
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)
|
|
54
74
|
end
|
|
55
75
|
end
|
|
56
76
|
|
|
57
|
-
#
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
60
86
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
@
|
|
64
|
-
@start_profile_time = nil
|
|
65
|
-
@timestamps = []
|
|
87
|
+
# Array-like access methods
|
|
88
|
+
def [](index)
|
|
89
|
+
@items[index]
|
|
66
90
|
end
|
|
67
91
|
|
|
68
|
-
def
|
|
69
|
-
@
|
|
70
|
-
@timestamps = []
|
|
71
|
-
@start_profile_time = Time.now
|
|
72
|
-
add_timestamp("START")
|
|
73
|
-
@word_counts = {}
|
|
92
|
+
def []=(index, value)
|
|
93
|
+
@items[index] = value
|
|
74
94
|
end
|
|
75
95
|
|
|
76
|
-
def
|
|
77
|
-
|
|
78
|
-
@is_profiling = false
|
|
96
|
+
def length
|
|
97
|
+
@items.length
|
|
79
98
|
end
|
|
80
99
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
@
|
|
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
|
|
85
110
|
end
|
|
86
111
|
|
|
87
|
-
def
|
|
88
|
-
|
|
89
|
-
timestamp = {label: label, time_ms: (Time.now - @start_profile_time) * 1000}
|
|
90
|
-
@timestamps.push(timestamp)
|
|
112
|
+
def get_raw_items
|
|
113
|
+
@items
|
|
91
114
|
end
|
|
92
115
|
|
|
93
|
-
def
|
|
94
|
-
@
|
|
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)
|
|
95
135
|
end
|
|
96
136
|
end
|
|
97
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.
|
|
98
153
|
class Interpreter
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
# Screen and module management
|
|
102
|
-
attr_accessor :screens, :default_module_flags, :module_flags
|
|
103
|
-
# State objects
|
|
104
|
-
attr_reader :execution_state, :profiling_state
|
|
105
|
-
|
|
106
|
-
# Token handler lookup table
|
|
107
|
-
TOKEN_HANDLERS = {
|
|
108
|
-
TokenType::STRING => :handle_string_token,
|
|
109
|
-
TokenType::COMMENT => :handle_comment_token,
|
|
110
|
-
TokenType::START_ARRAY => :handle_start_array_token,
|
|
111
|
-
TokenType::END_ARRAY => :handle_end_array_token,
|
|
112
|
-
TokenType::START_MODULE => :handle_start_module_token,
|
|
113
|
-
TokenType::END_MODULE => :handle_end_module_token,
|
|
114
|
-
TokenType::START_DEF => :handle_start_definition_token,
|
|
115
|
-
TokenType::START_MEMO => :handle_start_memo_token,
|
|
116
|
-
TokenType::END_DEF => :handle_end_definition_token,
|
|
117
|
-
TokenType::WORD => :handle_word_token
|
|
118
|
-
}.freeze
|
|
154
|
+
attr_reader :timezone, :stack
|
|
155
|
+
attr_accessor :handle_error, :max_attempts, :on_word_execute
|
|
119
156
|
|
|
120
|
-
def initialize
|
|
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]
|
|
121
168
|
@registered_modules = {}
|
|
122
|
-
@
|
|
123
|
-
@
|
|
124
|
-
@
|
|
169
|
+
@is_compiling = false
|
|
170
|
+
@is_memo_definition = false
|
|
171
|
+
@cur_definition = nil
|
|
125
172
|
|
|
126
|
-
|
|
127
|
-
@
|
|
173
|
+
# Debug support
|
|
174
|
+
@string_location = nil
|
|
175
|
+
@previous_token = nil
|
|
128
176
|
|
|
129
|
-
|
|
130
|
-
@
|
|
131
|
-
|
|
177
|
+
# Profiling support
|
|
178
|
+
@word_counts = {}
|
|
179
|
+
@is_profiling = false
|
|
180
|
+
@start_profile_time = nil
|
|
181
|
+
@timestamps = []
|
|
132
182
|
|
|
133
|
-
|
|
134
|
-
@
|
|
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)
|
|
135
199
|
end
|
|
136
200
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
@app_module
|
|
201
|
+
def get_timezone
|
|
202
|
+
@timezone
|
|
140
203
|
end
|
|
141
204
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
@execution_state.string_location
|
|
205
|
+
def set_timezone(timezone)
|
|
206
|
+
@timezone = timezone
|
|
145
207
|
end
|
|
146
208
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
@execution_state.stack
|
|
209
|
+
def get_app_module
|
|
210
|
+
@app_module
|
|
150
211
|
end
|
|
151
212
|
|
|
152
|
-
def
|
|
153
|
-
@
|
|
213
|
+
def get_top_input_string
|
|
214
|
+
return "" if @tokenizer_stack.empty?
|
|
215
|
+
@tokenizer_stack[0].get_input_string
|
|
154
216
|
end
|
|
155
217
|
|
|
156
|
-
def
|
|
157
|
-
@
|
|
218
|
+
def get_tokenizer
|
|
219
|
+
@tokenizer_stack.last
|
|
158
220
|
end
|
|
159
221
|
|
|
160
|
-
def
|
|
161
|
-
@
|
|
222
|
+
def get_string_location
|
|
223
|
+
@string_location
|
|
162
224
|
end
|
|
163
225
|
|
|
164
|
-
def
|
|
165
|
-
@
|
|
226
|
+
def set_max_attempts(max_attempts)
|
|
227
|
+
@max_attempts = max_attempts
|
|
166
228
|
end
|
|
167
229
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
def set_flags(module_id, flags)
|
|
171
|
-
@default_module_flags[module_id] = flags
|
|
172
|
-
@module_flags[module_id] = flags
|
|
230
|
+
def set_error_handler(handle_error)
|
|
231
|
+
@handle_error = handle_error
|
|
173
232
|
end
|
|
174
233
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def get_flags(module_id)
|
|
178
|
-
module_flags = @module_flags[module_id] || {}
|
|
179
|
-
result = module_flags.dup
|
|
180
|
-
@module_flags[module_id] = @default_module_flags[module_id].dup
|
|
181
|
-
result
|
|
234
|
+
def get_max_attempts
|
|
235
|
+
@max_attempts
|
|
182
236
|
end
|
|
183
237
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
def modify_flags(module_id, flags)
|
|
187
|
-
module_flags = @module_flags[module_id] || {}
|
|
188
|
-
@module_flags[module_id] = module_flags.merge(flags)
|
|
238
|
+
def get_error_handler
|
|
239
|
+
@handle_error
|
|
189
240
|
end
|
|
190
241
|
|
|
191
242
|
def reset
|
|
243
|
+
@stack = Stack.new
|
|
192
244
|
@app_module.variables = {}
|
|
193
|
-
@execution_state.reset(@app_module)
|
|
194
|
-
end
|
|
195
245
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
246
|
+
@module_stack = [@app_module]
|
|
247
|
+
@is_compiling = false
|
|
248
|
+
@is_memo_definition = false
|
|
249
|
+
@cur_definition = nil
|
|
250
|
+
|
|
251
|
+
# Debug support
|
|
252
|
+
@string_location = nil
|
|
202
253
|
end
|
|
203
254
|
|
|
204
|
-
# @param [String] string
|
|
205
|
-
# @param [CodeLocation, nil] reference_location
|
|
206
|
-
# @return [Boolean]
|
|
207
255
|
def run(string, reference_location = nil)
|
|
208
|
-
|
|
209
|
-
|
|
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)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def continue_execution
|
|
286
|
+
run_with_tokenizer(@tokenizer_stack.last)
|
|
210
287
|
end
|
|
211
288
|
|
|
212
|
-
# @param [Tokenizer] tokenizer
|
|
213
|
-
# @return [Boolean]
|
|
214
289
|
def run_with_tokenizer(tokenizer)
|
|
215
290
|
token = nil
|
|
216
291
|
loop do
|
|
292
|
+
@previous_token = token
|
|
217
293
|
token = tokenizer.next_token
|
|
218
294
|
handle_token(token)
|
|
219
|
-
break if token.type == TokenType::EOS
|
|
220
|
-
next if [TokenType::START_DEF, TokenType::END_DEF, TokenType::COMMENT].include?(token.type) || @execution_state.is_compiling
|
|
295
|
+
break if token.type == TokenType::EOS
|
|
221
296
|
end
|
|
222
|
-
true
|
|
223
|
-
rescue => e
|
|
224
|
-
error = ForthicError.new(ErrorCodes::EXECUTION_ERROR, "Error executing token '#{token&.string}'", "An unexpected error occurred during execution. Check the token syntax and try again.", token&.location)
|
|
225
|
-
error.set_caught_error(e)
|
|
226
|
-
raise error
|
|
297
|
+
true # Done executing
|
|
227
298
|
end
|
|
228
299
|
|
|
229
|
-
# @return [ForthicModule]
|
|
230
300
|
def cur_module
|
|
231
|
-
@
|
|
301
|
+
@module_stack.last
|
|
232
302
|
end
|
|
233
303
|
|
|
234
|
-
# @param [String] name
|
|
235
|
-
# @return [ForthicModule]
|
|
236
304
|
def find_module(name)
|
|
237
|
-
raise ArgumentError, "Module name cannot be nil" if name.nil?
|
|
238
|
-
raise ArgumentError, "Module name cannot be empty" if name.empty?
|
|
239
|
-
|
|
240
305
|
result = @registered_modules[name]
|
|
241
|
-
|
|
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)
|
|
242
320
|
result
|
|
243
321
|
end
|
|
244
322
|
|
|
245
|
-
# @param [Object] val
|
|
246
323
|
def stack_push(val)
|
|
247
|
-
@
|
|
324
|
+
@stack.push(val)
|
|
248
325
|
end
|
|
249
326
|
|
|
250
|
-
# @return [Object]
|
|
251
327
|
def stack_pop
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
335
|
+
result = @stack.pop
|
|
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
|
|
256
353
|
end
|
|
257
354
|
|
|
258
|
-
# @param [ForthicModule] mod
|
|
259
355
|
def module_stack_push(mod)
|
|
260
|
-
|
|
261
|
-
@execution_state.module_stack.push(mod)
|
|
356
|
+
@module_stack.push(mod)
|
|
262
357
|
end
|
|
263
358
|
|
|
264
359
|
def module_stack_pop
|
|
265
|
-
@
|
|
360
|
+
@module_stack.pop
|
|
266
361
|
end
|
|
267
362
|
|
|
268
|
-
# @param [ForthicModule] mod
|
|
269
363
|
def register_module(mod)
|
|
270
|
-
raise ArgumentError, "Module cannot be nil" if mod.nil?
|
|
271
|
-
raise ArgumentError, "Module must respond to :name" unless mod.respond_to?(:name)
|
|
272
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
|
|
273
382
|
end
|
|
274
383
|
|
|
275
|
-
#
|
|
276
|
-
# @param [String] prefix
|
|
384
|
+
# A convenience method to register and use a module
|
|
277
385
|
def import_module(mod, prefix = "")
|
|
278
|
-
raise ArgumentError, "Module cannot be nil" if mod.nil?
|
|
279
386
|
register_module(mod)
|
|
280
|
-
|
|
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)
|
|
281
403
|
end
|
|
282
404
|
|
|
283
|
-
# @param [ForthicModule] mod
|
|
284
405
|
def run_module_code(mod)
|
|
285
|
-
raise ArgumentError, "Module cannot be nil" if mod.nil?
|
|
286
406
|
module_stack_push(mod)
|
|
287
|
-
|
|
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
|
+
|
|
288
421
|
module_stack_pop
|
|
289
|
-
rescue => e
|
|
290
|
-
error = ForthicError.new(ErrorCodes::MODULE_EXECUTION_ERROR, "Error executing module '#{mod.name}'", "An error occurred while running the module code. Check the module implementation for syntax errors.")
|
|
291
|
-
error.set_caught_error(e)
|
|
292
|
-
raise error
|
|
293
422
|
end
|
|
294
423
|
|
|
295
|
-
#
|
|
296
|
-
#
|
|
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
|
+
|
|
297
464
|
def find_word(name)
|
|
465
|
+
# 1. Check module stack (dictionary words + variables)
|
|
298
466
|
result = nil
|
|
299
|
-
@
|
|
467
|
+
(@module_stack.length - 1).downto(0) do |i|
|
|
468
|
+
m = @module_stack[i]
|
|
300
469
|
result = m.find_word(name)
|
|
301
470
|
break if result
|
|
302
471
|
end
|
|
303
|
-
|
|
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
|
+
|
|
304
485
|
result
|
|
305
486
|
end
|
|
306
487
|
|
|
307
|
-
#
|
|
488
|
+
# ======================
|
|
489
|
+
# Profiling
|
|
490
|
+
|
|
308
491
|
def start_profiling
|
|
309
|
-
@
|
|
492
|
+
@is_profiling = true
|
|
493
|
+
@timestamps = []
|
|
494
|
+
@start_profile_time = Time.now.to_f * 1000 # milliseconds
|
|
495
|
+
add_timestamp("START")
|
|
496
|
+
@word_counts = {}
|
|
310
497
|
end
|
|
311
498
|
|
|
312
|
-
def
|
|
313
|
-
@
|
|
499
|
+
def count_word(word)
|
|
500
|
+
return unless @is_profiling
|
|
501
|
+
name = word.name
|
|
502
|
+
@word_counts[name] ||= 0
|
|
503
|
+
@word_counts[name] += 1
|
|
314
504
|
end
|
|
315
505
|
|
|
316
|
-
def
|
|
317
|
-
|
|
506
|
+
def stop_profiling
|
|
507
|
+
add_timestamp("END")
|
|
508
|
+
@is_profiling = false
|
|
318
509
|
end
|
|
319
510
|
|
|
320
511
|
def add_timestamp(label)
|
|
321
|
-
@
|
|
512
|
+
return unless @is_profiling
|
|
513
|
+
timestamp = Timestamp.new(
|
|
514
|
+
label: label,
|
|
515
|
+
time_ms: (Time.now.to_f * 1000) - @start_profile_time
|
|
516
|
+
)
|
|
517
|
+
@timestamps << timestamp
|
|
322
518
|
end
|
|
323
519
|
|
|
324
520
|
def word_histogram
|
|
325
|
-
|
|
521
|
+
items = []
|
|
522
|
+
@word_counts.each do |name, count|
|
|
523
|
+
items << { word: name, count: count }
|
|
524
|
+
end
|
|
525
|
+
items.sort_by { |item| -item[:count] }
|
|
326
526
|
end
|
|
327
527
|
|
|
328
528
|
def profile_timestamps
|
|
329
|
-
@
|
|
529
|
+
@timestamps
|
|
330
530
|
end
|
|
331
531
|
|
|
332
|
-
#
|
|
333
|
-
|
|
334
|
-
return if token.type == TokenType::EOS
|
|
532
|
+
# ======================
|
|
533
|
+
# Handle tokens
|
|
335
534
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
535
|
+
def handle_token(token)
|
|
536
|
+
case token.type
|
|
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
|
|
339
566
|
else
|
|
340
|
-
raise
|
|
567
|
+
raise UnknownTokenError.new(
|
|
568
|
+
get_top_input_string,
|
|
569
|
+
token.string,
|
|
570
|
+
location: @string_location
|
|
571
|
+
)
|
|
341
572
|
end
|
|
342
573
|
end
|
|
343
574
|
|
|
344
|
-
# @param [Token] token
|
|
345
575
|
def handle_string_token(token)
|
|
346
576
|
value = PositionedString.new(token.string, token.location)
|
|
347
577
|
handle_word(PushValueWord.new("<string>", value))
|
|
348
578
|
end
|
|
349
579
|
|
|
350
|
-
|
|
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
|
|
351
586
|
def handle_start_module_token(token)
|
|
352
587
|
word = StartModuleWord.new(token.string)
|
|
353
|
-
|
|
354
|
-
|
|
588
|
+
|
|
589
|
+
@cur_definition.add_word(word) if @is_compiling
|
|
590
|
+
count_word(word) # For profiling
|
|
355
591
|
word.execute(self)
|
|
356
592
|
end
|
|
357
593
|
|
|
358
|
-
# @param [Token] _token
|
|
359
594
|
def handle_end_module_token(_token)
|
|
360
595
|
word = EndModuleWord.new
|
|
361
|
-
|
|
596
|
+
|
|
597
|
+
@cur_definition.add_word(word) if @is_compiling
|
|
362
598
|
count_word(word)
|
|
363
599
|
word.execute(self)
|
|
364
600
|
end
|
|
365
601
|
|
|
366
|
-
# @param [Token] token
|
|
367
602
|
def handle_start_array_token(token)
|
|
368
603
|
handle_word(PushValueWord.new("<start_array_token>", token))
|
|
369
604
|
end
|
|
370
605
|
|
|
371
|
-
# @param [Token] _token
|
|
372
606
|
def handle_end_array_token(_token)
|
|
373
607
|
handle_word(EndArrayWord.new)
|
|
374
608
|
end
|
|
375
609
|
|
|
376
|
-
# @param [Token] _token
|
|
377
610
|
def handle_comment_token(_token)
|
|
378
|
-
#
|
|
611
|
+
# Comment handling (currently no-op)
|
|
379
612
|
end
|
|
380
613
|
|
|
381
|
-
# @param [Token] token
|
|
382
614
|
def handle_start_definition_token(token)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
615
|
+
if @is_compiling
|
|
616
|
+
raise MissingSemicolonError.new(
|
|
617
|
+
get_top_input_string,
|
|
618
|
+
location: @previous_token&.location
|
|
619
|
+
)
|
|
620
|
+
end
|
|
621
|
+
@cur_definition = DefinitionWord.new(token.string)
|
|
622
|
+
@is_compiling = true
|
|
623
|
+
@is_memo_definition = false
|
|
387
624
|
end
|
|
388
625
|
|
|
389
|
-
# @param [Token] token
|
|
390
626
|
def handle_start_memo_token(token)
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
627
|
+
if @is_compiling
|
|
628
|
+
raise MissingSemicolonError.new(
|
|
629
|
+
get_top_input_string,
|
|
630
|
+
location: @previous_token&.location
|
|
631
|
+
)
|
|
632
|
+
end
|
|
633
|
+
@cur_definition = DefinitionWord.new(token.string)
|
|
634
|
+
@is_compiling = true
|
|
635
|
+
@is_memo_definition = true
|
|
395
636
|
end
|
|
396
637
|
|
|
397
|
-
# @param [Token] token
|
|
398
638
|
def handle_end_definition_token(token)
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
639
|
+
unless @is_compiling && @cur_definition
|
|
640
|
+
raise ExtraSemicolonError.new(
|
|
641
|
+
get_top_input_string,
|
|
642
|
+
location: token.location
|
|
643
|
+
)
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
if @is_memo_definition
|
|
647
|
+
cur_module.add_memo_words(@cur_definition)
|
|
403
648
|
else
|
|
404
|
-
cur_module.add_word(@
|
|
649
|
+
cur_module.add_word(@cur_definition)
|
|
405
650
|
end
|
|
406
|
-
@
|
|
651
|
+
@is_compiling = false
|
|
407
652
|
end
|
|
408
653
|
|
|
409
|
-
# @param [Token] token
|
|
410
654
|
def handle_word_token(token)
|
|
411
|
-
word = find_word(token.string)
|
|
412
|
-
raise ForthicError.new(ErrorCodes::WORD_NOT_FOUND, "Word '#{token.string}' not found", "Check for typos in the word name or ensure the word has been defined.", token.location) unless word
|
|
655
|
+
word = find_word(token.string) # Throws UnknownWordError if not found
|
|
413
656
|
handle_word(word, token.location)
|
|
414
657
|
end
|
|
415
658
|
|
|
416
|
-
# @param [Word] word
|
|
417
|
-
# @param [CodeLocation, nil] location
|
|
418
659
|
def handle_word(word, location = nil)
|
|
419
|
-
if @
|
|
660
|
+
if @is_compiling
|
|
420
661
|
word.set_location(location)
|
|
421
|
-
@
|
|
662
|
+
@cur_definition.add_word(word)
|
|
422
663
|
else
|
|
423
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
|
+
|
|
424
672
|
word.execute(self)
|
|
425
673
|
end
|
|
426
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
|
|
427
876
|
end
|
|
428
877
|
end
|