@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.
- package/lib/BarefootJS/DevReload.pm +1 -1
- package/lib/BarefootJS/Evaluator.pm +635 -0
- package/lib/BarefootJS.pm +120 -8
- package/package.json +1 -1
- package/t/eval_vectors.t +113 -0
- package/t/evaluator.t +333 -0
- package/t/helper_vectors.t +15 -5
- package/t/query.t +49 -0
- package/t/template_primitives.t +15 -4
|
@@ -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;
|