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,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
|