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,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'runtime_manager'
4
+ require_relative 'remote_module'
5
+
6
+ module Forthic
7
+ module Grpc
8
+ # RemoteRuntimeModule provides words for connecting to and using remote Forthic runtimes
9
+ class RemoteRuntimeModule < Forthic::Decorators::DecoratedModule
10
+ module_doc <<~DOC
11
+ Module for connecting to and using remote Forthic runtimes (TypeScript, Python).
12
+
13
+ Enables cross-runtime execution by connecting to remote gRPC servers and importing
14
+ their modules into the current interpreter.
15
+
16
+ Example usage:
17
+ "python" "localhost:50051" CONNECT-RUNTIME
18
+ ["pandas"] USE-PY-MODULES
19
+
20
+ "typescript" "localhost:50052" CONNECT-RUNTIME
21
+ ["fs"] "ts" USE-TS-MODULES-AS
22
+ DOC
23
+
24
+ def initialize
25
+ super('remote_runtime')
26
+ @runtime_manager = RuntimeManager.instance
27
+ end
28
+
29
+ forthic_direct_word :CONNECT_RUNTIME, '( name:str address:str -- )', 'Connect to a remote runtime', 'CONNECT-RUNTIME'
30
+ def CONNECT_RUNTIME(interp)
31
+ address = interp.stack_pop
32
+ name = interp.stack_pop
33
+ @runtime_manager.connect_runtime(name, address)
34
+ nil
35
+ end
36
+
37
+ forthic_direct_word :DISCONNECT_RUNTIME, '( name:str -- )', 'Disconnect from a remote runtime', 'DISCONNECT-RUNTIME'
38
+ def DISCONNECT_RUNTIME(interp)
39
+ name = interp.stack_pop
40
+ @runtime_manager.disconnect_runtime(name)
41
+ nil
42
+ end
43
+
44
+ forthic_direct_word :DISCONNECT_ALL, '( -- )', 'Disconnect from all remote runtimes', 'DISCONNECT-ALL'
45
+ def DISCONNECT_ALL(_interp)
46
+ @runtime_manager.disconnect_all
47
+ nil
48
+ end
49
+
50
+ forthic_direct_word :CONNECTED_RUNTIMES, '( -- runtimes:array )', 'Get list of connected runtime names', 'CONNECTED-RUNTIMES'
51
+ def CONNECTED_RUNTIMES(interp)
52
+ interp.stack_push(@runtime_manager.connected_runtimes)
53
+ end
54
+
55
+ forthic_direct_word :USE_TS_MODULES, '( modules:array -- )', 'Import TypeScript modules', 'USE-TS-MODULES'
56
+ def USE_TS_MODULES(interp)
57
+ modules = interp.stack_pop
58
+ load_modules('typescript', modules, nil, interp)
59
+ nil
60
+ end
61
+
62
+ forthic_direct_word :USE_TS_MODULES_AS, '( modules:array prefix:str -- )', 'Import TypeScript modules with prefix', 'USE-TS-MODULES-AS'
63
+ def USE_TS_MODULES_AS(interp)
64
+ prefix = interp.stack_pop
65
+ modules = interp.stack_pop
66
+ load_modules('typescript', modules, prefix, interp)
67
+ nil
68
+ end
69
+
70
+ forthic_direct_word :USE_PY_MODULES, '( modules:array -- )', 'Import Python modules', 'USE-PY-MODULES'
71
+ def USE_PY_MODULES(interp)
72
+ modules = interp.stack_pop
73
+ load_modules('python', modules, nil, interp)
74
+ nil
75
+ end
76
+
77
+ forthic_direct_word :USE_PY_MODULES_AS, '( modules:array prefix:str -- )', 'Import Python modules with prefix', 'USE-PY-MODULES-AS'
78
+ def USE_PY_MODULES_AS(interp)
79
+ prefix = interp.stack_pop
80
+ modules = interp.stack_pop
81
+ load_modules('python', modules, prefix, interp)
82
+ nil
83
+ end
84
+
85
+ forthic_direct_word :USE_RB_MODULES, '( modules:array -- )', 'Import Ruby modules from another runtime', 'USE-RB-MODULES'
86
+ def USE_RB_MODULES(interp)
87
+ modules = interp.stack_pop
88
+ load_modules('ruby', modules, nil, interp)
89
+ nil
90
+ end
91
+
92
+ forthic_direct_word :USE_RB_MODULES_AS, '( modules:array prefix:str -- )', 'Import Ruby modules with prefix', 'USE-RB-MODULES-AS'
93
+ def USE_RB_MODULES_AS(interp)
94
+ prefix = interp.stack_pop
95
+ modules = interp.stack_pop
96
+ load_modules('ruby', modules, prefix, interp)
97
+ nil
98
+ end
99
+
100
+ forthic_direct_word :LIST_REMOTE_MODULES, '( runtime:str -- modules:array )', 'List available modules in a remote runtime', 'LIST-REMOTE-MODULES'
101
+ def LIST_REMOTE_MODULES(interp)
102
+ runtime_name = interp.stack_pop
103
+ client = @runtime_manager.get_runtime(runtime_name)
104
+ raise "Runtime '#{runtime_name}' not connected" unless client
105
+
106
+ modules = client.list_modules
107
+ module_summaries = modules.map do |mod|
108
+ {
109
+ 'name' => mod[:name],
110
+ 'description' => mod[:description],
111
+ 'word_count' => mod[:word_count]
112
+ }
113
+ end
114
+ interp.stack_push(module_summaries)
115
+ end
116
+
117
+ private
118
+
119
+ # Load modules from a remote runtime
120
+ # @param runtime_name [String] The name of the runtime (e.g., 'python', 'typescript')
121
+ # @param module_names [Array<String>] List of module names to load
122
+ # @param prefix [String, nil] Optional prefix for module names
123
+ # @param interp [Interpreter] The interpreter instance
124
+ def load_modules(runtime_name, module_names, prefix, interp)
125
+ client = @runtime_manager.get_runtime(runtime_name)
126
+ raise "Runtime '#{runtime_name}' not connected. Use CONNECT-RUNTIME first." unless client
127
+
128
+ module_names.each do |module_name|
129
+ # Create remote module
130
+ remote_module = RemoteModule.new(module_name, client, runtime_name)
131
+ remote_module.initialize!
132
+
133
+ # Register module (always with original name)
134
+ interp.register_module(remote_module)
135
+
136
+ # Import with optional prefix
137
+ if prefix
138
+ # Use array format: [module_name, prefix]
139
+ interp.use_modules([[remote_module.name, prefix]])
140
+ else
141
+ # Use array format with empty prefix
142
+ interp.use_modules([remote_module.name])
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../module'
4
+ require_relative 'client'
5
+
6
+ module Forthic
7
+ module Grpc
8
+ # RemoteWord - Proxy word that delegates execution to a remote runtime via gRPC
9
+ #
10
+ # When executed:
11
+ # 1. Captures current interpreter stack
12
+ # 2. Sends word name + stack to remote runtime via gRPC
13
+ # 3. Replaces local stack with result stack from remote execution
14
+ #
15
+ # This allows seamless integration of remote runtime words (like TypeScript's FILE-EXISTS?
16
+ # or Python's DF-FROM-RECORDS) into the local Ruby interpreter.
17
+ #
18
+ # Example usage:
19
+ # client = GrpcClient.new('localhost:50052')
20
+ # remote_word = RemoteWord.new('REVERSE', client, 'typescript', 'array',
21
+ # '( array -- reversed )', 'Reverse array')
22
+ # remote_word.execute(interp) # Executes in TypeScript runtime!
23
+ class RemoteWord < Forthic::Word
24
+ attr_reader :client, :runtime_name, :module_name, :stack_effect, :description
25
+
26
+ # @param name [String] Word name (e.g., "DF_FROM_RECORDS")
27
+ # @param client [GrpcClient] gRPC client connected to remote runtime
28
+ # @param runtime_name [String] Name of remote runtime (e.g., "python", "typescript")
29
+ # @param module_name [String] Module name (e.g., "pandas", "fs")
30
+ # @param stack_effect [String] Stack notation (e.g., "( records:array -- df:DataFrame )")
31
+ # @param description [String] Human-readable description
32
+ def initialize(name, client, runtime_name, module_name, stack_effect = '( -- )', description = '')
33
+ super(name)
34
+ @client = client
35
+ @runtime_name = runtime_name
36
+ @module_name = module_name
37
+ @stack_effect = stack_effect
38
+ @description = description
39
+ end
40
+
41
+ # Execute word in remote runtime
42
+ #
43
+ # Captures entire stack, sends to remote runtime, and replaces stack with result.
44
+ # This is inefficient but correct.
45
+ #
46
+ # @param interp [Interpreter] The Forthic interpreter
47
+ def execute(interp)
48
+ # Capture current stack state
49
+ stack = interp.get_stack
50
+ stack_items = stack.get_items
51
+
52
+ # Execute word in remote runtime
53
+ # The server has already imported the module, so just send the word name
54
+ result_stack = @client.execute_word(@name, stack_items)
55
+
56
+ # Clear local stack and replace with result
57
+ stack.set_raw_items([])
58
+
59
+ # Push all result items
60
+ result_stack.each { |item| interp.stack_push(item) }
61
+ rescue StandardError => e
62
+ raise "Error executing remote word #{@module_name}.#{@name} in #{@runtime_name} runtime: #{e.message}"
63
+ end
64
+
65
+ # Get runtime name for debugging/introspection
66
+ # @return [String] Runtime name
67
+ def get_runtime_name
68
+ @runtime_name
69
+ end
70
+
71
+ # Get module name for debugging/introspection
72
+ # @return [String] Module name
73
+ def get_module_name
74
+ @module_name
75
+ end
76
+
77
+ # Get runtime execution information
78
+ # RemoteWords are runtime-specific and can only execute in their designated runtime
79
+ #
80
+ # @return [Hash] Runtime info with keys: runtime, is_remote, is_standard, available_in
81
+ def get_runtime_info
82
+ {
83
+ runtime: @runtime_name,
84
+ is_remote: true,
85
+ is_standard: false,
86
+ available_in: [@runtime_name]
87
+ }
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+
5
+ module Forthic
6
+ module Grpc
7
+ # RuntimeManager manages connections to remote Forthic runtimes
8
+ # Uses Singleton pattern to ensure a single registry of runtime connections
9
+ class RuntimeManager
10
+ include Singleton
11
+
12
+ attr_reader :connections
13
+
14
+ def initialize
15
+ @connections = {}
16
+ end
17
+
18
+ # Connect to a remote runtime
19
+ # @param name [String] The name of the runtime (e.g., 'python', 'typescript')
20
+ # @param address [String] The gRPC address (e.g., 'localhost:50051')
21
+ # @return [GrpcClient] The client connection
22
+ def connect_runtime(name, address)
23
+ @connections[name] ||= GrpcClient.new(address)
24
+ end
25
+
26
+ # Get an existing runtime connection
27
+ # @param name [String] The name of the runtime
28
+ # @return [GrpcClient, nil] The client connection or nil if not found
29
+ def get_runtime(name)
30
+ @connections[name]
31
+ end
32
+
33
+ # Disconnect from a specific runtime
34
+ # @param name [String] The name of the runtime
35
+ def disconnect_runtime(name)
36
+ @connections[name]&.close
37
+ @connections.delete(name)
38
+ end
39
+
40
+ # Disconnect from all runtimes
41
+ def disconnect_all
42
+ @connections.each_value(&:close)
43
+ @connections.clear
44
+ end
45
+
46
+ # Check if a runtime is connected
47
+ # @param name [String] The name of the runtime
48
+ # @return [Boolean]
49
+ def connected?(name)
50
+ @connections.key?(name)
51
+ end
52
+
53
+ # Get list of connected runtime names
54
+ # @return [Array<String>]
55
+ def connected_runtimes
56
+ @connections.keys
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'forthic_runtime_pb'
4
+ require 'date'
5
+ require 'time'
6
+
7
+ module Forthic
8
+ module Grpc
9
+ # Serializer for converting Ruby values to/from gRPC StackValue protobuf messages
10
+ #
11
+ # Handles all Forthic types:
12
+ # - Primitives: Integer, Float, String, Boolean, Nil
13
+ # - Collections: Array, Hash
14
+ # - Temporal: Date, Time, DateTime
15
+ #
16
+ # Pattern: Mirrors Python serializer.py and TypeScript serializer.ts
17
+ module Serializer
18
+ module_function
19
+
20
+ # Serialize a Ruby value to a StackValue protobuf message
21
+ #
22
+ # @param value [Object] Ruby value to serialize
23
+ # @return [Forthic::StackValue] Protobuf StackValue message
24
+ def serialize_value(value)
25
+ stack_value = Forthic::StackValue.new
26
+
27
+ case value
28
+ when Integer
29
+ stack_value.int_value = value
30
+ when Float
31
+ stack_value.float_value = value
32
+ when String
33
+ stack_value.string_value = value
34
+ when TrueClass, FalseClass
35
+ stack_value.bool_value = value
36
+ when NilClass
37
+ stack_value.null_value = Forthic::NullValue.new
38
+ when Array
39
+ stack_value.array_value = serialize_array(value)
40
+ when Hash
41
+ stack_value.record_value = serialize_record(value)
42
+ when Time, DateTime
43
+ # Handle Time and DateTime before Date (DateTime is a subclass of Date)
44
+ stack_value.instant_value = serialize_instant(value)
45
+ when Date
46
+ stack_value.plain_date_value = serialize_date(value)
47
+ else
48
+ raise ArgumentError, "Cannot serialize type #{value.class}: #{value.inspect}"
49
+ end
50
+
51
+ stack_value
52
+ end
53
+
54
+ # Deserialize a StackValue protobuf message to a Ruby value
55
+ #
56
+ # @param stack_value [Forthic::StackValue] Protobuf message
57
+ # @return [Object] Ruby value
58
+ def deserialize_value(stack_value)
59
+ case stack_value.value
60
+ when :int_value
61
+ stack_value.int_value
62
+ when :float_value
63
+ stack_value.float_value
64
+ when :string_value
65
+ stack_value.string_value
66
+ when :bool_value
67
+ stack_value.bool_value
68
+ when :null_value
69
+ nil
70
+ when :array_value
71
+ deserialize_array(stack_value.array_value)
72
+ when :record_value
73
+ deserialize_record(stack_value.record_value)
74
+ when :plain_date_value
75
+ deserialize_date(stack_value.plain_date_value)
76
+ when :instant_value
77
+ deserialize_instant(stack_value.instant_value)
78
+ when :zoned_datetime_value
79
+ deserialize_zoned_datetime(stack_value.zoned_datetime_value)
80
+ else
81
+ raise ArgumentError, "Unknown StackValue type: #{stack_value.value}"
82
+ end
83
+ end
84
+
85
+ # Serialize a Ruby array to ArrayValue
86
+ #
87
+ # @param array [Array] Ruby array
88
+ # @return [Forthic::ArrayValue] Protobuf ArrayValue
89
+ def serialize_array(array)
90
+ array_value = Forthic::ArrayValue.new
91
+ # Use push to add items to repeated field (protobuf convention)
92
+ array.each do |item|
93
+ array_value.items.push(serialize_value(item))
94
+ end
95
+ array_value
96
+ end
97
+
98
+ # Deserialize an ArrayValue to Ruby array
99
+ #
100
+ # @param array_value [Forthic::ArrayValue] Protobuf ArrayValue
101
+ # @return [Array] Ruby array
102
+ def deserialize_array(array_value)
103
+ array_value.items.map { |item| deserialize_value(item) }
104
+ end
105
+
106
+ # Serialize a Ruby hash to RecordValue
107
+ #
108
+ # @param hash [Hash] Ruby hash with string keys
109
+ # @return [Forthic::RecordValue] Protobuf RecordValue
110
+ def serialize_record(hash)
111
+ record_value = Forthic::RecordValue.new
112
+ hash.each do |key, value|
113
+ record_value.fields[key.to_s] = serialize_value(value)
114
+ end
115
+ record_value
116
+ end
117
+
118
+ # Deserialize a RecordValue to Ruby hash
119
+ #
120
+ # @param record_value [Forthic::RecordValue] Protobuf RecordValue
121
+ # @return [Hash] Ruby hash with string keys
122
+ def deserialize_record(record_value)
123
+ result = {}
124
+ record_value.fields.each do |key, value|
125
+ result[key] = deserialize_value(value)
126
+ end
127
+ result
128
+ end
129
+
130
+ # Serialize a Ruby Date to PlainDateValue
131
+ #
132
+ # @param date [Date] Ruby Date
133
+ # @return [Forthic::PlainDateValue] Protobuf PlainDateValue
134
+ def serialize_date(date)
135
+ plain_date_value = Forthic::PlainDateValue.new
136
+ plain_date_value.iso8601_date = date.iso8601
137
+ plain_date_value
138
+ end
139
+
140
+ # Deserialize a PlainDateValue to Ruby Date
141
+ #
142
+ # @param plain_date_value [Forthic::PlainDateValue] Protobuf PlainDateValue
143
+ # @return [Date] Ruby Date
144
+ def deserialize_date(plain_date_value)
145
+ Date.iso8601(plain_date_value.iso8601_date)
146
+ end
147
+
148
+ # Serialize a Ruby Time/DateTime to InstantValue
149
+ #
150
+ # @param time [Time, DateTime] Ruby Time or DateTime
151
+ # @return [Forthic::InstantValue] Protobuf InstantValue
152
+ def serialize_instant(time)
153
+ instant_value = Forthic::InstantValue.new
154
+ # Convert to UTC and format as ISO 8601 with timezone
155
+ if time.is_a?(DateTime)
156
+ # DateTime: convert to Time first
157
+ utc_time = time.to_time.utc
158
+ else
159
+ # Time: call utc directly
160
+ utc_time = time.utc
161
+ end
162
+ instant_value.iso8601 = utc_time.iso8601
163
+ instant_value
164
+ end
165
+
166
+ # Deserialize an InstantValue to Ruby Time
167
+ #
168
+ # @param instant_value [Forthic::InstantValue] Protobuf InstantValue
169
+ # @return [Time] Ruby Time (UTC)
170
+ def deserialize_instant(instant_value)
171
+ Time.iso8601(instant_value.iso8601)
172
+ end
173
+
174
+ # Deserialize a ZonedDateTimeValue to Ruby Time with timezone
175
+ #
176
+ # @param zoned_value [Forthic::ZonedDateTimeValue] Protobuf ZonedDateTimeValue
177
+ # @return [Time] Ruby Time with timezone info
178
+ def deserialize_zoned_datetime(zoned_value)
179
+ # Parse ISO 8601 string, Ruby Time will preserve timezone
180
+ Time.iso8601(zoned_value.iso8601)
181
+ end
182
+ end
183
+ end
184
+ end