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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +314 -14
  3. data/Rakefile +36 -7
  4. data/lib/forthic/decorators/docs.rb +69 -0
  5. data/lib/forthic/decorators/word.rb +331 -0
  6. data/lib/forthic/errors.rb +270 -0
  7. data/lib/forthic/grpc/client.rb +223 -0
  8. data/lib/forthic/grpc/errors.rb +149 -0
  9. data/lib/forthic/grpc/forthic_runtime_pb.rb +32 -0
  10. data/lib/forthic/grpc/forthic_runtime_services_pb.rb +31 -0
  11. data/lib/forthic/grpc/remote_module.rb +120 -0
  12. data/lib/forthic/grpc/remote_runtime_module.rb +148 -0
  13. data/lib/forthic/grpc/remote_word.rb +91 -0
  14. data/lib/forthic/grpc/runtime_manager.rb +60 -0
  15. data/lib/forthic/grpc/serializer.rb +184 -0
  16. data/lib/forthic/grpc/server.rb +361 -0
  17. data/lib/forthic/interpreter.rb +694 -245
  18. data/lib/forthic/literals.rb +170 -0
  19. data/lib/forthic/module.rb +383 -0
  20. data/lib/forthic/modules/standard/array_module.rb +940 -0
  21. data/lib/forthic/modules/standard/boolean_module.rb +176 -0
  22. data/lib/forthic/modules/standard/core_module.rb +362 -0
  23. data/lib/forthic/modules/standard/datetime_module.rb +349 -0
  24. data/lib/forthic/modules/standard/json_module.rb +55 -0
  25. data/lib/forthic/modules/standard/math_module.rb +365 -0
  26. data/lib/forthic/modules/standard/record_module.rb +203 -0
  27. data/lib/forthic/modules/standard/string_module.rb +170 -0
  28. data/lib/forthic/tokenizer.rb +224 -77
  29. data/lib/forthic/utils.rb +35 -0
  30. data/lib/forthic/websocket/handler.rb +548 -0
  31. data/lib/forthic/websocket/serializer.rb +160 -0
  32. data/lib/forthic/word_options.rb +141 -0
  33. data/lib/forthic.rb +30 -20
  34. data/protos/README.md +43 -0
  35. data/protos/v1/forthic_runtime.proto +200 -0
  36. metadata +72 -39
  37. data/.standard.yml +0 -3
  38. data/CHANGELOG.md +0 -11
  39. data/CLAUDE.md +0 -74
  40. data/Guardfile +0 -42
  41. data/lib/forthic/code_location.rb +0 -20
  42. data/lib/forthic/forthic_error.rb +0 -50
  43. data/lib/forthic/forthic_module.rb +0 -146
  44. data/lib/forthic/global_module.rb +0 -2328
  45. data/lib/forthic/positioned_string.rb +0 -19
  46. data/lib/forthic/token.rb +0 -37
  47. data/lib/forthic/variable.rb +0 -34
  48. data/lib/forthic/version.rb +0 -5
  49. data/lib/forthic/words/definition_word.rb +0 -38
  50. data/lib/forthic/words/end_array_word.rb +0 -28
  51. data/lib/forthic/words/end_module_word.rb +0 -16
  52. data/lib/forthic/words/imported_word.rb +0 -27
  53. data/lib/forthic/words/map_word.rb +0 -169
  54. data/lib/forthic/words/module_memo_bang_at_word.rb +0 -22
  55. data/lib/forthic/words/module_memo_bang_word.rb +0 -21
  56. data/lib/forthic/words/module_memo_word.rb +0 -35
  57. data/lib/forthic/words/module_word.rb +0 -21
  58. data/lib/forthic/words/push_value_word.rb +0 -21
  59. data/lib/forthic/words/start_module_word.rb +0 -31
  60. data/lib/forthic/words/word.rb +0 -30
  61. 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