@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.
@@ -0,0 +1,370 @@
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ /**
6
+ * JS-compat helper coverage (#1189), ported from
7
+ * packages/adapter-perl/t/template_primitives.t (see also the Python port's
8
+ * test_template_primitives.py).
9
+ *
10
+ * Covers the array/string method surface NOT already exercised byte-for-byte
11
+ * by the shared golden vectors (test_helper_vectors.php) -- receiver-type
12
+ * dispatch edge cases, mutation isolation (a helper must return a NEW array,
13
+ * never alias the caller's -- trivially true in PHP since arrays are
14
+ * value types with copy-on-write, but asserted anyway for parity with the
15
+ * Perl/Python ports, where it is NOT automatic), the structured `sort`
16
+ * comparator dispatch, and the `bf.*_eval` JSON-string delegation seam.
17
+ */
18
+
19
+ require_once __DIR__ . '/_harness.php';
20
+ bf_require_runtime();
21
+ bf_reset();
22
+
23
+ use Barefoot\BarefootJS;
24
+
25
+ $backend = new class {
26
+ public function encode_json($data): string
27
+ {
28
+ return \Barefoot\Json::canonicalEncode($data);
29
+ }
30
+
31
+ public function mark_raw($s)
32
+ {
33
+ return $s;
34
+ }
35
+
36
+ public function materialize($value)
37
+ {
38
+ return is_callable($value) ? $value() : $value;
39
+ }
40
+
41
+ public function render_named(...$args)
42
+ {
43
+ return '';
44
+ }
45
+ };
46
+
47
+ $bf = new BarefootJS(null, ['backend' => $backend]);
48
+
49
+ function bfp_is_nan($n): bool
50
+ {
51
+ return is_float($n) && is_nan($n);
52
+ }
53
+
54
+ // -----------------------------------------------------------------
55
+ // json / string / number / floor / ceil / round
56
+ // -----------------------------------------------------------------
57
+
58
+ bf_test('json', function () use ($bf) {
59
+ bf_assert_eq($bf->json(['a' => 1]), '{"a":1}');
60
+ bf_assert_eq($bf->json([1, 2, 3]), '[1,2,3]');
61
+ bf_assert_eq($bf->json('hi'), '"hi"');
62
+ bf_assert_eq($bf->json(null), 'null');
63
+ });
64
+
65
+ bf_test('string', function () use ($bf) {
66
+ bf_assert_eq($bf->string(42), '42');
67
+ bf_assert_eq($bf->string('hi'), 'hi');
68
+ bf_assert_eq($bf->string(null), '');
69
+ bf_assert_eq($bf->string(true), 'true');
70
+ bf_assert_eq($bf->string(false), 'false');
71
+ bf_assert_eq($bf->string(1.0), '1');
72
+ });
73
+
74
+ bf_test('number', function () use ($bf) {
75
+ bf_assert_eq($bf->number('3.14'), 3.14);
76
+ bf_assert_eq($bf->number(42), 42.0);
77
+ bf_assert(bfp_is_nan($bf->number('not a num')), 'expected NaN');
78
+ bf_assert(bfp_is_nan($bf->number(null)), 'expected NaN');
79
+ });
80
+
81
+ bf_test('floor/ceil/round', function () use ($bf) {
82
+ bf_assert_eq($bf->floor(3.7), 3.0);
83
+ bf_assert_eq($bf->floor(-3.2), -4.0);
84
+ bf_assert(bfp_is_nan($bf->floor('not')), 'expected NaN');
85
+
86
+ bf_assert_eq($bf->ceil(3.1), 4.0);
87
+ bf_assert_eq($bf->ceil(-3.7), -3.0);
88
+ bf_assert(bfp_is_nan($bf->ceil('not')), 'expected NaN');
89
+
90
+ bf_assert_eq($bf->round(3.5), 4.0);
91
+ bf_assert_eq($bf->round(3.4), 3.0);
92
+ // JS Math.round ties go toward +Infinity, not away from zero.
93
+ bf_assert_eq($bf->round(-1.5), -1.0);
94
+ bf_assert_eq($bf->round(-1.6), -2.0);
95
+ bf_assert(bfp_is_nan($bf->round('not')), 'expected NaN');
96
+ });
97
+
98
+ // -----------------------------------------------------------------
99
+ // includes dispatch
100
+ // -----------------------------------------------------------------
101
+
102
+ bf_test('includes dispatch', function () use ($bf) {
103
+ bf_assert_eq($bf->includes(['a', 'b', 'c'], 'b'), true);
104
+ bf_assert_eq($bf->includes(['a', 'b', 'c'], 'z'), false);
105
+ bf_assert_eq($bf->includes([1, 2, 3], 2), true);
106
+ bf_assert_eq($bf->includes([], 'a'), false);
107
+ bf_assert_eq($bf->includes([null, 'a'], null), true);
108
+ bf_assert_eq($bf->includes(['a', 'b'], null), false);
109
+
110
+ // SameValueZero never coerces across types.
111
+ bf_assert_eq($bf->includes([2], '2'), false);
112
+ bf_assert_eq($bf->includes([2], 2), true);
113
+ bf_assert_eq($bf->includes(['2'], '2'), true);
114
+
115
+ bf_assert_eq($bf->includes('hello world', 'world'), true);
116
+ bf_assert_eq($bf->includes('hello world', 'earth'), false);
117
+ bf_assert_eq($bf->includes('hello', ''), true);
118
+ bf_assert_eq($bf->includes('', 'x'), false);
119
+ bf_assert_eq($bf->includes(null, 'x'), false);
120
+
121
+ bf_assert_eq($bf->includes(['a' => 1], 'a'), false);
122
+ });
123
+
124
+ // -----------------------------------------------------------------
125
+ // index_of / last_index_of / at
126
+ // -----------------------------------------------------------------
127
+
128
+ bf_test('index_of / last_index_of', function () use ($bf) {
129
+ $arr = ['a', 'b', 'c', 'b', 'd'];
130
+ bf_assert_eq($bf->index_of($arr, 'a'), 0);
131
+ bf_assert_eq($bf->index_of($arr, 'b'), 1);
132
+ bf_assert_eq($bf->index_of($arr, 'd'), 4);
133
+ bf_assert_eq($bf->index_of($arr, 'z'), -1);
134
+ bf_assert_eq($bf->index_of([], 'a'), -1);
135
+ bf_assert_eq($bf->index_of('not an array', 'a'), -1);
136
+
137
+ bf_assert_eq($bf->last_index_of($arr, 'b'), 3);
138
+ bf_assert_eq($bf->last_index_of($arr, 'a'), 0);
139
+ bf_assert_eq($bf->last_index_of($arr, 'z'), -1);
140
+
141
+ bf_assert_eq($bf->index_of([null, 'x', null], null), 0);
142
+ bf_assert_eq($bf->last_index_of([null, 'x', null], null), 2);
143
+ });
144
+
145
+ bf_test('at', function () use ($bf) {
146
+ $arr = ['a', 'b', 'c'];
147
+ bf_assert_eq($bf->at($arr, 0), 'a');
148
+ bf_assert_eq($bf->at($arr, 2), 'c');
149
+ bf_assert_eq($bf->at($arr, -1), 'c');
150
+ bf_assert_eq($bf->at($arr, -3), 'a');
151
+ bf_assert_eq($bf->at($arr, 3), null);
152
+ bf_assert_eq($bf->at($arr, -4), null);
153
+ bf_assert_eq($bf->at([], 0), null);
154
+ bf_assert_eq($bf->at(null, 0), null);
155
+ bf_assert_eq($bf->at(['a' => 1], 0), null);
156
+ });
157
+
158
+ // -----------------------------------------------------------------
159
+ // concat / slice / reverse mutation isolation
160
+ // -----------------------------------------------------------------
161
+
162
+ bf_test('concat mutation isolation', function () use ($bf) {
163
+ bf_assert_eq($bf->concat(['a', 'b'], ['c', 'd']), ['a', 'b', 'c', 'd']);
164
+ bf_assert_eq($bf->concat(null, ['a']), ['a']);
165
+ bf_assert_eq($bf->concat(['a'], null), ['a']);
166
+
167
+ $left = ['a', 'b'];
168
+ $right = ['c', 'd'];
169
+ $out = $bf->concat($left, $right);
170
+ $out[] = 'mutated';
171
+ bf_assert_eq($left, ['a', 'b']);
172
+ bf_assert_eq($right, ['c', 'd']);
173
+ });
174
+
175
+ bf_test('slice mutation isolation and clamping', function () use ($bf) {
176
+ $arr = ['a', 'b', 'c', 'd', 'e'];
177
+ bf_assert_eq($bf->slice($arr, 1, 3), ['b', 'c']);
178
+ bf_assert_eq($bf->slice($arr, 2, null), ['c', 'd', 'e']);
179
+ bf_assert_eq($bf->slice($arr, -2, null), ['d', 'e']);
180
+ bf_assert_eq($bf->slice($arr, 0, -1), ['a', 'b', 'c', 'd']);
181
+ bf_assert_eq($bf->slice($arr, 100, null), []);
182
+ bf_assert_eq($bf->slice($arr, 3, 1), []);
183
+ bf_assert_eq($bf->slice(null, 0, null), []);
184
+
185
+ $src = ['a', 'b', 'c'];
186
+ $out = $bf->slice($src, 0, 2);
187
+ $out[] = 'mutated';
188
+ bf_assert_eq($src, ['a', 'b', 'c']);
189
+ });
190
+
191
+ bf_test('reverse mutation isolation', function () use ($bf) {
192
+ bf_assert_eq($bf->reverse(['a', 'b', 'c']), ['c', 'b', 'a']);
193
+ bf_assert_eq($bf->reverse([]), []);
194
+
195
+ $src = ['a', 'b', 'c'];
196
+ $out = $bf->reverse($src);
197
+ $out[] = 'mutated';
198
+ bf_assert_eq($src, ['a', 'b', 'c']);
199
+ bf_assert_eq($bf->reverse(null), []);
200
+ });
201
+
202
+ // -----------------------------------------------------------------
203
+ // trim / split / starts_with / ends_with / replace / repeat / pad
204
+ // -----------------------------------------------------------------
205
+
206
+ bf_test('trim', function () use ($bf) {
207
+ bf_assert_eq($bf->trim(' padded '), 'padded');
208
+ bf_assert_eq($bf->trim(''), '');
209
+ bf_assert_eq($bf->trim(null), '');
210
+ bf_assert_eq($bf->trim(['a' => 1]), '');
211
+ bf_assert_eq($bf->trim(42), '42');
212
+ });
213
+
214
+ bf_test('split', function () use ($bf) {
215
+ bf_assert_eq($bf->split('a,b,c', ','), ['a', 'b', 'c']);
216
+ bf_assert_eq($bf->split('a.b.c', '.'), ['a', 'b', 'c']);
217
+ bf_assert_eq($bf->split('a,', ','), ['a', '']);
218
+ bf_assert_eq($bf->split(',a', ','), ['', 'a']);
219
+ bf_assert_eq($bf->split('abc', ''), ['a', 'b', 'c']);
220
+ bf_assert_eq($bf->split('', ''), []);
221
+ bf_assert_eq($bf->split('abc', ','), ['abc']);
222
+ bf_assert_eq($bf->split('a,b,c'), ['a,b,c']);
223
+ bf_assert_eq($bf->split('a,b,c,d', ',', 2), ['a', 'b']);
224
+ bf_assert_eq($bf->split('a,b', ',', 0), []);
225
+ bf_assert_eq($bf->split(null, ','), ['']);
226
+ bf_assert_eq($bf->split(42, ','), ['42']);
227
+ });
228
+
229
+ bf_test('starts_with / ends_with positions', function () use ($bf) {
230
+ bf_assert_eq($bf->starts_with('hello world', 'hello'), true);
231
+ bf_assert_eq($bf->starts_with('anything', ''), true);
232
+ bf_assert_eq($bf->ends_with('hello world', 'world'), true);
233
+ bf_assert_eq($bf->starts_with('abc', 'b', 1), true);
234
+ bf_assert_eq($bf->starts_with('abc', 'a', 99), false);
235
+ bf_assert_eq($bf->starts_with('abc', 'a', -5), true);
236
+ bf_assert_eq($bf->ends_with('abc', 'b', 2), true);
237
+ bf_assert_eq($bf->ends_with('abc', 'c', 99), true);
238
+ bf_assert_eq($bf->ends_with('abc', 'a', -1), false);
239
+ });
240
+
241
+ bf_test('replace', function () use ($bf) {
242
+ bf_assert_eq($bf->replace('hello world', 'o', '0'), 'hell0 world');
243
+ bf_assert_eq($bf->replace('abc', '', 'X'), 'Xabc');
244
+ bf_assert_eq($bf->replace('ab', 'a', '$&'), '$&b');
245
+ });
246
+
247
+ bf_test('repeat', function () use ($bf) {
248
+ bf_assert_eq($bf->repeat('ab', 3), 'ababab');
249
+ bf_assert_eq($bf->repeat('ab', 0), '');
250
+ bf_assert_eq($bf->repeat('ab', -2), '');
251
+ bf_assert_eq($bf->repeat('ab', 2.9), 'abab');
252
+ });
253
+
254
+ bf_test('pad_start / pad_end', function () use ($bf) {
255
+ bf_assert_eq($bf->pad_start('42', 5, '0'), '00042');
256
+ bf_assert_eq($bf->pad_end('42', 5, '.'), '42...');
257
+ bf_assert_eq($bf->pad_start('42', 5), ' 42');
258
+ bf_assert_eq($bf->pad_start('x', 5, 'ab'), 'ababx');
259
+ bf_assert_eq($bf->pad_start('hello', 3, '0'), 'hello');
260
+ bf_assert_eq($bf->pad_start('42', 5, ''), '42');
261
+ bf_assert_eq($bf->pad_start('7', 4.9, '0'), '0007');
262
+ });
263
+
264
+ // -----------------------------------------------------------------
265
+ // Structured sort() comparator dispatch
266
+ // -----------------------------------------------------------------
267
+
268
+ bf_test('sort: structured comparator dispatch', function () use ($bf) {
269
+ $items = [
270
+ ['name' => 'c', 'price' => 30],
271
+ ['name' => 'a', 'price' => 10],
272
+ ['name' => 'b', 'price' => 20],
273
+ ];
274
+ bf_assert_eq(
275
+ $bf->sort($items, ['keys' => [['key_kind' => 'field', 'key' => 'price', 'compare_type' => 'numeric', 'direction' => 'asc']]]),
276
+ [['name' => 'a', 'price' => 10], ['name' => 'b', 'price' => 20], ['name' => 'c', 'price' => 30]]
277
+ );
278
+ bf_assert_eq(
279
+ $bf->sort($items, ['keys' => [['key_kind' => 'field', 'key' => 'price', 'compare_type' => 'numeric', 'direction' => 'desc']]]),
280
+ [['name' => 'c', 'price' => 30], ['name' => 'b', 'price' => 20], ['name' => 'a', 'price' => 10]]
281
+ );
282
+ bf_assert_eq(
283
+ $bf->sort([3, 1, 2], ['keys' => [['key_kind' => 'self', 'compare_type' => 'numeric', 'direction' => 'asc']]]),
284
+ [1, 2, 3]
285
+ );
286
+
287
+ // Mutation isolation.
288
+ $src = [['price' => 3], ['price' => 1], ['price' => 2]];
289
+ $out = $bf->sort($src, ['keys' => [['key_kind' => 'field', 'key' => 'price', 'compare_type' => 'numeric', 'direction' => 'asc']]]);
290
+ $out[] = ['price' => 99];
291
+ bf_assert_eq($src, [['price' => 3], ['price' => 1], ['price' => 2]]);
292
+
293
+ bf_assert_eq($bf->sort(null, ['keys' => [['key_kind' => 'self', 'compare_type' => 'numeric', 'direction' => 'asc']]]), []);
294
+ bf_assert_eq($bf->sort([], ['keys' => [['key_kind' => 'field', 'key' => 'price']]]), []);
295
+ });
296
+
297
+ bf_test('sort: multi-key tie-break', function () use ($bf) {
298
+ $items = [['p' => 1, 'name' => 'b'], ['p' => 1, 'name' => 'a'], ['p' => 0, 'name' => 'c']];
299
+ bf_assert_eq(
300
+ $bf->sort($items, ['keys' => [
301
+ ['key_kind' => 'field', 'key' => 'p', 'compare_type' => 'numeric', 'direction' => 'asc'],
302
+ ['key_kind' => 'field', 'key' => 'name', 'compare_type' => 'string', 'direction' => 'asc'],
303
+ ]]),
304
+ [['p' => 0, 'name' => 'c'], ['p' => 1, 'name' => 'a'], ['p' => 1, 'name' => 'b']]
305
+ );
306
+ });
307
+
308
+ bf_test('sort: auto compare', function () use ($bf) {
309
+ bf_assert_eq(
310
+ $bf->sort([3, 1, 2], ['keys' => [['key_kind' => 'self', 'compare_type' => 'auto', 'direction' => 'asc']]]),
311
+ [1, 2, 3]
312
+ );
313
+ bf_assert_eq(
314
+ $bf->sort(['charlie', 'alice', 'bob'], ['keys' => [['key_kind' => 'self', 'compare_type' => 'auto', 'direction' => 'asc']]]),
315
+ ['alice', 'bob', 'charlie']
316
+ );
317
+ });
318
+
319
+ // -----------------------------------------------------------------
320
+ // EvalDelegation: `bf.*_eval` JSON-string seam wiring (not just the
321
+ // underlying Evaluator functions, tested directly in test_evaluator.php /
322
+ // test_eval_vectors.php).
323
+ // -----------------------------------------------------------------
324
+
325
+ bf_test('sort_eval delegates through the JSON seam', function () use ($bf) {
326
+ $cmp = [
327
+ 'kind' => 'binary', 'op' => '-',
328
+ 'left' => ['kind' => 'member', 'object' => ['kind' => 'identifier', 'name' => 'a'], 'property' => 'v'],
329
+ 'right' => ['kind' => 'member', 'object' => ['kind' => 'identifier', 'name' => 'b'], 'property' => 'v'],
330
+ ];
331
+ $out = $bf->sort_eval([['v' => 3], ['v' => 1], ['v' => 2]], json_encode($cmp), 'a', 'b');
332
+ bf_assert_eq(array_map(fn ($x) => $x['v'], $out), [1, 2, 3]);
333
+ });
334
+
335
+ bf_test('reduce_eval delegates through the JSON seam', function () use ($bf) {
336
+ $body = [
337
+ 'kind' => 'binary', 'op' => '+',
338
+ 'left' => ['kind' => 'identifier', 'name' => 'acc'],
339
+ 'right' => ['kind' => 'identifier', 'name' => 'item'],
340
+ ];
341
+ $out = $bf->reduce_eval([1, 2, 3], json_encode($body), 'acc', 'item', 0);
342
+ bf_assert_eq($out, 6.0);
343
+ });
344
+
345
+ bf_test('filter_eval / every_eval / some_eval / find_eval / find_index_eval delegate through the JSON seam', function () use ($bf) {
346
+ $pred = [
347
+ 'kind' => 'binary', 'op' => '>=',
348
+ 'left' => ['kind' => 'member', 'object' => ['kind' => 'identifier', 'name' => 'u'], 'property' => 'age'],
349
+ 'right' => ['kind' => 'literal', 'value' => 18],
350
+ ];
351
+ $predJson = json_encode($pred);
352
+ $rows = [['age' => 15], ['age' => 30], ['age' => 18]];
353
+ bf_assert_eq(array_map(fn ($r) => $r['age'], $bf->filter_eval($rows, $predJson, 'u')), [30, 18]);
354
+ bf_assert_eq($bf->every_eval($rows, $predJson, 'u'), false);
355
+ bf_assert_eq($bf->some_eval($rows, $predJson, 'u'), true);
356
+ bf_assert_eq($bf->find_eval($rows, $predJson, 'u')['age'], 30);
357
+ bf_assert_eq($bf->find_index_eval($rows, $predJson, 'u', false), 2);
358
+ });
359
+
360
+ bf_test('flat_map_eval / map_eval delegate through the JSON seam', function () use ($bf) {
361
+ $field = ['kind' => 'member', 'object' => ['kind' => 'identifier', 'name' => 'i'], 'property' => 'tags'];
362
+ $rows = [['tags' => ['a', 'b']], ['tags' => ['c']]];
363
+ bf_assert_eq($bf->flat_map_eval($rows, json_encode($field), 'i'), ['a', 'b', 'c']);
364
+
365
+ $nameField = ['kind' => 'member', 'object' => ['kind' => 'identifier', 'name' => 'u'], 'property' => 'name'];
366
+ $users = [['name' => 'Ada'], ['name' => 'Grace']];
367
+ bf_assert_eq($bf->map_eval($users, json_encode($nameField), 'u'), ['Ada', 'Grace']);
368
+ });
369
+
370
+ return bf_finish();
@@ -0,0 +1,42 @@
1
+ {
2
+ "version": 1,
3
+ "backend": "php",
4
+ "runner": "packages/adapter-php/tests/test_helper_vectors.php",
5
+ "spec": "spec/template-helpers.md",
6
+ "divergences": {
7
+ "add/beyond the safe-integer edge rounds as a double": {
8
+ "expect": 9007199254740993,
9
+ "reason": "PHP int arithmetic is exact (64-bit), not double-rounded"
10
+ },
11
+ "div/zero divisor yields Infinity": {
12
+ "throws": true,
13
+ "exception": "DivisionByZeroError",
14
+ "reason": "PHP native / throws DivisionByZeroError on a zero divisor (PHP 8+)"
15
+ },
16
+ "number/empty string coerces to 0": {
17
+ "expect": { "$num": "NaN" },
18
+ "reason": "deliberate: empty input must not silently zero downstream arithmetic (matches the Perl/Python ports)"
19
+ },
20
+ "number/null coerces to 0": {
21
+ "expect": { "$num": "NaN" },
22
+ "reason": "deliberate: unset props must not silently zero downstream arithmetic (matches the Perl/Python ports)"
23
+ },
24
+ "string/null renders as the string \"null\"": {
25
+ "expect": "",
26
+ "reason": "deliberate: an unset prop must not surface a literal \"null\" in HTML (matches the Perl/Python ports)"
27
+ },
28
+ "sort/localeCompare orders case-insensitively (ICU collation)": {
29
+ "expect": ["B", "a"],
30
+ "reason": "PHP string comparison (<=>) is codepoint/byte order, not ICU collation"
31
+ },
32
+ "sort/relational compare on numeric strings is lexical": {
33
+ "expect": ["9", "10"],
34
+ "reason": "the \"auto\" compare goes numeric when both keys look_like_number (Perl/Python/Go parity)"
35
+ },
36
+ "reduce/numeric-string items concatenate under JS +": {
37
+ "expect": 11.0,
38
+ "reason": "numeric folds parse numeric strings instead of concatenating (Perl/Python/Go parity)"
39
+ }
40
+ },
41
+ "unsupported": {}
42
+ }