@forthix/forthic 0.10.0 → 0.12.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 (147) hide show
  1. package/dist/cjs/common/runtime_client.d.ts +30 -0
  2. package/dist/cjs/common/runtime_client.js +10 -0
  3. package/dist/cjs/common/runtime_client.js.map +1 -0
  4. package/dist/cjs/common/type_utils.d.ts +7 -1
  5. package/dist/cjs/common/type_utils.js +13 -2
  6. package/dist/cjs/common/type_utils.js.map +1 -1
  7. package/dist/cjs/forthic/interpreter.js +6 -3
  8. package/dist/cjs/forthic/interpreter.js.map +1 -1
  9. package/dist/cjs/forthic/modules/standard/array_module.d.ts +17 -7
  10. package/dist/cjs/forthic/modules/standard/array_module.js +326 -78
  11. package/dist/cjs/forthic/modules/standard/array_module.js.map +1 -1
  12. package/dist/cjs/forthic/modules/standard/boolean_module.d.ts +5 -6
  13. package/dist/cjs/forthic/modules/standard/boolean_module.js +43 -60
  14. package/dist/cjs/forthic/modules/standard/boolean_module.js.map +1 -1
  15. package/dist/cjs/forthic/modules/standard/classic/classic_module.d.ts +56 -0
  16. package/dist/cjs/forthic/modules/standard/classic/classic_module.js +616 -0
  17. package/dist/cjs/forthic/modules/standard/classic/classic_module.js.map +1 -0
  18. package/dist/cjs/forthic/modules/standard/core_module.d.ts +11 -14
  19. package/dist/cjs/forthic/modules/standard/core_module.js +91 -97
  20. package/dist/cjs/forthic/modules/standard/core_module.js.map +1 -1
  21. package/dist/cjs/forthic/modules/standard/datetime_module.d.ts +4 -2
  22. package/dist/cjs/forthic/modules/standard/datetime_module.js +39 -20
  23. package/dist/cjs/forthic/modules/standard/datetime_module.js.map +1 -1
  24. package/dist/cjs/forthic/modules/standard/json_module.d.ts +0 -1
  25. package/dist/cjs/forthic/modules/standard/json_module.js +0 -15
  26. package/dist/cjs/forthic/modules/standard/json_module.js.map +1 -1
  27. package/dist/cjs/forthic/modules/standard/math_module.d.ts +7 -12
  28. package/dist/cjs/forthic/modules/standard/math_module.js +76 -137
  29. package/dist/cjs/forthic/modules/standard/math_module.js.map +1 -1
  30. package/dist/cjs/forthic/modules/standard/record_module.d.ts +15 -4
  31. package/dist/cjs/forthic/modules/standard/record_module.js +311 -45
  32. package/dist/cjs/forthic/modules/standard/record_module.js.map +1 -1
  33. package/dist/cjs/forthic/modules/standard/string_module.d.ts +14 -6
  34. package/dist/cjs/forthic/modules/standard/string_module.js +183 -60
  35. package/dist/cjs/forthic/modules/standard/string_module.js.map +1 -1
  36. package/dist/cjs/forthic/tokenizer.js +29 -1
  37. package/dist/cjs/forthic/tokenizer.js.map +1 -1
  38. package/dist/cjs/grpc/config_loader.d.ts +3 -0
  39. package/dist/cjs/grpc/config_loader.js +5 -0
  40. package/dist/cjs/grpc/config_loader.js.map +1 -1
  41. package/dist/cjs/grpc/remote_module.d.ts +3 -3
  42. package/dist/cjs/grpc/remote_module.js +1 -1
  43. package/dist/cjs/grpc/remote_module.js.map +1 -1
  44. package/dist/cjs/grpc/remote_word.d.ts +3 -3
  45. package/dist/cjs/grpc/remote_word.js +1 -1
  46. package/dist/cjs/grpc/remote_word.js.map +1 -1
  47. package/dist/cjs/grpc/runtime_manager.d.ts +5 -4
  48. package/dist/cjs/grpc/runtime_manager.js +2 -2
  49. package/dist/cjs/grpc/runtime_manager.js.map +1 -1
  50. package/dist/cjs/grpc/serializer.d.ts +2 -2
  51. package/dist/cjs/grpc/serializer.js +11 -10
  52. package/dist/cjs/grpc/serializer.js.map +1 -1
  53. package/dist/cjs/jsonrpc/browser-stub.d.ts +8 -0
  54. package/dist/cjs/jsonrpc/browser-stub.js +18 -0
  55. package/dist/cjs/jsonrpc/browser-stub.js.map +1 -0
  56. package/dist/cjs/jsonrpc/client.d.ts +31 -0
  57. package/dist/cjs/jsonrpc/client.js +98 -0
  58. package/dist/cjs/jsonrpc/client.js.map +1 -0
  59. package/dist/cjs/jsonrpc/errors.d.ts +18 -0
  60. package/dist/cjs/jsonrpc/errors.js +23 -0
  61. package/dist/cjs/jsonrpc/errors.js.map +1 -0
  62. package/dist/cjs/jsonrpc/index.d.ts +16 -0
  63. package/dist/cjs/jsonrpc/index.js +26 -0
  64. package/dist/cjs/jsonrpc/index.js.map +1 -0
  65. package/dist/cjs/jsonrpc/serializer.d.ts +8 -0
  66. package/dist/cjs/jsonrpc/serializer.js +14 -0
  67. package/dist/cjs/jsonrpc/serializer.js.map +1 -0
  68. package/dist/cjs/jsonrpc/server.d.ts +15 -0
  69. package/dist/cjs/jsonrpc/server.js +386 -0
  70. package/dist/cjs/jsonrpc/server.js.map +1 -0
  71. package/dist/cjs/websocket/serializer.d.ts +4 -4
  72. package/dist/cjs/websocket/serializer.js +15 -14
  73. package/dist/cjs/websocket/serializer.js.map +1 -1
  74. package/dist/esm/common/runtime_client.d.ts +30 -0
  75. package/dist/esm/common/runtime_client.js +9 -0
  76. package/dist/esm/common/runtime_client.js.map +1 -0
  77. package/dist/esm/common/type_utils.d.ts +7 -1
  78. package/dist/esm/common/type_utils.js +12 -2
  79. package/dist/esm/common/type_utils.js.map +1 -1
  80. package/dist/esm/forthic/interpreter.js +6 -3
  81. package/dist/esm/forthic/interpreter.js.map +1 -1
  82. package/dist/esm/forthic/modules/standard/array_module.d.ts +17 -7
  83. package/dist/esm/forthic/modules/standard/array_module.js +293 -72
  84. package/dist/esm/forthic/modules/standard/array_module.js.map +1 -1
  85. package/dist/esm/forthic/modules/standard/boolean_module.d.ts +5 -6
  86. package/dist/esm/forthic/modules/standard/boolean_module.js +38 -54
  87. package/dist/esm/forthic/modules/standard/boolean_module.js.map +1 -1
  88. package/dist/esm/forthic/modules/standard/classic/classic_module.d.ts +56 -0
  89. package/dist/esm/forthic/modules/standard/classic/classic_module.js +500 -0
  90. package/dist/esm/forthic/modules/standard/classic/classic_module.js.map +1 -0
  91. package/dist/esm/forthic/modules/standard/core_module.d.ts +11 -14
  92. package/dist/esm/forthic/modules/standard/core_module.js +76 -82
  93. package/dist/esm/forthic/modules/standard/core_module.js.map +1 -1
  94. package/dist/esm/forthic/modules/standard/datetime_module.d.ts +4 -2
  95. package/dist/esm/forthic/modules/standard/datetime_module.js +30 -17
  96. package/dist/esm/forthic/modules/standard/datetime_module.js.map +1 -1
  97. package/dist/esm/forthic/modules/standard/json_module.d.ts +0 -1
  98. package/dist/esm/forthic/modules/standard/json_module.js +0 -12
  99. package/dist/esm/forthic/modules/standard/json_module.js.map +1 -1
  100. package/dist/esm/forthic/modules/standard/math_module.d.ts +7 -12
  101. package/dist/esm/forthic/modules/standard/math_module.js +69 -117
  102. package/dist/esm/forthic/modules/standard/math_module.js.map +1 -1
  103. package/dist/esm/forthic/modules/standard/record_module.d.ts +15 -4
  104. package/dist/esm/forthic/modules/standard/record_module.js +292 -44
  105. package/dist/esm/forthic/modules/standard/record_module.js.map +1 -1
  106. package/dist/esm/forthic/modules/standard/string_module.d.ts +14 -6
  107. package/dist/esm/forthic/modules/standard/string_module.js +151 -54
  108. package/dist/esm/forthic/modules/standard/string_module.js.map +1 -1
  109. package/dist/esm/forthic/tokenizer.js +29 -1
  110. package/dist/esm/forthic/tokenizer.js.map +1 -1
  111. package/dist/esm/grpc/config_loader.d.ts +3 -0
  112. package/dist/esm/grpc/config_loader.js +5 -0
  113. package/dist/esm/grpc/config_loader.js.map +1 -1
  114. package/dist/esm/grpc/remote_module.d.ts +3 -3
  115. package/dist/esm/grpc/remote_module.js +1 -1
  116. package/dist/esm/grpc/remote_module.js.map +1 -1
  117. package/dist/esm/grpc/remote_word.d.ts +3 -3
  118. package/dist/esm/grpc/remote_word.js +1 -1
  119. package/dist/esm/grpc/remote_word.js.map +1 -1
  120. package/dist/esm/grpc/runtime_manager.d.ts +5 -4
  121. package/dist/esm/grpc/runtime_manager.js +2 -2
  122. package/dist/esm/grpc/runtime_manager.js.map +1 -1
  123. package/dist/esm/grpc/serializer.d.ts +2 -2
  124. package/dist/esm/grpc/serializer.js +12 -11
  125. package/dist/esm/grpc/serializer.js.map +1 -1
  126. package/dist/esm/jsonrpc/browser-stub.d.ts +8 -0
  127. package/dist/esm/jsonrpc/browser-stub.js +12 -0
  128. package/dist/esm/jsonrpc/browser-stub.js.map +1 -0
  129. package/dist/esm/jsonrpc/client.d.ts +31 -0
  130. package/dist/esm/jsonrpc/client.js +94 -0
  131. package/dist/esm/jsonrpc/client.js.map +1 -0
  132. package/dist/esm/jsonrpc/errors.d.ts +18 -0
  133. package/dist/esm/jsonrpc/errors.js +18 -0
  134. package/dist/esm/jsonrpc/errors.js.map +1 -0
  135. package/dist/esm/jsonrpc/index.d.ts +16 -0
  136. package/dist/esm/jsonrpc/index.js +16 -0
  137. package/dist/esm/jsonrpc/index.js.map +1 -0
  138. package/dist/esm/jsonrpc/serializer.d.ts +8 -0
  139. package/dist/esm/jsonrpc/serializer.js +9 -0
  140. package/dist/esm/jsonrpc/serializer.js.map +1 -0
  141. package/dist/esm/jsonrpc/server.d.ts +15 -0
  142. package/dist/esm/jsonrpc/server.js +349 -0
  143. package/dist/esm/jsonrpc/server.js.map +1 -0
  144. package/dist/esm/websocket/serializer.d.ts +4 -4
  145. package/dist/esm/websocket/serializer.js +16 -15
  146. package/dist/esm/websocket/serializer.js.map +1 -1
  147. package/package.json +15 -2
@@ -5,20 +5,23 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { dup_interpreter } from "../../interpreter.js";
8
- import { DecoratedModule, ForthicWord, ForthicDirectWord, registerModuleDoc } from "../../decorators/word.js";
8
+ import { DecoratedModule, ForthicWord, registerModuleDoc } from "../../decorators/word.js";
9
9
  export class ArrayModule extends DecoratedModule {
10
10
  static {
11
11
  registerModuleDoc(ArrayModule, `
12
12
  Array and collection operations for manipulating arrays and records.
13
13
 
14
14
  ## Categories
15
- - Access: NTH, LAST, SLICE, TAKE, DROP, LENGTH, INDEX, KEY-OF
16
- - Transform: MAP, REVERSE
17
- - Combine: APPEND, ZIP, ZIP_WITH, CONCAT
18
- - Filter: SELECT, UNIQUE, DIFFERENCE, INTERSECTION, UNION
19
- - Sort: SORT, SHUFFLE, ROTATE
20
- - Group: BY_FIELD, GROUP-BY-FIELD, GROUP_BY, GROUPS_OF
21
- - Utility: <REPEAT, FOREACH, REDUCE, UNPACK, FLATTEN
15
+ - Access: NTH, FIRST, LAST, SLICE, TAKE, TAKE-LAST, SKIP, LENGTH, INDEX, KEY-OF
16
+ - Transform: MAP, MAP-AT, REVERSE
17
+ - Combine: APPEND, ZIP, ZIP-WITH
18
+ - Filter: FILTER, UNIQUE, UNIQUE-BY, DIFFERENCE, INTERSECTION, UNION
19
+ - Sort: SORT, SORT-BY, SORT-U
20
+ - Search: FIND, COUNT
21
+ - Extrema: MIN-BY, MAX-BY
22
+ - Indexing: NUMBERED
23
+ - Group: BY-FIELD, GROUP-BY, GROUP-BY-FIELD, GROUPS-OF
24
+ - Iteration: FOREACH, REDUCE, UNPACK, FLATTEN, TIMES-RUN
22
25
 
23
26
  ## Options
24
27
  Several words support options via the ~> operator using syntax: [.option_name value ...] ~> WORD
@@ -39,17 +42,14 @@ Several words support options via the ~> operator using syntax: [.option_name va
39
42
  constructor() {
40
43
  super("array");
41
44
  }
42
- async APPEND(container, item) {
43
- let result = container;
44
- if (!result)
45
+ async APPEND(array, item) {
46
+ let result = array;
47
+ if (result === null || result === undefined)
45
48
  result = [];
46
- if (result instanceof Array) {
47
- result.push(item);
48
- }
49
- else {
50
- // If not a list, treat as record - item should be [key, value]
51
- result[item[0]] = item[1];
49
+ if (!(result instanceof Array)) {
50
+ throw new Error("APPEND requires an array. For records, use JQ! to set a key.");
52
51
  }
52
+ result.push(item);
53
53
  return result;
54
54
  }
55
55
  async REVERSE(container) {
@@ -71,14 +71,16 @@ Several words support options via the ~> operator using syntax: [.option_name va
71
71
  return result;
72
72
  }
73
73
  async LENGTH(container) {
74
- if (!container)
74
+ if (container === null || container === undefined)
75
75
  return 0;
76
- if (container instanceof Array) {
76
+ if (container instanceof Array)
77
77
  return container.length;
78
+ if (typeof container === "string") {
79
+ throw new Error("LENGTH operates on arrays and records. For strings, use STR-LENGTH.");
78
80
  }
79
- else {
81
+ if (typeof container === "object")
80
82
  return Object.keys(container).length;
81
- }
83
+ throw new Error("LENGTH operates on arrays and records.");
82
84
  }
83
85
  async NTH(container, n) {
84
86
  if (n === null || !container)
@@ -96,6 +98,21 @@ Several words support options via the ~> operator using syntax: [.option_name va
96
98
  return container[key];
97
99
  }
98
100
  }
101
+ async FIRST(container) {
102
+ if (!container)
103
+ return null;
104
+ if (container instanceof Array) {
105
+ if (container.length === 0)
106
+ return null;
107
+ return container[0];
108
+ }
109
+ else {
110
+ const keys = Object.keys(container).sort();
111
+ if (keys.length === 0)
112
+ return null;
113
+ return container[keys[0]];
114
+ }
115
+ }
99
116
  async LAST(container) {
100
117
  if (!container)
101
118
  return null;
@@ -194,7 +211,7 @@ Several words support options via the ~> operator using syntax: [.option_name va
194
211
  }
195
212
  return taken;
196
213
  }
197
- async DROP(container, n) {
214
+ async SKIP(container, n) {
198
215
  if (!container)
199
216
  return [];
200
217
  if (n <= 0)
@@ -208,6 +225,23 @@ Several words support options via the ~> operator using syntax: [.option_name va
208
225
  return rest_keys.map((k) => container[k]);
209
226
  }
210
227
  }
228
+ async TAKE_LAST(container, n) {
229
+ if (!container)
230
+ return [];
231
+ if (n <= 0)
232
+ return container instanceof Array ? [] : {};
233
+ if (container instanceof Array) {
234
+ return container.slice(Math.max(0, container.length - n));
235
+ }
236
+ else {
237
+ const keys = Object.keys(container).sort();
238
+ const tail = keys.slice(Math.max(0, keys.length - n));
239
+ const result = {};
240
+ for (const k of tail)
241
+ result[k] = container[k];
242
+ return result;
243
+ }
244
+ }
211
245
  async DIFFERENCE(lcontainer, rcontainer) {
212
246
  let _lcontainer = lcontainer || [];
213
247
  let _rcontainer = rcontainer || [];
@@ -364,29 +398,6 @@ Several words support options via the ~> operator using syntax: [.option_name va
364
398
  }
365
399
  return result;
366
400
  }
367
- async SHUFFLE(array) {
368
- if (!array)
369
- return array;
370
- const result = [...array];
371
- for (let i = result.length - 1; i > 0; i--) {
372
- const j = Math.floor(Math.random() * (i + 1));
373
- [result[i], result[j]] = [result[j], result[i]];
374
- }
375
- return result;
376
- }
377
- async ROTATE(container) {
378
- if (!container)
379
- return container;
380
- let result = container;
381
- if (container instanceof Array) {
382
- if (container.length > 0) {
383
- result = [...container];
384
- const val = result.pop();
385
- result.unshift(val);
386
- }
387
- }
388
- return result;
389
- }
390
401
  async UNPACK(container) {
391
402
  let _container = container;
392
403
  if (!_container)
@@ -599,7 +610,7 @@ Several words support options via the ~> operator using syntax: [.option_name va
599
610
  return null;
600
611
  }
601
612
  }
602
- async SELECT(container, forthic, options) {
613
+ async FILTER(container, forthic, options) {
603
614
  const interp = this.interp;
604
615
  const string_location = interp.get_string_location();
605
616
  const flags = {
@@ -814,24 +825,207 @@ Several words support options via the ~> operator using syntax: [.option_name va
814
825
  await map_word.execute(this.interp);
815
826
  return undefined; // MapWord pushes result directly
816
827
  }
817
- async l_REPEAT(interp) {
818
- const num_times = interp.stack_pop();
819
- const forthic = interp.stack_pop();
820
- const string_location = interp.get_string_location();
828
+ async MAP_AT(container, key, forthic) {
829
+ if (container === null || container === undefined)
830
+ return container;
831
+ const string_location = this.interp.get_string_location();
832
+ // Path-array form: walk the path, transforming the value at the leaf
833
+ if (Array.isArray(key)) {
834
+ if (key.length === 0) {
835
+ // Apply forthic to the entire container
836
+ this.interp.stack_push(container);
837
+ await this.interp.run(forthic, string_location);
838
+ return this.interp.stack_pop();
839
+ }
840
+ const [head, ...rest] = key;
841
+ return this._mapAtRecursive(container, head, rest, forthic, string_location);
842
+ }
843
+ // Single-key form
844
+ return this._mapAtSingle(container, key, forthic, string_location);
845
+ }
846
+ async _mapAtRecursive(container, head, rest, forthic, string_location) {
847
+ if (rest.length === 0) {
848
+ return this._mapAtSingle(container, head, forthic, string_location);
849
+ }
850
+ if (container instanceof Array) {
851
+ const idx = typeof head === "number" ? head : Number(head);
852
+ if (!Number.isInteger(idx) || idx < 0 || idx >= container.length)
853
+ return container;
854
+ const result = [...container];
855
+ result[idx] = await this._mapAtRecursive(result[idx], rest[0], rest.slice(1), forthic, string_location);
856
+ return result;
857
+ }
858
+ if (typeof container === "object" && container !== null) {
859
+ if (!Object.prototype.hasOwnProperty.call(container, head))
860
+ return container;
861
+ const result = { ...container };
862
+ result[head] = await this._mapAtRecursive(result[head], rest[0], rest.slice(1), forthic, string_location);
863
+ return result;
864
+ }
865
+ return container;
866
+ }
867
+ async _mapAtSingle(container, key, forthic, string_location) {
868
+ if (container instanceof Array) {
869
+ const idx = typeof key === "number" ? key : Number(key);
870
+ if (!Number.isInteger(idx) || idx < 0 || idx >= container.length)
871
+ return container;
872
+ const result = [...container];
873
+ this.interp.stack_push(result[idx]);
874
+ await this.interp.run(forthic, string_location);
875
+ result[idx] = this.interp.stack_pop();
876
+ return result;
877
+ }
878
+ if (typeof container === "object" && container !== null) {
879
+ if (!Object.prototype.hasOwnProperty.call(container, key))
880
+ return container;
881
+ const result = { ...container };
882
+ this.interp.stack_push(result[key]);
883
+ await this.interp.run(forthic, string_location);
884
+ result[key] = this.interp.stack_pop();
885
+ return result;
886
+ }
887
+ return container;
888
+ }
889
+ async TIMES_RUN(num_times, forthic) {
890
+ if (num_times === null || num_times === undefined || !forthic)
891
+ return;
892
+ const string_location = this.interp.get_string_location();
821
893
  for (let i = 0; i < num_times; i++) {
822
- // Store item so we can push it back later
823
- const item = interp.stack_pop();
824
- interp.stack_push(item);
825
- await interp.run(forthic, string_location);
826
- const res = interp.stack_pop();
827
- // Push original item and result
828
- interp.stack_push(item);
829
- interp.stack_push(res);
894
+ await this.interp.run(forthic, string_location);
895
+ }
896
+ }
897
+ // ========================================
898
+ // Functional / jq-style additions
899
+ // ========================================
900
+ async FIND(items, forthic) {
901
+ if (!items)
902
+ return null;
903
+ const string_location = this.interp.get_string_location();
904
+ const seq = items instanceof Array ? items : Object.keys(items).map((k) => items[k]);
905
+ for (const item of seq) {
906
+ this.interp.stack_push(item);
907
+ await this.interp.run(forthic, string_location);
908
+ const matched = this.interp.stack_pop();
909
+ if (matched)
910
+ return item;
911
+ }
912
+ return null;
913
+ }
914
+ async COUNT(items, forthic) {
915
+ if (!items)
916
+ return 0;
917
+ const string_location = this.interp.get_string_location();
918
+ const seq = items instanceof Array ? items : Object.keys(items).map((k) => items[k]);
919
+ let n = 0;
920
+ for (const item of seq) {
921
+ this.interp.stack_push(item);
922
+ await this.interp.run(forthic, string_location);
923
+ if (this.interp.stack_pop())
924
+ n++;
925
+ }
926
+ return n;
927
+ }
928
+ async SORT_BY(items, forthic) {
929
+ if (!Array.isArray(items))
930
+ return items;
931
+ const string_location = this.interp.get_string_location();
932
+ const decorated = [];
933
+ for (const item of items) {
934
+ this.interp.stack_push(item);
935
+ await this.interp.run(forthic, string_location);
936
+ decorated.push({ item, key: this.interp.stack_pop() });
937
+ }
938
+ decorated.sort((a, b) => {
939
+ if (a.key < b.key)
940
+ return -1;
941
+ if (a.key > b.key)
942
+ return 1;
943
+ return 0;
944
+ });
945
+ return decorated.map((d) => d.item);
946
+ }
947
+ async MIN_BY(items, forthic) {
948
+ if (!Array.isArray(items) || items.length === 0)
949
+ return null;
950
+ const string_location = this.interp.get_string_location();
951
+ let best_item = null;
952
+ let best_key = null;
953
+ let first = true;
954
+ for (const item of items) {
955
+ this.interp.stack_push(item);
956
+ await this.interp.run(forthic, string_location);
957
+ const key = this.interp.stack_pop();
958
+ if (first || key < best_key) {
959
+ best_item = item;
960
+ best_key = key;
961
+ first = false;
962
+ }
830
963
  }
964
+ return best_item;
965
+ }
966
+ async MAX_BY(items, forthic) {
967
+ if (!Array.isArray(items) || items.length === 0)
968
+ return null;
969
+ const string_location = this.interp.get_string_location();
970
+ let best_item = null;
971
+ let best_key = null;
972
+ let first = true;
973
+ for (const item of items) {
974
+ this.interp.stack_push(item);
975
+ await this.interp.run(forthic, string_location);
976
+ const key = this.interp.stack_pop();
977
+ if (first || key > best_key) {
978
+ best_item = item;
979
+ best_key = key;
980
+ first = false;
981
+ }
982
+ }
983
+ return best_item;
984
+ }
985
+ async UNIQUE_BY(items, forthic) {
986
+ if (!Array.isArray(items))
987
+ return items;
988
+ const string_location = this.interp.get_string_location();
989
+ const seen = new Set();
990
+ const result = [];
991
+ for (const item of items) {
992
+ this.interp.stack_push(item);
993
+ await this.interp.run(forthic, string_location);
994
+ const key = this.interp.stack_pop();
995
+ const skey = JSON.stringify(key);
996
+ if (!seen.has(skey)) {
997
+ seen.add(skey);
998
+ result.push(item);
999
+ }
1000
+ }
1001
+ return result;
1002
+ }
1003
+ async NUMBERED(items) {
1004
+ if (!Array.isArray(items))
1005
+ return [];
1006
+ return items.map((item, i) => [i, item]);
1007
+ }
1008
+ // ========================================
1009
+ // Bash/shell-flavored array additions (PR 7)
1010
+ // ========================================
1011
+ async SORT_U(strings) {
1012
+ if (!Array.isArray(strings))
1013
+ return strings;
1014
+ const sorted = [...strings].sort();
1015
+ const seen = new Set();
1016
+ const result = [];
1017
+ for (const s of sorted) {
1018
+ const key = JSON.stringify(s);
1019
+ if (!seen.has(key)) {
1020
+ seen.add(key);
1021
+ result.push(s);
1022
+ }
1023
+ }
1024
+ return result;
831
1025
  }
832
1026
  }
833
1027
  __decorate([
834
- ForthicWord("( container:any item:any -- container:any )", "Append item to array or add key-value to record")
1028
+ ForthicWord("( array:any[] item:any -- array:any[] )", "Append item to array. For records, use JQ! to set a key.", "APPEND")
835
1029
  ], ArrayModule.prototype, "APPEND", null);
836
1030
  __decorate([
837
1031
  ForthicWord("( container:any -- container:any )", "Reverse array")
@@ -840,11 +1034,14 @@ __decorate([
840
1034
  ForthicWord("( array:any[] -- array:any[] )", "Remove duplicates from array")
841
1035
  ], ArrayModule.prototype, "UNIQUE", null);
842
1036
  __decorate([
843
- ForthicWord("( container:any -- length:number )", "Get length of array or record")
1037
+ ForthicWord("( container:any -- length:number )", "Length of an array or record. For strings, use STR-LENGTH.")
844
1038
  ], ArrayModule.prototype, "LENGTH", null);
845
1039
  __decorate([
846
1040
  ForthicWord("( container:any n:number -- item:any )", "Get nth element from array or record")
847
1041
  ], ArrayModule.prototype, "NTH", null);
1042
+ __decorate([
1043
+ ForthicWord("( container:any -- item:any )", "Get first element from array or record (sorted-key order for records)")
1044
+ ], ArrayModule.prototype, "FIRST", null);
848
1045
  __decorate([
849
1046
  ForthicWord("( container:any -- item:any )", "Get last element from array or record")
850
1047
  ], ArrayModule.prototype, "LAST", null);
@@ -855,8 +1052,11 @@ __decorate([
855
1052
  ForthicWord("( container:any[] n:number [options:WordOptions] -- result:any[] )", "Take first n elements")
856
1053
  ], ArrayModule.prototype, "TAKE", null);
857
1054
  __decorate([
858
- ForthicWord("( container:any n:number -- result:any )", "Drop first n elements from array or record")
859
- ], ArrayModule.prototype, "DROP", null);
1055
+ ForthicWord("( container:any n:number -- result:any )", "Skip first n elements from array or record")
1056
+ ], ArrayModule.prototype, "SKIP", null);
1057
+ __decorate([
1058
+ ForthicWord("( container:any n:number -- result:any )", "Take last n elements from array or record (sorted-key order for records).", "TAKE-LAST")
1059
+ ], ArrayModule.prototype, "TAKE_LAST", null);
860
1060
  __decorate([
861
1061
  ForthicWord("( lcontainer:any rcontainer:any -- result:any )", "Set difference between two containers")
862
1062
  ], ArrayModule.prototype, "DIFFERENCE", null);
@@ -869,12 +1069,6 @@ __decorate([
869
1069
  __decorate([
870
1070
  ForthicWord("( container:any[] [options:WordOptions] -- array:any[] )", "Sort container. Options: comparator (string or function). Example: [3 1 4] [.comparator \"-1 *\"] ~> SORT")
871
1071
  ], ArrayModule.prototype, "SORT", null);
872
- __decorate([
873
- ForthicWord("( array:any[] -- array:any[] )", "Shuffle array randomly")
874
- ], ArrayModule.prototype, "SHUFFLE", null);
875
- __decorate([
876
- ForthicWord("( container:any -- container:any )", "Rotate container by moving last element to front")
877
- ], ArrayModule.prototype, "ROTATE", null);
878
1072
  __decorate([
879
1073
  ForthicWord("( container:any -- elements:any )", "Unpack array or record elements onto stack")
880
1074
  ], ArrayModule.prototype, "UNPACK", null);
@@ -897,8 +1091,8 @@ __decorate([
897
1091
  ForthicWord("( container:any value:any -- key:any )", "Find key of value in container", "KEY-OF")
898
1092
  ], ArrayModule.prototype, "KEY_OF", null);
899
1093
  __decorate([
900
- ForthicWord("( container:any forthic:string [options:WordOptions] -- filtered:any )", "Filter items with predicate. Options: with_key (bool)")
901
- ], ArrayModule.prototype, "SELECT", null);
1094
+ ForthicWord("( container:any forthic:string [options:WordOptions] -- filtered:any )", "Filter items with predicate. Options: with_key (bool)", "FILTER")
1095
+ ], ArrayModule.prototype, "FILTER", null);
902
1096
  __decorate([
903
1097
  ForthicWord("( container:any[] field:string -- indexed:any )", "Index records by field value", "BY-FIELD")
904
1098
  ], ArrayModule.prototype, "BY_FIELD", null);
@@ -918,8 +1112,35 @@ __decorate([
918
1112
  ForthicWord("( 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")
919
1113
  ], ArrayModule.prototype, "MAP", null);
920
1114
  __decorate([
921
- ForthicDirectWord("( item:any forthic:string num_times:number -- )", "Repeat execution of forthic num_times", "<REPEAT")
922
- ], ArrayModule.prototype, "l_REPEAT", null);
1115
+ ForthicWord("( container:any key:any|any[] forthic:string -- container:any )", "Apply forthic to the value at key/index, returning a new container with that slot transformed. The key arg may be a single key (one-level update) or a path-array for deep updates. Polymorphic over arrays and records. Equivalent of jq's |= operator.", "MAP-AT")
1116
+ ], ArrayModule.prototype, "MAP_AT", null);
1117
+ __decorate([
1118
+ ForthicWord("( num_times:number forthic:string -- )", "Run forthic num_times. Each invocation runs in the current stack — no automatic per-iteration value passing.", "TIMES-RUN")
1119
+ ], ArrayModule.prototype, "TIMES_RUN", null);
1120
+ __decorate([
1121
+ ForthicWord("( items:any forthic:string -- item:any )", "Return the first item where forthic returns truthy, or null if none.", "FIND")
1122
+ ], ArrayModule.prototype, "FIND", null);
1123
+ __decorate([
1124
+ ForthicWord("( items:any forthic:string -- n:number )", "Count items where forthic returns truthy.", "COUNT")
1125
+ ], ArrayModule.prototype, "COUNT", null);
1126
+ __decorate([
1127
+ ForthicWord("( items:any[] forthic:string -- sorted:any[] )", "Sort items by the value forthic produces (ascending).", "SORT-BY")
1128
+ ], ArrayModule.prototype, "SORT_BY", null);
1129
+ __decorate([
1130
+ ForthicWord("( items:any[] forthic:string -- item:any )", "Return the item with the smallest value produced by forthic. Null on empty input.", "MIN-BY")
1131
+ ], ArrayModule.prototype, "MIN_BY", null);
1132
+ __decorate([
1133
+ ForthicWord("( items:any[] forthic:string -- item:any )", "Return the item with the largest value produced by forthic. Null on empty input.", "MAX-BY")
1134
+ ], ArrayModule.prototype, "MAX_BY", null);
1135
+ __decorate([
1136
+ ForthicWord("( items:any[] forthic:string -- items:any[] )", "Dedupe items by the key forthic produces (keeps first occurrence).", "UNIQUE-BY")
1137
+ ], ArrayModule.prototype, "UNIQUE_BY", null);
1138
+ __decorate([
1139
+ ForthicWord("( items:any[] -- pairs:any[] )", "Pair each item with its index: [v0 v1 v2] -> [[0 v0] [1 v1] [2 v2]]. (Python's enumerate.)", "NUMBERED")
1140
+ ], ArrayModule.prototype, "NUMBERED", null);
1141
+ __decorate([
1142
+ ForthicWord("( strings:any[] -- strings:any[] )", "Sort an array and remove duplicates (bash sort -u).", "SORT-U")
1143
+ ], ArrayModule.prototype, "SORT_U", null);
923
1144
  // ( items forthic -- [ ? ] )
924
1145
  class MapWord {
925
1146
  forthic;