@barefootjs/perl 0.17.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 +58 -1
- package/lib/BarefootJS.pm +17 -8
- package/package.json +1 -1
- package/t/evaluator.t +68 -0
- package/t/helper_vectors.t +0 -4
- package/t/template_primitives.t +15 -4
|
@@ -108,8 +108,32 @@ sub evaluate ($node, $env) {
|
|
|
108
108
|
}
|
|
109
109
|
return \%out;
|
|
110
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
|
+
}
|
|
111
135
|
|
|
112
|
-
# arrow-fn / higher-order / array-method
|
|
136
|
+
# arrow-fn / higher-order / unsupported array-method: a callback body
|
|
113
137
|
# containing these is refused upstream (BF101); never reached here.
|
|
114
138
|
return undef;
|
|
115
139
|
}
|
|
@@ -276,6 +300,19 @@ sub _strict_eq ($l, $r) {
|
|
|
276
300
|
return 0;
|
|
277
301
|
}
|
|
278
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
|
+
|
|
279
316
|
sub _unary ($op, $v) {
|
|
280
317
|
return _bool(!_truthy($v)) if $op eq '!';
|
|
281
318
|
return -_to_number($v) if $op eq '-';
|
|
@@ -539,6 +576,21 @@ sub flat_map ($items, $proj, $param, $base_env = undef) {
|
|
|
539
576
|
return \@out;
|
|
540
577
|
}
|
|
541
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
|
+
|
|
542
594
|
# ---------------------------------------------------------------------------
|
|
543
595
|
# JSON-string seams — the adapters emit `bf->filter_eval($recv, '<json>', …)`;
|
|
544
596
|
# the predicate body arrives as a JSON string here, decoded then handed to the
|
|
@@ -575,4 +627,9 @@ sub flat_map_json ($items, $proj_json, $param, $base_env = undef) {
|
|
|
575
627
|
return flat_map($items, JSON::PP->new->decode($proj_json), $param, $base_env);
|
|
576
628
|
}
|
|
577
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
|
+
|
|
578
635
|
1;
|
package/lib/BarefootJS.pm
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
package BarefootJS;
|
|
2
|
-
our $VERSION = "0.
|
|
2
|
+
our $VERSION = "0.17.0";
|
|
3
3
|
use strict;
|
|
4
4
|
use warnings;
|
|
5
5
|
use utf8;
|
|
@@ -581,8 +581,14 @@ sub round ($self, $value) {
|
|
|
581
581
|
# receiver shapes apart without TS type inference, so both lower to
|
|
582
582
|
# the same IR node (`array-method` / method `includes`). This helper
|
|
583
583
|
# dispatches at the Perl level via `ref()`:
|
|
584
|
-
# - ARRAY ref: scan elements with `
|
|
585
|
-
#
|
|
584
|
+
# - ARRAY ref: scan elements with `BarefootJS::Evaluator::_same_value_zero`,
|
|
585
|
+
# matching `Array.prototype.includes`'s SameValueZero
|
|
586
|
+
# semantics (no cross-type coercion, e.g. `[2].includes("2")`
|
|
587
|
+
# is false; NaN matches NaN) — the same algorithm the
|
|
588
|
+
# evaluator's serialized-callback path already uses for
|
|
589
|
+
# `.includes`, so both positions agree. This used to be a
|
|
590
|
+
# stringy `eq` scan, which coerced numbers to strings
|
|
591
|
+
# (`[2].includes("2")` was true) and diverged from JS.
|
|
586
592
|
# - scalar: `index($recv, $sub) != -1`, with both args
|
|
587
593
|
# coerced through `// ''` so an undef receiver /
|
|
588
594
|
# needle doesn't trip Perl's substr warning.
|
|
@@ -593,11 +599,7 @@ sub round ($self, $value) {
|
|
|
593
599
|
sub includes ($self, $recv, $elem) {
|
|
594
600
|
if (ref($recv) eq 'ARRAY') {
|
|
595
601
|
for my $item (@$recv) {
|
|
596
|
-
if (
|
|
597
|
-
return 1 if !defined $elem;
|
|
598
|
-
next;
|
|
599
|
-
}
|
|
600
|
-
return 1 if defined $elem && $item eq $elem;
|
|
602
|
+
return 1 if BarefootJS::Evaluator::_same_value_zero($item, $elem);
|
|
601
603
|
}
|
|
602
604
|
return 0;
|
|
603
605
|
}
|
|
@@ -1180,6 +1182,13 @@ sub flat_map_eval ($self, $recv, $proj_json, $param, $base_env = {}) {
|
|
|
1180
1182
|
return BarefootJS::Evaluator::flat_map_json($recv, $proj_json, $param, $base_env);
|
|
1181
1183
|
}
|
|
1182
1184
|
|
|
1185
|
+
# Value-producing `.map(cb)` (#2073): project each element through the
|
|
1186
|
+
# serialized projection body, one result per element (no flatten). Composes
|
|
1187
|
+
# through the array-method chain (`.map(cb).join(' ')`).
|
|
1188
|
+
sub map_eval ($self, $recv, $proj_json, $param, $base_env = {}) {
|
|
1189
|
+
return BarefootJS::Evaluator::map_json($recv, $proj_json, $param, $base_env);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1183
1192
|
sub sort ($self, $recv, $opts = {}) {
|
|
1184
1193
|
return [] unless ref($recv) eq 'ARRAY';
|
|
1185
1194
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@barefootjs/perl",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "BarefootJS engine-agnostic Perl runtime (BarefootJS.pm) — pluggable rendering backend; the shared basis for Mojolicious and other Perl framework integrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
package/t/evaluator.t
CHANGED
|
@@ -20,6 +20,11 @@ sub ncall_math {
|
|
|
20
20
|
return { kind => 'call', callee => nmem(nid('Math'), $fn), args => [$arg] };
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
+
sub nincludes {
|
|
24
|
+
my ($object, $needle) = @_;
|
|
25
|
+
return { kind => 'array-method', method => 'includes', object => $object, args => [$needle] };
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
# A plain false for the `computed` flag; the evaluator only checks truthiness.
|
|
24
29
|
sub JSON_false { return 0 }
|
|
25
30
|
|
|
@@ -134,6 +139,40 @@ subtest 'boolean-valued ops return JS booleans, not 1/0' => sub {
|
|
|
134
139
|
ok(!defined $len, '(123).length is null, not 3');
|
|
135
140
|
};
|
|
136
141
|
|
|
142
|
+
# `.includes` (#2075) is the one `array-method` in the evaluator subset,
|
|
143
|
+
# dispatching on the receiver type like the SSR template lowering does at
|
|
144
|
+
# runtime (`bf_includes` / `$bf->includes`): array → SameValueZero membership
|
|
145
|
+
# (the same value rules as `===`, so a numeric 2 does NOT match the string
|
|
146
|
+
# "2"); string → substring search; anything else degrades to false rather
|
|
147
|
+
# than dying.
|
|
148
|
+
subtest 'array-method includes' => sub {
|
|
149
|
+
my $hit = BarefootJS::Evaluator::evaluate(nincludes(nid('tags'), nstr('go')), { tags => [ 'perl', 'go' ] });
|
|
150
|
+
ok(JSON::PP::is_bool($hit), 'array hit is a JS boolean');
|
|
151
|
+
ok($hit, 'array .includes: hit');
|
|
152
|
+
|
|
153
|
+
my $miss = BarefootJS::Evaluator::evaluate(nincludes(nid('tags'), nstr('rust')), { tags => [ 'perl', 'go' ] });
|
|
154
|
+
ok(JSON::PP::is_bool($miss), 'array miss is a JS boolean');
|
|
155
|
+
ok(!$miss, 'array .includes: miss');
|
|
156
|
+
|
|
157
|
+
# SameValueZero, not loose equality: the numeric element 2 matches the
|
|
158
|
+
# numeric needle 2, but the string needle "2" (a different JS type) does
|
|
159
|
+
# not — mirroring `===`'s type-sensitivity.
|
|
160
|
+
my $num_hit = BarefootJS::Evaluator::evaluate(nincludes(nid('nums'), { kind => 'literal', value => 2 }), { nums => [ 1, 2, 3 ] });
|
|
161
|
+
ok($num_hit, 'array .includes: numeric element hit');
|
|
162
|
+
my $num_vs_string = BarefootJS::Evaluator::evaluate(nincludes(nid('nums'), nstr('2')), { nums => [ 1, 2, 3 ] });
|
|
163
|
+
ok(!$num_vs_string, 'array .includes: numeric element does not match a string needle');
|
|
164
|
+
|
|
165
|
+
my $sub = BarefootJS::Evaluator::evaluate(nincludes(nid('name'), nstr('ar')), { name => 'bare' });
|
|
166
|
+
ok($sub, 'string .includes: substring hit');
|
|
167
|
+
|
|
168
|
+
# A non-array, non-string receiver (number, null, object) is not a JS
|
|
169
|
+
# `.includes` target; the evaluator degrades to false rather than dying.
|
|
170
|
+
my $scalar_recv = BarefootJS::Evaluator::evaluate(nincludes(nid('n'), { kind => 'literal', value => 1 }), { n => 42 });
|
|
171
|
+
ok(!$scalar_recv, 'non-collection receiver (number) is false, not a die');
|
|
172
|
+
my $null_recv = BarefootJS::Evaluator::evaluate(nincludes(nid('n'), nstr('x')), { n => undef });
|
|
173
|
+
ok(!$null_recv, 'non-collection receiver (null) is false, not a die');
|
|
174
|
+
};
|
|
175
|
+
|
|
137
176
|
# sort_by tolerates a non-array receiver by returning an empty arrayref (the
|
|
138
177
|
# BarefootJS->sort convention), never undef — so callers can always deref it.
|
|
139
178
|
subtest 'sort_by non-array receiver returns []' => sub {
|
|
@@ -262,4 +301,33 @@ subtest 'flat_map projects + flattens one level' => sub {
|
|
|
262
301
|
is_deeply($fj, [ 'a', 'b', 'c' ], 'flat_map_json decodes + projects');
|
|
263
302
|
};
|
|
264
303
|
|
|
304
|
+
# #2073: map_items is the value-producing `.map(cb)` — one result per element,
|
|
305
|
+
# NO flatten (an array-valued projection stays one element). Mirrors the Go
|
|
306
|
+
# TestMapEval shapes.
|
|
307
|
+
subtest 'map_items projects one result per element (no flatten)' => sub {
|
|
308
|
+
my $tmpl = {
|
|
309
|
+
kind => 'template-literal',
|
|
310
|
+
parts => [
|
|
311
|
+
{ type => 'string', value => '#' },
|
|
312
|
+
{ type => 'expression', expr => nid('t') },
|
|
313
|
+
],
|
|
314
|
+
};
|
|
315
|
+
is_deeply(BarefootJS::Evaluator::map_items([ 'perl', 'go' ], $tmpl, 't'),
|
|
316
|
+
[ '#perl', '#go' ], 'template-literal projection maps each element');
|
|
317
|
+
|
|
318
|
+
my @users = ({ name => 'Ada' }, { name => 'Grace' });
|
|
319
|
+
my $field = nmem(nid('u'), 'name');
|
|
320
|
+
is_deeply(BarefootJS::Evaluator::map_items(\@users, $field, 'u'),
|
|
321
|
+
[ 'Ada', 'Grace' ], 'field projection maps each element');
|
|
322
|
+
|
|
323
|
+
my @rows = ({ tags => [ 'a', 'b' ] });
|
|
324
|
+
is_deeply(BarefootJS::Evaluator::map_items(\@rows, nmem(nid('i'), 'tags'), 'i'),
|
|
325
|
+
[ [ 'a', 'b' ] ], 'array-valued projection stays ONE element (no flatten)');
|
|
326
|
+
|
|
327
|
+
# JSON seam.
|
|
328
|
+
my $mj = BarefootJS::Evaluator::map_json(\@users,
|
|
329
|
+
JSON::PP->new->encode($field), 'u');
|
|
330
|
+
is_deeply($mj, [ 'Ada', 'Grace' ], 'map_json decodes + projects');
|
|
331
|
+
};
|
|
332
|
+
|
|
265
333
|
done_testing;
|
package/t/helper_vectors.t
CHANGED
|
@@ -224,10 +224,6 @@ my %DIVERGENCES = (
|
|
|
224
224
|
expect => '0.3',
|
|
225
225
|
reason => 'Perl stringifies doubles via %.15g',
|
|
226
226
|
},
|
|
227
|
-
'includes/cross-type probe is strict-equality false' => {
|
|
228
|
-
expect => 1,
|
|
229
|
-
reason => 'bf->includes scans with eq (string equality), so 2 eq "2" matches',
|
|
230
|
-
},
|
|
231
227
|
'filter_truthy/the string "0" is truthy' => {
|
|
232
228
|
expect => ['x'],
|
|
233
229
|
reason => 'Perl truthiness treats the string "0" as false',
|
package/t/template_primitives.t
CHANGED
|
@@ -81,11 +81,16 @@ subtest 'floor / ceil / round — Math.* mirrors; propagate NaN' => sub {
|
|
|
81
81
|
# `Array.prototype.includes(x)` + `String.prototype.includes(sub)` lower
|
|
82
82
|
# to the same `$bf->includes($recv, $elem)` shape — see #1448 Tier A.
|
|
83
83
|
# The Perl helper dispatches on `ref()`: ARRAY ref scans elements with
|
|
84
|
-
# `
|
|
85
|
-
#
|
|
86
|
-
# `.includes`
|
|
84
|
+
# `BarefootJS::Evaluator::_same_value_zero` (SameValueZero — no cross-type
|
|
85
|
+
# coercion, NaN matches NaN), matching the evaluator's serialized-callback
|
|
86
|
+
# `.includes` path so both positions agree; scalar falls back to
|
|
87
|
+
# `index(..., ...) != -1`. Anything else (HASH ref, code ref) returns
|
|
88
|
+
# false to match the JS semantic that `.includes` is only defined on
|
|
89
|
+
# Array / TypedArray / String.
|
|
87
90
|
subtest 'includes — array + string + non-array/string dispatch' => sub {
|
|
88
|
-
# Array receiver: element
|
|
91
|
+
# Array receiver: SameValueZero element search (handles defined/undef
|
|
92
|
+
# parity, and no numeric/string coercion — see the cross-type cases
|
|
93
|
+
# below).
|
|
89
94
|
ok $bf->includes(['a', 'b', 'c'], 'b'), 'array contains element → 1';
|
|
90
95
|
ok !$bf->includes(['a', 'b', 'c'], 'z'), 'array does not contain → 0';
|
|
91
96
|
ok $bf->includes([1, 2, 3], 2), 'numeric element';
|
|
@@ -93,6 +98,12 @@ subtest 'includes — array + string + non-array/string dispatch' => sub {
|
|
|
93
98
|
ok $bf->includes([undef, 'a'], undef), 'undef element matches undef needle';
|
|
94
99
|
ok !$bf->includes(['a', 'b'], undef), 'undef needle, no undef element → 0';
|
|
95
100
|
|
|
101
|
+
# SameValueZero never coerces across types — pins the divergence from
|
|
102
|
+
# the old stringy `eq` scan (where `[2].includes("2")` was true).
|
|
103
|
+
ok !$bf->includes([2], '2'), '[2].includes("2") → 0 (no numeric→string coercion)';
|
|
104
|
+
ok $bf->includes([2], 2), '[2].includes(2) → 1';
|
|
105
|
+
ok $bf->includes(['2'], '2'), '["2"].includes("2") → 1 (string vs string still matches)';
|
|
106
|
+
|
|
96
107
|
# String receiver: substring search.
|
|
97
108
|
ok $bf->includes('hello world', 'world'), 'substring present → 1';
|
|
98
109
|
ok !$bf->includes('hello world', 'earth'), 'substring absent → 0';
|