@barefootjs/perl 0.16.0 → 0.17.1

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.
@@ -1,5 +1,5 @@
1
1
  package BarefootJS::DevReload;
2
- our $VERSION = "0.15.2";
2
+ our $VERSION = "0.17.0";
3
3
  use strict;
4
4
  use warnings;
5
5
  use feature 'signatures';
@@ -0,0 +1,635 @@
1
+ package BarefootJS::Evaluator;
2
+ our $VERSION = "0.14.0";
3
+ use strict;
4
+ use warnings;
5
+ use utf8;
6
+ use feature 'signatures';
7
+ no warnings 'experimental::signatures';
8
+
9
+ use B ();
10
+ use POSIX ();
11
+ use JSON::PP ();
12
+ use Scalar::Util qw(looks_like_number);
13
+
14
+ # Lightweight evaluator for the pure `ParsedExpr` subset, scoped to
15
+ # higher-order callback bodies (reduce / sort / map / filter / find
16
+ # `(…) => expr`) — issue #2018. Templates cannot carry a lambda in
17
+ # expression position, which is why the adapters historically special-cased
18
+ # these callbacks into fixed shapes (bf_sort's comparator catalogue,
19
+ # bf_reduce's +/* fold). Instead, the callback BODY rides as a pure
20
+ # `ParsedExpr` subtree (the structured IR the compiler already produces) and
21
+ # is evaluated here against an environment (`{acc, item, …captured free
22
+ # vars}`).
23
+ #
24
+ # ONE shared implementation for both Perl backends (Mojo + Xslate), living
25
+ # alongside SearchParams.pm in the engine-agnostic core (no CPAN deps beyond
26
+ # core B / POSIX / Scalar::Util). The accepted subset and its semantics are
27
+ # documented in spec/compiler.md ("ParsedExpr Evaluator Semantics") and pinned
28
+ # isomorphically by the cross-language golden vectors
29
+ # (packages/adapter-tests/helper-vectors/eval-vectors.json), shared with the Go
30
+ # evaluator (bf.go) — same input → same output.
31
+ #
32
+ # The coercion below is JS-faithful (ToNumber / ToString / ToBoolean, strict
33
+ # equality) and deliberately distinct from the divergent bf->string / number
34
+ # helpers in BarefootJS.pm, so the contract is unambiguous and the two
35
+ # template adapters stay byte-equal with each other and with Go.
36
+
37
+ # evaluate($node, $env)
38
+ #
39
+ # Evaluate a decoded ParsedExpr node (a hashref keyed by `kind`) against the
40
+ # environment hashref ($env), returning a Perl value (number, string,
41
+ # JSON::PP::Boolean, undef for null, arrayref, hashref). The matching JSON
42
+ # entry point is eval_json() below.
43
+ sub evaluate ($node, $env) {
44
+ return undef unless ref $node eq 'HASH';
45
+ my $kind = $node->{kind} // '';
46
+
47
+ if ($kind eq 'literal') {
48
+ return $node->{value};
49
+ }
50
+ if ($kind eq 'identifier') {
51
+ return $env->{ $node->{name} };
52
+ }
53
+ if ($kind eq 'binary') {
54
+ return _binary($node->{op},
55
+ evaluate($node->{left}, $env), evaluate($node->{right}, $env));
56
+ }
57
+ if ($kind eq 'unary') {
58
+ return _unary($node->{op}, evaluate($node->{argument}, $env));
59
+ }
60
+ if ($kind eq 'logical') {
61
+ my $op = $node->{op};
62
+ my $left = evaluate($node->{left}, $env);
63
+ if ($op eq '&&') {
64
+ return _truthy($left) ? evaluate($node->{right}, $env) : $left;
65
+ }
66
+ if ($op eq '||') {
67
+ return _truthy($left) ? $left : evaluate($node->{right}, $env);
68
+ }
69
+ # `??`
70
+ return defined $left ? $left : evaluate($node->{right}, $env);
71
+ }
72
+ if ($kind eq 'conditional') {
73
+ return _truthy(evaluate($node->{test}, $env))
74
+ ? evaluate($node->{consequent}, $env)
75
+ : evaluate($node->{alternate}, $env);
76
+ }
77
+ if ($kind eq 'member') {
78
+ return _read_property(evaluate($node->{object}, $env), $node->{property});
79
+ }
80
+ if ($kind eq 'index-access') {
81
+ return _read_index(evaluate($node->{object}, $env), evaluate($node->{index}, $env));
82
+ }
83
+ if ($kind eq 'call') {
84
+ my $name = _builtin_name($node->{callee});
85
+ return undef unless defined $name && $name ne '';
86
+ my @args = map { evaluate($_, $env) } @{ $node->{args} // [] };
87
+ return _call_builtin($name, \@args);
88
+ }
89
+ if ($kind eq 'template-literal') {
90
+ my $out = '';
91
+ for my $p (@{ $node->{parts} // [] }) {
92
+ if (($p->{type} // '') eq 'string') {
93
+ $out .= $p->{value} // '';
94
+ }
95
+ else {
96
+ $out .= _to_string(evaluate($p->{expr}, $env));
97
+ }
98
+ }
99
+ return $out;
100
+ }
101
+ if ($kind eq 'array-literal') {
102
+ return [ map { evaluate($_, $env) } @{ $node->{elements} // [] } ];
103
+ }
104
+ if ($kind eq 'object-literal') {
105
+ my %out;
106
+ for my $prop (@{ $node->{properties} // [] }) {
107
+ $out{ $prop->{key} } = evaluate($prop->{value}, $env);
108
+ }
109
+ return \%out;
110
+ }
111
+ if ($kind eq 'array-method' && ($node->{method} // '') eq 'includes'
112
+ && @{ $node->{args} // [] } == 1)
113
+ {
114
+ # `.includes(x)` (#2075) — the one `array-method` in the evaluator
115
+ # subset, shared between `Array.prototype.includes` (SameValueZero
116
+ # membership) and `String.prototype.includes` (substring search),
117
+ # matching the receiver-type dispatch the SSR template lowering does
118
+ # at runtime (`bf_includes` / `$bf->includes`). Mirrors the JS
119
+ # reference's `includes()` (eval-reference.ts).
120
+ my $obj = evaluate($node->{object}, $env);
121
+ my $needle = evaluate($node->{args}[0], $env);
122
+ if (ref $obj eq 'ARRAY') {
123
+ for my $el (@$obj) {
124
+ return _bool(1) if _same_value_zero($el, $needle);
125
+ }
126
+ return _bool(0);
127
+ }
128
+ if (_is_string($obj)) {
129
+ return _bool(index($obj, _to_string($needle)) != -1);
130
+ }
131
+ # Any other receiver is not a JS `.includes` target — degrade to
132
+ # false rather than throwing, mirroring the reference.
133
+ return _bool(0);
134
+ }
135
+
136
+ # arrow-fn / higher-order / unsupported array-method: a callback body
137
+ # containing these is refused upstream (BF101); never reached here.
138
+ return undef;
139
+ }
140
+
141
+ # eval_json($json, $env): decode a ParsedExpr JSON string and evaluate it.
142
+ # Mirrors the Go EvalExpr entry point. Requires JSON::PP only when used.
143
+ sub eval_json ($json, $env) {
144
+ require JSON::PP;
145
+ my $node = JSON::PP->new->decode($json);
146
+ return evaluate($node, $env);
147
+ }
148
+
149
+ # ---------------------------------------------------------------------------
150
+ # JS value classification. JSON-decoded strings carry the string flag (POK)
151
+ # but no numeric flag; JSON-decoded numbers carry IOK/NOK. This lets the
152
+ # evaluator tell the JS *string* "10" from the JS *number* 10 — essential for
153
+ # the `+` overload and relational comparison — which looks_like_number alone
154
+ # cannot (it is true for both).
155
+ # ---------------------------------------------------------------------------
156
+
157
+ sub _is_string ($v) {
158
+ return 0 if !defined $v || ref $v;
159
+ my $f = B::svref_2object(\$v)->FLAGS;
160
+ return (($f & B::SVf_POK) && !($f & (B::SVf_IOK | B::SVf_NOK))) ? 1 : 0;
161
+ }
162
+
163
+ sub _is_number ($v) {
164
+ return (defined $v && !ref $v && !_is_string($v)) ? 1 : 0;
165
+ }
166
+
167
+ sub _nan {
168
+ my $inf = 9**9**9;
169
+ return $inf - $inf;
170
+ }
171
+
172
+ # ---------------------------------------------------------------------------
173
+ # JS coercion primitives (ToNumber / ToString / ToBoolean).
174
+ # ---------------------------------------------------------------------------
175
+
176
+ sub _to_number ($v) {
177
+ return 0 if !defined $v;
178
+ if (ref $v eq 'JSON::PP::Boolean') { return $v ? 1 : 0 }
179
+ return _nan() if ref $v;
180
+ if (_is_string($v)) {
181
+ my $t = $v;
182
+ $t =~ s/\A\s+//;
183
+ $t =~ s/\s+\z//;
184
+ return 0 if $t eq '';
185
+ return looks_like_number($t) ? ($t + 0) : _nan();
186
+ }
187
+ return $v + 0;
188
+ }
189
+
190
+ sub _to_string ($v) {
191
+ return 'null' if !defined $v;
192
+ if (ref $v eq 'JSON::PP::Boolean') { return $v ? 'true' : 'false' }
193
+ # JS spells the non-finite doubles "Infinity" / "-Infinity" / "NaN";
194
+ # Perl stringifies them "Inf" / "-Inf" / "NaN", so the non-finite cases
195
+ # are pinned here to stay JS-faithful (and match the Go evaluator's
196
+ # evalToString). Finite numbers and strings fall through to plain
197
+ # interpolation.
198
+ if (_is_number($v)) {
199
+ my $n = $v + 0;
200
+ return 'NaN' if $n != $n;
201
+ return 'Infinity' if $n == 9**9**9;
202
+ return '-Infinity' if $n == -(9**9**9);
203
+ }
204
+ return "$v";
205
+ }
206
+
207
+ sub _truthy ($v) {
208
+ return 0 if !defined $v;
209
+ if (ref $v eq 'JSON::PP::Boolean') { return $v ? 1 : 0 }
210
+ return 1 if ref $v; # arrays / objects are always truthy in JS
211
+ if (_is_string($v)) { return $v ne '' ? 1 : 0 } # incl. the truthy "0"
212
+ my $n = $v + 0;
213
+ return ($n != 0 && $n == $n) ? 1 : 0; # nonzero and not NaN
214
+ }
215
+
216
+ # _bool: wrap a Perl truthy/falsy into a JS boolean (JSON::PP::Boolean), so
217
+ # boolean-valued operators (relational, ===, !, Boolean()) return a real
218
+ # boolean rather than 1/0 — matching the Go evaluator's bool (e.g.
219
+ # String(a < b) is "true", and `'x' + (a < b)` is "xtrue"). The coercions
220
+ # above already treat JSON::PP::Boolean correctly.
221
+ sub _bool ($t) { $t ? JSON::PP::true() : JSON::PP::false() }
222
+
223
+ # ---------------------------------------------------------------------------
224
+ # Operators
225
+ # ---------------------------------------------------------------------------
226
+
227
+ sub _binary ($op, $l, $r) {
228
+ if ($op eq '+') {
229
+ # JS `+`: string concatenation once either operand is a string,
230
+ # numeric addition otherwise.
231
+ return _to_string($l) . _to_string($r) if _is_string($l) || _is_string($r);
232
+ return _to_number($l) + _to_number($r);
233
+ }
234
+ return _to_number($l) - _to_number($r) if $op eq '-';
235
+ return _to_number($l) * _to_number($r) if $op eq '*';
236
+ if ($op eq '/') {
237
+ my $ln = _to_number($l);
238
+ my $rn = _to_number($r);
239
+ # JS division by zero is finite-valued, not an error: x/0 is ±Infinity
240
+ # (NaN for 0/0). Perl's native `/` dies on a zero divisor, so guard it
241
+ # to stay JS-faithful and match the Go evaluator (Go float division
242
+ # already yields ±Inf / NaN).
243
+ if ($rn == 0) {
244
+ return _nan() if $ln == 0 || $ln != $ln;
245
+ return $ln > 0 ? 9**9**9 : -(9**9**9);
246
+ }
247
+ return $ln / $rn;
248
+ }
249
+ if ($op eq '%') {
250
+ my $rn = _to_number($r);
251
+ return _nan() if $rn == 0;
252
+ return POSIX::fmod(_to_number($l), $rn);
253
+ }
254
+ return _relational($op, $l, $r) if $op eq '<' || $op eq '<=' || $op eq '>' || $op eq '>=';
255
+ return _bool(_strict_eq($l, $r)) if $op eq '===';
256
+ return _bool(!_strict_eq($l, $r)) if $op eq '!==';
257
+ # Loose equality / bitwise / shift are out of the subset.
258
+ return undef;
259
+ }
260
+
261
+ sub _relational ($op, $l, $r) {
262
+ # JS Abstract Relational Comparison: both strings → compare by code unit;
263
+ # otherwise coerce both to numbers (a NaN operand makes it false).
264
+ my $c;
265
+ if (_is_string($l) && _is_string($r)) {
266
+ $c = $l lt $r ? -1 : $l gt $r ? 1 : 0;
267
+ }
268
+ else {
269
+ my $ln = _to_number($l);
270
+ my $rn = _to_number($r);
271
+ return _bool(0) if $ln != $ln || $rn != $rn; # NaN → false
272
+ $c = $ln < $rn ? -1 : $ln > $rn ? 1 : 0;
273
+ }
274
+ return _bool($c < 0) if $op eq '<';
275
+ return _bool($c <= 0) if $op eq '<=';
276
+ return _bool($c > 0) if $op eq '>';
277
+ return _bool($c >= 0) if $op eq '>=';
278
+ return _bool(0);
279
+ }
280
+
281
+ sub _strict_eq ($l, $r) {
282
+ # Strict `===`: equal JS type and value, no coercion.
283
+ my $ln = _is_number($l);
284
+ my $rn = _is_number($r);
285
+ if ($ln && $rn) {
286
+ my ($lf, $rf) = ($l + 0, $r + 0);
287
+ return 0 if $lf != $lf || $rf != $rf; # NaN
288
+ return $lf == $rf ? 1 : 0;
289
+ }
290
+ return 0 if $ln != $rn; # one numeric, one not
291
+ if (!defined $l) { return !defined $r ? 1 : 0 }
292
+ return 0 if !defined $r;
293
+ my $lb = ref $l eq 'JSON::PP::Boolean';
294
+ my $rb = ref $r eq 'JSON::PP::Boolean';
295
+ if ($lb || $rb) {
296
+ return 0 unless $lb && $rb;
297
+ return ((!!$l) == (!!$r)) ? 1 : 0;
298
+ }
299
+ return ($l eq $r ? 1 : 0) if _is_string($l) && _is_string($r);
300
+ return 0;
301
+ }
302
+
303
+ # _same_value_zero: `Array.prototype.includes` membership test — `===`
304
+ # except `NaN` equals itself (and +0/-0 are not distinguished, which the
305
+ # JSON-decoded values here can't represent anyway). Reuses `_strict_eq`'s
306
+ # type/value rules and only special-cases the two-NaN case that `_strict_eq`
307
+ # (deliberately, for `===`) reports as unequal.
308
+ sub _same_value_zero ($l, $r) {
309
+ if (_is_number($l) && _is_number($r)) {
310
+ my ($lf, $rf) = ($l + 0, $r + 0);
311
+ return 1 if $lf != $lf && $rf != $rf; # NaN sameValueZero NaN
312
+ }
313
+ return _strict_eq($l, $r);
314
+ }
315
+
316
+ sub _unary ($op, $v) {
317
+ return _bool(!_truthy($v)) if $op eq '!';
318
+ return -_to_number($v) if $op eq '-';
319
+ return _to_number($v) if $op eq '+';
320
+ return undef;
321
+ }
322
+
323
+ # ---------------------------------------------------------------------------
324
+ # Built-in calls (the deterministic allowlist). Locale-sensitive builtins
325
+ # (localeCompare) are deliberately excluded to keep the backends isomorphic.
326
+ # ---------------------------------------------------------------------------
327
+
328
+ # _builtin_name: resolve a `call` callee to its builtin name (e.g.
329
+ # "Math.max"), or '' when the callee is not an allowlisted builtin reference.
330
+ sub _builtin_name ($callee) {
331
+ return '' unless ref $callee eq 'HASH';
332
+ my $kind = $callee->{kind} // '';
333
+ if ($kind eq 'identifier') {
334
+ return $callee->{name} // '';
335
+ }
336
+ if ($kind eq 'member' && !$callee->{computed}) {
337
+ my $obj = $callee->{object};
338
+ return '' unless ref $obj eq 'HASH' && ($obj->{kind} // '') eq 'identifier';
339
+ return ($obj->{name} // '') . '.' . ($callee->{property} // '');
340
+ }
341
+ return '';
342
+ }
343
+
344
+ # _math_round: half rounds toward +Infinity (JS Math.round: 2.5→3, -2.5→-2),
345
+ # matching the existing round helper rather than half-away-from-zero.
346
+ sub _math_round ($n) {
347
+ return POSIX::floor($n + 0.5);
348
+ }
349
+
350
+ sub _call_builtin ($name, $args) {
351
+ if ($name eq 'Math.max') {
352
+ my $m = -(9**9**9); # JS Math.max() with no args is -Infinity
353
+ for my $a (@$args) {
354
+ my $n = _to_number($a);
355
+ return $n if $n != $n; # any NaN argument ⇒ NaN (JS / Go)
356
+ $m = $n if $n > $m;
357
+ }
358
+ return $m;
359
+ }
360
+ if ($name eq 'Math.min') {
361
+ my $m = 9**9**9; # JS Math.min() with no args is +Infinity
362
+ for my $a (@$args) {
363
+ my $n = _to_number($a);
364
+ return $n if $n != $n; # any NaN argument ⇒ NaN (JS / Go)
365
+ $m = $n if $n < $m;
366
+ }
367
+ return $m;
368
+ }
369
+ return abs(_to_number($args->[0])) if $name eq 'Math.abs';
370
+ return POSIX::floor(_to_number($args->[0])) if $name eq 'Math.floor';
371
+ return POSIX::ceil(_to_number($args->[0])) if $name eq 'Math.ceil';
372
+ return _math_round(_to_number($args->[0])) if $name eq 'Math.round';
373
+ return _to_string($args->[0]) if $name eq 'String';
374
+ return _to_number($args->[0]) if $name eq 'Number';
375
+ return _bool(_truthy($args->[0])) if $name eq 'Boolean';
376
+ # Any other callee is outside the subset (refused upstream).
377
+ return undef;
378
+ }
379
+
380
+ # ---------------------------------------------------------------------------
381
+ # Member / index access
382
+ # ---------------------------------------------------------------------------
383
+
384
+ sub _read_property ($obj, $key) {
385
+ return undef unless defined $obj;
386
+ if (ref $obj eq 'HASH') {
387
+ return exists $obj->{$key} ? $obj->{$key} : undef;
388
+ }
389
+ if (ref $obj eq 'ARRAY') {
390
+ return $key eq 'length' ? scalar(@$obj) : undef;
391
+ }
392
+ return undef if ref $obj;
393
+ # `.length` is a string property only — a numeric scalar (123) has no
394
+ # `.length` in the subset (JS `(123).length` is undefined → null), so
395
+ # guard on _is_string rather than coercing the number to a string.
396
+ # Matches the Go evaluator (numbers fall through to nil there).
397
+ return length($obj) if $key eq 'length' && _is_string($obj);
398
+ return undef;
399
+ }
400
+
401
+ sub _read_index ($obj, $index) {
402
+ if (ref $obj eq 'ARRAY') {
403
+ my $f = _to_number($index);
404
+ my $i = int($f);
405
+ return undef if $i != $f || $i < 0 || $i >= @$obj;
406
+ return $obj->[$i];
407
+ }
408
+ if (ref $obj eq 'HASH') {
409
+ return $obj->{ _to_string($index) };
410
+ }
411
+ return undef;
412
+ }
413
+
414
+ # ---------------------------------------------------------------------------
415
+ # Evaluator-driven higher-order folds (the generalization of bf_reduce /
416
+ # bf_sort onto the evaluator) — the runtime half both Perl backends share.
417
+ # ---------------------------------------------------------------------------
418
+
419
+ # fold($items, $body, $acc_name, $item_name, $init, $direction, $base_env)
420
+ #
421
+ # Fold an arrayref into a value via the evaluator. The reducer $body is a
422
+ # pure ParsedExpr node evaluated against `{$acc_name => acc, $item_name =>
423
+ # item}` plus the captured free vars in $base_env per element; $init seeds the
424
+ # accumulator and $direction is "left" (reduce) or "right" (reduceRight).
425
+ # Generalizes bf_reduce — any reducer body, not just the +/* arithmetic
426
+ # catalogue, and acc may appear anywhere. $base_env is optional; the
427
+ # acc/item keys shadow any same-named base key. Mirrors Go's FoldEval.
428
+ sub fold ($items, $body, $acc_name, $item_name, $init, $direction = 'left', $base_env = undef) {
429
+ my @arr = ref $items eq 'ARRAY' ? @$items : ();
430
+ @arr = reverse @arr if ($direction // '') eq 'right';
431
+ # Seed the env from the captured free vars once; acc / item are
432
+ # overwritten each iteration (constant base keys carry through).
433
+ my %env = $base_env ? %$base_env : ();
434
+ my $acc = $init;
435
+ for my $item (@arr) {
436
+ $env{$acc_name} = $acc;
437
+ $env{$item_name} = $item;
438
+ $acc = evaluate($body, \%env);
439
+ }
440
+ return $acc;
441
+ }
442
+
443
+ # sort_by($items, $cmp, $param_a, $param_b, $base_env)
444
+ #
445
+ # Return a new arrayref ordered by a ParsedExpr comparator $cmp evaluated
446
+ # against `{$param_a => a, $param_b => b}` plus the captured free vars in
447
+ # $base_env to a number (negative / zero / positive, like a JS comparator).
448
+ # Generalizes bf_sort — any comparator body. $base_env is optional. Stable
449
+ # and non-mutating. Mirrors Go's SortEval.
450
+ sub sort_by ($items, $cmp, $param_a, $param_b, $base_env = undef) {
451
+ # Non-array receiver → empty arrayref, matching the nil-tolerant
452
+ # BarefootJS->sort helper convention (and avoiding an undef-deref footgun
453
+ # for callers that use the result as an arrayref). Value-compatible with
454
+ # the Go SortEval, whose nil slice iterates as empty too.
455
+ return [] unless ref $items eq 'ARRAY';
456
+ my %env = $base_env ? %$base_env : ();
457
+ # Decorate each element with its original index and tie-break on it when
458
+ # the comparator returns 0, so stability is explicit and independent of
459
+ # the `sort` pragma / build (portable to the declared minimum Perl, and
460
+ # matching Go's sort.SliceStable).
461
+ my @decorated = map { [ $_, $items->[$_] ] } 0 .. $#$items;
462
+ my @sorted = sort {
463
+ $env{$param_a} = $a->[1];
464
+ $env{$param_b} = $b->[1];
465
+ my $c = _to_number(evaluate($cmp, \%env));
466
+ # Explicit sign test rather than `<=> 0`: a NaN comparator result
467
+ # warns / is undefined under `<=>`, whereas `< 0` / `> 0` are both
468
+ # false for NaN, yielding 0 (no reordering) — matching JS (NaN
469
+ # comparator ⇒ keep order) and the Go SortEval sign test. The
470
+ # original-index tie-break then preserves input order for equal keys.
471
+ ($c < 0 ? -1 : $c > 0 ? 1 : 0) || ($a->[0] <=> $b->[0])
472
+ } @decorated;
473
+ return [ map { $_->[1] } @sorted ];
474
+ }
475
+
476
+ # JSON entry points for the adapters: decode the callback body once, then fold /
477
+ # sort. Mirror the Go `bf_reduce_eval` / `bf_sort_eval` template funcs, which the
478
+ # adapters emit with a serialized-ParsedExpr body argument.
479
+ sub fold_json ($items, $body_json, $acc_name, $item_name, $init, $direction = 'left', $base_env = undef) {
480
+ require JSON::PP;
481
+ return fold($items, JSON::PP->new->decode($body_json), $acc_name, $item_name, $init, $direction, $base_env);
482
+ }
483
+
484
+ sub sort_by_json ($items, $cmp_json, $param_a, $param_b, $base_env = undef) {
485
+ require JSON::PP;
486
+ return sort_by($items, JSON::PP->new->decode($cmp_json), $param_a, $param_b, $base_env);
487
+ }
488
+
489
+ # ---------------------------------------------------------------------------
490
+ # Higher-order predicates (#2018, P2) — the generalization of bf_filter /
491
+ # bf_find / bf_find_index / bf_every / bf_some onto the evaluator. The
492
+ # predicate $pred is a pure ParsedExpr evaluated against `{$param => item}`
493
+ # plus the captured free vars in $base_env per element, lifting the
494
+ # field-equality / truthiness restriction to any pure predicate body. Each
495
+ # mirrors the corresponding Go helper (FilterEval / EveryEval / SomeEval /
496
+ # FindEval / FindIndexEval). $base_env is optional.
497
+ # ---------------------------------------------------------------------------
498
+
499
+ # filter — new arrayref of the elements the predicate keeps. Non-array receiver
500
+ # → empty arrayref (the BarefootJS->filter nil-tolerant convention).
501
+ sub filter ($items, $pred, $param, $base_env = undef) {
502
+ return [] unless ref $items eq 'ARRAY';
503
+ my %env = $base_env ? %$base_env : ();
504
+ my @out;
505
+ for my $item (@$items) {
506
+ $env{$param} = $item;
507
+ push @out, $item if _truthy(evaluate($pred, \%env));
508
+ }
509
+ return \@out;
510
+ }
511
+
512
+ # every — 1 iff the predicate holds for every element (vacuously 1 for an empty
513
+ # receiver, like JS).
514
+ sub every ($items, $pred, $param, $base_env = undef) {
515
+ my @arr = ref $items eq 'ARRAY' ? @$items : ();
516
+ my %env = $base_env ? %$base_env : ();
517
+ for my $item (@arr) {
518
+ $env{$param} = $item;
519
+ return 0 unless _truthy(evaluate($pred, \%env));
520
+ }
521
+ return 1;
522
+ }
523
+
524
+ # some — 1 iff the predicate holds for any element (0 for an empty receiver).
525
+ sub some ($items, $pred, $param, $base_env = undef) {
526
+ my @arr = ref $items eq 'ARRAY' ? @$items : ();
527
+ my %env = $base_env ? %$base_env : ();
528
+ for my $item (@arr) {
529
+ $env{$param} = $item;
530
+ return 1 if _truthy(evaluate($pred, \%env));
531
+ }
532
+ return 0;
533
+ }
534
+
535
+ # find — first matching element, or undef. $forward false searches from the end
536
+ # (findLast).
537
+ sub find ($items, $pred, $param, $forward = 1, $base_env = undef) {
538
+ my @arr = ref $items eq 'ARRAY' ? @$items : ();
539
+ @arr = reverse @arr unless $forward;
540
+ my %env = $base_env ? %$base_env : ();
541
+ for my $item (@arr) {
542
+ $env{$param} = $item;
543
+ return $item if _truthy(evaluate($pred, \%env));
544
+ }
545
+ return undef;
546
+ }
547
+
548
+ # find_index — index of the first matching element, or -1. $forward false →
549
+ # findLastIndex (the index is into the original array either way).
550
+ sub find_index ($items, $pred, $param, $forward = 1, $base_env = undef) {
551
+ my @arr = ref $items eq 'ARRAY' ? @$items : ();
552
+ my %env = $base_env ? %$base_env : ();
553
+ my @idx = $forward ? ( 0 .. $#arr ) : reverse( 0 .. $#arr );
554
+ for my $i (@idx) {
555
+ $env{$param} = $arr[$i];
556
+ return $i if _truthy(evaluate($pred, \%env));
557
+ }
558
+ return -1;
559
+ }
560
+
561
+ # flat_map — project each element through $proj (a pure ParsedExpr) and flatten
562
+ # the results one level. A projection yielding an arrayref contributes its
563
+ # elements; any other value contributes itself (JS `.flatMap` keeps a non-array
564
+ # return as a single element). Generalizes bf->flat_map / flat_map_tuple to any
565
+ # pure projection. Mirrors Go's FlatMapEval. $base_env is optional.
566
+ sub flat_map ($items, $proj, $param, $base_env = undef) {
567
+ my @arr = ref $items eq 'ARRAY' ? @$items : ();
568
+ my %env = $base_env ? %$base_env : ();
569
+ my @out;
570
+ for my $item (@arr) {
571
+ $env{$param} = $item;
572
+ my $v = evaluate($proj, \%env);
573
+ if (ref $v eq 'ARRAY') { push @out, @$v }
574
+ else { push @out, $v }
575
+ }
576
+ return \@out;
577
+ }
578
+
579
+ # map_items — project each element through $proj (a pure ParsedExpr), keeping
580
+ # each result as one element (no flatten): JS value-producing `.map(cb)`
581
+ # (#2073). Named map_items (not `map`) so the Perl builtin stays unshadowed.
582
+ # Mirrors Go's MapEval. $base_env is optional.
583
+ sub map_items ($items, $proj, $param, $base_env = undef) {
584
+ my @arr = ref $items eq 'ARRAY' ? @$items : ();
585
+ my %env = $base_env ? %$base_env : ();
586
+ my @out;
587
+ for my $item (@arr) {
588
+ $env{$param} = $item;
589
+ push @out, evaluate($proj, \%env);
590
+ }
591
+ return \@out;
592
+ }
593
+
594
+ # ---------------------------------------------------------------------------
595
+ # JSON-string seams — the adapters emit `bf->filter_eval($recv, '<json>', …)`;
596
+ # the predicate body arrives as a JSON string here, decoded then handed to the
597
+ # helper above (mirroring fold_json / sort_by_json).
598
+ # ---------------------------------------------------------------------------
599
+
600
+ sub filter_json ($items, $pred_json, $param, $base_env = undef) {
601
+ require JSON::PP;
602
+ return filter($items, JSON::PP->new->decode($pred_json), $param, $base_env);
603
+ }
604
+
605
+ sub every_json ($items, $pred_json, $param, $base_env = undef) {
606
+ require JSON::PP;
607
+ return every($items, JSON::PP->new->decode($pred_json), $param, $base_env);
608
+ }
609
+
610
+ sub some_json ($items, $pred_json, $param, $base_env = undef) {
611
+ require JSON::PP;
612
+ return some($items, JSON::PP->new->decode($pred_json), $param, $base_env);
613
+ }
614
+
615
+ sub find_json ($items, $pred_json, $param, $forward = 1, $base_env = undef) {
616
+ require JSON::PP;
617
+ return find($items, JSON::PP->new->decode($pred_json), $param, $forward, $base_env);
618
+ }
619
+
620
+ sub find_index_json ($items, $pred_json, $param, $forward = 1, $base_env = undef) {
621
+ require JSON::PP;
622
+ return find_index($items, JSON::PP->new->decode($pred_json), $param, $forward, $base_env);
623
+ }
624
+
625
+ sub flat_map_json ($items, $proj_json, $param, $base_env = undef) {
626
+ require JSON::PP;
627
+ return flat_map($items, JSON::PP->new->decode($proj_json), $param, $base_env);
628
+ }
629
+
630
+ sub map_json ($items, $proj_json, $param, $base_env = undef) {
631
+ require JSON::PP;
632
+ return map_items($items, JSON::PP->new->decode($proj_json), $param, $base_env);
633
+ }
634
+
635
+ 1;