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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'time'
|
|
4
|
+
|
|
5
|
+
module Forthic
|
|
6
|
+
# Parse a datetime string and create a Time object in the specified timezone
|
|
7
|
+
#
|
|
8
|
+
# Expected format: "YYYY-MM-DDTHH:MM:SS"
|
|
9
|
+
# Example: "2025-05-24T10:15:30"
|
|
10
|
+
#
|
|
11
|
+
# @param date_string [String] The datetime string to parse
|
|
12
|
+
# @param timezone [String] The timezone identifier (e.g., "UTC", "America/New_York")
|
|
13
|
+
# @return [Time, nil] The parsed Time object with timezone, or nil if parsing fails
|
|
14
|
+
def self.parse_zoned_datetime(date_string, timezone)
|
|
15
|
+
# Parse the date string and create a Time in the specified timezone
|
|
16
|
+
year = date_string[0, 4].to_i
|
|
17
|
+
month = date_string[5, 2].to_i
|
|
18
|
+
day = date_string[8, 2].to_i
|
|
19
|
+
hour = date_string[11, 2].to_i
|
|
20
|
+
minute = date_string[14, 2].to_i
|
|
21
|
+
second = date_string[17, 2].to_i
|
|
22
|
+
|
|
23
|
+
# Create Time object in the specified timezone using ENV['TZ']
|
|
24
|
+
old_tz = ENV['TZ']
|
|
25
|
+
begin
|
|
26
|
+
ENV['TZ'] = timezone
|
|
27
|
+
Time.new(year, month, day, hour, minute, second)
|
|
28
|
+
ensure
|
|
29
|
+
ENV['TZ'] = old_tz
|
|
30
|
+
end
|
|
31
|
+
rescue StandardError
|
|
32
|
+
# Return nil if parsing fails
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,548 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'serializer'
|
|
4
|
+
require_relative '../interpreter'
|
|
5
|
+
|
|
6
|
+
module Forthic
|
|
7
|
+
module WebSocket
|
|
8
|
+
# ForthicWebSocketHandler - WebSocket message handler for Forthic runtime
|
|
9
|
+
#
|
|
10
|
+
# Mirrors the gRPC server functionality but uses JSON messages over WebSocket.
|
|
11
|
+
# Designed to work with Rails ActionCable for browser-server communication.
|
|
12
|
+
#
|
|
13
|
+
# Features:
|
|
14
|
+
# - Execute individual words with stack state
|
|
15
|
+
# - Execute sequences of words (batched optimization)
|
|
16
|
+
# - List available runtime-specific modules
|
|
17
|
+
# - Get detailed module information with word metadata
|
|
18
|
+
# - Streaming execution with progress updates (NEW for WebSocket)
|
|
19
|
+
# - Rich error handling with stack traces and context
|
|
20
|
+
#
|
|
21
|
+
# Message protocol defined in BROWSER_SERVER_WEBSOCKET_PLAN.md
|
|
22
|
+
class Handler
|
|
23
|
+
# Standard library modules (available in all runtimes)
|
|
24
|
+
# These are excluded from list_modules as they're not runtime-specific
|
|
25
|
+
STANDARD_MODULES = %w[
|
|
26
|
+
array record string math datetime json boolean core
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
attr_reader :runtime_modules, :timezone
|
|
30
|
+
|
|
31
|
+
# Initialize handler with optional configuration
|
|
32
|
+
#
|
|
33
|
+
# @param timezone [String] Timezone for the interpreter (default: "UTC")
|
|
34
|
+
# @param modules_config [Hash, nil] Optional modules configuration
|
|
35
|
+
def initialize(timezone: "UTC", modules_config: nil)
|
|
36
|
+
@timezone = timezone
|
|
37
|
+
@runtime_modules = {}
|
|
38
|
+
|
|
39
|
+
# TODO: Implement module loading from config when needed
|
|
40
|
+
# For now, we only have stdlib modules in Ruby
|
|
41
|
+
if modules_config
|
|
42
|
+
puts "[WEBSOCKET_HANDLER] Module config support not yet implemented"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Handle incoming WebSocket message
|
|
47
|
+
#
|
|
48
|
+
# Routes messages to appropriate handler based on message type.
|
|
49
|
+
#
|
|
50
|
+
# @param message [Hash] Parsed JSON message from client
|
|
51
|
+
# @return [Hash, Array<Hash>] Response message(s) to send back
|
|
52
|
+
def handle_message(message)
|
|
53
|
+
message_type = message['type']
|
|
54
|
+
message_id = message['id']
|
|
55
|
+
|
|
56
|
+
puts "[WEBSOCKET] Received message: type=#{message_type} id=#{message_id}"
|
|
57
|
+
|
|
58
|
+
case message_type
|
|
59
|
+
when 'execute_word'
|
|
60
|
+
execute_word(message)
|
|
61
|
+
when 'execute_sequence'
|
|
62
|
+
execute_sequence(message)
|
|
63
|
+
when 'list_modules'
|
|
64
|
+
list_modules(message)
|
|
65
|
+
when 'get_module_info'
|
|
66
|
+
get_module_info(message)
|
|
67
|
+
when 'streaming_execute'
|
|
68
|
+
streaming_execute(message)
|
|
69
|
+
else
|
|
70
|
+
error_response(message_id, "Unknown message type: #{message_type}")
|
|
71
|
+
end
|
|
72
|
+
rescue => e
|
|
73
|
+
puts "[WEBSOCKET] ERROR handling message: #{e.message}"
|
|
74
|
+
puts e.backtrace[0..5].join("\n")
|
|
75
|
+
error_response(message['id'], "Handler error: #{e.message}")
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Execute a single word in the Ruby runtime
|
|
79
|
+
#
|
|
80
|
+
# Message format:
|
|
81
|
+
# {
|
|
82
|
+
# "type": "execute_word",
|
|
83
|
+
# "id": "uuid",
|
|
84
|
+
# "params": {
|
|
85
|
+
# "word": "MY-WORD",
|
|
86
|
+
# "stack": [{"type": "int", "value": 42}, ...]
|
|
87
|
+
# }
|
|
88
|
+
# }
|
|
89
|
+
#
|
|
90
|
+
# @param message [Hash] The execution request message
|
|
91
|
+
# @return [Hash] The execution response message
|
|
92
|
+
def execute_word(message)
|
|
93
|
+
params = message['params']
|
|
94
|
+
word_name = params['word']
|
|
95
|
+
stack_json = params['stack'] || []
|
|
96
|
+
|
|
97
|
+
puts "[EXECUTE_WORD] word='#{word_name}' stack_size=#{stack_json.length}"
|
|
98
|
+
|
|
99
|
+
# Deserialize the stack
|
|
100
|
+
stack = Serializer.deserialize_stack(stack_json)
|
|
101
|
+
puts "[EXECUTE_WORD] Deserialized stack: #{stack.map { |x| x.class.name }}"
|
|
102
|
+
|
|
103
|
+
# Execute word with stack-based execution
|
|
104
|
+
result_stack = execute_with_stack(word_name, stack)
|
|
105
|
+
puts "[EXECUTE_WORD] Result stack: #{result_stack.map { |x| x.class.name }}"
|
|
106
|
+
|
|
107
|
+
# Serialize result stack
|
|
108
|
+
response_stack = Serializer.serialize_stack(result_stack)
|
|
109
|
+
puts "[EXECUTE_WORD] Success"
|
|
110
|
+
|
|
111
|
+
{
|
|
112
|
+
'type' => 'execute_word_response',
|
|
113
|
+
'id' => message['id'],
|
|
114
|
+
'result' => {
|
|
115
|
+
'stack' => response_stack
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
rescue => e
|
|
119
|
+
puts "[EXECUTE_WORD] ERROR: #{e.message}"
|
|
120
|
+
puts "[EXECUTE_WORD] Backtrace:"
|
|
121
|
+
puts e.backtrace[0..5].join("\n")
|
|
122
|
+
|
|
123
|
+
build_error_response(message['id'], 'execute_word_response', e, word_name)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Execute a sequence of words in one batch (optimization for remote execution)
|
|
127
|
+
#
|
|
128
|
+
# Message format:
|
|
129
|
+
# {
|
|
130
|
+
# "type": "execute_sequence",
|
|
131
|
+
# "id": "uuid",
|
|
132
|
+
# "params": {
|
|
133
|
+
# "words": ["WORD1", "WORD2", "WORD3"],
|
|
134
|
+
# "stack": [...]
|
|
135
|
+
# }
|
|
136
|
+
# }
|
|
137
|
+
#
|
|
138
|
+
# @param message [Hash] The execution request message
|
|
139
|
+
# @return [Hash] The execution response message
|
|
140
|
+
def execute_sequence(message)
|
|
141
|
+
params = message['params']
|
|
142
|
+
word_names = params['words'] || []
|
|
143
|
+
stack_json = params['stack'] || []
|
|
144
|
+
|
|
145
|
+
puts "[EXECUTE_SEQUENCE] words=#{word_names} stack_size=#{stack_json.length}"
|
|
146
|
+
|
|
147
|
+
# Deserialize the initial stack
|
|
148
|
+
stack = Serializer.deserialize_stack(stack_json)
|
|
149
|
+
puts "[EXECUTE_SEQUENCE] Deserialized stack: #{stack.map { |x| x.class.name }}"
|
|
150
|
+
|
|
151
|
+
# Execute the word sequence
|
|
152
|
+
result_stack = execute_sequence_with_stack(word_names, stack)
|
|
153
|
+
puts "[EXECUTE_SEQUENCE] Result stack: #{result_stack.map { |x| x.class.name }}"
|
|
154
|
+
|
|
155
|
+
# Serialize result stack
|
|
156
|
+
response_stack = Serializer.serialize_stack(result_stack)
|
|
157
|
+
puts "[EXECUTE_SEQUENCE] Success"
|
|
158
|
+
|
|
159
|
+
{
|
|
160
|
+
'type' => 'execute_sequence_response',
|
|
161
|
+
'id' => message['id'],
|
|
162
|
+
'result' => {
|
|
163
|
+
'stack' => response_stack
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
rescue => e
|
|
167
|
+
puts "[EXECUTE_SEQUENCE] ERROR: #{e.message}"
|
|
168
|
+
puts "[EXECUTE_SEQUENCE] Backtrace:"
|
|
169
|
+
puts e.backtrace[0..5].join("\n")
|
|
170
|
+
|
|
171
|
+
build_error_response(
|
|
172
|
+
message['id'],
|
|
173
|
+
'execute_sequence_response',
|
|
174
|
+
e,
|
|
175
|
+
nil,
|
|
176
|
+
{ 'word_sequence' => word_names.join(', ') }
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# List available runtime-specific modules (excludes standard library)
|
|
181
|
+
#
|
|
182
|
+
# Message format:
|
|
183
|
+
# {
|
|
184
|
+
# "type": "list_modules",
|
|
185
|
+
# "id": "uuid"
|
|
186
|
+
# }
|
|
187
|
+
#
|
|
188
|
+
# @param message [Hash] The request message
|
|
189
|
+
# @return [Hash] List of module summaries
|
|
190
|
+
def list_modules(message)
|
|
191
|
+
modules = []
|
|
192
|
+
|
|
193
|
+
# Only return runtime-specific modules (not standard library)
|
|
194
|
+
@runtime_modules.each do |name, mod|
|
|
195
|
+
# Get module metadata
|
|
196
|
+
word_count = if mod.respond_to?(:get_word_docs)
|
|
197
|
+
mod.get_word_docs.length
|
|
198
|
+
else
|
|
199
|
+
mod.exportable.length
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
summary = {
|
|
203
|
+
'name' => name,
|
|
204
|
+
'description' => "Ruby-specific #{name} module",
|
|
205
|
+
'word_count' => word_count
|
|
206
|
+
}
|
|
207
|
+
modules << summary
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
{
|
|
211
|
+
'type' => 'list_modules_response',
|
|
212
|
+
'id' => message['id'],
|
|
213
|
+
'result' => {
|
|
214
|
+
'modules' => modules
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
rescue => e
|
|
218
|
+
puts "[LIST_MODULES] ERROR: #{e.message}"
|
|
219
|
+
build_error_response(message['id'], 'list_modules_response', e)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
# Get detailed information about a specific module
|
|
223
|
+
#
|
|
224
|
+
# Message format:
|
|
225
|
+
# {
|
|
226
|
+
# "type": "get_module_info",
|
|
227
|
+
# "id": "uuid",
|
|
228
|
+
# "params": {
|
|
229
|
+
# "module_name": "pandas"
|
|
230
|
+
# }
|
|
231
|
+
# }
|
|
232
|
+
#
|
|
233
|
+
# @param message [Hash] The request message
|
|
234
|
+
# @return [Hash] Module information with word details
|
|
235
|
+
def get_module_info(message)
|
|
236
|
+
params = message['params']
|
|
237
|
+
module_name = params['module_name']
|
|
238
|
+
|
|
239
|
+
unless @runtime_modules.key?(module_name)
|
|
240
|
+
puts "[GET_MODULE_INFO] Module '#{module_name}' not found"
|
|
241
|
+
return {
|
|
242
|
+
'type' => 'get_module_info_response',
|
|
243
|
+
'id' => message['id'],
|
|
244
|
+
'result' => {
|
|
245
|
+
'module_name' => module_name,
|
|
246
|
+
'description' => 'Module not found',
|
|
247
|
+
'words' => []
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
mod = @runtime_modules[module_name]
|
|
253
|
+
words = []
|
|
254
|
+
|
|
255
|
+
# Use DecoratedModule.get_word_docs() if available
|
|
256
|
+
if mod.respond_to?(:get_word_docs)
|
|
257
|
+
# DecoratedModule with word/direct_word decorators
|
|
258
|
+
word_docs = mod.get_word_docs
|
|
259
|
+
word_docs.each do |doc|
|
|
260
|
+
word_info = {
|
|
261
|
+
'name' => doc[:name],
|
|
262
|
+
'stack_effect' => doc[:stack_effect],
|
|
263
|
+
'description' => doc[:description]
|
|
264
|
+
}
|
|
265
|
+
words << word_info
|
|
266
|
+
end
|
|
267
|
+
else
|
|
268
|
+
# Fallback: Extract word information from exportable words
|
|
269
|
+
mod.exportable.each do |word_name|
|
|
270
|
+
word_info = {
|
|
271
|
+
'name' => word_name,
|
|
272
|
+
'stack_effect' => '( -- )',
|
|
273
|
+
'description' => "#{word_name} word from #{module_name} module"
|
|
274
|
+
}
|
|
275
|
+
words << word_info
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
{
|
|
280
|
+
'type' => 'get_module_info_response',
|
|
281
|
+
'id' => message['id'],
|
|
282
|
+
'result' => {
|
|
283
|
+
'module_name' => module_name,
|
|
284
|
+
'description' => "Ruby-specific #{module_name} module",
|
|
285
|
+
'words' => words
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
rescue => e
|
|
289
|
+
puts "[GET_MODULE_INFO] ERROR: #{e.message}"
|
|
290
|
+
puts e.backtrace[0..5].join("\n")
|
|
291
|
+
build_error_response(message['id'], 'get_module_info_response', e)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Execute Forthic code with streaming progress updates
|
|
295
|
+
#
|
|
296
|
+
# This is a NEW feature for WebSocket (not available in gRPC).
|
|
297
|
+
# Sends progress updates during execution for long-running operations.
|
|
298
|
+
#
|
|
299
|
+
# Message format:
|
|
300
|
+
# {
|
|
301
|
+
# "type": "streaming_execute",
|
|
302
|
+
# "id": "uuid",
|
|
303
|
+
# "params": {
|
|
304
|
+
# "code": "LOAD-DATA PROCESS ANALYZE SAVE",
|
|
305
|
+
# "stack": [...]
|
|
306
|
+
# }
|
|
307
|
+
# }
|
|
308
|
+
#
|
|
309
|
+
# Returns an array of messages: [progress_updates..., final_response]
|
|
310
|
+
#
|
|
311
|
+
# @param message [Hash] The execution request message
|
|
312
|
+
# @param transmit_callback [Proc, nil] Optional callback for transmitting progress (ActionCable)
|
|
313
|
+
# @return [Array<Hash>] Array of progress updates and final response
|
|
314
|
+
def streaming_execute(message, transmit_callback = nil)
|
|
315
|
+
params = message['params']
|
|
316
|
+
code = params['code']
|
|
317
|
+
stack_json = params['stack'] || []
|
|
318
|
+
|
|
319
|
+
puts "[STREAMING_EXECUTE] code='#{code}' stack_size=#{stack_json.length}"
|
|
320
|
+
|
|
321
|
+
responses = []
|
|
322
|
+
|
|
323
|
+
# Deserialize the initial stack
|
|
324
|
+
stack = Serializer.deserialize_stack(stack_json)
|
|
325
|
+
|
|
326
|
+
# Create interpreter
|
|
327
|
+
interp = StandardInterpreter.new([], @timezone)
|
|
328
|
+
|
|
329
|
+
# Register runtime-specific modules
|
|
330
|
+
@runtime_modules.each do |name, mod|
|
|
331
|
+
interp.register_module(mod)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Import all runtime-specific modules
|
|
335
|
+
unless @runtime_modules.empty?
|
|
336
|
+
module_names = @runtime_modules.keys
|
|
337
|
+
interp.use_modules(module_names)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Push stack items
|
|
341
|
+
stack.each { |item| interp.stack_push(item) }
|
|
342
|
+
|
|
343
|
+
# Set up progress tracking
|
|
344
|
+
interp.setup_progress_tracking(code)
|
|
345
|
+
|
|
346
|
+
# Set up progress callback
|
|
347
|
+
interp.on_word_execute = lambda do |word_name, step, total_steps|
|
|
348
|
+
# Create progress message
|
|
349
|
+
progress_message = {
|
|
350
|
+
'type' => 'streaming_progress',
|
|
351
|
+
'id' => message['id'],
|
|
352
|
+
'progress' => {
|
|
353
|
+
'step' => step,
|
|
354
|
+
'total_steps' => total_steps,
|
|
355
|
+
'current_word' => word_name,
|
|
356
|
+
'message' => "Executing #{word_name}...",
|
|
357
|
+
'partial_stack' => Serializer.serialize_stack(interp.get_stack.get_items)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
puts "[STREAMING_EXECUTE] Progress: #{step}/#{total_steps} - #{word_name}"
|
|
362
|
+
|
|
363
|
+
# If we have a transmit callback (from ActionCable), use it immediately
|
|
364
|
+
# Otherwise, collect responses to return
|
|
365
|
+
if transmit_callback
|
|
366
|
+
transmit_callback.call(progress_message)
|
|
367
|
+
else
|
|
368
|
+
responses << progress_message
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
# Execute the code
|
|
373
|
+
interp.run(code)
|
|
374
|
+
|
|
375
|
+
# Clean up progress tracking
|
|
376
|
+
interp.reset_progress_tracking
|
|
377
|
+
|
|
378
|
+
# Get final stack
|
|
379
|
+
final_stack = interp.get_stack.get_items
|
|
380
|
+
response_stack = Serializer.serialize_stack(final_stack)
|
|
381
|
+
|
|
382
|
+
# Create final response
|
|
383
|
+
final_response = {
|
|
384
|
+
'type' => 'streaming_execute_response',
|
|
385
|
+
'id' => message['id'],
|
|
386
|
+
'result' => {
|
|
387
|
+
'stack' => response_stack
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
puts "[STREAMING_EXECUTE] Success"
|
|
392
|
+
|
|
393
|
+
# Return all responses (progress updates + final)
|
|
394
|
+
responses << final_response
|
|
395
|
+
responses
|
|
396
|
+
rescue => e
|
|
397
|
+
puts "[STREAMING_EXECUTE] ERROR: #{e.message}"
|
|
398
|
+
puts e.backtrace[0..5].join("\n")
|
|
399
|
+
|
|
400
|
+
# Clean up on error
|
|
401
|
+
interp.reset_progress_tracking if interp
|
|
402
|
+
|
|
403
|
+
error_response = build_error_response(message['id'], 'streaming_execute_response', e)
|
|
404
|
+
[error_response]
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
# Cleanup handler resources
|
|
408
|
+
def cleanup
|
|
409
|
+
# Currently no cleanup needed
|
|
410
|
+
# Future: Could close interpreter, clear modules, etc.
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
private
|
|
414
|
+
|
|
415
|
+
# Execute a word with given stack state
|
|
416
|
+
#
|
|
417
|
+
# @param word_name [String] The word to execute
|
|
418
|
+
# @param stack [Array] The initial stack state
|
|
419
|
+
# @return [Array] The resulting stack
|
|
420
|
+
def execute_with_stack(word_name, stack)
|
|
421
|
+
# Create a fresh interpreter for this execution
|
|
422
|
+
# (to avoid state pollution between requests)
|
|
423
|
+
interp = StandardInterpreter.new([], @timezone)
|
|
424
|
+
|
|
425
|
+
# Register runtime-specific modules in fresh interpreter
|
|
426
|
+
@runtime_modules.each do |name, mod|
|
|
427
|
+
interp.register_module(mod)
|
|
428
|
+
end
|
|
429
|
+
|
|
430
|
+
# Import all runtime-specific modules so their words are available
|
|
431
|
+
unless @runtime_modules.empty?
|
|
432
|
+
module_names = @runtime_modules.keys
|
|
433
|
+
interp.use_modules(module_names)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Push all stack items onto interpreter stack
|
|
437
|
+
stack.each do |item|
|
|
438
|
+
interp.stack_push(item)
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
# Execute the word
|
|
442
|
+
interp.run(word_name)
|
|
443
|
+
|
|
444
|
+
# Get resulting stack as a list
|
|
445
|
+
interp.get_stack.get_items
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
# Execute a sequence of words in one go (batched execution)
|
|
449
|
+
#
|
|
450
|
+
# @param word_names [Array<String>] List of word names to execute in order
|
|
451
|
+
# @param stack [Array] Initial stack state
|
|
452
|
+
# @return [Array] Final stack state after executing all words
|
|
453
|
+
def execute_sequence_with_stack(word_names, stack)
|
|
454
|
+
# Create a fresh interpreter for this execution
|
|
455
|
+
interp = StandardInterpreter.new([], @timezone)
|
|
456
|
+
|
|
457
|
+
# Register runtime-specific modules
|
|
458
|
+
@runtime_modules.each do |name, mod|
|
|
459
|
+
interp.register_module(mod)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Import all runtime-specific modules
|
|
463
|
+
unless @runtime_modules.empty?
|
|
464
|
+
module_names = @runtime_modules.keys
|
|
465
|
+
interp.use_modules(module_names)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# Push all stack items onto interpreter stack
|
|
469
|
+
stack.each do |item|
|
|
470
|
+
interp.stack_push(item)
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
# Execute each word in sequence
|
|
474
|
+
word_names.each do |word_name|
|
|
475
|
+
interp.run(word_name)
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Get resulting stack as a list
|
|
479
|
+
interp.get_stack.get_items
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# Build error response message
|
|
483
|
+
#
|
|
484
|
+
# @param message_id [String] The message ID
|
|
485
|
+
# @param response_type [String] The response message type
|
|
486
|
+
# @param exception [Exception] The exception that was raised
|
|
487
|
+
# @param word_name [String, nil] The word that was being executed (optional)
|
|
488
|
+
# @param context [Hash, nil] Additional context information (optional)
|
|
489
|
+
# @return [Hash] Error response message
|
|
490
|
+
def build_error_response(message_id, response_type, exception, word_name = nil, context = nil)
|
|
491
|
+
# Extract stack trace
|
|
492
|
+
stack_trace = exception.backtrace || []
|
|
493
|
+
|
|
494
|
+
# Get error type name
|
|
495
|
+
error_type = exception.class.name
|
|
496
|
+
|
|
497
|
+
# Build context dictionary
|
|
498
|
+
error_context = {}
|
|
499
|
+
error_context['word_name'] = word_name if word_name
|
|
500
|
+
error_context.merge!(context) if context
|
|
501
|
+
|
|
502
|
+
# Try to extract module information from the backtrace
|
|
503
|
+
module_name = nil
|
|
504
|
+
word_location = nil
|
|
505
|
+
|
|
506
|
+
stack_trace.each do |line|
|
|
507
|
+
# Check if this is a Forthic module
|
|
508
|
+
if line.include?('forthic/modules/')
|
|
509
|
+
module_name = line.split('forthic/modules/').last.split('.').first.gsub('_module', '')
|
|
510
|
+
word_location = line
|
|
511
|
+
break
|
|
512
|
+
elsif line.include?('forthic/websocket/')
|
|
513
|
+
word_location = line
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
{
|
|
518
|
+
'type' => response_type,
|
|
519
|
+
'id' => message_id,
|
|
520
|
+
'error' => {
|
|
521
|
+
'message' => exception.message,
|
|
522
|
+
'error_type' => error_type,
|
|
523
|
+
'stack_trace' => stack_trace,
|
|
524
|
+
'module_name' => module_name,
|
|
525
|
+
'word_location' => word_location,
|
|
526
|
+
'context' => error_context
|
|
527
|
+
}.compact # Remove nil values
|
|
528
|
+
}
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# Build a simple error response for unknown message types
|
|
532
|
+
#
|
|
533
|
+
# @param message_id [String] The message ID
|
|
534
|
+
# @param error_message [String] The error message
|
|
535
|
+
# @return [Hash] Error response message
|
|
536
|
+
def error_response(message_id, error_message)
|
|
537
|
+
{
|
|
538
|
+
'type' => 'error',
|
|
539
|
+
'id' => message_id,
|
|
540
|
+
'error' => {
|
|
541
|
+
'message' => error_message,
|
|
542
|
+
'error_type' => 'MessageError'
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
end
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
end
|