@barefootjs/php 0.1.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.
- package/README.md +69 -0
- package/composer.json +21 -0
- package/package.json +26 -0
- package/src/BarefootJS.php +1705 -0
- package/src/Evaluator.php +968 -0
- package/src/Json.php +67 -0
- package/src/SearchParams.php +77 -0
- package/tests/_harness.php +164 -0
- package/tests/run.php +72 -0
- package/tests/test_eval_vectors.php +119 -0
- package/tests/test_evaluator.php +274 -0
- package/tests/test_helper_vectors.php +334 -0
- package/tests/test_omit.php +57 -0
- package/tests/test_props_attr.php +63 -0
- package/tests/test_query.php +47 -0
- package/tests/test_render_child.php +123 -0
- package/tests/test_search_params.php +56 -0
- package/tests/test_spread_attrs.php +98 -0
- package/tests/test_template_primitives.php +370 -0
- package/tests/vector-divergences.json +42 -0
|
@@ -0,0 +1,968 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Barefoot;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Port of packages/adapter-perl/lib/BarefootJS/Evaluator.pm (see also the
|
|
9
|
+
* Python port packages/adapter-jinja/python/barefootjs/evaluator.py).
|
|
10
|
+
*
|
|
11
|
+
* Lightweight evaluator for the pure `ParsedExpr` subset, scoped to
|
|
12
|
+
* higher-order callback bodies (reduce / sort / map / filter / find
|
|
13
|
+
* `(...) => expr`) -- issue #2018. Templates cannot carry a lambda in
|
|
14
|
+
* expression position, so the callback BODY rides as a pure `ParsedExpr`
|
|
15
|
+
* subtree (the structured IR the compiler already produces) and is
|
|
16
|
+
* evaluated here against an environment (`[acc, item, ...captured free
|
|
17
|
+
* vars]`).
|
|
18
|
+
*
|
|
19
|
+
* ONE shared implementation for the Twig backend (mirroring the two Perl
|
|
20
|
+
* backends sharing Evaluator.pm, and the Jinja Python port's evaluator.py).
|
|
21
|
+
* The accepted subset and its semantics are documented in
|
|
22
|
+
* spec/compiler.md ("ParsedExpr Evaluator Semantics") and pinned
|
|
23
|
+
* isomorphically by the cross-language golden vectors
|
|
24
|
+
* (packages/adapter-tests/vectors/eval-vectors.json), shared with the
|
|
25
|
+
* Go, Perl and Python evaluators -- same input -> same output.
|
|
26
|
+
*
|
|
27
|
+
* Node access: unlike Perl/Python (whose JSON decoders always produce a
|
|
28
|
+
* hashref/dict for a JSON object), PHP's canonical `json_decode($s)` (no
|
|
29
|
+
* `assoc`) decodes a JSON object into `stdClass` so that `{}` and `[]`
|
|
30
|
+
* round-trip distinctly (see the design doc's "canonical value convention").
|
|
31
|
+
* `evaluate()` therefore accepts a ParsedExpr node as EITHER a decoded
|
|
32
|
+
* `stdClass`/array value OR a hand-built PHP associative array (the
|
|
33
|
+
* ergonomic shape `php/tests/test_evaluator.php` hand-builds, mirroring how
|
|
34
|
+
* the Perl/Python ports hand-build hashref/dict trees) -- `get()` below is
|
|
35
|
+
* the single point that tolerates both shapes for structural node access.
|
|
36
|
+
* JSON *arrays* (`args`, `elements`, `parts`, `properties`, ...) always
|
|
37
|
+
* decode as plain PHP lists regardless of the `assoc` flag, so no such
|
|
38
|
+
* dual-shape handling is needed for those.
|
|
39
|
+
*/
|
|
40
|
+
final class Evaluator
|
|
41
|
+
{
|
|
42
|
+
private function __construct()
|
|
43
|
+
{
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private const NUM_RE = '/^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$/';
|
|
47
|
+
private const INF_NAN_RE = '/^[+-]?(inf(inity)?|nan)$/i';
|
|
48
|
+
|
|
49
|
+
/** Mirrors Scalar::Util::looks_like_number / the Python `looks_like_number`. */
|
|
50
|
+
public static function looksLikeNumber(string $s): bool
|
|
51
|
+
{
|
|
52
|
+
$t = trim($s);
|
|
53
|
+
if ($t === '') {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return (bool) preg_match(self::NUM_RE, $t) || (bool) preg_match(self::INF_NAN_RE, $t);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Parse a string already known to satisfy looksLikeNumber() into a float. */
|
|
60
|
+
public static function parseNumberLiteral(string $s): float
|
|
61
|
+
{
|
|
62
|
+
$t = trim($s);
|
|
63
|
+
if (preg_match(self::INF_NAN_RE, $t)) {
|
|
64
|
+
$low = strtolower($t);
|
|
65
|
+
if (str_contains($low, 'nan')) {
|
|
66
|
+
return NAN;
|
|
67
|
+
}
|
|
68
|
+
return str_starts_with($low, '-') ? -INF : INF;
|
|
69
|
+
}
|
|
70
|
+
return (float) $t;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Shared JS `Number.prototype.toString` formatting for a FINITE, non-NaN
|
|
75
|
+
* float (callers handle NaN/Infinity first). Needed because PHP's plain
|
|
76
|
+
* `(string)` cast on a float is lossy -- bounded by the `precision` ini
|
|
77
|
+
* setting (default 14 significant digits) -- unlike Perl, whose native
|
|
78
|
+
* stringification is already shortest-round-trip. `serialize_precision`
|
|
79
|
+
* (default -1 since PHP 7.1, shortest round-trip, matching V8) is what
|
|
80
|
+
* `var_export()`/`json_encode()` honour but plain `(string)` casts do
|
|
81
|
+
* not, hence the explicit routine (mirrors runtime.py's
|
|
82
|
+
* `_format_js_number` / this module's own `_format_number` in the
|
|
83
|
+
* Python port -- two independent copies there too, not shared, since
|
|
84
|
+
* runtime.py and evaluator.py are standalone modules; here it is
|
|
85
|
+
* shared between BarefootJS::string() and self::toStringJs() since nothing
|
|
86
|
+
* requires them to be independent implementations in PHP).
|
|
87
|
+
*/
|
|
88
|
+
public static function formatNumber(float $n): string
|
|
89
|
+
{
|
|
90
|
+
if ($n === 0.0) {
|
|
91
|
+
return '0'; // normalises -0.0 to JS's "0" spelling
|
|
92
|
+
}
|
|
93
|
+
if ($n == floor($n) && abs($n) < 1e21) {
|
|
94
|
+
return sprintf('%.0f', $n);
|
|
95
|
+
}
|
|
96
|
+
// Shortest round-trip decimal representation (serialize_precision=-1).
|
|
97
|
+
return var_export($n, true);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private static function get($node, string $key)
|
|
101
|
+
{
|
|
102
|
+
if (is_array($node)) {
|
|
103
|
+
return $node[$key] ?? null;
|
|
104
|
+
}
|
|
105
|
+
if (is_object($node)) {
|
|
106
|
+
return $node->$key ?? null;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private static function kind($node): string
|
|
112
|
+
{
|
|
113
|
+
if (is_array($node)) {
|
|
114
|
+
return (string) ($node['kind'] ?? '');
|
|
115
|
+
}
|
|
116
|
+
if (is_object($node)) {
|
|
117
|
+
return (string) ($node->kind ?? '');
|
|
118
|
+
}
|
|
119
|
+
return '';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** True for a JSON *array* value under the canonical value convention:
|
|
123
|
+
* a plain PHP list (empty arrays count as lists -- see the design doc). */
|
|
124
|
+
public static function isJsArray($v): bool
|
|
125
|
+
{
|
|
126
|
+
return is_array($v) && array_is_list($v);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Evaluate a decoded ParsedExpr node against the environment array,
|
|
131
|
+
* returning a PHP value (float, string, bool, null for JS null/undefined,
|
|
132
|
+
* list array, stdClass). The matching JSON entry point is evalJson().
|
|
133
|
+
*/
|
|
134
|
+
public static function evaluate($node, array $env)
|
|
135
|
+
{
|
|
136
|
+
if (!is_array($node) && !is_object($node)) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
$kind = self::kind($node);
|
|
140
|
+
|
|
141
|
+
if ($kind === 'literal') {
|
|
142
|
+
return self::get($node, 'value');
|
|
143
|
+
}
|
|
144
|
+
if ($kind === 'identifier') {
|
|
145
|
+
$name = self::get($node, 'name');
|
|
146
|
+
return $env[$name] ?? null;
|
|
147
|
+
}
|
|
148
|
+
if ($kind === 'binary') {
|
|
149
|
+
return self::binary(
|
|
150
|
+
(string) self::get($node, 'op'),
|
|
151
|
+
self::evaluate(self::get($node, 'left'), $env),
|
|
152
|
+
self::evaluate(self::get($node, 'right'), $env)
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
if ($kind === 'unary') {
|
|
156
|
+
return self::unary((string) self::get($node, 'op'), self::evaluate(self::get($node, 'argument'), $env));
|
|
157
|
+
}
|
|
158
|
+
if ($kind === 'logical') {
|
|
159
|
+
$op = self::get($node, 'op');
|
|
160
|
+
$left = self::evaluate(self::get($node, 'left'), $env);
|
|
161
|
+
if ($op === '&&') {
|
|
162
|
+
return self::truthy($left) ? self::evaluate(self::get($node, 'right'), $env) : $left;
|
|
163
|
+
}
|
|
164
|
+
if ($op === '||') {
|
|
165
|
+
return self::truthy($left) ? $left : self::evaluate(self::get($node, 'right'), $env);
|
|
166
|
+
}
|
|
167
|
+
// `??`
|
|
168
|
+
return $left !== null ? $left : self::evaluate(self::get($node, 'right'), $env);
|
|
169
|
+
}
|
|
170
|
+
if ($kind === 'conditional') {
|
|
171
|
+
return self::truthy(self::evaluate(self::get($node, 'test'), $env))
|
|
172
|
+
? self::evaluate(self::get($node, 'consequent'), $env)
|
|
173
|
+
: self::evaluate(self::get($node, 'alternate'), $env);
|
|
174
|
+
}
|
|
175
|
+
if ($kind === 'member') {
|
|
176
|
+
return self::readProperty(
|
|
177
|
+
self::evaluate(self::get($node, 'object'), $env),
|
|
178
|
+
self::get($node, 'property')
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if ($kind === 'index-access') {
|
|
182
|
+
return self::readIndex(
|
|
183
|
+
self::evaluate(self::get($node, 'object'), $env),
|
|
184
|
+
self::evaluate(self::get($node, 'index'), $env)
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if ($kind === 'call') {
|
|
188
|
+
$callback = self::arrayCallbackCall($node);
|
|
189
|
+
if ($callback !== null) {
|
|
190
|
+
[$method, $objectNode, $arrowNode] = $callback;
|
|
191
|
+
return self::evalArrayCallback($method, $objectNode, $arrowNode, $env);
|
|
192
|
+
}
|
|
193
|
+
$name = self::builtinName(self::get($node, 'callee'));
|
|
194
|
+
if ($name === '') {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
$argsNode = self::get($node, 'args');
|
|
198
|
+
$args = [];
|
|
199
|
+
foreach ((is_array($argsNode) ? $argsNode : []) as $a) {
|
|
200
|
+
$args[] = self::evaluate($a, $env);
|
|
201
|
+
}
|
|
202
|
+
return self::callBuiltin($name, $args);
|
|
203
|
+
}
|
|
204
|
+
if ($kind === 'template-literal') {
|
|
205
|
+
$out = '';
|
|
206
|
+
$partsNode = self::get($node, 'parts');
|
|
207
|
+
foreach ((is_array($partsNode) ? $partsNode : []) as $p) {
|
|
208
|
+
$type = self::get($p, 'type') ?? '';
|
|
209
|
+
if ($type === 'string') {
|
|
210
|
+
$out .= self::get($p, 'value') ?? '';
|
|
211
|
+
} else {
|
|
212
|
+
$out .= self::toStringJs(self::evaluate(self::get($p, 'expr'), $env));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return $out;
|
|
216
|
+
}
|
|
217
|
+
if ($kind === 'array-literal') {
|
|
218
|
+
$out = [];
|
|
219
|
+
$elementsNode = self::get($node, 'elements');
|
|
220
|
+
foreach ((is_array($elementsNode) ? $elementsNode : []) as $e) {
|
|
221
|
+
$out[] = self::evaluate($e, $env);
|
|
222
|
+
}
|
|
223
|
+
return $out;
|
|
224
|
+
}
|
|
225
|
+
if ($kind === 'object-literal') {
|
|
226
|
+
$out = new \stdClass();
|
|
227
|
+
$propsNode = self::get($node, 'properties');
|
|
228
|
+
foreach ((is_array($propsNode) ? $propsNode : []) as $prop) {
|
|
229
|
+
$key = (string) self::get($prop, 'key');
|
|
230
|
+
$out->$key = self::evaluate(self::get($prop, 'value'), $env);
|
|
231
|
+
}
|
|
232
|
+
return $out;
|
|
233
|
+
}
|
|
234
|
+
if ($kind === 'array-method') {
|
|
235
|
+
$method = (string) (self::get($node, 'method') ?? '');
|
|
236
|
+
$argsNode = self::get($node, 'args');
|
|
237
|
+
$argsArr = is_array($argsNode) ? $argsNode : [];
|
|
238
|
+
if ($method === 'includes' && count($argsArr) === 1) {
|
|
239
|
+
// `.includes(x)` (#2075) -- the one `array-method` in the
|
|
240
|
+
// evaluator subset, shared between `Array.prototype.includes`
|
|
241
|
+
// (SameValueZero membership) and `String.prototype.includes`
|
|
242
|
+
// (substring search).
|
|
243
|
+
$obj = self::evaluate(self::get($node, 'object'), $env);
|
|
244
|
+
$needle = self::evaluate($argsArr[0], $env);
|
|
245
|
+
if (self::isJsArray($obj)) {
|
|
246
|
+
foreach ($obj as $el) {
|
|
247
|
+
if (self::sameValueZero($el, $needle)) {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
if (is_string($obj)) {
|
|
254
|
+
return str_contains($obj, self::toStringJs($needle));
|
|
255
|
+
}
|
|
256
|
+
// Any other receiver is not a JS `.includes` target -- degrade
|
|
257
|
+
// to false rather than raising, mirroring the reference.
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
if ($method === 'join' && count($argsArr) <= 1) {
|
|
261
|
+
// `.join(sep?)` (#2094) -- default separator is `,`; a
|
|
262
|
+
// `null`/`undefined` element joins as `''`, not the string
|
|
263
|
+
// "null" (mirrors evalJoin in the Go reference).
|
|
264
|
+
$sep = count($argsArr) === 1
|
|
265
|
+
? self::toStringJs(self::evaluate($argsArr[0], $env))
|
|
266
|
+
: ',';
|
|
267
|
+
$obj = self::evaluate(self::get($node, 'object'), $env);
|
|
268
|
+
return self::evalJoin($obj, $sep);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// arrow-fn / higher-order / unsupported array-method: a callback
|
|
273
|
+
// body containing these is refused upstream (BF101); never reached.
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Recognize a nested `.map(cb)` / `.filter(cb)` callback call inside a
|
|
279
|
+
* `call` node (#2094): `callee` is a non-computed `member` node whose
|
|
280
|
+
* `property` is `map`/`filter`, and the first argument is an `arrow`
|
|
281
|
+
* node. Returns `[method, objectNode, arrowNode]` or `null` when the
|
|
282
|
+
* shape doesn't match (mirrors Go's `evalArrayCallbackCall`). Everything
|
|
283
|
+
* else nested (`.some`/`.find`/`.every`/`.sort`/`.reduce`/`.flat`/
|
|
284
|
+
* `.flatMap`, standalone arrows) stays refused upstream (BF101) -- this
|
|
285
|
+
* function only widens the two cases the compiler now allows to nest.
|
|
286
|
+
*/
|
|
287
|
+
private static function arrayCallbackCall($node): ?array
|
|
288
|
+
{
|
|
289
|
+
$callee = self::get($node, 'callee');
|
|
290
|
+
if ((!is_array($callee) && !is_object($callee)) || self::kind($callee) !== 'member') {
|
|
291
|
+
return null;
|
|
292
|
+
}
|
|
293
|
+
if (self::get($callee, 'computed')) {
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
$prop = (string) (self::get($callee, 'property') ?? '');
|
|
297
|
+
if ($prop !== 'map' && $prop !== 'filter') {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
$argsNode = self::get($node, 'args');
|
|
301
|
+
$argsArr = is_array($argsNode) ? $argsNode : [];
|
|
302
|
+
if (count($argsArr) === 0) {
|
|
303
|
+
return null;
|
|
304
|
+
}
|
|
305
|
+
$arrowNode = $argsArr[0];
|
|
306
|
+
if ((!is_array($arrowNode) && !is_object($arrowNode)) || self::kind($arrowNode) !== 'arrow') {
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return [$prop, self::get($callee, 'object'), $arrowNode];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Evaluate a nested `.map`/`.filter` callback call: evaluate the
|
|
314
|
+
* receiver, then invoke the arrow body once per element against a FRESH
|
|
315
|
+
* CHILD ENV (a copy of the parent env with the param(s) bound) --
|
|
316
|
+
* `$env` is passed by value here, and PHP arrays are copy-on-write, so
|
|
317
|
+
* mutating `$inner` never leaks back into the caller's `$env`. The
|
|
318
|
+
* arrow's 1st param binds the element, the 2nd (if present) binds the
|
|
319
|
+
* integer index -- mirrors Go's `evalArrayCallback`.
|
|
320
|
+
*/
|
|
321
|
+
private static function evalArrayCallback(string $method, $objectNode, $arrowNode, array $env)
|
|
322
|
+
{
|
|
323
|
+
$arr = self::evaluate($objectNode, $env);
|
|
324
|
+
if (!self::isJsArray($arr)) {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
$paramsNode = self::get($arrowNode, 'params');
|
|
328
|
+
$params = [];
|
|
329
|
+
foreach ((is_array($paramsNode) ? $paramsNode : []) as $p) {
|
|
330
|
+
$params[] = (string) $p;
|
|
331
|
+
}
|
|
332
|
+
$body = self::get($arrowNode, 'body');
|
|
333
|
+
$callCb = function ($item, int $index) use ($body, $params, $env) {
|
|
334
|
+
$inner = $env; // copy: fresh child scope per invocation
|
|
335
|
+
if (count($params) > 0) {
|
|
336
|
+
$inner[$params[0]] = $item;
|
|
337
|
+
}
|
|
338
|
+
if (count($params) > 1) {
|
|
339
|
+
$inner[$params[1]] = $index;
|
|
340
|
+
}
|
|
341
|
+
return self::evaluate($body, $inner);
|
|
342
|
+
};
|
|
343
|
+
if ($method === 'map') {
|
|
344
|
+
$out = [];
|
|
345
|
+
$i = 0;
|
|
346
|
+
foreach ($arr as $item) {
|
|
347
|
+
$out[] = $callCb($item, $i);
|
|
348
|
+
++$i;
|
|
349
|
+
}
|
|
350
|
+
return $out;
|
|
351
|
+
}
|
|
352
|
+
$out = [];
|
|
353
|
+
$i = 0;
|
|
354
|
+
foreach ($arr as $item) {
|
|
355
|
+
if (self::truthy($callCb($item, $i))) {
|
|
356
|
+
$out[] = $item;
|
|
357
|
+
}
|
|
358
|
+
++$i;
|
|
359
|
+
}
|
|
360
|
+
return $out;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/** `Array.prototype.join(sep?)` -- default separator `,`; a `null`
|
|
364
|
+
* element joins as `''`, not the string "null" (#2094). */
|
|
365
|
+
private static function evalJoin($obj, string $sep): string
|
|
366
|
+
{
|
|
367
|
+
if (!self::isJsArray($obj)) {
|
|
368
|
+
return '';
|
|
369
|
+
}
|
|
370
|
+
$parts = [];
|
|
371
|
+
foreach ($obj as $el) {
|
|
372
|
+
$parts[] = $el === null ? '' : self::toStringJs($el);
|
|
373
|
+
}
|
|
374
|
+
return implode($sep, $parts);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Decode a ParsedExpr JSON string and evaluate it. Mirrors the Go
|
|
378
|
+
* EvalExpr entry point and Perl/Python's eval_json. */
|
|
379
|
+
public static function evalJson(string $json, array $env)
|
|
380
|
+
{
|
|
381
|
+
return self::evaluate(json_decode($json), $env);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// -----------------------------------------------------------------
|
|
385
|
+
// JS coercion primitives (ToNumber / ToString / ToBoolean).
|
|
386
|
+
// -----------------------------------------------------------------
|
|
387
|
+
|
|
388
|
+
private static function toNumber($v): float
|
|
389
|
+
{
|
|
390
|
+
if ($v === null) {
|
|
391
|
+
return 0.0;
|
|
392
|
+
}
|
|
393
|
+
if (is_bool($v)) {
|
|
394
|
+
return $v ? 1.0 : 0.0;
|
|
395
|
+
}
|
|
396
|
+
if (is_int($v) || is_float($v)) {
|
|
397
|
+
return (float) $v;
|
|
398
|
+
}
|
|
399
|
+
if (is_string($v)) {
|
|
400
|
+
$t = trim($v);
|
|
401
|
+
if ($t === '') {
|
|
402
|
+
return 0.0;
|
|
403
|
+
}
|
|
404
|
+
return self::looksLikeNumber($t) ? self::parseNumberLiteral($t) : NAN;
|
|
405
|
+
}
|
|
406
|
+
return NAN; // array / object
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private static function toStringJs($v): string
|
|
410
|
+
{
|
|
411
|
+
if ($v === null) {
|
|
412
|
+
return 'null';
|
|
413
|
+
}
|
|
414
|
+
if (is_bool($v)) {
|
|
415
|
+
return $v ? 'true' : 'false';
|
|
416
|
+
}
|
|
417
|
+
if (is_int($v) || is_float($v)) {
|
|
418
|
+
$n = (float) $v;
|
|
419
|
+
if (is_nan($n)) {
|
|
420
|
+
return 'NaN';
|
|
421
|
+
}
|
|
422
|
+
if ($n === INF) {
|
|
423
|
+
return 'Infinity';
|
|
424
|
+
}
|
|
425
|
+
if ($n === -INF) {
|
|
426
|
+
return '-Infinity';
|
|
427
|
+
}
|
|
428
|
+
return self::formatNumber($n);
|
|
429
|
+
}
|
|
430
|
+
if (is_string($v)) {
|
|
431
|
+
return $v;
|
|
432
|
+
}
|
|
433
|
+
return ''; // arrays/objects -- not exercised by the evaluator subset's tested paths
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/** Public JS truthiness -- shared by BarefootJS::truthy() so both the
|
|
437
|
+
* evaluator and the runtime helper agree on one implementation. */
|
|
438
|
+
public static function truthy($v): bool
|
|
439
|
+
{
|
|
440
|
+
if ($v === null || $v === false) {
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
if ($v === true) {
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
if (is_int($v) || is_float($v)) {
|
|
447
|
+
$n = (float) $v;
|
|
448
|
+
return $n === $n && $n != 0.0; // not NaN, and nonzero
|
|
449
|
+
}
|
|
450
|
+
if (is_string($v)) {
|
|
451
|
+
return $v !== ''; // incl. the JS-truthy "0"
|
|
452
|
+
}
|
|
453
|
+
return true; // arrays / objects are always truthy in JS
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// -----------------------------------------------------------------
|
|
457
|
+
// Operators
|
|
458
|
+
// -----------------------------------------------------------------
|
|
459
|
+
|
|
460
|
+
private static function binary(string $op, $l, $r)
|
|
461
|
+
{
|
|
462
|
+
if ($op === '+') {
|
|
463
|
+
// JS `+`: string concatenation once either operand is a string,
|
|
464
|
+
// numeric addition otherwise.
|
|
465
|
+
if (is_string($l) || is_string($r)) {
|
|
466
|
+
return self::toStringJs($l) . self::toStringJs($r);
|
|
467
|
+
}
|
|
468
|
+
return self::toNumber($l) + self::toNumber($r);
|
|
469
|
+
}
|
|
470
|
+
if ($op === '-') {
|
|
471
|
+
return self::toNumber($l) - self::toNumber($r);
|
|
472
|
+
}
|
|
473
|
+
if ($op === '*') {
|
|
474
|
+
return self::toNumber($l) * self::toNumber($r);
|
|
475
|
+
}
|
|
476
|
+
if ($op === '/') {
|
|
477
|
+
$ln = self::toNumber($l);
|
|
478
|
+
$rn = self::toNumber($r);
|
|
479
|
+
if ($rn === 0.0) {
|
|
480
|
+
if ($ln === 0.0 || is_nan($ln)) {
|
|
481
|
+
return NAN;
|
|
482
|
+
}
|
|
483
|
+
return $ln > 0 ? INF : -INF;
|
|
484
|
+
}
|
|
485
|
+
return $ln / $rn;
|
|
486
|
+
}
|
|
487
|
+
if ($op === '%') {
|
|
488
|
+
$rn = self::toNumber($r);
|
|
489
|
+
if ($rn === 0.0) {
|
|
490
|
+
return NAN;
|
|
491
|
+
}
|
|
492
|
+
return fmod(self::toNumber($l), $rn);
|
|
493
|
+
}
|
|
494
|
+
if ($op === '<' || $op === '<=' || $op === '>' || $op === '>=') {
|
|
495
|
+
return self::relational($op, $l, $r);
|
|
496
|
+
}
|
|
497
|
+
if ($op === '===') {
|
|
498
|
+
return self::strictEq($l, $r);
|
|
499
|
+
}
|
|
500
|
+
if ($op === '!==') {
|
|
501
|
+
return !self::strictEq($l, $r);
|
|
502
|
+
}
|
|
503
|
+
// Loose equality / bitwise / shift are out of the subset.
|
|
504
|
+
return null;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
private static function relational(string $op, $l, $r): bool
|
|
508
|
+
{
|
|
509
|
+
// JS Abstract Relational Comparison: both strings -> compare by code
|
|
510
|
+
// unit; otherwise coerce both to numbers (a NaN operand -> false).
|
|
511
|
+
// MUST use strcmp(), not PHP's `<=>`/`<` operators: PHP applies
|
|
512
|
+
// "smart" numeric-string comparison when both operands look
|
|
513
|
+
// numeric ("10" <=> "9" compares 10 > 9 numerically), which would
|
|
514
|
+
// silently defeat the eval-vectors.json pin that two numeric
|
|
515
|
+
// strings compare LEXICALLY under JS `<` (see the golden vector
|
|
516
|
+
// "two numeric strings compare lexically, not numerically").
|
|
517
|
+
// strcmp() always does a raw byte comparison.
|
|
518
|
+
if (is_string($l) && is_string($r)) {
|
|
519
|
+
$c = strcmp($l, $r) <=> 0;
|
|
520
|
+
} else {
|
|
521
|
+
$ln = self::toNumber($l);
|
|
522
|
+
$rn = self::toNumber($r);
|
|
523
|
+
if (is_nan($ln) || is_nan($rn)) {
|
|
524
|
+
return false;
|
|
525
|
+
}
|
|
526
|
+
$c = $ln <=> $rn;
|
|
527
|
+
}
|
|
528
|
+
return match ($op) {
|
|
529
|
+
'<' => $c < 0,
|
|
530
|
+
'<=' => $c <= 0,
|
|
531
|
+
'>' => $c > 0,
|
|
532
|
+
'>=' => $c >= 0,
|
|
533
|
+
default => false,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/** Strict `===`: equal JS type and value, no coercion. Also the shared
|
|
538
|
+
* semantics behind `BarefootJS::eq()`/`neq()` (design doc "NEW helpers"
|
|
539
|
+
* section) -- int/float unify numerically (JS has one number type),
|
|
540
|
+
* bool/null strict, NaN !== NaN, arrays/objects fall back to PHP `===`
|
|
541
|
+
* (value-equality for arrays, identity for stdClass -- a documented
|
|
542
|
+
* divergence, same territory as the Perl port's refaddr comparison). */
|
|
543
|
+
public static function strictEq($l, $r): bool
|
|
544
|
+
{
|
|
545
|
+
$ln = is_int($l) || is_float($l);
|
|
546
|
+
$rn = is_int($r) || is_float($r);
|
|
547
|
+
if ($ln && $rn) {
|
|
548
|
+
$lf = (float) $l;
|
|
549
|
+
$rf = (float) $r;
|
|
550
|
+
if (is_nan($lf) || is_nan($rf)) {
|
|
551
|
+
return false;
|
|
552
|
+
}
|
|
553
|
+
return $lf === $rf;
|
|
554
|
+
}
|
|
555
|
+
if ($ln !== $rn) {
|
|
556
|
+
return false; // one numeric, one not
|
|
557
|
+
}
|
|
558
|
+
if ($l === null) {
|
|
559
|
+
return $r === null;
|
|
560
|
+
}
|
|
561
|
+
if ($r === null) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
$lb = is_bool($l);
|
|
565
|
+
$rb = is_bool($r);
|
|
566
|
+
if ($lb || $rb) {
|
|
567
|
+
if (!($lb && $rb)) {
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
return $l === $r;
|
|
571
|
+
}
|
|
572
|
+
if (is_string($l) && is_string($r)) {
|
|
573
|
+
return $l === $r;
|
|
574
|
+
}
|
|
575
|
+
// arrays/objects: PHP `===` fallback (see docstring above).
|
|
576
|
+
return $l === $r;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** `Array.prototype.includes` membership test -- `===` except `NaN`
|
|
580
|
+
* equals itself. Reuses strictEq()'s type/value rules and only
|
|
581
|
+
* special-cases the two-NaN case strictEq (deliberately, for `===`)
|
|
582
|
+
* reports as unequal. */
|
|
583
|
+
public static function sameValueZero($l, $r): bool
|
|
584
|
+
{
|
|
585
|
+
if ((is_int($l) || is_float($l)) && (is_int($r) || is_float($r))) {
|
|
586
|
+
$lf = (float) $l;
|
|
587
|
+
$rf = (float) $r;
|
|
588
|
+
if (is_nan($lf) && is_nan($rf)) {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return self::strictEq($l, $r);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
private static function unary(string $op, $v)
|
|
596
|
+
{
|
|
597
|
+
if ($op === '!') {
|
|
598
|
+
return !self::truthy($v);
|
|
599
|
+
}
|
|
600
|
+
if ($op === '-') {
|
|
601
|
+
return -self::toNumber($v);
|
|
602
|
+
}
|
|
603
|
+
if ($op === '+') {
|
|
604
|
+
return self::toNumber($v);
|
|
605
|
+
}
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// -----------------------------------------------------------------
|
|
610
|
+
// Built-in calls (the deterministic allowlist). Locale-sensitive
|
|
611
|
+
// builtins (localeCompare) are deliberately excluded to keep the
|
|
612
|
+
// backends isomorphic.
|
|
613
|
+
// -----------------------------------------------------------------
|
|
614
|
+
|
|
615
|
+
private static function builtinName($callee): string
|
|
616
|
+
{
|
|
617
|
+
if (!is_array($callee) && !is_object($callee)) {
|
|
618
|
+
return '';
|
|
619
|
+
}
|
|
620
|
+
$kind = self::get($callee, 'kind') ?? '';
|
|
621
|
+
if ($kind === 'identifier') {
|
|
622
|
+
return (string) (self::get($callee, 'name') ?? '');
|
|
623
|
+
}
|
|
624
|
+
if ($kind === 'member' && !self::get($callee, 'computed')) {
|
|
625
|
+
$obj = self::get($callee, 'object');
|
|
626
|
+
if ((!is_array($obj) && !is_object($obj)) || self::get($obj, 'kind') !== 'identifier') {
|
|
627
|
+
return '';
|
|
628
|
+
}
|
|
629
|
+
return (string) (self::get($obj, 'name') ?? '') . '.' . (string) (self::get($callee, 'property') ?? '');
|
|
630
|
+
}
|
|
631
|
+
return '';
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private static function safeFloor(float $n): float
|
|
635
|
+
{
|
|
636
|
+
return (is_nan($n) || is_infinite($n)) ? $n : floor($n);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private static function safeCeil(float $n): float
|
|
640
|
+
{
|
|
641
|
+
return (is_nan($n) || is_infinite($n)) ? $n : ceil($n);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/** Half rounds toward +Infinity (JS Math.round: 2.5 -> 3, -2.5 -> -2). */
|
|
645
|
+
private static function mathRound(float $n): float
|
|
646
|
+
{
|
|
647
|
+
return (is_nan($n) || is_infinite($n)) ? $n : floor($n + 0.5);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
private static function callBuiltin(string $name, array $args)
|
|
651
|
+
{
|
|
652
|
+
$arg = fn (int $i) => $args[$i] ?? null;
|
|
653
|
+
|
|
654
|
+
if ($name === 'Math.max') {
|
|
655
|
+
$m = -INF; // JS Math.max() with no args is -Infinity
|
|
656
|
+
foreach ($args as $a) {
|
|
657
|
+
$n = self::toNumber($a);
|
|
658
|
+
if (is_nan($n)) {
|
|
659
|
+
return $n; // any NaN argument -> NaN
|
|
660
|
+
}
|
|
661
|
+
if ($n > $m) {
|
|
662
|
+
$m = $n;
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
return $m;
|
|
666
|
+
}
|
|
667
|
+
if ($name === 'Math.min') {
|
|
668
|
+
$m = INF; // JS Math.min() with no args is +Infinity
|
|
669
|
+
foreach ($args as $a) {
|
|
670
|
+
$n = self::toNumber($a);
|
|
671
|
+
if (is_nan($n)) {
|
|
672
|
+
return $n;
|
|
673
|
+
}
|
|
674
|
+
if ($n < $m) {
|
|
675
|
+
$m = $n;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return $m;
|
|
679
|
+
}
|
|
680
|
+
if ($name === 'Math.abs') {
|
|
681
|
+
return abs(self::toNumber($arg(0)));
|
|
682
|
+
}
|
|
683
|
+
if ($name === 'Math.floor') {
|
|
684
|
+
return self::safeFloor(self::toNumber($arg(0)));
|
|
685
|
+
}
|
|
686
|
+
if ($name === 'Math.ceil') {
|
|
687
|
+
return self::safeCeil(self::toNumber($arg(0)));
|
|
688
|
+
}
|
|
689
|
+
if ($name === 'Math.round') {
|
|
690
|
+
return self::mathRound(self::toNumber($arg(0)));
|
|
691
|
+
}
|
|
692
|
+
if ($name === 'String') {
|
|
693
|
+
return self::toStringJs($arg(0));
|
|
694
|
+
}
|
|
695
|
+
if ($name === 'Number') {
|
|
696
|
+
return self::toNumber($arg(0));
|
|
697
|
+
}
|
|
698
|
+
if ($name === 'Boolean') {
|
|
699
|
+
return self::truthy($arg(0));
|
|
700
|
+
}
|
|
701
|
+
// Any other callee is outside the subset (refused upstream).
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// -----------------------------------------------------------------
|
|
706
|
+
// Member / index access
|
|
707
|
+
// -----------------------------------------------------------------
|
|
708
|
+
|
|
709
|
+
/** Read a property from a JS-shaped value. Per the canonical value
|
|
710
|
+
* convention, an "object" is a stdClass OR a non-list (associative)
|
|
711
|
+
* PHP array -- both are treated as objects for member access. */
|
|
712
|
+
private static function readProperty($obj, $key)
|
|
713
|
+
{
|
|
714
|
+
if ($obj === null) {
|
|
715
|
+
return null;
|
|
716
|
+
}
|
|
717
|
+
if ($obj instanceof \stdClass) {
|
|
718
|
+
return property_exists($obj, $key) ? $obj->$key : null;
|
|
719
|
+
}
|
|
720
|
+
if (is_array($obj)) {
|
|
721
|
+
if (array_is_list($obj)) {
|
|
722
|
+
return $key === 'length' ? count($obj) : null;
|
|
723
|
+
}
|
|
724
|
+
return array_key_exists($key, $obj) ? $obj[$key] : null;
|
|
725
|
+
}
|
|
726
|
+
if (is_string($obj) && $key === 'length') {
|
|
727
|
+
// `.length` is a string property only -- a numeric scalar has
|
|
728
|
+
// no `.length` in the subset (matches the Go/Perl/Python
|
|
729
|
+
// evaluators). Code-point length (mirrors BarefootJS::length()).
|
|
730
|
+
return mb_strlen($obj, 'UTF-8');
|
|
731
|
+
}
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
private static function readIndex($obj, $index)
|
|
736
|
+
{
|
|
737
|
+
if (is_array($obj) && array_is_list($obj)) {
|
|
738
|
+
$f = self::toNumber($index);
|
|
739
|
+
if (is_nan($f) || is_infinite($f)) {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
$i = (int) $f;
|
|
743
|
+
if ((float) $i !== $f || $i < 0 || $i >= count($obj)) {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
return $obj[$i];
|
|
747
|
+
}
|
|
748
|
+
if ($obj instanceof \stdClass) {
|
|
749
|
+
$k = self::toStringJs($index);
|
|
750
|
+
return property_exists($obj, $k) ? $obj->$k : null;
|
|
751
|
+
}
|
|
752
|
+
if (is_array($obj)) { // assoc, treated as object
|
|
753
|
+
$k = self::toStringJs($index);
|
|
754
|
+
return array_key_exists($k, $obj) ? $obj[$k] : null;
|
|
755
|
+
}
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// -----------------------------------------------------------------
|
|
760
|
+
// Evaluator-driven higher-order folds / predicates (#2018).
|
|
761
|
+
// -----------------------------------------------------------------
|
|
762
|
+
|
|
763
|
+
/** Fold an array into a value via the evaluator. Non-array receiver ->
|
|
764
|
+
* the seed `$init` unchanged (mirrors the Perl/Python nil-tolerant
|
|
765
|
+
* convention -- an empty fold degenerates to the seed). */
|
|
766
|
+
public static function fold($items, $body, string $accName, string $itemName, $init, string $direction = 'left', array $baseEnv = [])
|
|
767
|
+
{
|
|
768
|
+
$arr = self::isJsArray($items) ? $items : [];
|
|
769
|
+
if ($direction === 'right') {
|
|
770
|
+
$arr = array_reverse($arr);
|
|
771
|
+
}
|
|
772
|
+
$env = $baseEnv;
|
|
773
|
+
$acc = $init;
|
|
774
|
+
foreach ($arr as $item) {
|
|
775
|
+
$env[$accName] = $acc;
|
|
776
|
+
$env[$itemName] = $item;
|
|
777
|
+
$acc = self::evaluate($body, $env);
|
|
778
|
+
}
|
|
779
|
+
return $acc;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/** Return a new array ordered by a ParsedExpr comparator. Non-mutating,
|
|
783
|
+
* stable (PHP's usort() is a stable sort as of PHP 8.0; an original-index
|
|
784
|
+
* tie-break is decorated in anyway for defensiveness/portability,
|
|
785
|
+
* mirroring the Perl port). */
|
|
786
|
+
public static function sortBy($items, $cmp, string $paramA, string $paramB, array $baseEnv = []): array
|
|
787
|
+
{
|
|
788
|
+
if (!self::isJsArray($items)) {
|
|
789
|
+
return [];
|
|
790
|
+
}
|
|
791
|
+
$env = $baseEnv;
|
|
792
|
+
$decorated = [];
|
|
793
|
+
foreach (array_values($items) as $i => $v) {
|
|
794
|
+
$decorated[] = [$i, $v];
|
|
795
|
+
}
|
|
796
|
+
usort($decorated, function ($a, $b) use ($cmp, $paramA, $paramB, &$env) {
|
|
797
|
+
$env[$paramA] = $a[1];
|
|
798
|
+
$env[$paramB] = $b[1];
|
|
799
|
+
$c = self::toNumber(self::evaluate($cmp, $env));
|
|
800
|
+
if (is_nan($c)) {
|
|
801
|
+
return $a[0] <=> $b[0]; // NaN comparator result -> keep input order
|
|
802
|
+
}
|
|
803
|
+
$sign = $c <=> 0.0;
|
|
804
|
+
return $sign !== 0 ? $sign : ($a[0] <=> $b[0]);
|
|
805
|
+
});
|
|
806
|
+
return array_map(fn ($d) => $d[1], $decorated);
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
public static function filter($items, $pred, string $param, array $baseEnv = []): array
|
|
810
|
+
{
|
|
811
|
+
if (!self::isJsArray($items)) {
|
|
812
|
+
return [];
|
|
813
|
+
}
|
|
814
|
+
$env = $baseEnv;
|
|
815
|
+
$out = [];
|
|
816
|
+
foreach ($items as $item) {
|
|
817
|
+
$env[$param] = $item;
|
|
818
|
+
if (self::truthy(self::evaluate($pred, $env))) {
|
|
819
|
+
$out[] = $item;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
return $out;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
public static function every($items, $pred, string $param, array $baseEnv = []): bool
|
|
826
|
+
{
|
|
827
|
+
$arr = self::isJsArray($items) ? $items : [];
|
|
828
|
+
$env = $baseEnv;
|
|
829
|
+
foreach ($arr as $item) {
|
|
830
|
+
$env[$param] = $item;
|
|
831
|
+
if (!self::truthy(self::evaluate($pred, $env))) {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return true;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
public static function some($items, $pred, string $param, array $baseEnv = []): bool
|
|
839
|
+
{
|
|
840
|
+
$arr = self::isJsArray($items) ? $items : [];
|
|
841
|
+
$env = $baseEnv;
|
|
842
|
+
foreach ($arr as $item) {
|
|
843
|
+
$env[$param] = $item;
|
|
844
|
+
if (self::truthy(self::evaluate($pred, $env))) {
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
return false;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
public static function find($items, $pred, string $param, bool $forward = true, array $baseEnv = [])
|
|
852
|
+
{
|
|
853
|
+
$arr = self::isJsArray($items) ? $items : [];
|
|
854
|
+
if (!$forward) {
|
|
855
|
+
$arr = array_reverse($arr);
|
|
856
|
+
}
|
|
857
|
+
$env = $baseEnv;
|
|
858
|
+
foreach ($arr as $item) {
|
|
859
|
+
$env[$param] = $item;
|
|
860
|
+
if (self::truthy(self::evaluate($pred, $env))) {
|
|
861
|
+
return $item;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
public static function findIndex($items, $pred, string $param, bool $forward = true, array $baseEnv = []): int
|
|
868
|
+
{
|
|
869
|
+
$arr = self::isJsArray($items) ? array_values($items) : [];
|
|
870
|
+
$n = count($arr);
|
|
871
|
+
if ($n === 0) {
|
|
872
|
+
return -1;
|
|
873
|
+
}
|
|
874
|
+
$env = $baseEnv;
|
|
875
|
+
$indices = $forward ? range(0, $n - 1) : range($n - 1, 0);
|
|
876
|
+
foreach ($indices as $i) {
|
|
877
|
+
$env[$param] = $arr[$i];
|
|
878
|
+
if (self::truthy(self::evaluate($pred, $env))) {
|
|
879
|
+
return $i;
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return -1;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
public static function flatMap($items, $proj, string $param, array $baseEnv = []): array
|
|
886
|
+
{
|
|
887
|
+
$arr = self::isJsArray($items) ? $items : [];
|
|
888
|
+
$env = $baseEnv;
|
|
889
|
+
$out = [];
|
|
890
|
+
foreach ($arr as $item) {
|
|
891
|
+
$env[$param] = $item;
|
|
892
|
+
$v = self::evaluate($proj, $env);
|
|
893
|
+
if (self::isJsArray($v)) {
|
|
894
|
+
foreach ($v as $x) {
|
|
895
|
+
$out[] = $x;
|
|
896
|
+
}
|
|
897
|
+
} else {
|
|
898
|
+
$out[] = $v;
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
return $out;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** Value-producing `.map(cb)` (#2073): project each element, one result
|
|
905
|
+
* per element (no flatten). */
|
|
906
|
+
public static function mapItems($items, $proj, string $param, array $baseEnv = []): array
|
|
907
|
+
{
|
|
908
|
+
$arr = self::isJsArray($items) ? $items : [];
|
|
909
|
+
$env = $baseEnv;
|
|
910
|
+
$out = [];
|
|
911
|
+
foreach ($arr as $item) {
|
|
912
|
+
$env[$param] = $item;
|
|
913
|
+
$out[] = self::evaluate($proj, $env);
|
|
914
|
+
}
|
|
915
|
+
return $out;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// -----------------------------------------------------------------
|
|
919
|
+
// JSON-string seams -- the adapter emits `bf.filter_eval(recv, '<json>', ...)`;
|
|
920
|
+
// the predicate body arrives as a JSON string, decoded then handed to
|
|
921
|
+
// the helper above.
|
|
922
|
+
// -----------------------------------------------------------------
|
|
923
|
+
|
|
924
|
+
public static function foldJson($items, string $bodyJson, string $accName, string $itemName, $init, string $direction = 'left', array $baseEnv = [])
|
|
925
|
+
{
|
|
926
|
+
return self::fold($items, json_decode($bodyJson), $accName, $itemName, $init, $direction, $baseEnv);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
public static function sortByJson($items, string $cmpJson, string $paramA, string $paramB, array $baseEnv = []): array
|
|
930
|
+
{
|
|
931
|
+
return self::sortBy($items, json_decode($cmpJson), $paramA, $paramB, $baseEnv);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
public static function filterJson($items, string $predJson, string $param, array $baseEnv = []): array
|
|
935
|
+
{
|
|
936
|
+
return self::filter($items, json_decode($predJson), $param, $baseEnv);
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
public static function everyJson($items, string $predJson, string $param, array $baseEnv = []): bool
|
|
940
|
+
{
|
|
941
|
+
return self::every($items, json_decode($predJson), $param, $baseEnv);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
public static function someJson($items, string $predJson, string $param, array $baseEnv = []): bool
|
|
945
|
+
{
|
|
946
|
+
return self::some($items, json_decode($predJson), $param, $baseEnv);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
public static function findJson($items, string $predJson, string $param, bool $forward = true, array $baseEnv = [])
|
|
950
|
+
{
|
|
951
|
+
return self::find($items, json_decode($predJson), $param, $forward, $baseEnv);
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
public static function findIndexJson($items, string $predJson, string $param, bool $forward = true, array $baseEnv = []): int
|
|
955
|
+
{
|
|
956
|
+
return self::findIndex($items, json_decode($predJson), $param, $forward, $baseEnv);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
public static function flatMapJson($items, string $projJson, string $param, array $baseEnv = []): array
|
|
960
|
+
{
|
|
961
|
+
return self::flatMap($items, json_decode($projJson), $param, $baseEnv);
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
public static function mapJson($items, string $projJson, string $param, array $baseEnv = []): array
|
|
965
|
+
{
|
|
966
|
+
return self::mapItems($items, json_decode($projJson), $param, $baseEnv);
|
|
967
|
+
}
|
|
968
|
+
}
|