@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.
@@ -1,5 +1,5 @@
1
1
  package BarefootJS::DevReload;
2
- our $VERSION = "0.16.0";
2
+ our $VERSION = "0.17.0";
3
3
  use strict;
4
4
  use warnings;
5
5
  use feature 'signatures';
@@ -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 / unsupported: a callback body
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.16.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 `eq`; one defined-vs-undef
585
- # hop matches JS's `===` for null/undefined.
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 (!defined $item) {
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.0",
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;
@@ -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',
@@ -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
- # `eq`; scalar falls back to `index(..., ...) != -1`. Anything else
85
- # (HASH ref, code ref) returns false to match the JS semantic that
86
- # `.includes` is only defined on Array / TypedArray / String.
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-wise `eq` (handles defined/undef parity).
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';