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,940 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../decorators/word'
4
+ require 'set'
5
+
6
+ module Forthic
7
+ module Modules
8
+ # ArrayModule - Array and collection operations
9
+ #
10
+ # Provides operations for manipulating arrays and records (hashes).
11
+ class ArrayModule < Decorators::DecoratedModule
12
+ module_doc <<~DOC
13
+ Array and collection operations for manipulating arrays and records.
14
+
15
+ ## Categories
16
+ - Access: NTH, LAST, SLICE, TAKE, DROP, LENGTH, INDEX, KEY-OF
17
+ - Transform: MAP, REVERSE
18
+ - Combine: APPEND, ZIP, ZIP_WITH, CONCAT
19
+ - Filter: SELECT, UNIQUE, DIFFERENCE, INTERSECTION, UNION
20
+ - Sort: SORT, SHUFFLE, ROTATE
21
+ - Group: BY_FIELD, GROUP-BY-FIELD, GROUP_BY, GROUPS_OF
22
+ - Utility: <REPEAT, FOREACH, REDUCE, UNPACK, FLATTEN
23
+
24
+ ## Options
25
+ Several words support options via the ~> operator using syntax: [.option_name value ...] ~> WORD
26
+ - with_key: Push index/key before value (MAP, FOREACH, GROUP-BY, SELECT)
27
+ - push_error: Push error array after execution (MAP, FOREACH)
28
+ - depth: Recursion depth for nested operations (MAP, FLATTEN)
29
+ - push_rest: Push remaining items after operation (MAP, TAKE)
30
+ - comparator: Custom comparison function as Forthic string (SORT)
31
+
32
+ ## Examples
33
+ [10 20 30] '2 *' MAP
34
+ [10 20 30] '+ 2 *' [.with_key TRUE] ~> MAP
35
+ [[[1 2]] [[3 4]]] [.depth 1] ~> FLATTEN
36
+ [3 1 4 1 5] [.comparator "SWAP -"] ~> SORT
37
+ [.with_key TRUE .push_error TRUE] ~> MAP
38
+ DOC
39
+
40
+ def initialize
41
+ super("array")
42
+ end
43
+
44
+ # Basic operations
45
+
46
+ forthic_word :APPEND, "( container:any item:any -- container:any )", "Append item to array or add key-value to record"
47
+ def APPEND(container, item)
48
+ result = container || []
49
+
50
+ if result.is_a?(Array)
51
+ result.push(item)
52
+ else
53
+ # If not a list, treat as record - item should be [key, value]
54
+ result[item[0]] = item[1]
55
+ end
56
+
57
+ result
58
+ end
59
+
60
+ forthic_word :REVERSE, "( container:any -- container:any )", "Reverse array"
61
+ def REVERSE(container)
62
+ return container unless container
63
+
64
+ result = container
65
+ result = result.reverse if result.is_a?(Array)
66
+
67
+ result
68
+ end
69
+
70
+ forthic_word :UNIQUE, "( array:any[] -- array:any[] )", "Remove duplicates from array"
71
+ def UNIQUE(array)
72
+ return array unless array
73
+
74
+ result = array
75
+ result = array.uniq if array.is_a?(Array)
76
+
77
+ result
78
+ end
79
+
80
+ forthic_word :LENGTH, "( container:any -- length:number )", "Get length of array, record, or string"
81
+ def LENGTH(container)
82
+ return 0 unless container
83
+
84
+ if container.is_a?(Array)
85
+ container.length
86
+ elsif container.is_a?(String)
87
+ container.length
88
+ else
89
+ container.keys.length
90
+ end
91
+ end
92
+
93
+ forthic_direct_word :NTH, "( container:any n:number -- item:any )", "Get nth element from array or record"
94
+ def NTH(interp)
95
+ n = interp.stack_pop
96
+ container = interp.stack_pop
97
+
98
+ result = if n.nil? || !container
99
+ nil
100
+ elsif container.is_a?(Array)
101
+ (n < 0 || n >= container.length) ? nil : container[n]
102
+ else
103
+ keys = container.keys.sort
104
+ if n < 0 || n >= keys.length
105
+ nil
106
+ else
107
+ container[keys[n]]
108
+ end
109
+ end
110
+
111
+ interp.stack_push(result)
112
+ end
113
+
114
+ forthic_word :LAST, "( container:any -- item:any )", "Get last element from array or record"
115
+ def LAST(container)
116
+ return nil unless container
117
+
118
+ if container.is_a?(Array)
119
+ return nil if container.empty?
120
+ container.last
121
+ else
122
+ keys = container.keys.sort
123
+ return nil if keys.empty?
124
+ container[keys.last]
125
+ end
126
+ end
127
+
128
+ forthic_word :SLICE, "( container:any start:number end:number -- result:any )", "Extract slice from array or record"
129
+ def SLICE(container, start, end_pos)
130
+ _container = container || []
131
+
132
+ start = start.to_i
133
+ end_pos = end_pos.to_i
134
+
135
+ length = if _container.is_a?(Array)
136
+ _container.length
137
+ else
138
+ _container.keys.length
139
+ end
140
+
141
+ normalize_index = lambda do |index|
142
+ index < 0 ? index + length : index
143
+ end
144
+
145
+ start = normalize_index.call(start)
146
+ end_pos = normalize_index.call(end_pos)
147
+
148
+ step = start > end_pos ? -1 : 1
149
+ indexes = [start]
150
+
151
+ if start < 0 || start >= length
152
+ # Return empty result
153
+ return _container.is_a?(Array) ? [] : {}
154
+ end
155
+
156
+ while start != end_pos
157
+ start += step
158
+ if start < 0 || start >= length
159
+ indexes << nil
160
+ else
161
+ indexes << start
162
+ end
163
+ end
164
+
165
+ if _container.is_a?(Array)
166
+ result = []
167
+ indexes.each do |i|
168
+ result << (i.nil? ? nil : _container[i])
169
+ end
170
+ result
171
+ else
172
+ keys = _container.keys.sort
173
+ result = {}
174
+ indexes.each do |i|
175
+ unless i.nil?
176
+ k = keys[i]
177
+ result[k] = _container[k]
178
+ end
179
+ end
180
+ result
181
+ end
182
+ end
183
+
184
+ forthic_word :TAKE, "( container:any[] n:number [options:WordOptions] -- result:any[] )", "Take first n elements"
185
+ def TAKE(container, n, options = {})
186
+ flags = {
187
+ with_key: options[:with_key] || options['with_key'],
188
+ push_rest: options[:push_rest] || options['push_rest']
189
+ }
190
+
191
+ container ||= []
192
+
193
+ taken, rest = if container.is_a?(Array)
194
+ [container[0...n], container[n..-1] || []]
195
+ else
196
+ keys = container.keys.sort
197
+ taken_keys = keys[0...n]
198
+ rest_keys = keys[n..-1] || []
199
+ [taken_keys.map { |k| container[k] }, rest_keys.map { |k| container[k] }]
200
+ end
201
+
202
+ if flags[:push_rest]
203
+ interp.stack_push(taken)
204
+ return rest
205
+ end
206
+
207
+ taken
208
+ end
209
+
210
+ forthic_word :DROP, "( container:any n:number -- result:any )", "Drop first n elements from array or record"
211
+ def DROP(container, n)
212
+ return [] unless container
213
+ return container if n <= 0
214
+
215
+ if container.is_a?(Array)
216
+ container[n..-1] || []
217
+ else
218
+ keys = container.keys.sort
219
+ rest_keys = keys[n..-1] || []
220
+ rest_keys.map { |k| container[k] }
221
+ end
222
+ end
223
+
224
+ # Set operations
225
+
226
+ forthic_word :DIFFERENCE, "( lcontainer:any rcontainer:any -- result:any )", "Set difference between two containers"
227
+ def DIFFERENCE(lcontainer, rcontainer)
228
+ _lcontainer = lcontainer || []
229
+ _rcontainer = rcontainer || []
230
+
231
+ difference = lambda do |l, r|
232
+ l.select { |item| !r.include?(item) }
233
+ end
234
+
235
+ if _rcontainer.is_a?(Array)
236
+ difference.call(_lcontainer, _rcontainer)
237
+ else
238
+ lkeys = _lcontainer.keys
239
+ rkeys = _rcontainer.keys
240
+ diff = difference.call(lkeys, rkeys)
241
+ result = {}
242
+ diff.each { |k| result[k] = _lcontainer[k] }
243
+ result
244
+ end
245
+ end
246
+
247
+ forthic_word :INTERSECTION, "( lcontainer:any rcontainer:any -- result:any )", "Set intersection between two containers"
248
+ def INTERSECTION(lcontainer, rcontainer)
249
+ _lcontainer = lcontainer || []
250
+ _rcontainer = rcontainer || []
251
+
252
+ intersection = lambda do |l, r|
253
+ l.select { |item| r.include?(item) }
254
+ end
255
+
256
+ if _rcontainer.is_a?(Array)
257
+ intersection.call(_lcontainer, _rcontainer)
258
+ else
259
+ lkeys = _lcontainer.keys
260
+ rkeys = _rcontainer.keys
261
+ inter = intersection.call(lkeys, rkeys)
262
+ result = {}
263
+ inter.each { |k| result[k] = _lcontainer[k] }
264
+ result
265
+ end
266
+ end
267
+
268
+ forthic_word :UNION, "( lcontainer:any rcontainer:any -- result:any )", "Set union between two containers"
269
+ def UNION(lcontainer, rcontainer)
270
+ lcontainer ||= []
271
+ rcontainer ||= []
272
+
273
+ union_fn = lambda do |l, r|
274
+ (l + r).uniq
275
+ end
276
+
277
+ if rcontainer.is_a?(Array)
278
+ union_fn.call(lcontainer, rcontainer)
279
+ else
280
+ lkeys = lcontainer.keys
281
+ rkeys = rcontainer.keys
282
+ keys = union_fn.call(lkeys, rkeys)
283
+ result = {}
284
+ keys.each do |k|
285
+ val = lcontainer[k]
286
+ val = rcontainer[k] if val.nil?
287
+ result[k] = val
288
+ end
289
+ result
290
+ end
291
+ end
292
+
293
+ # Sort and shuffle
294
+
295
+ forthic_word :SORT, "( container:any[] [options:WordOptions] -- array:any[] )", "Sort container. Options: comparator (string or function). Example: [3 1 4] [.comparator \"-1 *\"] ~> SORT"
296
+ def SORT(container, options = {})
297
+ return container unless container
298
+ return container unless container.is_a?(Array)
299
+
300
+ comparator = options[:comparator] || options['comparator']
301
+ flag_string_position = interp.get_string_location
302
+
303
+ # Default sort - handle nils by putting them at the end
304
+ if comparator.nil?
305
+ return container.sort do |a, b|
306
+ if a.nil? && b.nil?
307
+ 0
308
+ elsif a.nil?
309
+ 1 # nil goes after non-nil
310
+ elsif b.nil?
311
+ -1 # non-nil goes before nil
312
+ else
313
+ a <=> b
314
+ end
315
+ end
316
+ end
317
+
318
+ # Sort using a forthic string as a key function
319
+ if comparator.is_a?(String)
320
+ aug_array = container.map do |val|
321
+ interp.stack_push(val)
322
+ interp.run(comparator, flag_string_position)
323
+ aug_val = interp.stack_pop
324
+ [val, aug_val]
325
+ end
326
+
327
+ aug_array.sort! do |l, r|
328
+ l_val = l[1]
329
+ r_val = r[1]
330
+ l_val <=> r_val
331
+ end
332
+
333
+ return aug_array.map { |aug_val| aug_val[0] }
334
+ end
335
+
336
+ # Sort with key function (proc/lambda)
337
+ container.sort do |l, r|
338
+ l_val = comparator.call(l)
339
+ r_val = comparator.call(r)
340
+ l_val <=> r_val
341
+ end
342
+ end
343
+
344
+ forthic_word :SHUFFLE, "( array:any[] -- array:any[] )", "Shuffle array randomly"
345
+ def SHUFFLE(array)
346
+ return array unless array
347
+
348
+ array.shuffle
349
+ end
350
+
351
+ forthic_word :ROTATE, "( container:any -- container:any )", "Rotate container by moving last element to front"
352
+ def ROTATE(container)
353
+ return container unless container
354
+
355
+ result = container
356
+ if container.is_a?(Array) && !container.empty?
357
+ result = container.dup
358
+ val = result.pop
359
+ result.unshift(val)
360
+ end
361
+
362
+ result
363
+ end
364
+
365
+ # Unpacking and flattening
366
+
367
+ forthic_word :UNPACK, "( container:any -- elements:any )", "Unpack array or record elements onto stack"
368
+ def UNPACK(container)
369
+ _container = container || []
370
+
371
+ if _container.is_a?(Array)
372
+ _container.each do |item|
373
+ interp.stack_push(item)
374
+ end
375
+ else
376
+ keys = _container.keys.sort
377
+ keys.each do |k|
378
+ interp.stack_push(_container[k])
379
+ end
380
+ end
381
+
382
+ nil # Return nil so nothing gets auto-pushed
383
+ end
384
+
385
+ forthic_word :FLATTEN, "( container:any [options:WordOptions] -- flat:any )", "Flatten nested arrays or records. Options: depth (number). Example: [[[1 2]]] [.depth 1] ~> FLATTEN"
386
+ def FLATTEN(container, options = {})
387
+ return [] unless container
388
+
389
+ depth = options[:depth] || options['depth']
390
+
391
+ # Helpers defined as lambdas
392
+ fully_flatten_array = nil
393
+ fully_flatten_array = lambda do |items, accum|
394
+ items.each do |item|
395
+ if item.is_a?(Array)
396
+ fully_flatten_array.call(item, accum)
397
+ else
398
+ accum << item
399
+ end
400
+ end
401
+ accum
402
+ end
403
+
404
+ flatten_array = nil
405
+ flatten_array = lambda do |items, depth_val, accum = []|
406
+ return fully_flatten_array.call(items, accum) if depth_val.nil?
407
+
408
+ items.each do |item|
409
+ if depth_val > 0 && item.is_a?(Array)
410
+ flatten_array.call(item, depth_val - 1, accum)
411
+ else
412
+ accum << item
413
+ end
414
+ end
415
+ accum
416
+ end
417
+
418
+ is_record = lambda do |obj|
419
+ obj.is_a?(Hash) && !obj.keys.empty?
420
+ end
421
+
422
+ add_to_record_result = lambda do |item, key, keys, result|
423
+ new_key = (keys + [key]).join("\t")
424
+ result[new_key] = item
425
+ end
426
+
427
+ fully_flatten_record = nil
428
+ fully_flatten_record = lambda do |record, res, keys|
429
+ record.keys.each do |k|
430
+ item = record[k]
431
+ if is_record.call(item)
432
+ fully_flatten_record.call(item, res, keys + [k])
433
+ else
434
+ add_to_record_result.call(item, k, keys, res)
435
+ end
436
+ end
437
+ res
438
+ end
439
+
440
+ flatten_record = nil
441
+ flatten_record = lambda do |record, depth_val, res, keys|
442
+ return fully_flatten_record.call(record, res, keys) if depth_val.nil?
443
+
444
+ record.keys.each do |k|
445
+ item = record[k]
446
+ if depth_val > 0 && is_record.call(item)
447
+ flatten_record.call(item, depth_val - 1, res, keys + [k])
448
+ else
449
+ add_to_record_result.call(item, k, keys, res)
450
+ end
451
+ end
452
+ res
453
+ end
454
+
455
+ if container.is_a?(Array)
456
+ flatten_array.call(container, depth)
457
+ else
458
+ flatten_record.call(container, depth, {}, [])
459
+ end
460
+ end
461
+
462
+ forthic_word :REDUCE, "( container:any initial:any forthic:string -- result:any )", "Reduce array or record with accumulator"
463
+ def REDUCE(container, initial, forthic)
464
+ _container = container || []
465
+
466
+ string_location = interp.get_string_location
467
+
468
+ interp.stack_push(initial)
469
+
470
+ if _container.is_a?(Array)
471
+ _container.each do |item|
472
+ interp.stack_push(item)
473
+ interp.run(forthic, string_location)
474
+ end
475
+ else
476
+ _container.keys.each do |k|
477
+ v = _container[k]
478
+ interp.stack_push(v)
479
+ interp.run(forthic, string_location)
480
+ end
481
+ end
482
+
483
+ interp.stack_pop
484
+ end
485
+
486
+ # Zip operations
487
+
488
+ forthic_word :ZIP, "( container1:any[] container2:any[] -- result:any[] )", "Zip two arrays into array of pairs"
489
+ def ZIP(container1, container2)
490
+ container1 ||= []
491
+ container2 ||= []
492
+
493
+ if container2.is_a?(Array)
494
+ result = []
495
+ container1.each_with_index do |item, i|
496
+ value2 = i < container2.length ? container2[i] : nil
497
+ result << [item, value2]
498
+ end
499
+ result
500
+ else
501
+ result = {}
502
+ container1.keys.each do |k|
503
+ v = container1[k]
504
+ result[k] = [v, container2[k]]
505
+ end
506
+ result
507
+ end
508
+ end
509
+
510
+ forthic_word :ZIP_WITH, "( container1:any[] container2:any[] forthic:string -- result:any[] )", "Zip two arrays with combining function", "ZIP-WITH"
511
+ def ZIP_WITH(container1, container2, forthic)
512
+ string_location = interp.get_string_location
513
+
514
+ container1 ||= []
515
+ container2 ||= []
516
+
517
+ if container2.is_a?(Array)
518
+ result = []
519
+ container1.each_with_index do |item, i|
520
+ value2 = i < container2.length ? container2[i] : nil
521
+ interp.stack_push(item)
522
+ interp.stack_push(value2)
523
+ interp.run(forthic, string_location)
524
+ result << interp.stack_pop
525
+ end
526
+ result
527
+ else
528
+ result = {}
529
+ container1.keys.each do |k|
530
+ interp.stack_push(container1[k])
531
+ interp.stack_push(container2[k])
532
+ interp.run(forthic, string_location)
533
+ result[k] = interp.stack_pop
534
+ end
535
+ result
536
+ end
537
+ end
538
+
539
+ forthic_word :INDEX, "( items:any[] forthic:string -- indexed:any )", "Create index mapping from array indices to values"
540
+ def INDEX(items, forthic)
541
+ string_location = interp.get_string_location
542
+
543
+ unless items
544
+ interp.stack_push(items)
545
+ return nil
546
+ end
547
+
548
+ result = {}
549
+ items.each do |item|
550
+ interp.stack_push(item)
551
+ interp.run(forthic, string_location)
552
+ keys = interp.stack_pop
553
+ keys.each do |k|
554
+ lowercased_key = k.downcase
555
+ if result[lowercased_key]
556
+ result[lowercased_key] << item
557
+ else
558
+ result[lowercased_key] = [item]
559
+ end
560
+ end
561
+ end
562
+
563
+ result
564
+ end
565
+
566
+ forthic_direct_word :KEY_OF, "( container:any value:any -- key:any )", "Find key of value in container", "KEY-OF"
567
+ def KEY_OF(interp)
568
+ value = interp.stack_pop
569
+ container = interp.stack_pop
570
+
571
+ result = if !container
572
+ nil
573
+ elsif container.is_a?(Array)
574
+ container.index(value)
575
+ else
576
+ found_key = nil
577
+ container.keys.each do |key|
578
+ if container[key] == value
579
+ found_key = key
580
+ break
581
+ end
582
+ end
583
+ found_key
584
+ end
585
+
586
+ interp.stack_push(result)
587
+ end
588
+
589
+ # Selection and filtering
590
+
591
+ forthic_word :SELECT, "( container:any forthic:string [options:WordOptions] -- filtered:any )", "Filter items with predicate. Options: with_key (bool)"
592
+ def SELECT(container, forthic, options = {})
593
+ string_location = interp.get_string_location
594
+
595
+ with_key = options[:with_key] || options['with_key']
596
+
597
+ unless container
598
+ interp.stack_push(container)
599
+ return nil
600
+ end
601
+
602
+ if container.is_a?(Array)
603
+ result = []
604
+ container.each_with_index do |item, i|
605
+ interp.stack_push(i) if with_key
606
+ interp.stack_push(item)
607
+ interp.run(forthic, string_location)
608
+ should_select = interp.stack_pop
609
+ result << item if should_select
610
+ end
611
+ result
612
+ else
613
+ result = {}
614
+ container.keys.each do |k|
615
+ v = container[k]
616
+ interp.stack_push(k) if with_key
617
+ interp.stack_push(v)
618
+ interp.run(forthic, string_location)
619
+ should_select = interp.stack_pop
620
+ result[k] = v if should_select
621
+ end
622
+ result
623
+ end
624
+ end
625
+
626
+ # Grouping operations
627
+
628
+ forthic_word :BY_FIELD, "( container:any[] field:string -- indexed:any )", "Index records by field value", "BY-FIELD"
629
+ def BY_FIELD(container, field)
630
+ container ||= []
631
+
632
+ values = if container.is_a?(Array)
633
+ container
634
+ else
635
+ container.keys.map { |k| container[k] }
636
+ end
637
+
638
+ result = {}
639
+ values.each do |v|
640
+ result[v[field]] = v if v
641
+ end
642
+
643
+ result
644
+ end
645
+
646
+ forthic_word :GROUP_BY_FIELD, "( container:any[] field:string -- grouped:any )", "Group records by field value", "GROUP-BY-FIELD"
647
+ def GROUP_BY_FIELD(container, field)
648
+ container ||= []
649
+
650
+ values = if container.is_a?(Array)
651
+ container
652
+ else
653
+ container.keys.map { |k| container[k] }
654
+ end
655
+
656
+ result = {}
657
+ values.each do |v|
658
+ field_val = v[field]
659
+ if field_val.is_a?(Array)
660
+ field_val.each do |fv|
661
+ result[fv] ||= []
662
+ result[fv] << v
663
+ end
664
+ else
665
+ result[field_val] ||= []
666
+ result[field_val] << v
667
+ end
668
+ end
669
+
670
+ result
671
+ end
672
+
673
+ forthic_word :GROUP_BY, "( items:any forthic:string [options:WordOptions] -- grouped:any )", "Group items by function result. Options: with_key (bool). Example: [5 15 25] '10 /' [.with_key TRUE] ~> GROUP-BY", "GROUP-BY"
674
+ def GROUP_BY(items, forthic, options = {})
675
+ _items = items || []
676
+
677
+ string_location = interp.get_string_location
678
+ with_key = options[:with_key] || options['with_key']
679
+
680
+ result = {}
681
+
682
+ process_item = lambda do |item, key = nil|
683
+ interp.stack_push(key) if with_key
684
+ interp.stack_push(item)
685
+ interp.run(forthic, string_location)
686
+ group_key = interp.stack_pop
687
+ # Convert group_key to string to match JavaScript behavior (object keys are always strings)
688
+ group_key_str = group_key.to_s
689
+ result[group_key_str] ||= []
690
+ result[group_key_str] << item
691
+ end
692
+
693
+ if _items.is_a?(Array)
694
+ _items.each_with_index do |item, i|
695
+ process_item.call(item, i)
696
+ end
697
+ else
698
+ _items.keys.each do |key|
699
+ process_item.call(_items[key], key)
700
+ end
701
+ end
702
+
703
+ result
704
+ end
705
+
706
+ forthic_word :GROUPS_OF, "( container:any[] n:number -- groups:any[] )", "Split array into groups of size n", "GROUPS-OF"
707
+ def GROUPS_OF(container, n)
708
+ raise "GROUPS-OF requires group size > 0" if n <= 0
709
+
710
+ container ||= []
711
+
712
+ group_items = lambda do |items, group_size|
713
+ items.each_slice(group_size).to_a
714
+ end
715
+
716
+ extract_rec = lambda do |record, keys|
717
+ result = {}
718
+ keys.each { |k| result[k] = record[k] }
719
+ result
720
+ end
721
+
722
+ if container.is_a?(Array)
723
+ group_items.call(container, n)
724
+ else
725
+ keys = container.keys
726
+ key_groups = group_items.call(keys, n)
727
+ key_groups.map { |ks| extract_rec.call(container, ks) }
728
+ end
729
+ end
730
+
731
+ # Iteration operations
732
+
733
+ forthic_word :FOREACH, "( items:any forthic:string [options:WordOptions] -- ? )", "Execute forthic for each item. Options: with_key (bool), push_error (bool). Example: ['a' 'b'] 'PROCESS' [.with_key TRUE] ~> FOREACH"
734
+ def FOREACH(items, forthic, options = {})
735
+ _items = items || []
736
+
737
+ string_location = interp.get_string_location
738
+
739
+ flags = {
740
+ with_key: options[:with_key] || options['with_key'],
741
+ push_error: options[:push_error] || options['push_error']
742
+ }
743
+
744
+ errors = []
745
+
746
+ execute_with_error = lambda do |forthic_str, location|
747
+ begin
748
+ interp.run(forthic_str, location)
749
+ nil
750
+ rescue => error
751
+ error
752
+ end
753
+ end
754
+
755
+ if _items.is_a?(Array)
756
+ _items.each_with_index do |item, i|
757
+ interp.stack_push(i) if flags[:with_key]
758
+ interp.stack_push(item)
759
+
760
+ if flags[:push_error]
761
+ error = execute_with_error.call(forthic, string_location)
762
+ errors << error
763
+ else
764
+ interp.run(forthic, string_location)
765
+ end
766
+ end
767
+ else
768
+ _items.keys.each do |k|
769
+ item = _items[k]
770
+ interp.stack_push(k) if flags[:with_key]
771
+ interp.stack_push(item)
772
+
773
+ if flags[:push_error]
774
+ error = execute_with_error.call(forthic, string_location)
775
+ errors << error
776
+ else
777
+ interp.run(forthic, string_location)
778
+ end
779
+ end
780
+ end
781
+
782
+ interp.stack_push(errors) if flags[:push_error]
783
+
784
+ nil
785
+ end
786
+
787
+ forthic_word :MAP, "( items:any forthic:string [options:WordOptions] -- mapped:any )", "Map function over items. Options: with_key (bool), push_error (bool), depth (num), push_rest (bool). Example: [1 2 3] '2 *' [.with_key TRUE] ~> MAP"
788
+ def MAP(items, forthic, options = {})
789
+ string_location = interp.get_string_location
790
+
791
+ flags = {
792
+ with_key: options[:with_key] || options['with_key'],
793
+ push_error: options[:push_error] || options['push_error'],
794
+ depth: options[:depth] || options['depth'] || 0,
795
+ push_rest: options[:push_rest] || options['push_rest']
796
+ }
797
+
798
+ map_word = MapWord.new(items, forthic, string_location, flags)
799
+ map_word.execute(interp)
800
+
801
+ nil # MapWord pushes result directly
802
+ end
803
+
804
+ forthic_direct_word :l_REPEAT, "( item:any forthic:string num_times:number -- )", "Repeat execution of forthic num_times", "<REPEAT"
805
+ def l_REPEAT(interp)
806
+ num_times = interp.stack_pop
807
+ forthic = interp.stack_pop
808
+ string_location = interp.get_string_location
809
+
810
+ num_times.times do
811
+ # Store item so we can push it back later
812
+ item = interp.stack_pop
813
+ interp.stack_push(item)
814
+
815
+ interp.run(forthic, string_location)
816
+ res = interp.stack_pop
817
+
818
+ # Push original item and result
819
+ interp.stack_push(item)
820
+ interp.stack_push(res)
821
+ end
822
+ end
823
+ end
824
+
825
+ # MapWord - Support class for MAP operation
826
+ class MapWord
827
+ def initialize(items, forthic, forthic_location, flags)
828
+ @forthic = forthic
829
+ @forthic_location = forthic_location
830
+ @items = items
831
+ @flags = flags
832
+
833
+ @depth = flags[:depth] || 0
834
+ @num_interps = flags[:interps] || 1
835
+ @push_error = flags[:push_error]
836
+ @with_key = flags[:with_key]
837
+
838
+ @result = []
839
+ @errors = []
840
+ end
841
+
842
+ def execute(interp)
843
+ normal_execute(interp)
844
+ end
845
+
846
+ private
847
+
848
+ def normal_execute(interp)
849
+ items = @items
850
+ unless items && (!items.is_a?(Array) || !items.empty?)
851
+ interp.stack_push(items)
852
+ return
853
+ end
854
+
855
+ @result = []
856
+ @errors = []
857
+
858
+ map(interp, items)
859
+
860
+ # Return results
861
+ interp.stack_push(@result)
862
+ interp.stack_push(@errors) if @push_error
863
+ end
864
+
865
+ def map(interp, items)
866
+ return unless items
867
+
868
+ # This maps the forthic over an item, storing errors if needed
869
+ map_value = lambda do |key, value, errors|
870
+ interp.stack_push(key) if @with_key
871
+ interp.stack_push(value)
872
+
873
+ if @push_error
874
+ begin
875
+ interp.run(@forthic, @forthic_location)
876
+ rescue => e
877
+ interp.stack_push(nil)
878
+ errors << e
879
+ else
880
+ errors << nil
881
+ end
882
+ else
883
+ interp.run(@forthic, @forthic_location)
884
+ end
885
+ interp.stack_pop
886
+ end
887
+
888
+ # This recursively descends a record structure
889
+ descend_record = nil
890
+ descend_record = lambda do |record, depth, accum, errors|
891
+ record.keys.each do |k|
892
+ item = record[k]
893
+ if depth > 0
894
+ if item.is_a?(Array)
895
+ accum[k] = []
896
+ descend_list.call(item, depth - 1, accum[k], errors)
897
+ else
898
+ accum[k] = {}
899
+ descend_record.call(item, depth - 1, accum[k], errors)
900
+ end
901
+ else
902
+ accum[k] = map_value.call(k, item, errors)
903
+ end
904
+ end
905
+ accum
906
+ end
907
+
908
+ # This recursively descends a list
909
+ descend_list = nil
910
+ descend_list = lambda do |items_arr, depth, accum, errors|
911
+ items_arr.each_with_index do |item, i|
912
+ if depth > 0
913
+ if item.is_a?(Array)
914
+ accum << []
915
+ descend_list.call(item, depth - 1, accum.last, errors)
916
+ else
917
+ accum << {}
918
+ descend_record.call(item, depth - 1, accum.last, errors)
919
+ end
920
+ else
921
+ accum << map_value.call(i, item, errors)
922
+ end
923
+ end
924
+ accum
925
+ end
926
+
927
+ errors = []
928
+ result = if items.is_a?(Array)
929
+ descend_list.call(items, @depth, [], errors)
930
+ else
931
+ descend_record.call(items, @depth, {}, errors)
932
+ end
933
+
934
+ @result = result
935
+ @errors = errors
936
+ [result, errors]
937
+ end
938
+ end
939
+ end
940
+ end