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.
- checksums.yaml +4 -4
- data/README.md +314 -14
- data/Rakefile +37 -8
- 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 +682 -133
- 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 +225 -78
- 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 +76 -39
- data/.standard.yml +0 -3
- data/CHANGELOG.md +0 -5
- data/Guardfile +0 -42
- data/lib/forthic/code_location.rb +0 -20
- data/lib/forthic/forthic_error.rb +0 -51
- data/lib/forthic/forthic_module.rb +0 -145
- data/lib/forthic/global_module.rb +0 -2341
- data/lib/forthic/positioned_string.rb +0 -19
- data/lib/forthic/token.rb +0 -38
- data/lib/forthic/variable.rb +0 -34
- data/lib/forthic/version.rb +0 -5
- data/lib/forthic/words/definition_word.rb +0 -40
- 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,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
|