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,365 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../decorators/word'
4
+
5
+ module Forthic
6
+ module Modules
7
+ # MathModule - Mathematical operations and utilities
8
+ #
9
+ # Provides arithmetic operations, aggregation functions, type conversions,
10
+ # and common mathematical functions.
11
+ class MathModule < Decorators::DecoratedModule
12
+ # Register module documentation
13
+ module_doc <<~DOC
14
+ Mathematical operations and utilities including arithmetic, aggregation, and conversions.
15
+
16
+ ## Categories
17
+ - Arithmetic: +, -, *, /, ADD, SUBTRACT, MULTIPLY, DIVIDE, MOD
18
+ - Aggregates: MEAN, MAX, MIN, SUM
19
+ - Type conversion: >INT, >FLOAT, >FIXED, ROUND
20
+ - Special values: INFINITY, UNIFORM-RANDOM
21
+ - Math functions: ABS, SQRT, FLOOR, CEIL, CLAMP
22
+
23
+ ## Examples
24
+ 5 3 +
25
+ [1 2 3 4] SUM
26
+ [10 20 30] MEAN
27
+ 3.7 ROUND
28
+ 0 100 UNIFORM-RANDOM
29
+ DOC
30
+
31
+ def initialize
32
+ super("math")
33
+ end
34
+
35
+ # ========================================
36
+ # Arithmetic Operations
37
+ # ========================================
38
+
39
+ forthic_direct_word :plus, "( a:number b:number -- sum:number ) OR ( numbers:number[] -- sum:number )", "Add two numbers or sum array", "+"
40
+ def plus(interp)
41
+ b = interp.stack_pop
42
+
43
+ # Case 1: Array on top of stack
44
+ if b.is_a?(Array)
45
+ result = 0
46
+ b.each do |num|
47
+ result += num unless num.nil?
48
+ end
49
+ interp.stack_push(result)
50
+ return
51
+ end
52
+
53
+ # Case 2: Two numbers
54
+ a = interp.stack_pop
55
+ num_a = a.nil? ? 0 : a
56
+ num_b = b.nil? ? 0 : b
57
+ interp.stack_push(num_a + num_b)
58
+ end
59
+
60
+ forthic_direct_word :plus_ADD, "( a:number b:number -- sum:number ) OR ( numbers:number[] -- sum:number )", "Add two numbers or sum array", "ADD"
61
+ def plus_ADD(interp)
62
+ plus(interp)
63
+ end
64
+
65
+ forthic_word :minus, "( a:number b:number -- difference:number )", "Subtract b from a", "-"
66
+ def minus(a, b)
67
+ return nil if a.nil? || b.nil?
68
+ a - b
69
+ end
70
+
71
+ forthic_word :minus_SUBTRACT, "( a:number b:number -- difference:number )", "Subtract b from a", "SUBTRACT"
72
+ def minus_SUBTRACT(a, b)
73
+ return nil if a.nil? || b.nil?
74
+ a - b
75
+ end
76
+
77
+ forthic_direct_word :times, "( a:number b:number -- product:number ) OR ( numbers:number[] -- product:number )", "Multiply two numbers or product of array", "*"
78
+ def times(interp)
79
+ b = interp.stack_pop
80
+
81
+ # Case 1: Array on top of stack
82
+ if b.is_a?(Array)
83
+ result = 1
84
+ b.each do |num|
85
+ if num.nil?
86
+ interp.stack_push(nil)
87
+ return
88
+ end
89
+ result *= num
90
+ end
91
+ interp.stack_push(result)
92
+ return
93
+ end
94
+
95
+ # Case 2: Two numbers
96
+ a = interp.stack_pop
97
+ if a.nil? || b.nil?
98
+ interp.stack_push(nil)
99
+ return
100
+ end
101
+ interp.stack_push(a * b)
102
+ end
103
+
104
+ forthic_direct_word :times_MULTIPLY, "( a:number b:number -- product:number ) OR ( numbers:number[] -- product:number )", "Multiply two numbers or product of array", "MULTIPLY"
105
+ def times_MULTIPLY(interp)
106
+ times(interp)
107
+ end
108
+
109
+ forthic_word :divide_by, "( a:number b:number -- quotient:number )", "Divide a by b", "/"
110
+ def divide_by(a, b)
111
+ return nil if a.nil? || b.nil?
112
+ return nil if b == 0
113
+ a.to_f / b
114
+ end
115
+
116
+ forthic_word :divide_by_DIVIDE, "( a:number b:number -- quotient:number )", "Divide a by b", "DIVIDE"
117
+ def divide_by_DIVIDE(a, b)
118
+ return nil if a.nil? || b.nil?
119
+ return nil if b == 0
120
+ a.to_f / b
121
+ end
122
+
123
+ forthic_word :MOD, "( m:number n:number -- remainder:number )", "Modulo operation (m % n)"
124
+ def MOD(m, n)
125
+ return nil if m.nil? || n.nil?
126
+ m % n
127
+ end
128
+
129
+ # ========================================
130
+ # Aggregation Operations
131
+ # ========================================
132
+
133
+ forthic_word :MEAN, "( items:any[] -- mean:any )", "Calculate mean of array (handles numbers, strings, objects)"
134
+ def MEAN(items)
135
+ if items.nil? || (items.is_a?(Array) && items.empty?)
136
+ return 0
137
+ end
138
+
139
+ return items unless items.is_a?(Array)
140
+
141
+ return items[0] if items.length == 1
142
+
143
+ # Filter out nil values
144
+ filtered = items.reject(&:nil?)
145
+
146
+ return 0 if filtered.empty?
147
+
148
+ # Check type of first non-null item
149
+ first = filtered[0]
150
+
151
+ # Case 1: Numbers
152
+ if first.is_a?(Numeric)
153
+ sum = filtered.reduce(0) { |acc, val| acc + val }
154
+ return sum.to_f / filtered.length
155
+ end
156
+
157
+ # Case 2: Strings - return frequency distribution
158
+ if first.is_a?(String)
159
+ counts = {}
160
+ filtered.each do |item|
161
+ counts[item] = (counts[item] || 0) + 1
162
+ end
163
+ result = {}
164
+ counts.each do |key, count|
165
+ result[key] = count.to_f / filtered.length
166
+ end
167
+ return result
168
+ end
169
+
170
+ # Case 3: Objects - field-wise mean
171
+ if first.is_a?(Hash)
172
+ result = {}
173
+ all_keys = Set.new
174
+
175
+ # Collect all keys
176
+ filtered.each do |obj|
177
+ obj.each_key { |key| all_keys.add(key) }
178
+ end
179
+
180
+ # Compute mean for each key
181
+ all_keys.each do |key|
182
+ values = filtered.map { |obj| obj[key] }.reject(&:nil?)
183
+
184
+ next if values.empty?
185
+
186
+ first_val = values[0]
187
+
188
+ if first_val.is_a?(Numeric)
189
+ sum = values.reduce(0) { |acc, val| acc + val }
190
+ result[key] = sum.to_f / values.length
191
+ elsif first_val.is_a?(String)
192
+ counts = {}
193
+ values.each do |val|
194
+ counts[val] = (counts[val] || 0) + 1
195
+ end
196
+ freqs = {}
197
+ counts.each do |k, count|
198
+ freqs[k] = count.to_f / values.length
199
+ end
200
+ result[key] = freqs
201
+ end
202
+ end
203
+
204
+ return result
205
+ end
206
+
207
+ 0
208
+ end
209
+
210
+ forthic_direct_word :MAX, "( a:number b:number -- max:number ) OR ( items:number[] -- max:number )", "Maximum of two numbers or array"
211
+ def MAX(interp)
212
+ b = interp.stack_pop
213
+
214
+ # Case 1: Array on top of stack
215
+ if b.is_a?(Array)
216
+ if b.empty?
217
+ interp.stack_push(nil)
218
+ return
219
+ end
220
+ interp.stack_push(b.max)
221
+ return
222
+ end
223
+
224
+ # Case 2: Two values
225
+ a = interp.stack_pop
226
+ interp.stack_push([a, b].max)
227
+ end
228
+
229
+ forthic_direct_word :MIN, "( a:number b:number -- min:number ) OR ( items:number[] -- min:number )", "Minimum of two numbers or array"
230
+ def MIN(interp)
231
+ b = interp.stack_pop
232
+
233
+ # Case 1: Array on top of stack
234
+ if b.is_a?(Array)
235
+ if b.empty?
236
+ interp.stack_push(nil)
237
+ return
238
+ end
239
+ interp.stack_push(b.min)
240
+ return
241
+ end
242
+
243
+ # Case 2: Two values
244
+ a = interp.stack_pop
245
+ interp.stack_push([a, b].min)
246
+ end
247
+
248
+ forthic_word :SUM, "( numbers:number[] -- sum:number )", "Sum of array (explicit)"
249
+ def SUM(numbers)
250
+ return 0 if numbers.nil? || !numbers.is_a?(Array)
251
+
252
+ result = 0
253
+ numbers.each do |num|
254
+ result += num unless num.nil?
255
+ end
256
+ result
257
+ end
258
+
259
+ # ========================================
260
+ # Type Conversion
261
+ # ========================================
262
+
263
+ forthic_word :to_INT, "( a:any -- int:number )", "Convert to integer (returns length for arrays/objects, 0 for null)", ">INT"
264
+ def to_INT(a)
265
+ return 0 if a.nil?
266
+
267
+ if a.is_a?(Array)
268
+ return a.length
269
+ elsif a.is_a?(Hash)
270
+ return a.keys.length
271
+ end
272
+
273
+ begin
274
+ Float(a).truncate
275
+ rescue StandardError
276
+ nil
277
+ end
278
+ end
279
+
280
+ forthic_word :to_FLOAT, "( a:any -- float:number )", "Convert to float", ">FLOAT"
281
+ def to_FLOAT(a)
282
+ return 0.0 if a.nil?
283
+
284
+ begin
285
+ Float(a)
286
+ rescue StandardError
287
+ nil
288
+ end
289
+ end
290
+
291
+ forthic_word :to_FIXED, "( num:number digits:number -- result:string )", "Format number with fixed decimal places", ">FIXED"
292
+ def to_FIXED(num, digits)
293
+ return nil if num.nil?
294
+
295
+ format("%.#{digits}f", num)
296
+ end
297
+
298
+ forthic_word :ROUND, "( num:number -- int:number )", "Round to nearest integer"
299
+ def ROUND(num)
300
+ return nil if num.nil?
301
+
302
+ num.round
303
+ end
304
+
305
+ # ========================================
306
+ # Special Values
307
+ # ========================================
308
+
309
+ forthic_word :INFINITY, "( -- infinity:number )", "Push Infinity value"
310
+ def INFINITY
311
+ Float::INFINITY
312
+ end
313
+
314
+ forthic_word :UNIFORM_RANDOM, "( low:number high:number -- random:number )", "Generate random number in range [low, high)", "UNIFORM-RANDOM"
315
+ def UNIFORM_RANDOM(low, high)
316
+ rand * (high - low) + low
317
+ end
318
+
319
+ # ========================================
320
+ # Math Functions
321
+ # ========================================
322
+
323
+ forthic_direct_word :ABS, "( n:number -- abs:number )", "Absolute value"
324
+ def ABS(interp)
325
+ n = interp.stack_pop
326
+ result = n.nil? ? nil : n.abs
327
+ interp.stack_push(result)
328
+ end
329
+
330
+ forthic_direct_word :SQRT, "( n:number -- sqrt:number )", "Square root"
331
+ def SQRT(interp)
332
+ n = interp.stack_pop
333
+ result = n.nil? ? nil : Math.sqrt(n)
334
+ interp.stack_push(result)
335
+ end
336
+
337
+ forthic_direct_word :FLOOR, "( n:number -- floor:number )", "Round down to integer"
338
+ def FLOOR(interp)
339
+ n = interp.stack_pop
340
+ result = n.nil? ? nil : n.floor
341
+ interp.stack_push(result)
342
+ end
343
+
344
+ forthic_direct_word :CEIL, "( n:number -- ceil:number )", "Round up to integer"
345
+ def CEIL(interp)
346
+ n = interp.stack_pop
347
+ result = n.nil? ? nil : n.ceil
348
+ interp.stack_push(result)
349
+ end
350
+
351
+ forthic_direct_word :CLAMP, "( value:number min:number max:number -- clamped:number )", "Constrain value to range [min, max]"
352
+ def CLAMP(interp)
353
+ max = interp.stack_pop
354
+ min = interp.stack_pop
355
+ value = interp.stack_pop
356
+ result = if value.nil? || min.nil? || max.nil?
357
+ nil
358
+ else
359
+ [[min, value].max, max].min
360
+ end
361
+ interp.stack_push(result)
362
+ end
363
+ end
364
+ end
365
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../decorators/word'
4
+ require 'json'
5
+
6
+ module Forthic
7
+ module Modules
8
+ # RecordModule - Record (object/dictionary) manipulation operations
9
+ #
10
+ # Provides operations for working with key-value data structures (hashes).
11
+ class RecordModule < Decorators::DecoratedModule
12
+ # Register module documentation
13
+ module_doc <<~DOC
14
+ Record (object/dictionary) manipulation operations for working with key-value data structures.
15
+
16
+ ## Categories
17
+ - Core: REC, REC@, |REC@, <REC!
18
+ - Transform: RELABEL, INVERT-KEYS, REC-DEFAULTS, <DEL
19
+ - Access: KEYS, VALUES
20
+ DOC
21
+
22
+ def initialize
23
+ super("record")
24
+ end
25
+
26
+ # Helper function to drill down into nested record structure
27
+ #
28
+ # @param record [Hash] The record to drill into
29
+ # @param fields [Array<String>] Array of field names to traverse
30
+ # @return [Any] The value at the end of the field path, or nil if not found
31
+ def self.drill_for_value(record, fields)
32
+ result = record
33
+ fields.each do |field|
34
+ return nil if result.nil?
35
+ result = result[field]
36
+ end
37
+ result
38
+ end
39
+
40
+ forthic_word :REC, "( key_vals:any[] -- rec:any )", "Create record from [[key, val], ...] pairs"
41
+ def REC(key_vals)
42
+ _key_vals = key_vals || []
43
+
44
+ result = {}
45
+ _key_vals.each do |pair|
46
+ key = nil
47
+ val = nil
48
+ if pair
49
+ key = pair[0] if pair.length >= 1
50
+ val = pair[1] if pair.length >= 2
51
+ end
52
+ result[key] = val
53
+ end
54
+
55
+ result
56
+ end
57
+
58
+ forthic_word :REC_at, "( rec:any field:any -- value:any )", "Get value from record by field or array of fields", "REC@"
59
+ def REC_at(rec, field)
60
+ return nil unless rec
61
+
62
+ fields = field.is_a?(Array) ? field : [field]
63
+
64
+ self.class.drill_for_value(rec, fields)
65
+ end
66
+
67
+ forthic_word :pipe_REC_at, "( records:any field:any -- values:any )", "Map REC@ over array of records", "|REC@"
68
+ def pipe_REC_at(records, field)
69
+ # Push records back and field, then use MAP with REC@
70
+ interp.stack_push(records)
71
+ interp.run("'#{field.to_json} REC@' MAP")
72
+ nil # Result already on stack from MAP
73
+ end
74
+
75
+ forthic_word :l_REC_bang, "( rec:any value:any field:any -- rec:any )", "Set value in record at field path", "<REC!"
76
+ def l_REC_bang(rec, value, field)
77
+ _rec = rec || {}
78
+
79
+ fields = field.is_a?(Array) ? field : [field]
80
+
81
+ # Helper to ensure field exists
82
+ ensure_field = lambda do |record, field_name|
83
+ res = record[field_name]
84
+ if res.nil?
85
+ res = {}
86
+ record[field_name] = res
87
+ end
88
+ res
89
+ end
90
+
91
+ cur_rec = _rec
92
+ # Drill down up until the last value
93
+ (0...fields.length - 1).each do |i|
94
+ cur_rec = ensure_field.call(cur_rec, fields[i])
95
+ end
96
+
97
+ # Set the value at the right depth within rec
98
+ cur_rec[fields.last] = value
99
+
100
+ _rec
101
+ end
102
+
103
+ forthic_word :RELABEL, "( container:any old_keys:any[] new_keys:any[] -- container:any )", "Rename record keys"
104
+ def RELABEL(container, old_keys, new_keys)
105
+ return container unless container
106
+
107
+ if old_keys.length != new_keys.length
108
+ raise StandardError, "RELABEL: old_keys and new_keys must be same length"
109
+ end
110
+
111
+ new_to_old = {}
112
+ old_keys.each_with_index do |old_key, i|
113
+ new_to_old[new_keys[i]] = old_key
114
+ end
115
+
116
+ if container.is_a?(Array)
117
+ result = []
118
+ new_to_old.keys.sort.each do |k|
119
+ result.push(container[new_to_old[k]])
120
+ end
121
+ else
122
+ result = {}
123
+ new_to_old.each do |new_key, old_key|
124
+ result[new_key] = container[old_key]
125
+ end
126
+ end
127
+
128
+ result
129
+ end
130
+
131
+ forthic_word :INVERT_KEYS, "( record:any -- inverted:any )", "Invert two-level nested record structure", "INVERT-KEYS"
132
+ def INVERT_KEYS(record)
133
+ result = {}
134
+ record.each do |first_key, sub_record|
135
+ sub_record.each do |second_key, value|
136
+ result[second_key] ||= {}
137
+ result[second_key][first_key] = value
138
+ end
139
+ end
140
+
141
+ result
142
+ end
143
+
144
+ forthic_word :REC_DEFAULTS, "( record:any key_vals:any[] -- record:any )", "Set default values for missing/empty fields", "REC-DEFAULTS"
145
+ def REC_DEFAULTS(record, key_vals)
146
+ key_vals.each do |key_val|
147
+ key = key_val[0]
148
+ value = record[key]
149
+ if value.nil? || value == ""
150
+ record[key] = key_val[1]
151
+ end
152
+ end
153
+
154
+ record
155
+ end
156
+
157
+ forthic_word :l_DEL, "( container:any key:any -- container:any )", "Delete key from record or index from array", "<DEL"
158
+ def l_DEL(container, key)
159
+ return container unless container
160
+
161
+ if container.is_a?(Array)
162
+ container.delete_at(key)
163
+ else
164
+ container.delete(key)
165
+ end
166
+
167
+ container
168
+ end
169
+
170
+ forthic_word :KEYS, "( container:any -- keys:any[] )", "Get keys from record or indices from array"
171
+ def KEYS(container)
172
+ _container = container || []
173
+
174
+ if _container.is_a?(Array)
175
+ result = []
176
+ _container.length.times do |i|
177
+ result.push(i)
178
+ end
179
+ else
180
+ result = _container.keys
181
+ end
182
+
183
+ result
184
+ end
185
+
186
+ forthic_word :VALUES, "( container:any -- values:any[] )", "Get values from record or elements from array"
187
+ def VALUES(container)
188
+ _container = container || []
189
+
190
+ if _container.is_a?(Array)
191
+ result = _container
192
+ else
193
+ result = []
194
+ _container.each_key do |k|
195
+ result.push(_container[k])
196
+ end
197
+ end
198
+
199
+ result
200
+ end
201
+ end
202
+ end
203
+ end