forthic 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +314 -14
  3. data/Rakefile +37 -8
  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 +682 -133
  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 +225 -78
  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 +76 -39
  37. data/.standard.yml +0 -3
  38. data/CHANGELOG.md +0 -5
  39. data/Guardfile +0 -42
  40. data/lib/forthic/code_location.rb +0 -20
  41. data/lib/forthic/forthic_error.rb +0 -51
  42. data/lib/forthic/forthic_module.rb +0 -145
  43. data/lib/forthic/global_module.rb +0 -2341
  44. data/lib/forthic/positioned_string.rb +0 -19
  45. data/lib/forthic/token.rb +0 -38
  46. data/lib/forthic/variable.rb +0 -34
  47. data/lib/forthic/version.rb +0 -5
  48. data/lib/forthic/words/definition_word.rb +0 -40
  49. data/lib/forthic/words/end_array_word.rb +0 -28
  50. data/lib/forthic/words/end_module_word.rb +0 -16
  51. data/lib/forthic/words/imported_word.rb +0 -27
  52. data/lib/forthic/words/map_word.rb +0 -169
  53. data/lib/forthic/words/module_memo_bang_at_word.rb +0 -22
  54. data/lib/forthic/words/module_memo_bang_word.rb +0 -21
  55. data/lib/forthic/words/module_memo_word.rb +0 -35
  56. data/lib/forthic/words/module_word.rb +0 -21
  57. data/lib/forthic/words/push_value_word.rb +0 -21
  58. data/lib/forthic/words/start_module_word.rb +0 -31
  59. data/lib/forthic/words/word.rb +0 -30
  60. data/sig/forthic.rbs +0 -4
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../decorators/word'
4
+
5
+ module Forthic
6
+ module Modules
7
+ # BooleanModule - Comparison, logic, and membership operations
8
+ #
9
+ # Provides boolean operations including comparisons, logical operators,
10
+ # and membership testing.
11
+ class BooleanModule < Decorators::DecoratedModule
12
+ # Register module documentation
13
+ module_doc <<~DOC
14
+ Comparison, logic, and membership operations for boolean values and conditions.
15
+
16
+ ## Categories
17
+ - Comparison: ==, !=, <, <=, >, >=
18
+ - Logic: OR, AND, NOT, XOR, NAND
19
+ - Membership: IN, ANY, ALL
20
+ - Conversion: >BOOL
21
+
22
+ ## Examples
23
+ 5 3 >
24
+ "hello" "hello" ==
25
+ [1 2 3] [4 5 6] OR
26
+ 2 [1 2 3] IN
27
+ DOC
28
+
29
+ def initialize
30
+ super("boolean")
31
+ end
32
+
33
+ # ========================================
34
+ # Comparison Operations
35
+ # ========================================
36
+
37
+ forthic_word :equals, "( a:any b:any -- equal:boolean )", "Test equality", "=="
38
+ def equals(a, b)
39
+ a == b
40
+ end
41
+
42
+ forthic_word :not_equals, "( a:any b:any -- not_equal:boolean )", "Test inequality", "!="
43
+ def not_equals(a, b)
44
+ a != b
45
+ end
46
+
47
+ forthic_word :less_than, "( a:any b:any -- less_than:boolean )", "Less than", "<"
48
+ def less_than(a, b)
49
+ a < b
50
+ end
51
+
52
+ forthic_word :less_than_or_equal, "( a:any b:any -- less_equal:boolean )", "Less than or equal", "<="
53
+ def less_than_or_equal(a, b)
54
+ a <= b
55
+ end
56
+
57
+ forthic_word :greater_than, "( a:any b:any -- greater_than:boolean )", "Greater than", ">"
58
+ def greater_than(a, b)
59
+ a > b
60
+ end
61
+
62
+ forthic_word :greater_than_or_equal, "( a:any b:any -- greater_equal:boolean )", "Greater than or equal", ">="
63
+ def greater_than_or_equal(a, b)
64
+ a >= b
65
+ end
66
+
67
+ # ========================================
68
+ # Logic Operations
69
+ # ========================================
70
+
71
+ forthic_direct_word :OR, "( a:boolean b:boolean -- result:boolean ) OR ( bools:boolean[] -- result:boolean )", "Logical OR of two values or array"
72
+ def OR(interp)
73
+ b = interp.stack_pop
74
+
75
+ # Case 1: Array on top of stack
76
+ if b.is_a?(Array)
77
+ b.each do |val|
78
+ if val
79
+ interp.stack_push(true)
80
+ return
81
+ end
82
+ end
83
+ interp.stack_push(false)
84
+ return
85
+ end
86
+
87
+ # Case 2: Two values
88
+ a = interp.stack_pop
89
+ interp.stack_push(a || b)
90
+ end
91
+
92
+ forthic_direct_word :AND, "( a:boolean b:boolean -- result:boolean ) OR ( bools:boolean[] -- result:boolean )", "Logical AND of two values or array"
93
+ def AND(interp)
94
+ b = interp.stack_pop
95
+
96
+ # Case 1: Array on top of stack
97
+ if b.is_a?(Array)
98
+ b.each do |val|
99
+ unless val
100
+ interp.stack_push(false)
101
+ return
102
+ end
103
+ end
104
+ interp.stack_push(true)
105
+ return
106
+ end
107
+
108
+ # Case 2: Two values
109
+ a = interp.stack_pop
110
+ interp.stack_push(a && b)
111
+ end
112
+
113
+ forthic_word :NOT, "( bool:boolean -- result:boolean )", "Logical NOT"
114
+ def NOT(bool)
115
+ !bool
116
+ end
117
+
118
+ forthic_word :XOR, "( a:boolean b:boolean -- result:boolean )", "Logical XOR (exclusive or)"
119
+ def XOR(a, b)
120
+ (a || b) && !(a && b)
121
+ end
122
+
123
+ forthic_word :NAND, "( a:boolean b:boolean -- result:boolean )", "Logical NAND (not and)"
124
+ def NAND(a, b)
125
+ !(a && b)
126
+ end
127
+
128
+ # ========================================
129
+ # Membership Operations
130
+ # ========================================
131
+
132
+ forthic_word :IN, "( item:any array:any[] -- in:boolean )", "Check if item is in array"
133
+ def IN(item, array)
134
+ return false unless array.is_a?(Array)
135
+ array.include?(item)
136
+ end
137
+
138
+ forthic_word :ANY, "( items1:any[] items2:any[] -- any:boolean )", "Check if any item from items1 is in items2"
139
+ def ANY(items1, items2)
140
+ return false unless items1.is_a?(Array) && items2.is_a?(Array)
141
+
142
+ # If items2 is empty, return true (any items from items1 satisfy empty constraint)
143
+ return true if items2.empty?
144
+
145
+ # Check if any item from items1 is in items2
146
+ items1.any? { |item| items2.include?(item) }
147
+ end
148
+
149
+ forthic_word :ALL, "( items1:any[] items2:any[] -- all:boolean )", "Check if all items from items2 are in items1"
150
+ def ALL(items1, items2)
151
+ return false unless items1.is_a?(Array) && items2.is_a?(Array)
152
+
153
+ # If items2 is empty, return true (all zero items are in items1)
154
+ return true if items2.empty?
155
+
156
+ # Check if all items from items2 are in items1
157
+ items2.all? { |item| items1.include?(item) }
158
+ end
159
+
160
+ # ========================================
161
+ # Conversion Operations
162
+ # ========================================
163
+
164
+ forthic_word :to_BOOL, "( a:any -- bool:boolean )", "Convert to boolean (JavaScript-style truthiness)", ">BOOL"
165
+ def to_BOOL(a)
166
+ # Match JavaScript truthiness: null, undefined, 0, "", false are falsy
167
+ # Everything else (including [], {}) is truthy
168
+ return false if a.nil?
169
+ return false if a == false
170
+ return false if a == 0
171
+ return false if a == ""
172
+ true
173
+ end
174
+ end
175
+ end
176
+ end
@@ -0,0 +1,362 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../decorators/word'
4
+ require_relative '../../word_options'
5
+ require_relative '../../errors'
6
+ require 'json'
7
+
8
+ module Forthic
9
+ module Modules
10
+ # CoreModule - Essential interpreter operations
11
+ #
12
+ # Provides stack manipulation, variables, control flow, and module system operations.
13
+ class CoreModule < Decorators::DecoratedModule
14
+ # Register module documentation
15
+ module_doc <<~DOC
16
+ Essential interpreter operations for stack manipulation, variables, control flow, and module system.
17
+
18
+ ## Categories
19
+ - Stack: POP, DUP, SWAP
20
+ - Variables: VARIABLES, !, @, !@
21
+ - Module: EXPORT, USE-MODULES
22
+ - Execution: INTERPRET
23
+ - Control: IDENTITY, NOP, DEFAULT, *DEFAULT, NULL, ARRAY?
24
+ - Options: ~> (converts array to WordOptions)
25
+ - Profiling: PROFILE-START, PROFILE-TIMESTAMP, PROFILE-END, PROFILE-DATA
26
+ - Logging: START-LOG, END-LOG
27
+ - String: INTERPOLATE, PRINT
28
+ - Debug: PEEK!, STACK!
29
+
30
+ ## Options
31
+ INTERPOLATE and PRINT support options via the ~> operator using syntax: [.option_name value ...] ~> WORD
32
+ - separator: String to use when joining array values (default: ", ")
33
+ - null_text: Text to display for null/undefined values (default: "null")
34
+ - json: Use JSON.stringify for all values (default: false)
35
+
36
+ ## Examples
37
+ 5 .count ! "Count: .count" PRINT
38
+ "Items: .items" [.separator " | "] ~> PRINT
39
+ [1 2 3] PRINT # Direct printing: 1, 2, 3
40
+ [1 2 3] [.separator " | "] ~> PRINT # With options: 1 | 2 | 3
41
+ [ [.name "Alice"] ] REC [.json TRUE] ~> PRINT # JSON format: {"name":"Alice"}
42
+ "Hello .name" INTERPOLATE .greeting !
43
+ [1 2 3] DUP SWAP
44
+ DOC
45
+
46
+ def initialize
47
+ super("core")
48
+ end
49
+
50
+ # Helper method to get or create a variable
51
+ def self.get_or_create_variable(interp, name)
52
+ # Validate variable name - no __ prefix allowed
53
+ if name.match?(/^__/)
54
+ raise InvalidVariableNameError.new(
55
+ interp.get_top_input_string,
56
+ name,
57
+ location: interp.get_string_location
58
+ )
59
+ end
60
+
61
+ cur_module = interp.cur_module
62
+
63
+ # Check if variable already exists
64
+ variable = cur_module.variables[name]
65
+
66
+ # Create it if it doesn't exist
67
+ unless variable
68
+ cur_module.add_variable(name)
69
+ variable = cur_module.variables[name]
70
+ end
71
+
72
+ variable
73
+ end
74
+
75
+ # Stack operations
76
+
77
+ forthic_direct_word :POP, "( a:any -- )", "Removes top item from stack"
78
+ def POP(interp)
79
+ interp.stack_pop
80
+ end
81
+
82
+ forthic_direct_word :DUP, "( a:any -- a:any a:any )", "Duplicates top stack item"
83
+ def DUP(interp)
84
+ a = interp.stack_pop
85
+ interp.stack_push(a)
86
+ interp.stack_push(a)
87
+ end
88
+
89
+ forthic_direct_word :SWAP, "( a:any b:any -- b:any a:any )", "Swaps top two stack items"
90
+ def SWAP(interp)
91
+ b = interp.stack_pop
92
+ a = interp.stack_pop
93
+ interp.stack_push(b)
94
+ interp.stack_push(a)
95
+ end
96
+
97
+ forthic_word :PEEK_BANG, "( -- )", "Prints top of stack and stops execution", "PEEK!"
98
+ def PEEK_BANG
99
+ stack = interp.get_stack.get_items
100
+ if stack.length > 0
101
+ puts stack.last
102
+ else
103
+ puts "<STACK EMPTY>"
104
+ end
105
+ raise IntentionalStopError.new("PEEK!")
106
+ end
107
+
108
+ forthic_word :STACK_BANG, "( -- )", "Prints entire stack (reversed) and stops execution", "STACK!"
109
+ def STACK_BANG
110
+ stack = interp.get_stack.get_items.reverse
111
+ puts JSON.pretty_generate(stack)
112
+ raise IntentionalStopError.new("STACK!")
113
+ end
114
+
115
+ # Variable operations
116
+
117
+ forthic_word :VARIABLES, "( varnames:string[] -- )", "Creates variables in current module"
118
+ def VARIABLES(varnames)
119
+ mod = interp.cur_module
120
+ varnames.each do |v|
121
+ if v.match?(/^__/)
122
+ raise InvalidVariableNameError.new(
123
+ interp.get_top_input_string,
124
+ v,
125
+ location: interp.get_string_location
126
+ )
127
+ end
128
+ mod.add_variable(v)
129
+ end
130
+ nil
131
+ end
132
+
133
+ forthic_direct_word :set_var, "( value:any variable:any -- )", "Sets variable value (auto-creates if string name)", "!"
134
+ def set_var(interp)
135
+ variable = interp.stack_pop
136
+ value = interp.stack_pop
137
+ var_obj = if variable.is_a?(String)
138
+ self.class.get_or_create_variable(interp, variable)
139
+ else
140
+ variable
141
+ end
142
+ var_obj.set_value(value)
143
+ end
144
+
145
+ forthic_direct_word :get_var, "( variable:any -- value:any )", "Gets variable value (auto-creates if string name)", "@"
146
+ def get_var(interp)
147
+ variable = interp.stack_pop
148
+ var_obj = if variable.is_a?(String)
149
+ self.class.get_or_create_variable(interp, variable)
150
+ else
151
+ variable
152
+ end
153
+ interp.stack_push(var_obj.get_value)
154
+ end
155
+
156
+ forthic_direct_word :set_get_var, "( value:any variable:any -- value:any )", "Sets variable and returns value", "!@"
157
+ def set_get_var(interp)
158
+ variable = interp.stack_pop
159
+ value = interp.stack_pop
160
+ var_obj = if variable.is_a?(String)
161
+ self.class.get_or_create_variable(interp, variable)
162
+ else
163
+ variable
164
+ end
165
+ var_obj.set_value(value)
166
+ interp.stack_push(var_obj.get_value)
167
+ end
168
+
169
+ # Execution
170
+
171
+ forthic_direct_word :INTERPRET, "( string:string -- )", "Interprets Forthic string in current context"
172
+ def INTERPRET(interp)
173
+ string = interp.stack_pop
174
+ string_location = interp.get_string_location
175
+ interp.run(string, string_location) if string
176
+ end
177
+
178
+ # Module operations
179
+
180
+ forthic_word :EXPORT, "( names:string[] -- )", "Exports words from current module"
181
+ def EXPORT(names)
182
+ interp.cur_module.add_exportable(names)
183
+ nil
184
+ end
185
+
186
+ forthic_word :USE_MODULES, "( names:string[] -- )", "Imports modules by name", "USE-MODULES"
187
+ def USE_MODULES(names)
188
+ return nil unless names
189
+ interp.use_modules(names)
190
+ nil
191
+ end
192
+
193
+ # Control flow
194
+
195
+ forthic_word :IDENTITY, "( -- )", "Does nothing (identity operation)"
196
+ def IDENTITY
197
+ # No-op
198
+ nil
199
+ end
200
+
201
+ forthic_word :NOP, "( -- )", "Does nothing (no operation)"
202
+ def NOP
203
+ # No-op
204
+ nil
205
+ end
206
+
207
+ forthic_direct_word :NULL, "( -- null:null )", "Pushes null onto stack"
208
+ def NULL(interp)
209
+ interp.stack_push(nil)
210
+ end
211
+
212
+ forthic_word :ARRAY_Q, "( value:any -- boolean:boolean )", "Returns true if value is an array", "ARRAY?"
213
+ def ARRAY_Q(value)
214
+ value.is_a?(Array)
215
+ end
216
+
217
+ forthic_word :DEFAULT, "( value:any default_value:any -- result:any )", "Returns value or default if value is null/undefined/empty string"
218
+ def DEFAULT(value, default_value)
219
+ if value.nil? || value == ""
220
+ default_value
221
+ else
222
+ value
223
+ end
224
+ end
225
+
226
+ forthic_word :star_DEFAULT, "( value:any default_forthic:string -- result:any )", "Returns value or executes Forthic if value is null/undefined/empty string", "*DEFAULT"
227
+ def star_DEFAULT(value, default_forthic)
228
+ if value.nil? || value == ""
229
+ string_location = interp.get_string_location
230
+ interp.run(default_forthic, string_location)
231
+ interp.stack_pop
232
+ else
233
+ value
234
+ end
235
+ end
236
+
237
+ # Options
238
+
239
+ forthic_word :arrow, "( array:any[] -- options:WordOptions )", "Convert options array to WordOptions. Format: [.key1 val1 .key2 val2]", "~>"
240
+ def arrow(array)
241
+ WordOptions.new(array)
242
+ end
243
+
244
+ # Profiling
245
+
246
+ forthic_word :PROFILE_START, "( -- )", "Starts profiling word execution", "PROFILE-START"
247
+ def PROFILE_START
248
+ interp.start_profiling
249
+ nil
250
+ end
251
+
252
+ forthic_word :PROFILE_END, "( -- )", "Stops profiling word execution", "PROFILE-END"
253
+ def PROFILE_END
254
+ interp.stop_profiling
255
+ nil
256
+ end
257
+
258
+ forthic_word :PROFILE_TIMESTAMP, "( label:string -- )", "Records profiling timestamp with label", "PROFILE-TIMESTAMP"
259
+ def PROFILE_TIMESTAMP(label)
260
+ interp.add_timestamp(label)
261
+ nil
262
+ end
263
+
264
+ forthic_word :PROFILE_DATA, "( -- profile_data:object )", "Returns profiling data (word counts and timestamps)", "PROFILE-DATA"
265
+ def PROFILE_DATA
266
+ histogram = interp.word_histogram
267
+ timestamps = interp.profile_timestamps
268
+
269
+ result = {
270
+ word_counts: [],
271
+ timestamps: []
272
+ }
273
+
274
+ histogram.each do |val|
275
+ result[:word_counts] << { word: val[:word], count: val[:count] }
276
+ end
277
+
278
+ prev_time = 0.0
279
+ timestamps.each do |t|
280
+ result[:timestamps] << {
281
+ label: t.label,
282
+ time_ms: t.time_ms,
283
+ delta: t.time_ms - prev_time
284
+ }
285
+ prev_time = t.time_ms
286
+ end
287
+
288
+ result
289
+ end
290
+
291
+ # Logging
292
+
293
+ forthic_word :START_LOG, "( -- )", "Starts logging interpreter stream", "START-LOG"
294
+ def START_LOG
295
+ interp.start_stream
296
+ nil
297
+ end
298
+
299
+ forthic_word :END_LOG, "( -- )", "Ends logging interpreter stream", "END-LOG"
300
+ def END_LOG
301
+ interp.end_stream
302
+ nil
303
+ end
304
+
305
+ # String operations
306
+
307
+ forthic_word :INTERPOLATE, "( string:string [options:WordOptions] -- result:string )", "Interpolate variables (.name) and return result string. Use \\. to escape literal dots."
308
+ def INTERPOLATE(string, options = {})
309
+ separator = options[:separator] || options['separator'] || ", "
310
+ null_text = options[:null_text] || options['null_text'] || "null"
311
+ use_json = options[:json] || options['json'] || false
312
+
313
+ interpolate_string(string, separator, null_text, use_json)
314
+ end
315
+
316
+ forthic_word :PRINT, "( value:any [options:WordOptions] -- )", "Print value to stdout. Strings interpolate variables (.name). Non-strings formatted with options. Use \\. to escape literal dots in strings."
317
+ def PRINT(value, options = {})
318
+ separator = options[:separator] || options['separator'] || ", "
319
+ null_text = options[:null_text] || options['null_text'] || "null"
320
+ use_json = options[:json] || options['json'] || false
321
+
322
+ result = if value.is_a?(String)
323
+ # String: interpolate variables
324
+ interpolate_string(value, separator, null_text, use_json)
325
+ else
326
+ # Non-string: format directly
327
+ value_to_string(value, separator, null_text, use_json)
328
+ end
329
+ puts result
330
+ nil
331
+ end
332
+
333
+ private
334
+
335
+ def interpolate_string(string, separator, null_text, use_json)
336
+ string ||= ""
337
+
338
+ # First, handle escape sequences by replacing \. with a temporary placeholder
339
+ escaped = string.gsub(/\\\./, "\x00ESCAPED_DOT\x00")
340
+
341
+ # Replace whitespace-preceded or start-of-string .variable patterns
342
+ interpolated = escaped.gsub(/(?:^|(?<=\s))\.([a-zA-Z_][a-zA-Z0-9_-]*)/) do
343
+ var_name = ::Regexp.last_match(1)
344
+ variable = self.class.get_or_create_variable(interp, var_name)
345
+ value = variable.get_value
346
+ value_to_string(value, separator, null_text, use_json)
347
+ end
348
+
349
+ # Restore escaped dots
350
+ interpolated.gsub(/\x00ESCAPED_DOT\x00/, '.')
351
+ end
352
+
353
+ def value_to_string(value, separator, null_text, use_json)
354
+ return null_text if value.nil?
355
+ return value.to_json if use_json
356
+ return value.join(separator) if value.is_a?(Array)
357
+ return value.to_json if value.is_a?(Hash)
358
+ value.to_s
359
+ end
360
+ end
361
+ end
362
+ end