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,223 @@
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 'errors'
8
+
9
+ module Forthic
10
+ module Grpc
11
+ # gRPC client for executing words in remote Forthic runtimes
12
+ #
13
+ # Connects to remote runtimes (TypeScript, Python, other Ruby instances)
14
+ # and executes words with full stack serialization and error handling.
15
+ #
16
+ # Pattern: Mirrors Python client.py and TypeScript client.ts
17
+ #
18
+ # Features:
19
+ # - Execute individual words with stack state
20
+ # - Execute sequences of words (batched optimization)
21
+ # - List available runtime-specific modules
22
+ # - Get detailed module information with word metadata
23
+ # - Rich error handling with stack traces and context
24
+ #
25
+ # Example:
26
+ # client = Forthic::Grpc::GrpcClient.new('localhost:50052')
27
+ # result = client.execute_word('REVERSE', [[1, 2, 3]])
28
+ # # => [[3, 2, 1]]
29
+ # client.close
30
+ class GrpcClient
31
+ attr_reader :address, :stub
32
+
33
+ # Initialize the gRPC client
34
+ #
35
+ # @param address [String] Address of the remote runtime
36
+ # - 'localhost:50051' for Python
37
+ # - 'localhost:50052' for TypeScript
38
+ # - 'localhost:50053' for Ruby
39
+ def initialize(address = 'localhost:50052')
40
+ @address = address
41
+
42
+ # Create gRPC channel (insecure for now)
43
+ @stub = Forthic::ForthicRuntime::Stub.new(
44
+ address,
45
+ :this_channel_is_insecure
46
+ )
47
+
48
+ puts "[CLIENT] Connected to Forthic runtime at #{address}"
49
+ end
50
+
51
+ # Execute a word in the remote runtime
52
+ #
53
+ # @param word_name [String] Name of the word to execute
54
+ # @param stack [Array] Current stack values
55
+ # @return [Array] Result stack after execution
56
+ # @raise [RemoteRuntimeError] If the remote runtime raises an error
57
+ #
58
+ # Example:
59
+ # client.execute_word('REVERSE', [[1, 2, 3]])
60
+ # # => [[3, 2, 1]]
61
+ def execute_word(word_name, stack)
62
+ puts "[EXECUTE_WORD] word='#{word_name}' stack=#{stack.inspect}"
63
+
64
+ # Serialize the stack
65
+ serialized_stack = stack.map { |value| Serializer.serialize_value(value) }
66
+
67
+ # Create request
68
+ request = Forthic::ExecuteWordRequest.new(
69
+ word_name: word_name,
70
+ stack: serialized_stack
71
+ )
72
+
73
+ # Execute RPC call
74
+ response = @stub.execute_word(request)
75
+
76
+ # Check for errors
77
+ if response.error && !response.error.message.empty?
78
+ error_info = Forthic::Grpc.parse_error_info(response.error)
79
+ raise RemoteRuntimeError.new(error_info)
80
+ end
81
+
82
+ # Deserialize result stack
83
+ result_stack = response.result_stack.map { |value| Serializer.deserialize_value(value) }
84
+
85
+ puts "[EXECUTE_WORD] result=#{result_stack.inspect}"
86
+
87
+ result_stack
88
+ rescue GRPC::BadStatus => e
89
+ raise "gRPC error: #{e.message}"
90
+ end
91
+
92
+ # Execute a sequence of words in one remote call (batched execution)
93
+ #
94
+ # @param word_names [Array<String>] Array of word names to execute in order
95
+ # @param stack [Array] Current stack values
96
+ # @return [Array] Result stack after executing all words
97
+ # @raise [RemoteRuntimeError] If the remote runtime raises an error
98
+ #
99
+ # Example:
100
+ # client.execute_sequence(['DUP', '+'], [5])
101
+ # # => [10]
102
+ def execute_sequence(word_names, stack)
103
+ puts "[EXECUTE_SEQUENCE] words=#{word_names.inspect} stack=#{stack.inspect}"
104
+
105
+ # Serialize the stack
106
+ serialized_stack = stack.map { |value| Serializer.serialize_value(value) }
107
+
108
+ # Create request
109
+ request = Forthic::ExecuteSequenceRequest.new(
110
+ word_names: word_names,
111
+ stack: serialized_stack
112
+ )
113
+
114
+ # Execute RPC call
115
+ response = @stub.execute_sequence(request)
116
+
117
+ # Check for errors
118
+ if response.error && !response.error.message.empty?
119
+ error_info = Forthic::Grpc.parse_error_info(response.error)
120
+ raise RemoteRuntimeError.new(error_info)
121
+ end
122
+
123
+ # Deserialize result stack
124
+ result_stack = response.result_stack.map { |value| Serializer.deserialize_value(value) }
125
+
126
+ puts "[EXECUTE_SEQUENCE] result=#{result_stack.inspect}"
127
+
128
+ result_stack
129
+ rescue GRPC::BadStatus => e
130
+ raise "gRPC error: #{e.message}"
131
+ end
132
+
133
+ # List available runtime-specific modules
134
+ #
135
+ # @return [Array<Hash>] Array of module summaries with:
136
+ # - :name [String] Module name
137
+ # - :description [String] Module description
138
+ # - :word_count [Integer] Number of words in module
139
+ # - :runtime_specific [Boolean] Whether module is runtime-specific
140
+ #
141
+ # Example:
142
+ # client.list_modules
143
+ # # => [{name: 'pandas', description: '...', word_count: 50, runtime_specific: true}]
144
+ def list_modules
145
+ puts "[LIST_MODULES] Fetching module list..."
146
+
147
+ request = Forthic::ListModulesRequest.new
148
+
149
+ # Execute RPC call
150
+ response = @stub.list_modules(request)
151
+
152
+ # Convert to Ruby hashes
153
+ modules = response.modules.map do |module_summary|
154
+ {
155
+ name: module_summary.name,
156
+ description: module_summary.description,
157
+ word_count: module_summary.word_count,
158
+ runtime_specific: module_summary.runtime_specific
159
+ }
160
+ end
161
+
162
+ puts "[LIST_MODULES] Found #{modules.length} modules"
163
+
164
+ modules
165
+ rescue GRPC::BadStatus => e
166
+ raise "gRPC error: #{e.message}"
167
+ end
168
+
169
+ # Get detailed information about a specific module
170
+ #
171
+ # @param module_name [String] Name of the module
172
+ # @return [Hash] Module details with:
173
+ # - :name [String] Module name
174
+ # - :description [String] Module description
175
+ # - :words [Array<Hash>] Array of word info hashes with:
176
+ # - :name [String] Word name
177
+ # - :stack_effect [String] Stack effect notation
178
+ # - :description [String] Word description
179
+ #
180
+ # Example:
181
+ # client.get_module_info('pandas')
182
+ # # => {name: 'pandas', description: '...', words: [...]}
183
+ def get_module_info(module_name)
184
+ puts "[GET_MODULE_INFO] module='#{module_name}'"
185
+
186
+ request = Forthic::GetModuleInfoRequest.new(module_name: module_name)
187
+
188
+ # Execute RPC call
189
+ response = @stub.get_module_info(request)
190
+
191
+ # Convert to Ruby hash
192
+ words = response.words.map do |word_info|
193
+ {
194
+ name: word_info.name,
195
+ stack_effect: word_info.stack_effect,
196
+ description: word_info.description
197
+ }
198
+ end
199
+
200
+ result = {
201
+ name: response.name,
202
+ description: response.description,
203
+ words: words
204
+ }
205
+
206
+ puts "[GET_MODULE_INFO] Found #{words.length} words"
207
+
208
+ result
209
+ rescue GRPC::BadStatus => e
210
+ raise "gRPC error: #{e.message}"
211
+ end
212
+
213
+ # Close the gRPC channel
214
+ #
215
+ # Should be called when done using the client to clean up resources
216
+ def close
217
+ puts "[CLIENT] Closing connection to #{@address}"
218
+ # Note: Ruby gRPC doesn't have an explicit close method for stubs
219
+ # The channel will be cleaned up automatically when the object is GC'd
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'forthic_runtime_pb'
4
+
5
+ module Forthic
6
+ module Grpc
7
+ # Error information from a remote runtime
8
+ #
9
+ # Preserves context, stack traces, and metadata from errors that occur
10
+ # in remote Forthic runtimes (TypeScript, Python, etc.)
11
+ class RemoteErrorInfo
12
+ attr_reader :message, :runtime, :stack_trace, :error_type,
13
+ :word_location, :module_name, :context
14
+
15
+ def initialize(message:, runtime:, stack_trace: [], error_type: 'Error',
16
+ word_location: nil, module_name: nil, context: {})
17
+ @message = message
18
+ @runtime = runtime
19
+ @stack_trace = stack_trace
20
+ @error_type = error_type
21
+ @word_location = word_location
22
+ @module_name = module_name
23
+ @context = context || {}
24
+ end
25
+ end
26
+
27
+ # Custom error class for errors that occur in remote runtimes
28
+ #
29
+ # Preserves stack trace and context from the remote runtime and provides
30
+ # rich error reporting with full context.
31
+ #
32
+ # Pattern: Mirrors Python errors.py and TypeScript errors.ts
33
+ class RemoteRuntimeError < StandardError
34
+ attr_reader :runtime, :remote_stack_trace, :error_type,
35
+ :word_location, :module_name, :context, :error_info
36
+
37
+ def initialize(error_info)
38
+ @error_info = error_info
39
+ @runtime = error_info.runtime
40
+ @remote_stack_trace = error_info.stack_trace
41
+ @error_type = error_info.error_type
42
+ @word_location = error_info.word_location
43
+ @module_name = error_info.module_name
44
+ @context = error_info.context
45
+
46
+ # Build a rich error message
47
+ message = build_message
48
+
49
+ super(message)
50
+ end
51
+
52
+ # Build a rich error message with full context
53
+ #
54
+ # @return [String] Formatted error message
55
+ def build_message
56
+ msg = "Error in #{@runtime} runtime: #{@error_info.message}"
57
+
58
+ msg += "\n Module: #{@module_name}" if @module_name
59
+ msg += "\n Location: #{@word_location}" if @word_location
60
+
61
+ if @context && !@context.empty?
62
+ msg += "\n Context:"
63
+ @context.each do |key, value|
64
+ msg += "\n #{key}: #{value}"
65
+ end
66
+ end
67
+
68
+ msg
69
+ end
70
+
71
+ # Get the full stack trace including both local and remote context
72
+ #
73
+ # @return [String] Full stack trace
74
+ def get_full_stack_trace
75
+ result = "#{self.class.name}: #{message}\n"
76
+
77
+ # Add local Ruby stack
78
+ result += "\nLocal stack (Ruby):\n"
79
+ result += backtrace.join("\n") if backtrace
80
+
81
+ # Add remote stack trace
82
+ if @remote_stack_trace && !@remote_stack_trace.empty?
83
+ result += "\n\nRemote stack (#{@runtime}):\n"
84
+ result += @remote_stack_trace.join("\n")
85
+ end
86
+
87
+ result
88
+ end
89
+
90
+ # Get a formatted error report with all available context
91
+ #
92
+ # @return [String] Detailed error report
93
+ def get_error_report
94
+ report = "=" * 80 + "\n"
95
+ report += "REMOTE RUNTIME ERROR\n"
96
+ report += "=" * 80 + "\n\n"
97
+
98
+ report += "Runtime: #{@runtime}\n"
99
+ report += "Error Type: #{@error_type}\n"
100
+ report += "Message: #{@error_info.message}\n"
101
+
102
+ report += "Module: #{@module_name}\n" if @module_name
103
+ report += "Location: #{@word_location}\n" if @word_location
104
+
105
+ if @context && !@context.empty?
106
+ report += "\nContext:\n"
107
+ @context.each do |key, value|
108
+ report += " #{key}: #{value}\n"
109
+ end
110
+ end
111
+
112
+ report += "\n" + "-" * 80 + "\n"
113
+ report += "Stack Trace:\n"
114
+ report += "-" * 80 + "\n"
115
+
116
+ if @remote_stack_trace && !@remote_stack_trace.empty?
117
+ report += @remote_stack_trace.join("\n")
118
+ end
119
+
120
+ report += "\n" + "=" * 80 + "\n"
121
+
122
+ report
123
+ end
124
+ end
125
+
126
+ # Parse ErrorInfo from protobuf response into RemoteErrorInfo
127
+ #
128
+ # @param error_info [Forthic::ErrorInfo] Protobuf ErrorInfo message
129
+ # @return [RemoteErrorInfo] Parsed error information
130
+ def self.parse_error_info(error_info)
131
+ # Handle empty strings and nil values
132
+ message = error_info.message.to_s.empty? ? 'Unknown error' : error_info.message
133
+ runtime = error_info.runtime.to_s.empty? ? 'unknown' : error_info.runtime
134
+ error_type = error_info.error_type.to_s.empty? ? 'Error' : error_info.error_type
135
+ word_location = error_info.word_location.to_s.empty? ? nil : error_info.word_location
136
+ module_name = error_info.module_name.to_s.empty? ? nil : error_info.module_name
137
+
138
+ RemoteErrorInfo.new(
139
+ message: message,
140
+ runtime: runtime,
141
+ stack_trace: error_info.stack_trace.to_a,
142
+ error_type: error_type,
143
+ word_location: word_location,
144
+ module_name: module_name,
145
+ context: error_info.context.to_h
146
+ )
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: forthic_runtime.proto
4
+
5
+ require 'google/protobuf'
6
+
7
+
8
+ descriptor_data = "\n\x15\x66orthic_runtime.proto\x12\x07\x66orthic\"K\n\x12\x45xecuteWordRequest\x12\x11\n\tword_name\x18\x01 \x01(\t\x12\"\n\x05stack\x18\x02 \x03(\x0b\x32\x13.forthic.StackValue\"r\n\x13\x45xecuteWordResponse\x12)\n\x0cresult_stack\x18\x01 \x03(\x0b\x32\x13.forthic.StackValue\x12&\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x12.forthic.ErrorInfoH\x00\x88\x01\x01\x42\x08\n\x06_error\"P\n\x16\x45xecuteSequenceRequest\x12\x12\n\nword_names\x18\x01 \x03(\t\x12\"\n\x05stack\x18\x02 \x03(\x0b\x32\x13.forthic.StackValue\"v\n\x17\x45xecuteSequenceResponse\x12)\n\x0cresult_stack\x18\x01 \x03(\x0b\x32\x13.forthic.StackValue\x12&\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x12.forthic.ErrorInfoH\x00\x88\x01\x01\x42\x08\n\x06_error\"\x95\x03\n\nStackValue\x12\x13\n\tint_value\x18\x01 \x01(\x03H\x00\x12\x16\n\x0cstring_value\x18\x02 \x01(\tH\x00\x12\x14\n\nbool_value\x18\x03 \x01(\x08H\x00\x12\x15\n\x0b\x66loat_value\x18\x04 \x01(\x01H\x00\x12(\n\nnull_value\x18\x05 \x01(\x0b\x32\x12.forthic.NullValueH\x00\x12*\n\x0b\x61rray_value\x18\x06 \x01(\x0b\x32\x13.forthic.ArrayValueH\x00\x12,\n\x0crecord_value\x18\x07 \x01(\x0b\x32\x14.forthic.RecordValueH\x00\x12.\n\rinstant_value\x18\x08 \x01(\x0b\x32\x15.forthic.InstantValueH\x00\x12\x33\n\x10plain_date_value\x18\t \x01(\x0b\x32\x17.forthic.PlainDateValueH\x00\x12;\n\x14zoned_datetime_value\x18\n \x01(\x0b\x32\x1b.forthic.ZonedDateTimeValueH\x00\x42\x07\n\x05value\"\x0b\n\tNullValue\"0\n\nArrayValue\x12\"\n\x05items\x18\x01 \x03(\x0b\x32\x13.forthic.StackValue\"\x83\x01\n\x0bRecordValue\x12\x30\n\x06\x66ields\x18\x01 \x03(\x0b\x32 .forthic.RecordValue.FieldsEntry\x1a\x42\n\x0b\x46ieldsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\"\n\x05value\x18\x02 \x01(\x0b\x32\x13.forthic.StackValue:\x02\x38\x01\"\x1f\n\x0cInstantValue\x12\x0f\n\x07iso8601\x18\x01 \x01(\t\"&\n\x0ePlainDateValue\x12\x14\n\x0ciso8601_date\x18\x01 \x01(\t\"7\n\x12ZonedDateTimeValue\x12\x0f\n\x07iso8601\x18\x01 \x01(\t\x12\x10\n\x08timezone\x18\x02 \x01(\t\"\x90\x02\n\tErrorInfo\x12\x0f\n\x07message\x18\x01 \x01(\t\x12\x0f\n\x07runtime\x18\x02 \x01(\t\x12\x13\n\x0bstack_trace\x18\x03 \x03(\t\x12\x12\n\nerror_type\x18\x04 \x01(\t\x12\x1a\n\rword_location\x18\x05 \x01(\tH\x00\x88\x01\x01\x12\x18\n\x0bmodule_name\x18\x06 \x01(\tH\x01\x88\x01\x01\x12\x30\n\x07\x63ontext\x18\x07 \x03(\x0b\x32\x1f.forthic.ErrorInfo.ContextEntry\x1a.\n\x0c\x43ontextEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\t:\x02\x38\x01\x42\x10\n\x0e_word_locationB\x0e\n\x0c_module_name\"\x14\n\x12ListModulesRequest\">\n\x13ListModulesResponse\x12\'\n\x07modules\x18\x01 \x03(\x0b\x32\x16.forthic.ModuleSummary\"`\n\rModuleSummary\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12\x12\n\nword_count\x18\x03 \x01(\x05\x12\x18\n\x10runtime_specific\x18\x04 \x01(\x08\"+\n\x14GetModuleInfoRequest\x12\x13\n\x0bmodule_name\x18\x01 \x01(\t\"\\\n\x15GetModuleInfoResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x02 \x01(\t\x12 \n\x05words\x18\x03 \x03(\x0b\x32\x11.forthic.WordInfo\"C\n\x08WordInfo\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x14\n\x0cstack_effect\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t2\xca\x02\n\x0e\x46orthicRuntime\x12H\n\x0b\x45xecuteWord\x12\x1b.forthic.ExecuteWordRequest\x1a\x1c.forthic.ExecuteWordResponse\x12T\n\x0f\x45xecuteSequence\x12\x1f.forthic.ExecuteSequenceRequest\x1a .forthic.ExecuteSequenceResponse\x12H\n\x0bListModules\x12\x1b.forthic.ListModulesRequest\x1a\x1c.forthic.ListModulesResponse\x12N\n\rGetModuleInfo\x12\x1d.forthic.GetModuleInfoRequest\x1a\x1e.forthic.GetModuleInfoResponseb\x06proto3"
9
+
10
+ pool = ::Google::Protobuf::DescriptorPool.generated_pool
11
+ pool.add_serialized_file(descriptor_data)
12
+
13
+ module Forthic
14
+ ExecuteWordRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.ExecuteWordRequest").msgclass
15
+ ExecuteWordResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.ExecuteWordResponse").msgclass
16
+ ExecuteSequenceRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.ExecuteSequenceRequest").msgclass
17
+ ExecuteSequenceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.ExecuteSequenceResponse").msgclass
18
+ StackValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.StackValue").msgclass
19
+ NullValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.NullValue").msgclass
20
+ ArrayValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.ArrayValue").msgclass
21
+ RecordValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.RecordValue").msgclass
22
+ InstantValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.InstantValue").msgclass
23
+ PlainDateValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.PlainDateValue").msgclass
24
+ ZonedDateTimeValue = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.ZonedDateTimeValue").msgclass
25
+ ErrorInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.ErrorInfo").msgclass
26
+ ListModulesRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.ListModulesRequest").msgclass
27
+ ListModulesResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.ListModulesResponse").msgclass
28
+ ModuleSummary = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.ModuleSummary").msgclass
29
+ GetModuleInfoRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.GetModuleInfoRequest").msgclass
30
+ GetModuleInfoResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.GetModuleInfoResponse").msgclass
31
+ WordInfo = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("forthic.WordInfo").msgclass
32
+ end
@@ -0,0 +1,31 @@
1
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
2
+ # Source: forthic_runtime.proto for package 'forthic'
3
+
4
+ require 'grpc'
5
+ require_relative 'forthic_runtime_pb'
6
+
7
+ module Forthic
8
+ module ForthicRuntime
9
+ # Service for executing Forthic words across runtime boundaries
10
+ class Service
11
+
12
+ include ::GRPC::GenericService
13
+
14
+ self.marshal_class_method = :encode
15
+ self.unmarshal_class_method = :decode
16
+ self.service_name = 'forthic.ForthicRuntime'
17
+
18
+ # Execute a single word in the remote runtime
19
+ rpc :ExecuteWord, ::Forthic::ExecuteWordRequest, ::Forthic::ExecuteWordResponse
20
+ # Execute a sequence of words in one remote call (batched execution optimization)
21
+ rpc :ExecuteSequence, ::Forthic::ExecuteSequenceRequest, ::Forthic::ExecuteSequenceResponse
22
+ # Phase 3: Module discovery
23
+ # List available runtime-specific modules (excludes standard library)
24
+ rpc :ListModules, ::Forthic::ListModulesRequest, ::Forthic::ListModulesResponse
25
+ # Get detailed information about a specific module
26
+ rpc :GetModuleInfo, ::Forthic::GetModuleInfoRequest, ::Forthic::GetModuleInfoResponse
27
+ end
28
+
29
+ Stub = Service.rpc_stub_class
30
+ end
31
+ end
@@ -0,0 +1,120 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../module'
4
+ require_relative 'client'
5
+ require_relative 'remote_word'
6
+
7
+ module Forthic
8
+ module Grpc
9
+ # RemoteModule - Module that wraps runtime-specific words from a remote runtime
10
+ #
11
+ # This module discovers words from a remote runtime (e.g., pandas module in Python,
12
+ # fs module in TypeScript) and creates RemoteWord proxies for each discovered word.
13
+ # When used in Ruby Forthic code, these words execute in the remote runtime via gRPC.
14
+ #
15
+ # Example usage:
16
+ # client = GrpcClient.new('localhost:50051')
17
+ # pandas_module = RemoteModule.new('pandas', client, 'python')
18
+ # pandas_module.initialize!
19
+ # interp.register_module(pandas_module)
20
+ # interp.use_modules(['pandas'])
21
+ #
22
+ # # Now pandas words execute in Python runtime
23
+ # interp.run('[{"name": "Alice", "age": 30}]')
24
+ # interp.run('DF-FROM-RECORDS') # Executes in Python!
25
+ class RemoteModule < Forthic::Module
26
+ attr_reader :client, :runtime_name, :initialized, :module_info
27
+
28
+ # @param module_name [String] Name of the module in the remote runtime (e.g., "pandas", "fs")
29
+ # @param client [GrpcClient] gRPC client connected to the remote runtime
30
+ # @param runtime_name [String] Name of the runtime (e.g., "python", "typescript") for debugging
31
+ def initialize(module_name, client, runtime_name = 'remote')
32
+ super(module_name)
33
+ @client = client
34
+ @runtime_name = runtime_name
35
+ @initialized = false
36
+ @module_info = nil
37
+ end
38
+
39
+ # Initialize the module by discovering words from the remote runtime
40
+ #
41
+ # This must be called before the module is registered with an interpreter.
42
+ # It fetches the module metadata and creates RemoteWord proxies for each word.
43
+ #
44
+ # @raise [StandardError] If the module cannot be initialized
45
+ def initialize!
46
+ return if @initialized
47
+
48
+ # Discover module info from remote runtime
49
+ @module_info = @client.get_module_info(@name)
50
+
51
+ # Create RemoteWord for each discovered word
52
+ words = @module_info[:words] || @module_info.words
53
+ words.each do |word_info|
54
+ # Handle both hash and object access
55
+ word_name = word_info[:name] || word_info.name
56
+ stack_effect = word_info[:stack_effect] || word_info.stack_effect
57
+ description = word_info[:description] || word_info.description
58
+
59
+ remote_word = RemoteWord.new(
60
+ word_name,
61
+ @client,
62
+ @runtime_name,
63
+ @name,
64
+ stack_effect,
65
+ description
66
+ )
67
+
68
+ # Add as exportable word (visible when module is imported)
69
+ add_exportable_word(remote_word)
70
+ end
71
+
72
+ @initialized = true
73
+ rescue StandardError => e
74
+ raise "Failed to initialize remote module '#{@name}' from #{@runtime_name} runtime: #{e.message}"
75
+ end
76
+
77
+ # Override set_interp to ensure module is initialized
78
+ #
79
+ # @param interp [Interpreter] The Forthic interpreter
80
+ # @raise [StandardError] If module is not initialized
81
+ def set_interp(interp)
82
+ unless @initialized
83
+ raise "RemoteModule '#{@name}' must be initialized before being registered with an interpreter. " \
84
+ "Call module.initialize! first."
85
+ end
86
+ super(interp)
87
+ end
88
+
89
+ # Get the module metadata from the remote runtime
90
+ #
91
+ # @return [GetModuleInfoResponse, nil] Module info or nil if not initialized
92
+ def get_module_info
93
+ @module_info
94
+ end
95
+
96
+ # Get runtime name for debugging
97
+ #
98
+ # @return [String] Runtime name
99
+ def get_runtime_name
100
+ @runtime_name
101
+ end
102
+
103
+ # Check if module is initialized
104
+ #
105
+ # @return [Boolean] true if initialized, false otherwise
106
+ def initialized?
107
+ @initialized
108
+ end
109
+
110
+ # Get count of discovered words
111
+ #
112
+ # @return [Integer] Number of words in the module
113
+ def word_count
114
+ return 0 unless @module_info
115
+ words = @module_info[:words] || @module_info.words
116
+ words ? words.length : 0
117
+ end
118
+ end
119
+ end
120
+ end