@barefootjs/rust 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.
Files changed (87) hide show
  1. package/README.md +194 -0
  2. package/dist/adapter/analysis/component-tree.d.ts +26 -0
  3. package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
  4. package/dist/adapter/boolean-result.d.ts +85 -0
  5. package/dist/adapter/boolean-result.d.ts.map +1 -0
  6. package/dist/adapter/emit-context.d.ts +107 -0
  7. package/dist/adapter/emit-context.d.ts.map +1 -0
  8. package/dist/adapter/expr/array-method.d.ts +75 -0
  9. package/dist/adapter/expr/array-method.d.ts.map +1 -0
  10. package/dist/adapter/expr/emitters.d.ts +143 -0
  11. package/dist/adapter/expr/emitters.d.ts.map +1 -0
  12. package/dist/adapter/index.d.ts +6 -0
  13. package/dist/adapter/index.d.ts.map +1 -0
  14. package/dist/adapter/index.js +189091 -0
  15. package/dist/adapter/lib/constants.d.ts +25 -0
  16. package/dist/adapter/lib/constants.d.ts.map +1 -0
  17. package/dist/adapter/lib/ir-scope.d.ts +50 -0
  18. package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
  19. package/dist/adapter/lib/minijinja-naming.d.ts +64 -0
  20. package/dist/adapter/lib/minijinja-naming.d.ts.map +1 -0
  21. package/dist/adapter/lib/types.d.ts +32 -0
  22. package/dist/adapter/lib/types.d.ts.map +1 -0
  23. package/dist/adapter/memo/seed.d.ts +84 -0
  24. package/dist/adapter/memo/seed.d.ts.map +1 -0
  25. package/dist/adapter/minijinja-adapter.d.ts +421 -0
  26. package/dist/adapter/minijinja-adapter.d.ts.map +1 -0
  27. package/dist/adapter/props/prop-classes.d.ts +33 -0
  28. package/dist/adapter/props/prop-classes.d.ts.map +1 -0
  29. package/dist/adapter/spread/spread-codegen.d.ts +63 -0
  30. package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
  31. package/dist/adapter/value/parsed-literal.d.ts +28 -0
  32. package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
  33. package/dist/build.d.ts +29 -0
  34. package/dist/build.d.ts.map +1 -0
  35. package/dist/build.js +189111 -0
  36. package/dist/conformance-pins.d.ts +13 -0
  37. package/dist/conformance-pins.d.ts.map +1 -0
  38. package/dist/index.d.ts +12 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +189112 -0
  41. package/package.json +67 -0
  42. package/runtime/Cargo.lock +124 -0
  43. package/runtime/Cargo.toml +21 -0
  44. package/runtime/src/backend_minijinja.rs +176 -0
  45. package/runtime/src/bin/bf-render.rs +147 -0
  46. package/runtime/src/evaluator.rs +770 -0
  47. package/runtime/src/lib.rs +19 -0
  48. package/runtime/src/manifest.rs +258 -0
  49. package/runtime/src/num.rs +558 -0
  50. package/runtime/src/runtime.rs +1548 -0
  51. package/runtime/src/search_params.rs +173 -0
  52. package/runtime/tests/eval_vectors.rs +94 -0
  53. package/runtime/tests/evaluator.rs +407 -0
  54. package/runtime/tests/helper_vectors.rs +348 -0
  55. package/runtime/tests/manifest.rs +169 -0
  56. package/runtime/tests/omit.rs +79 -0
  57. package/runtime/tests/props_attr.rs +75 -0
  58. package/runtime/tests/query.rs +50 -0
  59. package/runtime/tests/render_child.rs +210 -0
  60. package/runtime/tests/search_params.rs +68 -0
  61. package/runtime/tests/spread_attrs.rs +94 -0
  62. package/runtime/tests/template_primitives.rs +376 -0
  63. package/runtime/tests/vector-divergences.json +33 -0
  64. package/src/__tests__/minijinja-adapter-unit.test.ts +392 -0
  65. package/src/__tests__/minijinja-adapter.test.ts +58 -0
  66. package/src/__tests__/minijinja-counter.test.ts +61 -0
  67. package/src/__tests__/minijinja-query-href.test.ts +101 -0
  68. package/src/__tests__/minijinja-spread-attrs.test.ts +227 -0
  69. package/src/adapter/analysis/component-tree.ts +119 -0
  70. package/src/adapter/boolean-result.ts +177 -0
  71. package/src/adapter/emit-context.ts +119 -0
  72. package/src/adapter/expr/array-method.ts +346 -0
  73. package/src/adapter/expr/emitters.ts +608 -0
  74. package/src/adapter/index.ts +6 -0
  75. package/src/adapter/lib/constants.ts +37 -0
  76. package/src/adapter/lib/ir-scope.ts +95 -0
  77. package/src/adapter/lib/minijinja-naming.ts +85 -0
  78. package/src/adapter/lib/types.ts +35 -0
  79. package/src/adapter/memo/seed.ts +135 -0
  80. package/src/adapter/minijinja-adapter.ts +1796 -0
  81. package/src/adapter/props/prop-classes.ts +65 -0
  82. package/src/adapter/spread/spread-codegen.ts +168 -0
  83. package/src/adapter/value/parsed-literal.ts +76 -0
  84. package/src/build.ts +38 -0
  85. package/src/conformance-pins.ts +101 -0
  86. package/src/index.ts +12 -0
  87. package/src/test-render.ts +680 -0
@@ -0,0 +1,407 @@
1
+ //! Hand-built ParsedExpr evaluator demonstrations, ported from
2
+ //! `packages/adapter-jinja/python/tests/test_evaluator.py` (itself a port
3
+ //! of `packages/adapter-perl/t/evaluator.t`).
4
+ //!
5
+ //! Mirrors the Go/Perl/Python demonstrations so all backends prove the SAME
6
+ //! restriction-lifting on the SAME shapes (a reducer/comparator/predicate
7
+ //! body the fixed bf_reduce/bf_sort/bf_filter catalogues can't express, but
8
+ //! the evaluator handles as just another pure expression).
9
+
10
+ use barefootjs::evaluator::{self, Env};
11
+ use barefootjs::num::JsValue;
12
+ use barefootjs::num::to_f64;
13
+ use serde_json::json;
14
+ use serde_json::Value as JsonValue;
15
+ use std::collections::BTreeMap;
16
+
17
+ fn nid(name: &str) -> JsonValue {
18
+ json!({"kind": "identifier", "name": name})
19
+ }
20
+
21
+ fn nmem(object: JsonValue, property: &str) -> JsonValue {
22
+ json!({"kind": "member", "object": object, "property": property, "computed": false})
23
+ }
24
+
25
+ fn nbin(op: &str, left: JsonValue, right: JsonValue) -> JsonValue {
26
+ json!({"kind": "binary", "op": op, "left": left, "right": right})
27
+ }
28
+
29
+ fn nnum(value: f64) -> JsonValue {
30
+ json!({"kind": "literal", "value": value, "literalType": "number"})
31
+ }
32
+
33
+ fn nstr(value: &str) -> JsonValue {
34
+ json!({"kind": "literal", "value": value, "literalType": "string"})
35
+ }
36
+
37
+ fn ncall_math(func: &str, argument: JsonValue) -> JsonValue {
38
+ json!({"kind": "call", "callee": nmem(nid("Math"), func), "args": [argument]})
39
+ }
40
+
41
+ fn nincludes(object: JsonValue, needle: JsonValue) -> JsonValue {
42
+ json!({"kind": "array-method", "method": "includes", "object": object, "args": [needle]})
43
+ }
44
+
45
+ fn env_of(pairs: &[(&str, JsValue)]) -> Env {
46
+ pairs.iter().map(|(k, v)| (k.to_string(), v.clone())).collect()
47
+ }
48
+
49
+ fn obj(pairs: &[(&str, JsValue)]) -> JsValue {
50
+ JsValue::Object(pairs.iter().map(|(k, v)| (k.to_string(), v.clone())).collect::<BTreeMap<_, _>>())
51
+ }
52
+
53
+ fn num(n: f64) -> JsValue {
54
+ JsValue::Number(n)
55
+ }
56
+
57
+ fn field(v: &JsValue, key: &str) -> JsValue {
58
+ v.as_object().and_then(|m| m.get(key)).cloned().unwrap_or(JsValue::Null)
59
+ }
60
+
61
+ #[test]
62
+ fn fold_arbitrary_reducer_body() {
63
+ // acc + item.price * item.qty
64
+ let body = nbin("+", nid("acc"), nbin("*", nmem(nid("item"), "price"), nmem(nid("item"), "qty")));
65
+ let items = JsValue::Array(vec![obj(&[("price", num(5.0)), ("qty", num(3.0))]), obj(&[("price", num(2.0)), ("qty", num(4.0))])]);
66
+ let total = evaluator::fold(&items, &body, "acc", "item", num(0.0), "left", &Env::new());
67
+ assert_eq!(total, num(23.0));
68
+ }
69
+
70
+ #[test]
71
+ fn fold_direction_observable_for_string_concat() {
72
+ let body = nbin("+", nid("acc"), nid("item"));
73
+ let items = JsValue::Array(vec![JsValue::from("a"), JsValue::from("b"), JsValue::from("c")]);
74
+ assert_eq!(evaluator::fold(&items, &body, "acc", "item", JsValue::from(""), "left", &Env::new()), JsValue::from("abc"));
75
+ assert_eq!(evaluator::fold(&items, &body, "acc", "item", JsValue::from(""), "right", &Env::new()), JsValue::from("cba"));
76
+ }
77
+
78
+ #[test]
79
+ fn sort_by_arbitrary_comparator_abs_of_field_difference() {
80
+ let cmp = nbin("-", ncall_math("abs", nmem(nid("a"), "v")), ncall_math("abs", nmem(nid("b"), "v")));
81
+ let items = JsValue::Array(vec![obj(&[("v", num(-5.0))]), obj(&[("v", num(3.0))]), obj(&[("v", num(-1.0))])]);
82
+ let sorted = evaluator::sort_by(&items, &cmp, "a", "b", &Env::new());
83
+ let vs: Vec<f64> = sorted.iter().map(|x| to_f64(&field(x, "v"))).collect();
84
+ assert_eq!(vs, vec![-1.0, 3.0, -5.0]);
85
+ }
86
+
87
+ #[test]
88
+ fn sort_by_descending_via_reversed_comparator() {
89
+ let cmp = nbin("-", nmem(nid("b"), "x"), nmem(nid("a"), "x"));
90
+ let items = JsValue::Array(vec![obj(&[("x", num(10.0))]), obj(&[("x", num(30.0))]), obj(&[("x", num(20.0))])]);
91
+ let sorted = evaluator::sort_by(&items, &cmp, "a", "b", &Env::new());
92
+ let xs: Vec<f64> = sorted.iter().map(|x| to_f64(&field(x, "x"))).collect();
93
+ assert_eq!(xs, vec![30.0, 20.0, 10.0]);
94
+ }
95
+
96
+ #[test]
97
+ fn nonfinite_division_and_js_stringification() {
98
+ let div = |a: f64, b: f64| evaluator::evaluate(&nbin("/", nid("a"), nid("b")), &env_of(&[("a", num(a)), ("b", num(b))]));
99
+ assert_eq!(div(1.0, 0.0), num(f64::INFINITY));
100
+ assert_eq!(div(-1.0, 0.0), num(f64::NEG_INFINITY));
101
+ assert!(matches!(div(0.0, 0.0), JsValue::Number(n) if n.is_nan()));
102
+
103
+ assert_eq!(evaluator::to_string(&num(f64::INFINITY)), "Infinity");
104
+ assert_eq!(evaluator::to_string(&num(f64::NEG_INFINITY)), "-Infinity");
105
+ assert_eq!(evaluator::to_string(&num(f64::INFINITY - f64::INFINITY)), "NaN");
106
+ }
107
+
108
+ #[test]
109
+ fn captured_free_vars_via_base_env() {
110
+ let body = nbin("+", nid("acc"), nbin("*", nid("item"), nid("factor")));
111
+ let base_env = env_of(&[("factor", num(10.0))]);
112
+ let items = JsValue::Array(vec![num(1.0), num(2.0), num(3.0)]);
113
+ let total = evaluator::fold(&items, &body, "acc", "item", num(0.0), "left", &base_env);
114
+ assert_eq!(total, num(60.0));
115
+
116
+ let cmp = nbin("-", ncall_math("abs", nbin("-", nid("a"), nid("pivot"))), ncall_math("abs", nbin("-", nid("b"), nid("pivot"))));
117
+ let pivot_env = env_of(&[("pivot", num(5.0))]);
118
+ let sorted = evaluator::sort_by(&JsValue::Array(vec![num(1.0), num(8.0), num(4.0)]), &cmp, "a", "b", &pivot_env);
119
+ assert_eq!(sorted, vec![num(4.0), num(8.0), num(1.0)]);
120
+ }
121
+
122
+ #[test]
123
+ fn boolean_valued_ops_return_real_booleans() {
124
+ let lt = evaluator::evaluate(&nbin("<", nid("a"), nid("b")), &env_of(&[("a", num(1.0)), ("b", num(2.0))]));
125
+ assert_eq!(lt, JsValue::Bool(true));
126
+ assert_eq!(evaluator::to_string(&lt), "true");
127
+
128
+ let cat = evaluator::evaluate(&nbin("+", nstr("x"), nbin("<", nid("a"), nid("b"))), &env_of(&[("a", num(1.0)), ("b", num(2.0))]));
129
+ assert_eq!(cat, JsValue::from("xtrue"));
130
+
131
+ let eq = evaluator::evaluate(&nbin("===", nid("a"), nid("b")), &env_of(&[("a", num(1.0)), ("b", num(1.0))]));
132
+ assert!(matches!(eq, JsValue::Bool(_)));
133
+
134
+ let not_ = evaluator::evaluate(&json!({"kind": "unary", "op": "!", "argument": nstr("")}), &Env::new());
135
+ assert_eq!(evaluator::to_string(&not_), "true");
136
+
137
+ let b = evaluator::evaluate(&json!({"kind": "call", "callee": nid("Boolean"), "args": [nstr("")]}), &Env::new());
138
+ assert!(matches!(b, JsValue::Bool(_)));
139
+ assert_eq!(evaluator::to_string(&b), "false");
140
+
141
+ // `.length` is a string/array property only; a numeric scalar has none.
142
+ let length = evaluator::evaluate(&nmem(nid("n"), "length"), &env_of(&[("n", num(123.0))]));
143
+ assert_eq!(length, JsValue::Null);
144
+ }
145
+
146
+ #[test]
147
+ fn array_method_includes() {
148
+ let tags = JsValue::Array(vec![JsValue::from("perl"), JsValue::from("go")]);
149
+ let hit = evaluator::evaluate(&nincludes(nid("tags"), nstr("go")), &env_of(&[("tags", tags.clone())]));
150
+ assert_eq!(hit, JsValue::Bool(true));
151
+
152
+ let miss = evaluator::evaluate(&nincludes(nid("tags"), nstr("rust")), &env_of(&[("tags", tags)]));
153
+ assert_eq!(miss, JsValue::Bool(false));
154
+
155
+ // SameValueZero, not loose equality: the numeric element 2 matches the
156
+ // numeric needle 2, but the string needle "2" (a different JS type)
157
+ // does not -- mirroring `===`'s type-sensitivity.
158
+ let nums = JsValue::Array(vec![num(1.0), num(2.0), num(3.0)]);
159
+ let num_hit = evaluator::evaluate(&nincludes(nid("nums"), nnum(2.0)), &env_of(&[("nums", nums.clone())]));
160
+ assert_eq!(num_hit, JsValue::Bool(true));
161
+ let num_vs_string = evaluator::evaluate(&nincludes(nid("nums"), nstr("2")), &env_of(&[("nums", nums)]));
162
+ assert_eq!(num_vs_string, JsValue::Bool(false));
163
+
164
+ let sub = evaluator::evaluate(&nincludes(nid("name"), nstr("ar")), &env_of(&[("name", JsValue::from("bare"))]));
165
+ assert_eq!(sub, JsValue::Bool(true));
166
+
167
+ // A non-array, non-string receiver (number, null, object) is not a JS
168
+ // `.includes` target; the evaluator degrades to false rather than
169
+ // raising.
170
+ let scalar_recv = evaluator::evaluate(&nincludes(nid("n"), nnum(1.0)), &env_of(&[("n", num(42.0))]));
171
+ assert_eq!(scalar_recv, JsValue::Bool(false));
172
+ let null_recv = evaluator::evaluate(&nincludes(nid("n"), nstr("x")), &env_of(&[("n", JsValue::Null)]));
173
+ assert_eq!(null_recv, JsValue::Bool(false));
174
+ }
175
+
176
+ #[test]
177
+ fn sort_by_non_array_receiver_returns_empty_list() {
178
+ let cmp = nbin("-", nid("a"), nid("b"));
179
+ assert_eq!(evaluator::sort_by(&JsValue::Null, &cmp, "a", "b", &Env::new()), Vec::<JsValue>::new());
180
+ assert_eq!(evaluator::sort_by(&num(42.0), &cmp, "a", "b", &Env::new()), Vec::<JsValue>::new());
181
+ }
182
+
183
+ #[test]
184
+ fn sort_by_is_stable_for_equal_keys() {
185
+ let cmp = nbin("-", nmem(nid("a"), "k"), nmem(nid("b"), "k"));
186
+ let items = JsValue::Array(vec![
187
+ obj(&[("k", num(1.0)), ("id", JsValue::from("a"))]),
188
+ obj(&[("k", num(1.0)), ("id", JsValue::from("b"))]),
189
+ obj(&[("k", num(1.0)), ("id", JsValue::from("c"))]),
190
+ ]);
191
+ let sorted = evaluator::sort_by(&items, &cmp, "a", "b", &Env::new());
192
+ let ids: Vec<String> = sorted.iter().map(|x| field(x, "id").as_str().unwrap().to_string()).collect();
193
+ assert_eq!(ids, vec!["a", "b", "c"]);
194
+
195
+ let mixed = JsValue::Array(vec![
196
+ obj(&[("k", num(2.0)), ("id", JsValue::from("x"))]),
197
+ obj(&[("k", num(1.0)), ("id", JsValue::from("y"))]),
198
+ obj(&[("k", num(2.0)), ("id", JsValue::from("z"))]),
199
+ ]);
200
+ let sorted = evaluator::sort_by(&mixed, &cmp, "a", "b", &Env::new());
201
+ let ids: Vec<String> = sorted.iter().map(|x| field(x, "id").as_str().unwrap().to_string()).collect();
202
+ assert_eq!(ids, vec!["y", "x", "z"]);
203
+ }
204
+
205
+ #[test]
206
+ fn fold_json_and_sort_by_json_decode_and_evaluate() {
207
+ let rows = JsValue::Array(vec![obj(&[("duration", num(95.0))]), obj(&[("duration", num(213.0))]), obj(&[("duration", num(185.0))])]);
208
+
209
+ let reduce_body = nbin("+", nid("sum"), nmem(nid("t"), "duration")).to_string();
210
+ let total = evaluator::fold_json(&rows, &reduce_body, "sum", "t", num(0.0), "left", &Env::new()).unwrap();
211
+ assert_eq!(total, num(493.0));
212
+
213
+ let labels = JsValue::Array(vec![obj(&[("label", JsValue::from("a"))]), obj(&[("label", JsValue::from("b"))]), obj(&[("label", JsValue::from("c"))])]);
214
+ let concat_body = nbin("+", nid("acc"), nmem(nid("x"), "label")).to_string();
215
+ assert_eq!(evaluator::fold_json(&labels, &concat_body, "acc", "x", JsValue::from(""), "left", &Env::new()).unwrap(), JsValue::from("abc"));
216
+ assert_eq!(evaluator::fold_json(&labels, &concat_body, "acc", "x", JsValue::from(""), "right", &Env::new()).unwrap(), JsValue::from("cba"));
217
+
218
+ let cmp_json = nbin("-", nmem(nid("a"), "duration"), nmem(nid("b"), "duration")).to_string();
219
+ let sorted_rows = evaluator::sort_by_json(&rows, &cmp_json, "a", "b", &Env::new()).unwrap();
220
+ let durations: Vec<f64> = sorted_rows.iter().map(|r| to_f64(&field(r, "duration"))).collect();
221
+ assert_eq!(durations, vec![95.0, 185.0, 213.0]);
222
+ }
223
+
224
+ #[test]
225
+ fn filter_every_some_find_find_index_over_predicate() {
226
+ let rows = JsValue::Array(vec![obj(&[("age", num(15.0))]), obj(&[("age", num(30.0))]), obj(&[("age", num(18.0))])]);
227
+ let pred = nbin(">=", nmem(nid("u"), "age"), nnum(18.0));
228
+
229
+ let f = evaluator::filter(&rows, &pred, "u", &Env::new());
230
+ let ages: Vec<f64> = f.iter().map(|r| to_f64(&field(r, "age"))).collect();
231
+ assert_eq!(ages, vec![30.0, 18.0]);
232
+
233
+ assert!(evaluator::some(&rows, &pred, "u", &Env::new()));
234
+ assert!(!evaluator::every(&rows, &pred, "u", &Env::new()));
235
+
236
+ assert_eq!(to_f64(&field(&evaluator::find(&rows, &pred, "u", true, &Env::new()), "age")), 30.0);
237
+ assert_eq!(to_f64(&field(&evaluator::find(&rows, &pred, "u", false, &Env::new()), "age")), 18.0);
238
+ assert_eq!(evaluator::find_index(&rows, &pred, "u", true, &Env::new()), 1);
239
+ assert_eq!(evaluator::find_index(&rows, &pred, "u", false, &Env::new()), 2);
240
+
241
+ let empty = JsValue::Array(vec![]);
242
+ assert!(evaluator::every(&empty, &pred, "u", &Env::new()));
243
+ assert!(!evaluator::some(&empty, &pred, "u", &Env::new()));
244
+ assert_eq!(evaluator::find(&empty, &pred, "u", true, &Env::new()), JsValue::Null);
245
+ assert_eq!(evaluator::find_index(&empty, &pred, "u", true, &Env::new()), -1);
246
+
247
+ let pred_json = pred.to_string();
248
+ let fj = evaluator::filter_json(&rows, &pred_json, "u", &Env::new()).unwrap();
249
+ let ages: Vec<f64> = fj.iter().map(|r| to_f64(&field(r, "age"))).collect();
250
+ assert_eq!(ages, vec![30.0, 18.0]);
251
+ assert!(!evaluator::every_json(&rows, &pred_json, "u", &Env::new()).unwrap());
252
+ assert!(evaluator::some_json(&rows, &pred_json, "u", &Env::new()).unwrap());
253
+ assert_eq!(to_f64(&field(&evaluator::find_json(&rows, &pred_json, "u", true, &Env::new()).unwrap(), "age")), 30.0);
254
+ assert_eq!(evaluator::find_index_json(&rows, &pred_json, "u", false, &Env::new()).unwrap(), 2);
255
+
256
+ let cap = nbin(">=", nmem(nid("u"), "age"), nid("threshold"));
257
+ let hi = evaluator::filter(&rows, &cap, "u", &env_of(&[("threshold", num(18.0))]));
258
+ let lo = evaluator::filter(&rows, &cap, "u", &env_of(&[("threshold", num(100.0))]));
259
+ assert_eq!(hi.len(), 2);
260
+ assert_eq!(lo.len(), 0);
261
+ assert_eq!(evaluator::find_index(&rows, &cap, "u", true, &env_of(&[("threshold", num(100.0))])), -1);
262
+ }
263
+
264
+ #[test]
265
+ fn flat_map_projects_and_flattens_one_level() {
266
+ let rows = JsValue::Array(vec![
267
+ obj(&[("tags", JsValue::Array(vec![JsValue::from("a"), JsValue::from("b")]))]),
268
+ obj(&[("tags", JsValue::Array(vec![JsValue::from("c")]))]),
269
+ ]);
270
+ let field_node = nmem(nid("i"), "tags");
271
+ let out = evaluator::flat_map(&rows, &field_node, "i", &Env::new());
272
+ assert_eq!(out, vec![JsValue::from("a"), JsValue::from("b"), JsValue::from("c")]);
273
+
274
+ let pts = JsValue::Array(vec![obj(&[("x", num(1.0)), ("y", num(2.0))]), obj(&[("x", num(3.0)), ("y", num(4.0))])]);
275
+ let tuple_proj = json!({"kind": "array-literal", "elements": [nmem(nid("p"), "x"), nmem(nid("p"), "y")]});
276
+ let out = evaluator::flat_map(&pts, &tuple_proj, "p", &Env::new());
277
+ assert_eq!(out, vec![num(1.0), num(2.0), num(3.0), num(4.0)]);
278
+
279
+ let fj = evaluator::flat_map_json(&rows, &field_node.to_string(), "i", &Env::new()).unwrap();
280
+ assert_eq!(fj, vec![JsValue::from("a"), JsValue::from("b"), JsValue::from("c")]);
281
+ }
282
+
283
+ #[test]
284
+ fn map_items_projects_one_result_per_element_no_flatten() {
285
+ let tmpl = json!({
286
+ "kind": "template-literal",
287
+ "parts": [
288
+ {"type": "string", "value": "#"},
289
+ {"type": "expression", "expr": nid("t")},
290
+ ],
291
+ });
292
+ let items = JsValue::Array(vec![JsValue::from("perl"), JsValue::from("go")]);
293
+ assert_eq!(evaluator::map_items(&items, &tmpl, "t", &Env::new()), vec![JsValue::from("#perl"), JsValue::from("#go")]);
294
+
295
+ let users = JsValue::Array(vec![obj(&[("name", JsValue::from("Ada"))]), obj(&[("name", JsValue::from("Grace"))])]);
296
+ let field_node = nmem(nid("u"), "name");
297
+ assert_eq!(evaluator::map_items(&users, &field_node, "u", &Env::new()), vec![JsValue::from("Ada"), JsValue::from("Grace")]);
298
+
299
+ let rows = JsValue::Array(vec![obj(&[("tags", JsValue::Array(vec![JsValue::from("a"), JsValue::from("b")]))])]);
300
+ assert_eq!(
301
+ evaluator::map_items(&rows, &nmem(nid("i"), "tags"), "i", &Env::new()),
302
+ vec![JsValue::Array(vec![JsValue::from("a"), JsValue::from("b")])]
303
+ );
304
+
305
+ let mj = evaluator::map_json(&users, &field_node.to_string(), "u", &Env::new()).unwrap();
306
+ assert_eq!(mj, vec![JsValue::from("Ada"), JsValue::from("Grace")]);
307
+ }
308
+
309
+ #[test]
310
+ fn nested_map_and_filter_inside_a_callback_body() {
311
+ // p => p.tags.map(t => '#' + t) -- the #1938 blog-showcase flatMap
312
+ // projection shape: the projection body itself contains a `.map` call.
313
+ let tags = JsValue::Array(vec![JsValue::from("go"), JsValue::from("perl")]);
314
+ let post = obj(&[("tags", tags)]);
315
+ let nested_map = json!({
316
+ "kind": "call",
317
+ "callee": {"kind": "member", "object": nmem(nid("p"), "tags"), "property": "map", "computed": false},
318
+ "args": [{
319
+ "kind": "arrow",
320
+ "params": ["t"],
321
+ "body": nbin("+", nstr("#"), nid("t")),
322
+ }],
323
+ });
324
+ let out = evaluator::evaluate(&nested_map, &env_of(&[("p", post.clone())]));
325
+ assert_eq!(out, JsValue::Array(vec![JsValue::from("#go"), JsValue::from("#perl")]));
326
+
327
+ // i => i.tags.filter(t => t !== 'x') -- nested .filter.
328
+ let tags2 = JsValue::Array(vec![JsValue::from("a"), JsValue::from("x"), JsValue::from("b")]);
329
+ let item = obj(&[("tags", tags2)]);
330
+ let nested_filter = json!({
331
+ "kind": "call",
332
+ "callee": {"kind": "member", "object": nmem(nid("i"), "tags"), "property": "filter", "computed": false},
333
+ "args": [{
334
+ "kind": "arrow",
335
+ "params": ["t"],
336
+ "body": {"kind": "binary", "op": "!==", "left": nid("t"), "right": nstr("x")},
337
+ }],
338
+ });
339
+ let out = evaluator::evaluate(&nested_filter, &env_of(&[("i", item)]));
340
+ assert_eq!(out, JsValue::Array(vec![JsValue::from("a"), JsValue::from("b")]));
341
+
342
+ // A 2-param arrow (value, index).
343
+ let nested_map_idx = json!({
344
+ "kind": "call",
345
+ "callee": {"kind": "member", "object": nid("xs"), "property": "map", "computed": false},
346
+ "args": [{
347
+ "kind": "arrow",
348
+ "params": ["t", "idx"],
349
+ "body": nbin("+", nid("t"), nid("idx")),
350
+ }],
351
+ });
352
+ let xs = JsValue::Array(vec![num(10.0), num(20.0), num(30.0)]);
353
+ let out = evaluator::evaluate(&nested_map_idx, &env_of(&[("xs", xs)]));
354
+ assert_eq!(out, JsValue::Array(vec![num(10.0), num(21.0), num(32.0)]));
355
+
356
+ // Non-array receiver -> null (mirrors Go's `toAnySlice(nil)` short-circuit).
357
+ let scalar_recv = json!({
358
+ "kind": "call",
359
+ "callee": {"kind": "member", "object": nid("n"), "property": "map", "computed": false},
360
+ "args": [{"kind": "arrow", "params": ["t"], "body": nid("t")}],
361
+ });
362
+ assert_eq!(evaluator::evaluate(&scalar_recv, &env_of(&[("n", num(42.0))])), JsValue::Null);
363
+ }
364
+
365
+ #[test]
366
+ fn array_method_join() {
367
+ let njoin = |object: JsonValue, args: Vec<JsonValue>| json!({"kind": "array-method", "method": "join", "object": object, "args": args});
368
+
369
+ let tags = JsValue::Array(vec![JsValue::from("a"), JsValue::from("b"), JsValue::from("c")]);
370
+ let custom_sep = evaluator::evaluate(&njoin(nid("tags"), vec![nstr("-")]), &env_of(&[("tags", tags.clone())]));
371
+ assert_eq!(custom_sep, JsValue::from("a-b-c"));
372
+
373
+ let default_sep = evaluator::evaluate(&njoin(nid("tags"), vec![]), &env_of(&[("tags", tags)]));
374
+ assert_eq!(default_sep, JsValue::from("a,b,c"));
375
+
376
+ let empty = evaluator::evaluate(&njoin(nid("tags"), vec![nstr(",")]), &env_of(&[("tags", JsValue::Array(vec![]))]));
377
+ assert_eq!(empty, JsValue::from(""));
378
+
379
+ // A null element joins as '' -- not the literal string "null".
380
+ let with_null = JsValue::Array(vec![JsValue::from("a"), JsValue::Null, JsValue::from("b")]);
381
+ let joined = evaluator::evaluate(&njoin(nid("tags"), vec![nstr(",")]), &env_of(&[("tags", with_null)]));
382
+ assert_eq!(joined, JsValue::from("a,,b"));
383
+
384
+ // Composed: doubly-nested .map + .join (the #1938 blog-showcase shape).
385
+ let posts = JsValue::Array(vec![
386
+ obj(&[("tags", JsValue::Array(vec![JsValue::from("a"), JsValue::from("b")]))]),
387
+ obj(&[("tags", JsValue::Array(vec![JsValue::from("c")]))]),
388
+ ]);
389
+ let inner_map_join = njoin(
390
+ json!({
391
+ "kind": "call",
392
+ "callee": {"kind": "member", "object": nmem(nid("p"), "tags"), "property": "map", "computed": false},
393
+ "args": [{"kind": "arrow", "params": ["t"], "body": nbin("+", nstr("#"), nid("t"))}],
394
+ }),
395
+ vec![nstr(" ")],
396
+ );
397
+ let outer = njoin(
398
+ json!({
399
+ "kind": "call",
400
+ "callee": {"kind": "member", "object": nid("posts"), "property": "map", "computed": false},
401
+ "args": [{"kind": "arrow", "params": ["p"], "body": inner_map_join}],
402
+ }),
403
+ vec![nstr(", ")],
404
+ );
405
+ let out = evaluator::evaluate(&outer, &env_of(&[("posts", posts)]));
406
+ assert_eq!(out, JsValue::from("#a #b, #c"));
407
+ }