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
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'grpc'
|
|
4
|
+
require_relative 'forthic_runtime_services_pb'
|
|
5
|
+
require_relative 'forthic_runtime_pb'
|
|
6
|
+
require_relative 'serializer'
|
|
7
|
+
require_relative '../interpreter'
|
|
8
|
+
|
|
9
|
+
module Forthic
|
|
10
|
+
module Grpc
|
|
11
|
+
# ForthicRuntimeServicer - Ruby gRPC server for Forthic runtime
|
|
12
|
+
#
|
|
13
|
+
# Implements the ForthicRuntime gRPC service with full stack-based execution
|
|
14
|
+
# and module discovery capabilities. Follows the pattern established in Python
|
|
15
|
+
# and TypeScript implementations.
|
|
16
|
+
#
|
|
17
|
+
# Features:
|
|
18
|
+
# - Execute individual words with stack state
|
|
19
|
+
# - Execute sequences of words (batched optimization)
|
|
20
|
+
# - List available runtime-specific modules
|
|
21
|
+
# - Get detailed module information with word metadata
|
|
22
|
+
# - Rich error handling with stack traces and context
|
|
23
|
+
class ForthicRuntimeServicer < Forthic::ForthicRuntime::Service
|
|
24
|
+
# Standard library modules (available in all runtimes)
|
|
25
|
+
# These are excluded from ListModules as they're not runtime-specific
|
|
26
|
+
STANDARD_MODULES = %w[
|
|
27
|
+
array record string math datetime json boolean core
|
|
28
|
+
].freeze
|
|
29
|
+
|
|
30
|
+
attr_reader :runtime_modules
|
|
31
|
+
|
|
32
|
+
def initialize(modules_config = nil)
|
|
33
|
+
super()
|
|
34
|
+
@runtime_modules = {}
|
|
35
|
+
|
|
36
|
+
# TODO: Implement module loading from config when needed
|
|
37
|
+
# For now, we only have stdlib modules in Ruby
|
|
38
|
+
if modules_config
|
|
39
|
+
puts "[SERVER] Module config support not yet implemented: #{modules_config}"
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Execute a single word in the Ruby runtime
|
|
44
|
+
#
|
|
45
|
+
# @param request [Forthic::ExecuteWordRequest] The execution request
|
|
46
|
+
# @param _call [GRPC::ActiveCall] The gRPC call context
|
|
47
|
+
# @return [Forthic::ExecuteWordResponse] The execution response
|
|
48
|
+
def execute_word(request, _call)
|
|
49
|
+
word_name = request.word_name
|
|
50
|
+
puts "[EXECUTE_WORD] word='#{word_name}' stack_size=#{request.stack.length}"
|
|
51
|
+
|
|
52
|
+
# Deserialize the stack
|
|
53
|
+
stack = request.stack.map { |sv| Serializer.deserialize_value(sv) }
|
|
54
|
+
puts "[EXECUTE_WORD] Deserialized stack: #{stack.map { |x| x.class.name }}"
|
|
55
|
+
|
|
56
|
+
# Execute word with stack-based execution
|
|
57
|
+
result_stack = execute_with_stack(word_name, stack)
|
|
58
|
+
puts "[EXECUTE_WORD] Result stack: #{result_stack.map { |x| x.class.name }}"
|
|
59
|
+
|
|
60
|
+
# Serialize result stack
|
|
61
|
+
response_stack = result_stack.map { |v| Serializer.serialize_value(v) }
|
|
62
|
+
puts "[EXECUTE_WORD] Success"
|
|
63
|
+
|
|
64
|
+
Forthic::ExecuteWordResponse.new(result_stack: response_stack)
|
|
65
|
+
rescue => e
|
|
66
|
+
puts "[EXECUTE_WORD] ERROR: #{e.message}"
|
|
67
|
+
puts "[EXECUTE_WORD] Backtrace:"
|
|
68
|
+
puts e.backtrace[0..5].join("\n")
|
|
69
|
+
|
|
70
|
+
# Build rich error context
|
|
71
|
+
error = build_error_info(e, word_name)
|
|
72
|
+
Forthic::ExecuteWordResponse.new(error: error)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Execute a sequence of words in one batch (optimization for remote execution)
|
|
76
|
+
#
|
|
77
|
+
# @param request [Forthic::ExecuteSequenceRequest] The execution request
|
|
78
|
+
# @param _call [GRPC::ActiveCall] The gRPC call context
|
|
79
|
+
# @return [Forthic::ExecuteSequenceResponse] The execution response
|
|
80
|
+
def execute_sequence(request, _call)
|
|
81
|
+
word_names = request.word_names.to_a
|
|
82
|
+
puts "[EXECUTE_SEQUENCE] words=#{word_names} stack_size=#{request.stack.length}"
|
|
83
|
+
|
|
84
|
+
# Deserialize the initial stack
|
|
85
|
+
stack = request.stack.map { |sv| Serializer.deserialize_value(sv) }
|
|
86
|
+
puts "[EXECUTE_SEQUENCE] Deserialized stack: #{stack.map { |x| x.class.name }}"
|
|
87
|
+
|
|
88
|
+
# Execute the word sequence
|
|
89
|
+
result_stack = execute_sequence_with_stack(word_names, stack)
|
|
90
|
+
puts "[EXECUTE_SEQUENCE] Result stack: #{result_stack.map { |x| x.class.name }}"
|
|
91
|
+
|
|
92
|
+
# Serialize result stack
|
|
93
|
+
response_stack = result_stack.map { |v| Serializer.serialize_value(v) }
|
|
94
|
+
puts "[EXECUTE_SEQUENCE] Success"
|
|
95
|
+
|
|
96
|
+
Forthic::ExecuteSequenceResponse.new(result_stack: response_stack)
|
|
97
|
+
rescue => e
|
|
98
|
+
puts "[EXECUTE_SEQUENCE] ERROR: #{e.message}"
|
|
99
|
+
puts "[EXECUTE_SEQUENCE] Backtrace:"
|
|
100
|
+
puts e.backtrace[0..5].join("\n")
|
|
101
|
+
|
|
102
|
+
# Build rich error context with word sequence
|
|
103
|
+
error = build_error_info(e, nil, { 'word_sequence' => word_names.join(', ') })
|
|
104
|
+
Forthic::ExecuteSequenceResponse.new(error: error)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# List available runtime-specific modules (excludes standard library)
|
|
108
|
+
#
|
|
109
|
+
# @param request [Forthic::ListModulesRequest] The request (unused)
|
|
110
|
+
# @param _call [GRPC::ActiveCall] The gRPC call context
|
|
111
|
+
# @return [Forthic::ListModulesResponse] List of module summaries
|
|
112
|
+
def list_modules(request, _call)
|
|
113
|
+
modules = []
|
|
114
|
+
|
|
115
|
+
# Only return runtime-specific modules (not standard library)
|
|
116
|
+
@runtime_modules.each do |name, mod|
|
|
117
|
+
# Get module metadata
|
|
118
|
+
word_count = if mod.respond_to?(:get_word_docs)
|
|
119
|
+
mod.get_word_docs.length
|
|
120
|
+
else
|
|
121
|
+
mod.exportable.length
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
summary = Forthic::ModuleSummary.new(
|
|
125
|
+
name: name,
|
|
126
|
+
description: "Ruby-specific #{name} module",
|
|
127
|
+
word_count: word_count,
|
|
128
|
+
runtime_specific: true
|
|
129
|
+
)
|
|
130
|
+
modules << summary
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
Forthic::ListModulesResponse.new(modules: modules)
|
|
134
|
+
rescue => e
|
|
135
|
+
puts "[LIST_MODULES] ERROR: #{e.message}"
|
|
136
|
+
Forthic::ListModulesResponse.new(modules: [])
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Get detailed information about a specific module
|
|
140
|
+
#
|
|
141
|
+
# @param request [Forthic::GetModuleInfoRequest] The request with module name
|
|
142
|
+
# @param _call [GRPC::ActiveCall] The gRPC call context
|
|
143
|
+
# @return [Forthic::GetModuleInfoResponse] Module information with word details
|
|
144
|
+
def get_module_info(request, _call)
|
|
145
|
+
module_name = request.module_name
|
|
146
|
+
|
|
147
|
+
unless @runtime_modules.key?(module_name)
|
|
148
|
+
puts "[GET_MODULE_INFO] Module '#{module_name}' not found"
|
|
149
|
+
return Forthic::GetModuleInfoResponse.new(
|
|
150
|
+
name: module_name,
|
|
151
|
+
description: "Module not found",
|
|
152
|
+
words: []
|
|
153
|
+
)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
mod = @runtime_modules[module_name]
|
|
157
|
+
words = []
|
|
158
|
+
|
|
159
|
+
# Use DecoratedModule.get_word_docs() if available
|
|
160
|
+
if mod.respond_to?(:get_word_docs)
|
|
161
|
+
# DecoratedModule with word/direct_word decorators
|
|
162
|
+
word_docs = mod.get_word_docs
|
|
163
|
+
word_docs.each do |doc|
|
|
164
|
+
word_info = Forthic::WordInfo.new(
|
|
165
|
+
name: doc[:name],
|
|
166
|
+
stack_effect: doc[:stack_effect],
|
|
167
|
+
description: doc[:description]
|
|
168
|
+
)
|
|
169
|
+
words << word_info
|
|
170
|
+
end
|
|
171
|
+
else
|
|
172
|
+
# Fallback: Extract word information from exportable words
|
|
173
|
+
mod.exportable.each do |word_name|
|
|
174
|
+
word_info = Forthic::WordInfo.new(
|
|
175
|
+
name: word_name,
|
|
176
|
+
stack_effect: "( -- )",
|
|
177
|
+
description: "#{word_name} word from #{module_name} module"
|
|
178
|
+
)
|
|
179
|
+
words << word_info
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
Forthic::GetModuleInfoResponse.new(
|
|
184
|
+
name: module_name,
|
|
185
|
+
description: "Ruby-specific #{module_name} module",
|
|
186
|
+
words: words
|
|
187
|
+
)
|
|
188
|
+
rescue => e
|
|
189
|
+
puts "[GET_MODULE_INFO] ERROR: #{e.message}"
|
|
190
|
+
puts e.backtrace[0..5].join("\n")
|
|
191
|
+
Forthic::GetModuleInfoResponse.new(
|
|
192
|
+
name: module_name || "unknown",
|
|
193
|
+
description: "Error getting module info",
|
|
194
|
+
words: []
|
|
195
|
+
)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
private
|
|
199
|
+
|
|
200
|
+
# Execute a word with given stack state
|
|
201
|
+
#
|
|
202
|
+
# @param word_name [String] The word to execute
|
|
203
|
+
# @param stack [Array] The initial stack state
|
|
204
|
+
# @return [Array] The resulting stack
|
|
205
|
+
def execute_with_stack(word_name, stack)
|
|
206
|
+
# Create a fresh interpreter for this execution
|
|
207
|
+
# (to avoid state pollution between requests)
|
|
208
|
+
interp = StandardInterpreter.new
|
|
209
|
+
|
|
210
|
+
# Register runtime-specific modules in fresh interpreter
|
|
211
|
+
@runtime_modules.each do |name, mod|
|
|
212
|
+
interp.register_module(mod)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Import all runtime-specific modules so their words are available
|
|
216
|
+
unless @runtime_modules.empty?
|
|
217
|
+
module_names = @runtime_modules.keys
|
|
218
|
+
interp.use_modules(module_names)
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Push all stack items onto interpreter stack
|
|
222
|
+
stack.each do |item|
|
|
223
|
+
interp.stack_push(item)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Execute the word
|
|
227
|
+
interp.run(word_name)
|
|
228
|
+
|
|
229
|
+
# Get resulting stack as a list
|
|
230
|
+
interp.get_stack.get_items
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Execute a sequence of words in one go (batched execution)
|
|
234
|
+
#
|
|
235
|
+
# @param word_names [Array<String>] List of word names to execute in order
|
|
236
|
+
# @param stack [Array] Initial stack state
|
|
237
|
+
# @return [Array] Final stack state after executing all words
|
|
238
|
+
def execute_sequence_with_stack(word_names, stack)
|
|
239
|
+
# Create a fresh interpreter for this execution
|
|
240
|
+
interp = StandardInterpreter.new
|
|
241
|
+
|
|
242
|
+
# Register runtime-specific modules
|
|
243
|
+
@runtime_modules.each do |name, mod|
|
|
244
|
+
interp.register_module(mod)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Import all runtime-specific modules
|
|
248
|
+
unless @runtime_modules.empty?
|
|
249
|
+
module_names = @runtime_modules.keys
|
|
250
|
+
interp.use_modules(module_names)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Push all stack items onto interpreter stack
|
|
254
|
+
stack.each do |item|
|
|
255
|
+
interp.stack_push(item)
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Execute each word in sequence
|
|
259
|
+
word_names.each do |word_name|
|
|
260
|
+
interp.run(word_name)
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Get resulting stack as a list
|
|
264
|
+
interp.get_stack.get_items
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# Build rich error information from an exception
|
|
268
|
+
#
|
|
269
|
+
# @param exception [Exception] The exception that was raised
|
|
270
|
+
# @param word_name [String, nil] The word that was being executed (optional)
|
|
271
|
+
# @param context [Hash, nil] Additional context information (optional)
|
|
272
|
+
# @return [Forthic::ErrorInfo] ErrorInfo protobuf message with rich context
|
|
273
|
+
def build_error_info(exception, word_name = nil, context = nil)
|
|
274
|
+
# Extract stack trace
|
|
275
|
+
stack_trace = exception.backtrace || []
|
|
276
|
+
|
|
277
|
+
# Get error type name
|
|
278
|
+
error_type = exception.class.name
|
|
279
|
+
|
|
280
|
+
puts "[_build_error_info] error_type=#{error_type}, stack_trace_lines=#{stack_trace.length}"
|
|
281
|
+
puts "[_build_error_info] word_name=#{word_name}, context=#{context}"
|
|
282
|
+
|
|
283
|
+
# Build context dictionary
|
|
284
|
+
error_context = {}
|
|
285
|
+
error_context['word_name'] = word_name if word_name
|
|
286
|
+
error_context.merge!(context) if context
|
|
287
|
+
|
|
288
|
+
puts "[_build_error_info] error_context=#{error_context}"
|
|
289
|
+
|
|
290
|
+
# Build ErrorInfo message
|
|
291
|
+
error_info = Forthic::ErrorInfo.new(
|
|
292
|
+
message: exception.message,
|
|
293
|
+
runtime: "ruby",
|
|
294
|
+
stack_trace: stack_trace,
|
|
295
|
+
error_type: error_type
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
# Try to extract module information from the backtrace
|
|
299
|
+
module_name = nil
|
|
300
|
+
word_location = nil
|
|
301
|
+
|
|
302
|
+
stack_trace.each do |line|
|
|
303
|
+
# Check if this is a Forthic module
|
|
304
|
+
if line.include?('forthic/modules/')
|
|
305
|
+
module_name = line.split('forthic/modules/').last.split('.').first.gsub('_module', '')
|
|
306
|
+
word_location = line
|
|
307
|
+
break
|
|
308
|
+
elsif line.include?('forthic/grpc/')
|
|
309
|
+
word_location = line
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Add optional fields
|
|
314
|
+
error_info.word_location = word_location if word_location
|
|
315
|
+
error_info.module_name = module_name if module_name
|
|
316
|
+
|
|
317
|
+
# Add context
|
|
318
|
+
error_context.each do |key, value|
|
|
319
|
+
error_info.context[key] = value.to_s
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
puts "[_build_error_info] Final context: #{error_info.context.to_h}"
|
|
323
|
+
|
|
324
|
+
error_info
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
# Start the gRPC server
|
|
329
|
+
#
|
|
330
|
+
# @param port [Integer] Port to listen on (default: 50053)
|
|
331
|
+
# @param modules_config [String, nil] Path to modules configuration file (optional)
|
|
332
|
+
def self.serve(port: 50053, modules_config: nil)
|
|
333
|
+
server = GRPC::RpcServer.new
|
|
334
|
+
server.add_http2_port("0.0.0.0:#{port}", :this_port_is_insecure)
|
|
335
|
+
|
|
336
|
+
# Create servicer with optional config
|
|
337
|
+
servicer = ForthicRuntimeServicer.new(modules_config)
|
|
338
|
+
server.handle(servicer)
|
|
339
|
+
|
|
340
|
+
puts "Forthic Ruby gRPC server listening on port #{port}"
|
|
341
|
+
|
|
342
|
+
if modules_config
|
|
343
|
+
puts " - Loaded modules from: #{modules_config}"
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
loaded = servicer.runtime_modules.keys
|
|
347
|
+
if loaded.any?
|
|
348
|
+
puts " - Available runtime modules: #{loaded.join(', ')}"
|
|
349
|
+
else
|
|
350
|
+
puts " - No runtime-specific modules loaded"
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
server.run_till_terminated_or_interrupted([1, 'int', 'SIGTERM'])
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
# Start the server when this file is run directly
|
|
359
|
+
if __FILE__ == $PROGRAM_NAME
|
|
360
|
+
Forthic::Grpc.serve
|
|
361
|
+
end
|