@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,770 @@
1
+ //! Port of `packages/adapter-jinja/python/barefootjs/evaluator.py` (itself a
2
+ //! port of `packages/adapter-perl/lib/BarefootJS/Evaluator.pm`).
3
+ //!
4
+ //! Lightweight evaluator for the pure `ParsedExpr` subset, scoped to
5
+ //! higher-order callback bodies (reduce / sort / map / filter / find
6
+ //! `(...) => expr`) -- issue #2018. The callback BODY rides as a pure
7
+ //! `ParsedExpr` subtree (the structured IR the compiler already produces,
8
+ //! decoded here as plain `serde_json::Value` -- see the `num` module
9
+ //! docstring for why the tree itself stays `serde_json::Value` while
10
+ //! evaluation RESULTS use [`crate::num::JsValue`]) and is evaluated against
11
+ //! an environment (`{acc, item, ...captured free vars}`).
12
+ //!
13
+ //! ONE shared implementation across all backends (mirrors the Perl module).
14
+ //! The accepted subset and its semantics are documented in
15
+ //! `spec/compiler.md` ("ParsedExpr Evaluator Semantics") and pinned
16
+ //! isomorphically by the cross-language golden vectors
17
+ //! (`packages/adapter-tests/vectors/eval-vectors.json`), shared with
18
+ //! the Go evaluator (`bf.go`) and the Perl evaluator -- same input -> same
19
+ //! output, so unlike the runtime helper vectors there are NO backend
20
+ //! divergences declared against this file's tests (see
21
+ //! `tests/eval_vectors.rs`).
22
+ //!
23
+ //! The coercion below is JS-faithful (ToNumber / ToString / ToBoolean,
24
+ //! strict equality) and deliberately distinct from the divergent
25
+ //! `bf.string` / `bf.number` helpers in `runtime.rs`, so the contract is
26
+ //! unambiguous and every template adapter stays byte-equal with each other
27
+ //! and with Go.
28
+
29
+ use crate::num::{self, JsValue};
30
+ use serde_json::Value as JsonValue;
31
+ use std::collections::HashMap;
32
+
33
+ pub type Env = HashMap<String, JsValue>;
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // evaluate(node, env): evaluate a decoded ParsedExpr node (a JSON object
37
+ // keyed by `kind`) against the environment, returning a `JsValue`. The
38
+ // matching JSON-string entry point is `eval_json` below.
39
+ // ---------------------------------------------------------------------------
40
+
41
+ pub fn evaluate(node: &JsonValue, env: &Env) -> JsValue {
42
+ let kind = node.get("kind").and_then(|v| v.as_str()).unwrap_or("");
43
+
44
+ match kind {
45
+ "literal" => node.get("value").map(JsValue::from_json).unwrap_or(JsValue::Null),
46
+ "identifier" => {
47
+ let name = node.get("name").and_then(|v| v.as_str()).unwrap_or("");
48
+ env.get(name).cloned().unwrap_or(JsValue::Null)
49
+ }
50
+ "binary" => {
51
+ let op = node.get("op").and_then(|v| v.as_str()).unwrap_or("");
52
+ let l = evaluate(node.get("left").unwrap_or(&JsonValue::Null), env);
53
+ let r = evaluate(node.get("right").unwrap_or(&JsonValue::Null), env);
54
+ binary(op, &l, &r)
55
+ }
56
+ "unary" => {
57
+ let op = node.get("op").and_then(|v| v.as_str()).unwrap_or("");
58
+ let v = evaluate(node.get("argument").unwrap_or(&JsonValue::Null), env);
59
+ unary(op, &v)
60
+ }
61
+ "logical" => {
62
+ let op = node.get("op").and_then(|v| v.as_str()).unwrap_or("");
63
+ let left = evaluate(node.get("left").unwrap_or(&JsonValue::Null), env);
64
+ match op {
65
+ "&&" => {
66
+ if truthy(&left) {
67
+ evaluate(node.get("right").unwrap_or(&JsonValue::Null), env)
68
+ } else {
69
+ left
70
+ }
71
+ }
72
+ "||" => {
73
+ if truthy(&left) {
74
+ left
75
+ } else {
76
+ evaluate(node.get("right").unwrap_or(&JsonValue::Null), env)
77
+ }
78
+ }
79
+ _ => {
80
+ // `??`
81
+ if !matches!(left, JsValue::Null) {
82
+ left
83
+ } else {
84
+ evaluate(node.get("right").unwrap_or(&JsonValue::Null), env)
85
+ }
86
+ }
87
+ }
88
+ }
89
+ "conditional" => {
90
+ let test = evaluate(node.get("test").unwrap_or(&JsonValue::Null), env);
91
+ if truthy(&test) {
92
+ evaluate(node.get("consequent").unwrap_or(&JsonValue::Null), env)
93
+ } else {
94
+ evaluate(node.get("alternate").unwrap_or(&JsonValue::Null), env)
95
+ }
96
+ }
97
+ "member" => {
98
+ let obj = evaluate(node.get("object").unwrap_or(&JsonValue::Null), env);
99
+ let key = node.get("property").and_then(|v| v.as_str()).unwrap_or("");
100
+ read_property(&obj, key)
101
+ }
102
+ "index-access" => {
103
+ let obj = evaluate(node.get("object").unwrap_or(&JsonValue::Null), env);
104
+ let idx = evaluate(node.get("index").unwrap_or(&JsonValue::Null), env);
105
+ read_index(&obj, &idx)
106
+ }
107
+ "call" => {
108
+ // Nested `.map(cb)` / `.filter(cb)` (#2094) -- e.g. a
109
+ // `.flatMap(p => p.tags.map(t => '#'+t))` projection body that
110
+ // itself contains a `.map` call. Checked BEFORE builtin-name
111
+ // resolution since a `member` callee here is a receiver method,
112
+ // not an allowlisted builtin reference. Mirrors Go's
113
+ // `evalArrayCallbackCall` / `evalArrayCallback`.
114
+ if let Some((method, object, arrow)) = array_callback_call(node) {
115
+ return eval_array_callback(method, object, arrow, env);
116
+ }
117
+ let name = builtin_name(node.get("callee").unwrap_or(&JsonValue::Null));
118
+ if name.is_empty() {
119
+ return JsValue::Null;
120
+ }
121
+ let args: Vec<JsValue> = node
122
+ .get("args")
123
+ .and_then(|v| v.as_array())
124
+ .map(|a| a.iter().map(|n| evaluate(n, env)).collect())
125
+ .unwrap_or_default();
126
+ call_builtin(&name, &args)
127
+ }
128
+ "template-literal" => {
129
+ let mut out = String::new();
130
+ if let Some(parts) = node.get("parts").and_then(|v| v.as_array()) {
131
+ for p in parts {
132
+ let ptype = p.get("type").and_then(|v| v.as_str()).unwrap_or("");
133
+ if ptype == "string" {
134
+ out.push_str(p.get("value").and_then(|v| v.as_str()).unwrap_or(""));
135
+ } else {
136
+ let expr = p.get("expr").unwrap_or(&JsonValue::Null);
137
+ out.push_str(&to_string(&evaluate(expr, env)));
138
+ }
139
+ }
140
+ }
141
+ JsValue::String(out)
142
+ }
143
+ "array-literal" => {
144
+ let elements = node
145
+ .get("elements")
146
+ .and_then(|v| v.as_array())
147
+ .map(|a| a.iter().map(|n| evaluate(n, env)).collect())
148
+ .unwrap_or_default();
149
+ JsValue::Array(elements)
150
+ }
151
+ "object-literal" => {
152
+ let mut out = std::collections::BTreeMap::new();
153
+ if let Some(props) = node.get("properties").and_then(|v| v.as_array()) {
154
+ for prop in props {
155
+ let key = prop.get("key").and_then(|v| v.as_str()).unwrap_or("").to_string();
156
+ let value = evaluate(prop.get("value").unwrap_or(&JsonValue::Null), env);
157
+ out.insert(key, value);
158
+ }
159
+ }
160
+ JsValue::Object(out)
161
+ }
162
+ "array-method" if node.get("method").and_then(|v| v.as_str()) == Some("includes") => {
163
+ // `.includes(x)` (#2075) -- the one `array-method` in the
164
+ // evaluator subset, shared between `Array.prototype.includes`
165
+ // (SameValueZero membership) and `String.prototype.includes`
166
+ // (substring search), matching the receiver-type dispatch the
167
+ // SSR template lowering does at runtime (`bf.includes`).
168
+ let args = node.get("args").and_then(|v| v.as_array()).cloned().unwrap_or_default();
169
+ if args.len() == 1 {
170
+ let obj = evaluate(node.get("object").unwrap_or(&JsonValue::Null), env);
171
+ let needle = evaluate(&args[0], env);
172
+ match &obj {
173
+ JsValue::Array(items) => {
174
+ JsValue::Bool(items.iter().any(|el| num::same_value_zero(el, &needle)))
175
+ }
176
+ JsValue::String(s) => JsValue::Bool(s.contains(&to_string(&needle))),
177
+ // Any other receiver is not a JS `.includes` target --
178
+ // degrade to false rather than raising, mirroring the
179
+ // reference.
180
+ _ => JsValue::Bool(false),
181
+ }
182
+ } else {
183
+ JsValue::Null
184
+ }
185
+ }
186
+ "array-method" if node.get("method").and_then(|v| v.as_str()) == Some("join") => {
187
+ // `.join(sep?)` (#2094), a plain `array-method` node (not a
188
+ // `call`): default separator `,`, a `null`/`undefined` element
189
+ // joins as `''` (NOT the string "null" -- `to_string` below
190
+ // intentionally renders `Null` as `"null"` for general ToString
191
+ // purposes, so this special-cases it first). Mirrors Go's
192
+ // `evalJoin`.
193
+ let obj = evaluate(node.get("object").unwrap_or(&JsonValue::Null), env);
194
+ let args = node.get("args").and_then(|v| v.as_array()).cloned().unwrap_or_default();
195
+ let sep = if args.is_empty() { ",".to_string() } else { to_string(&evaluate(&args[0], env)) };
196
+ eval_join(&obj, &sep)
197
+ }
198
+ // arrow-fn / higher-order / unsupported array-method: a callback
199
+ // body containing these is refused upstream (BF101); never reached
200
+ // here.
201
+ _ => JsValue::Null,
202
+ }
203
+ }
204
+
205
+ /// Recognize a nested `.map(cb)` / `.filter(cb)` call: the callee is
206
+ /// `{kind:'member', object, property:'map'|'filter', computed:false}` and
207
+ /// the first argument is `{kind:'arrow', params, body}`. Returns the method
208
+ /// name, the receiver node, and the arrow node. Mirrors Go's
209
+ /// `evalArrayCallbackCall`.
210
+ fn array_callback_call(node: &JsonValue) -> Option<(&str, &JsonValue, &JsonValue)> {
211
+ let callee = node.get("callee")?;
212
+ if callee.get("kind").and_then(|v| v.as_str()) != Some("member") {
213
+ return None;
214
+ }
215
+ if callee.get("computed").and_then(|v| v.as_bool()).unwrap_or(false) {
216
+ return None;
217
+ }
218
+ let prop = callee.get("property").and_then(|v| v.as_str())?;
219
+ if prop != "map" && prop != "filter" {
220
+ return None;
221
+ }
222
+ let arrow = node.get("args").and_then(|v| v.as_array())?.first()?;
223
+ if arrow.get("kind").and_then(|v| v.as_str()) != Some("arrow") {
224
+ return None;
225
+ }
226
+ let object = callee.get("object")?;
227
+ Some((prop, object, arrow))
228
+ }
229
+
230
+ /// Evaluate a nested `.map(cb)` / `.filter(cb)` call recognized by
231
+ /// [`array_callback_call`]. `arrow`'s params: 1 (element) or 2 (element,
232
+ /// index -- bound as a `JsValue::Number`). The callback's env is a COPY of
233
+ /// the parent env (child scope) per invocation, never mutated in place
234
+ /// across sibling iterations. A non-array receiver yields `Null` for both
235
+ /// methods (matches Go, whose `toAnySlice(nil)` short-circuit fires before
236
+ /// the method dispatch). Mirrors Go's `evalArrayCallback`.
237
+ fn eval_array_callback(method: &str, object: &JsonValue, arrow: &JsonValue, env: &Env) -> JsValue {
238
+ let arr: Vec<JsValue> = match evaluate(object, env) {
239
+ JsValue::Array(a) => a,
240
+ _ => return JsValue::Null,
241
+ };
242
+ let params: Vec<String> = arrow
243
+ .get("params")
244
+ .and_then(|v| v.as_array())
245
+ .map(|a| a.iter().filter_map(|p| p.as_str().map(|s| s.to_string())).collect())
246
+ .unwrap_or_default();
247
+ let body = arrow.get("body").unwrap_or(&JsonValue::Null);
248
+ let call_cb = |item: JsValue, index: usize| -> JsValue {
249
+ let mut inner = env.clone();
250
+ if let Some(p0) = params.first() {
251
+ inner.insert(p0.clone(), item);
252
+ }
253
+ if let Some(p1) = params.get(1) {
254
+ inner.insert(p1.clone(), JsValue::Number(index as f64));
255
+ }
256
+ evaluate(body, &inner)
257
+ };
258
+ if method == "map" {
259
+ JsValue::Array(arr.into_iter().enumerate().map(|(i, item)| call_cb(item, i)).collect())
260
+ } else {
261
+ let mut out = Vec::new();
262
+ for (i, item) in arr.into_iter().enumerate() {
263
+ if truthy(&call_cb(item.clone(), i)) {
264
+ out.push(item);
265
+ }
266
+ }
267
+ JsValue::Array(out)
268
+ }
269
+ }
270
+
271
+ /// `.join(sep)` (#2094): default separator `,` when `sep` is empty/absent
272
+ /// upstream (the caller passes the already-resolved separator string); a
273
+ /// `null`/`undefined` element joins as `''`, not the literal string
274
+ /// `"null"`. A non-array receiver joins as `''`. Mirrors Go's `evalJoin`.
275
+ fn eval_join(obj: &JsValue, sep: &str) -> JsValue {
276
+ let arr = match obj.as_array() {
277
+ Some(a) => a,
278
+ None => return JsValue::String(String::new()),
279
+ };
280
+ let parts: Vec<String> = arr.iter().map(|el| if matches!(el, JsValue::Null) { String::new() } else { to_string(el) }).collect();
281
+ JsValue::String(parts.join(sep))
282
+ }
283
+
284
+ pub fn eval_json(json_str: &str, env: &Env) -> Result<JsValue, serde_json::Error> {
285
+ let node: JsonValue = serde_json::from_str(json_str)?;
286
+ Ok(evaluate(&node, env))
287
+ }
288
+
289
+ // ---------------------------------------------------------------------------
290
+ // JS coercion primitives (ToNumber / ToString / ToBoolean).
291
+ // ---------------------------------------------------------------------------
292
+
293
+ pub fn to_number(v: &JsValue) -> f64 {
294
+ match v {
295
+ JsValue::Null => 0.0,
296
+ JsValue::Bool(b) => if *b { 1.0 } else { 0.0 },
297
+ JsValue::Number(n) => *n,
298
+ JsValue::String(s) => {
299
+ let t = s.trim();
300
+ if t.is_empty() {
301
+ 0.0
302
+ } else if num::looks_like_number(t) {
303
+ num::parse_number_literal(t)
304
+ } else {
305
+ f64::NAN
306
+ }
307
+ }
308
+ JsValue::Array(_) | JsValue::Object(_) => f64::NAN,
309
+ }
310
+ }
311
+
312
+ /// JS ToString. Diverges from `runtime::js_string` deliberately: `null`
313
+ /// (JS `null`/`undefined`, both collapsed to [`JsValue::Null`] in this
314
+ /// domain) stringifies as the literal `"null"` here, matching real JS
315
+ /// `String(null)` -- `bf.string()`'s "unset prop must not surface as
316
+ /// literal null in HTML" policy is a `runtime.rs`-only divergence, not part
317
+ /// of this JS-faithful evaluator.
318
+ ///
319
+ /// Array/Object stringification is intentionally MORE JS-faithful than the
320
+ /// Python port's `str(v)` fallback (which prints a Python `repr`, e.g.
321
+ /// `"[1, 2]"` -- not exercised by the golden vectors, and not actually
322
+ /// JS-correct): this mirrors real JS `Array.prototype.toString`
323
+ /// (`.join(',')`, recursively coercing each element, `null`/`undefined`
324
+ /// elements becoming `''`) and `Object.prototype.toString`
325
+ /// (`"[object Object]"`).
326
+ pub fn to_string(v: &JsValue) -> String {
327
+ match v {
328
+ JsValue::Null => "null".to_string(),
329
+ JsValue::Bool(b) => if *b { "true".to_string() } else { "false".to_string() },
330
+ JsValue::Number(n) => num::format_js_number(*n),
331
+ JsValue::String(s) => s.clone(),
332
+ JsValue::Array(items) => items
333
+ .iter()
334
+ .map(|x| if matches!(x, JsValue::Null) { String::new() } else { to_string(x) })
335
+ .collect::<Vec<_>>()
336
+ .join(","),
337
+ JsValue::Object(_) => "[object Object]".to_string(),
338
+ }
339
+ }
340
+
341
+ pub fn truthy(v: &JsValue) -> bool {
342
+ match v {
343
+ JsValue::Null => false,
344
+ JsValue::Bool(b) => *b,
345
+ JsValue::Number(n) => !n.is_nan() && *n != 0.0, // nonzero and not NaN
346
+ JsValue::String(s) => !s.is_empty(), // incl. the truthy "0"
347
+ JsValue::Array(_) | JsValue::Object(_) => true,
348
+ }
349
+ }
350
+
351
+ // ---------------------------------------------------------------------------
352
+ // Operators
353
+ // ---------------------------------------------------------------------------
354
+
355
+ fn binary(op: &str, l: &JsValue, r: &JsValue) -> JsValue {
356
+ match op {
357
+ "+" => {
358
+ // JS `+`: string concatenation once either operand is a string,
359
+ // numeric addition otherwise.
360
+ if num::is_string(l) || num::is_string(r) {
361
+ JsValue::String(format!("{}{}", to_string(l), to_string(r)))
362
+ } else {
363
+ JsValue::Number(to_number(l) + to_number(r))
364
+ }
365
+ }
366
+ "-" => JsValue::Number(to_number(l) - to_number(r)),
367
+ "*" => JsValue::Number(to_number(l) * to_number(r)),
368
+ // No zero-divisor special-casing: `f64` division already yields
369
+ // +-Infinity / NaN per IEEE-754 -- see `num` module docstring.
370
+ "/" => JsValue::Number(to_number(l) / to_number(r)),
371
+ "%" => JsValue::Number(num::js_mod(to_number(l), to_number(r))),
372
+ "<" | "<=" | ">" | ">=" => JsValue::Bool(relational(op, l, r)),
373
+ "===" => JsValue::Bool(num::strict_eq(l, r)),
374
+ "!==" => JsValue::Bool(!num::strict_eq(l, r)),
375
+ // Loose equality / bitwise / shift are out of the subset.
376
+ _ => JsValue::Null,
377
+ }
378
+ }
379
+
380
+ fn relational(op: &str, l: &JsValue, r: &JsValue) -> bool {
381
+ // JS Abstract Relational Comparison: both strings -> compare by code
382
+ // unit (UTF-8 byte order agrees with UTF-16 code-unit order for all but
383
+ // rare non-BMP edge cases -- same simplification the Python port's
384
+ // native `str` comparison makes); otherwise coerce both to numbers (a
385
+ // NaN operand makes it false).
386
+ let c = if let (JsValue::String(ls), JsValue::String(rs)) = (l, r) {
387
+ ls.cmp(rs)
388
+ } else {
389
+ let (ln, rn) = (to_number(l), to_number(r));
390
+ if ln.is_nan() || rn.is_nan() {
391
+ return false;
392
+ }
393
+ ln.partial_cmp(&rn).unwrap_or(std::cmp::Ordering::Equal)
394
+ };
395
+ match op {
396
+ "<" => c.is_lt(),
397
+ "<=" => c.is_le(),
398
+ ">" => c.is_gt(),
399
+ ">=" => c.is_ge(),
400
+ _ => false,
401
+ }
402
+ }
403
+
404
+ fn unary(op: &str, v: &JsValue) -> JsValue {
405
+ match op {
406
+ "!" => JsValue::Bool(!truthy(v)),
407
+ "-" => JsValue::Number(-to_number(v)),
408
+ "+" => JsValue::Number(to_number(v)),
409
+ _ => JsValue::Null,
410
+ }
411
+ }
412
+
413
+ // ---------------------------------------------------------------------------
414
+ // Built-in calls (the deterministic allowlist). Locale-sensitive builtins
415
+ // (localeCompare) are deliberately excluded to keep the backends isomorphic.
416
+ // ---------------------------------------------------------------------------
417
+
418
+ /// Resolve a `call` callee to its builtin name (e.g. "Math.max"), or ''
419
+ /// when the callee is not an allowlisted builtin reference.
420
+ fn builtin_name(callee: &JsonValue) -> String {
421
+ let kind = callee.get("kind").and_then(|v| v.as_str()).unwrap_or("");
422
+ if kind == "identifier" {
423
+ return callee.get("name").and_then(|v| v.as_str()).unwrap_or("").to_string();
424
+ }
425
+ if kind == "member" && !callee.get("computed").and_then(|v| v.as_bool()).unwrap_or(false) {
426
+ let obj = callee.get("object");
427
+ let obj_kind = obj.and_then(|o| o.get("kind")).and_then(|v| v.as_str()).unwrap_or("");
428
+ if obj_kind != "identifier" {
429
+ return String::new();
430
+ }
431
+ let obj_name = obj.and_then(|o| o.get("name")).and_then(|v| v.as_str()).unwrap_or("");
432
+ let prop = callee.get("property").and_then(|v| v.as_str()).unwrap_or("");
433
+ return format!("{obj_name}.{prop}");
434
+ }
435
+ String::new()
436
+ }
437
+
438
+ fn call_builtin(name: &str, args: &[JsValue]) -> JsValue {
439
+ let arg = |i: usize| -> JsValue { args.get(i).cloned().unwrap_or(JsValue::Null) };
440
+ match name {
441
+ "Math.max" => {
442
+ let mut m = f64::NEG_INFINITY; // JS Math.max() with no args is -Infinity
443
+ for a in args {
444
+ let n = to_number(a);
445
+ if n.is_nan() {
446
+ return JsValue::Number(n);
447
+ }
448
+ if n > m {
449
+ m = n;
450
+ }
451
+ }
452
+ JsValue::Number(m)
453
+ }
454
+ "Math.min" => {
455
+ let mut m = f64::INFINITY; // JS Math.min() with no args is +Infinity
456
+ for a in args {
457
+ let n = to_number(a);
458
+ if n.is_nan() {
459
+ return JsValue::Number(n);
460
+ }
461
+ if n < m {
462
+ m = n;
463
+ }
464
+ }
465
+ JsValue::Number(m)
466
+ }
467
+ "Math.abs" => JsValue::Number(to_number(&arg(0)).abs()),
468
+ "Math.floor" => JsValue::Number(num::js_floor(to_number(&arg(0)))),
469
+ "Math.ceil" => JsValue::Number(num::js_ceil(to_number(&arg(0)))),
470
+ "Math.round" => JsValue::Number(num::js_round(to_number(&arg(0)))),
471
+ "String" => JsValue::String(to_string(&arg(0))),
472
+ "Number" => JsValue::Number(to_number(&arg(0))),
473
+ "Boolean" => JsValue::Bool(truthy(&arg(0))),
474
+ // Any other callee is outside the subset (refused upstream).
475
+ _ => JsValue::Null,
476
+ }
477
+ }
478
+
479
+ // ---------------------------------------------------------------------------
480
+ // Member / index access
481
+ // ---------------------------------------------------------------------------
482
+
483
+ fn read_property(obj: &JsValue, key: &str) -> JsValue {
484
+ match obj {
485
+ JsValue::Null => JsValue::Null,
486
+ JsValue::Object(map) => map.get(key).cloned().unwrap_or(JsValue::Null),
487
+ JsValue::Array(a) => {
488
+ if key == "length" {
489
+ JsValue::Number(a.len() as f64)
490
+ } else {
491
+ JsValue::Null
492
+ }
493
+ }
494
+ JsValue::String(s) => {
495
+ // `.length` is a string property only -- a numeric scalar
496
+ // (123) has no `.length` in the subset (JS `(123).length` is
497
+ // undefined -> null); matches the Go/Perl evaluators (numbers
498
+ // fall through to the catch-all below).
499
+ if key == "length" {
500
+ JsValue::Number(s.chars().count() as f64)
501
+ } else {
502
+ JsValue::Null
503
+ }
504
+ }
505
+ _ => JsValue::Null,
506
+ }
507
+ }
508
+
509
+ fn read_index(obj: &JsValue, index: &JsValue) -> JsValue {
510
+ match obj {
511
+ JsValue::Array(a) => {
512
+ let f = to_number(index);
513
+ if f.is_nan() || f.is_infinite() {
514
+ return JsValue::Null;
515
+ }
516
+ let i = f as i64;
517
+ if i as f64 != f || i < 0 || i as usize >= a.len() {
518
+ return JsValue::Null;
519
+ }
520
+ a[i as usize].clone()
521
+ }
522
+ JsValue::Object(map) => map.get(&to_string(index)).cloned().unwrap_or(JsValue::Null),
523
+ _ => JsValue::Null,
524
+ }
525
+ }
526
+
527
+ // ---------------------------------------------------------------------------
528
+ // Evaluator-driven higher-order folds (the generalization of bf_reduce /
529
+ // bf_sort onto the evaluator).
530
+ // ---------------------------------------------------------------------------
531
+
532
+ /// Fold a list into a value via the evaluator. `body` is a pure ParsedExpr
533
+ /// node evaluated against `{acc_name: acc, item_name: item}` plus the
534
+ /// captured free vars in `base_env` per element; `init` seeds the
535
+ /// accumulator and `direction` is "left" (reduce) or "right" (reduceRight).
536
+ /// Mirrors Go's `FoldEval`.
537
+ pub fn fold(
538
+ items: &JsValue,
539
+ body: &JsonValue,
540
+ acc_name: &str,
541
+ item_name: &str,
542
+ init: JsValue,
543
+ direction: &str,
544
+ base_env: &Env,
545
+ ) -> JsValue {
546
+ let mut arr: Vec<JsValue> = items.as_array().map(|a| a.to_vec()).unwrap_or_default();
547
+ if direction == "right" {
548
+ arr.reverse();
549
+ }
550
+ let mut env = base_env.clone();
551
+ let mut acc = init;
552
+ for item in arr {
553
+ env.insert(acc_name.to_string(), acc);
554
+ env.insert(item_name.to_string(), item);
555
+ acc = evaluate(body, &env);
556
+ }
557
+ acc
558
+ }
559
+
560
+ /// Return a new list ordered by a ParsedExpr comparator `cmp` evaluated
561
+ /// against `{param_a: a, param_b: b}` plus the captured free vars in
562
+ /// `base_env`, to a number (negative / zero / positive, like a JS
563
+ /// comparator). Non-mutating. Stable: `Vec::sort_by` carries a formal
564
+ /// stability guarantee, matching Python's `sorted`. Mirrors Go's `SortEval`.
565
+ pub fn sort_by(items: &JsValue, cmp: &JsonValue, param_a: &str, param_b: &str, base_env: &Env) -> Vec<JsValue> {
566
+ let mut arr: Vec<JsValue> = match items.as_array() {
567
+ Some(a) => a.to_vec(),
568
+ None => return Vec::new(),
569
+ };
570
+ let env = base_env.clone();
571
+ arr.sort_by(|a, b| {
572
+ let mut e = env.clone();
573
+ e.insert(param_a.to_string(), a.clone());
574
+ e.insert(param_b.to_string(), b.clone());
575
+ let c = to_number(&evaluate(cmp, &e));
576
+ // NaN comparator result -> keep order (matches JS + the Go/Perl
577
+ // sign test, which treat a NaN comparator result as "no
578
+ // reordering").
579
+ if c.is_nan() {
580
+ std::cmp::Ordering::Equal
581
+ } else if c < 0.0 {
582
+ std::cmp::Ordering::Less
583
+ } else if c > 0.0 {
584
+ std::cmp::Ordering::Greater
585
+ } else {
586
+ std::cmp::Ordering::Equal
587
+ }
588
+ });
589
+ arr
590
+ }
591
+
592
+ pub fn fold_json(
593
+ items: &JsValue,
594
+ body_json: &str,
595
+ acc_name: &str,
596
+ item_name: &str,
597
+ init: JsValue,
598
+ direction: &str,
599
+ base_env: &Env,
600
+ ) -> Result<JsValue, serde_json::Error> {
601
+ let body: JsonValue = serde_json::from_str(body_json)?;
602
+ Ok(fold(items, &body, acc_name, item_name, init, direction, base_env))
603
+ }
604
+
605
+ pub fn sort_by_json(
606
+ items: &JsValue,
607
+ cmp_json: &str,
608
+ param_a: &str,
609
+ param_b: &str,
610
+ base_env: &Env,
611
+ ) -> Result<Vec<JsValue>, serde_json::Error> {
612
+ let cmp: JsonValue = serde_json::from_str(cmp_json)?;
613
+ Ok(sort_by(items, &cmp, param_a, param_b, base_env))
614
+ }
615
+
616
+ // ---------------------------------------------------------------------------
617
+ // Higher-order predicates (#2018, P2) -- the generalization of bf_filter /
618
+ // bf_find / bf_find_index / bf_every / bf_some onto the evaluator. Each
619
+ // mirrors the corresponding Go helper (FilterEval / EveryEval / SomeEval /
620
+ // FindEval / FindIndexEval).
621
+ // ---------------------------------------------------------------------------
622
+
623
+ pub fn filter(items: &JsValue, pred: &JsonValue, param: &str, base_env: &Env) -> Vec<JsValue> {
624
+ let arr: Vec<JsValue> = match items.as_array() {
625
+ Some(a) => a.to_vec(),
626
+ None => return Vec::new(),
627
+ };
628
+ let mut env = base_env.clone();
629
+ let mut out = Vec::new();
630
+ for item in arr {
631
+ env.insert(param.to_string(), item.clone());
632
+ if truthy(&evaluate(pred, &env)) {
633
+ out.push(item);
634
+ }
635
+ }
636
+ out
637
+ }
638
+
639
+ pub fn every(items: &JsValue, pred: &JsonValue, param: &str, base_env: &Env) -> bool {
640
+ let arr: Vec<JsValue> = items.as_array().map(|a| a.to_vec()).unwrap_or_default();
641
+ let mut env = base_env.clone();
642
+ for item in arr {
643
+ env.insert(param.to_string(), item);
644
+ if !truthy(&evaluate(pred, &env)) {
645
+ return false;
646
+ }
647
+ }
648
+ true
649
+ }
650
+
651
+ pub fn some(items: &JsValue, pred: &JsonValue, param: &str, base_env: &Env) -> bool {
652
+ let arr: Vec<JsValue> = items.as_array().map(|a| a.to_vec()).unwrap_or_default();
653
+ let mut env = base_env.clone();
654
+ for item in arr {
655
+ env.insert(param.to_string(), item);
656
+ if truthy(&evaluate(pred, &env)) {
657
+ return true;
658
+ }
659
+ }
660
+ false
661
+ }
662
+
663
+ pub fn find(items: &JsValue, pred: &JsonValue, param: &str, forward: bool, base_env: &Env) -> JsValue {
664
+ let mut arr: Vec<JsValue> = items.as_array().map(|a| a.to_vec()).unwrap_or_default();
665
+ if !forward {
666
+ arr.reverse();
667
+ }
668
+ let mut env = base_env.clone();
669
+ for item in arr {
670
+ env.insert(param.to_string(), item.clone());
671
+ if truthy(&evaluate(pred, &env)) {
672
+ return item;
673
+ }
674
+ }
675
+ JsValue::Null
676
+ }
677
+
678
+ pub fn find_index(items: &JsValue, pred: &JsonValue, param: &str, forward: bool, base_env: &Env) -> i64 {
679
+ let arr: Vec<JsValue> = items.as_array().map(|a| a.to_vec()).unwrap_or_default();
680
+ let mut env = base_env.clone();
681
+ let idxs: Vec<usize> = if forward { (0..arr.len()).collect() } else { (0..arr.len()).rev().collect() };
682
+ for i in idxs {
683
+ env.insert(param.to_string(), arr[i].clone());
684
+ if truthy(&evaluate(pred, &env)) {
685
+ return i as i64;
686
+ }
687
+ }
688
+ -1
689
+ }
690
+
691
+ pub fn flat_map(items: &JsValue, proj: &JsonValue, param: &str, base_env: &Env) -> Vec<JsValue> {
692
+ let arr: Vec<JsValue> = items.as_array().map(|a| a.to_vec()).unwrap_or_default();
693
+ let mut env = base_env.clone();
694
+ let mut out = Vec::new();
695
+ for item in arr {
696
+ env.insert(param.to_string(), item);
697
+ let v = evaluate(proj, &env);
698
+ match v {
699
+ JsValue::Array(inner) => out.extend(inner),
700
+ other => out.push(other),
701
+ }
702
+ }
703
+ out
704
+ }
705
+
706
+ /// Value-producing `.map(cb)` (#2073): project each element through
707
+ /// `proj`, one result per element (no flatten).
708
+ pub fn map_items(items: &JsValue, proj: &JsonValue, param: &str, base_env: &Env) -> Vec<JsValue> {
709
+ let arr: Vec<JsValue> = items.as_array().map(|a| a.to_vec()).unwrap_or_default();
710
+ let mut env = base_env.clone();
711
+ let mut out = Vec::new();
712
+ for item in arr {
713
+ env.insert(param.to_string(), item);
714
+ out.push(evaluate(proj, &env));
715
+ }
716
+ out
717
+ }
718
+
719
+ // ---------------------------------------------------------------------------
720
+ // JSON-string seams -- the adapters emit `bf.filter_eval(recv, '<json>', ...)`;
721
+ // the predicate body arrives as a JSON string here, decoded then handed to
722
+ // the helper above (mirroring fold_json / sort_by_json).
723
+ // ---------------------------------------------------------------------------
724
+
725
+ pub fn filter_json(items: &JsValue, pred_json: &str, param: &str, base_env: &Env) -> Result<Vec<JsValue>, serde_json::Error> {
726
+ let pred: JsonValue = serde_json::from_str(pred_json)?;
727
+ Ok(filter(items, &pred, param, base_env))
728
+ }
729
+
730
+ pub fn every_json(items: &JsValue, pred_json: &str, param: &str, base_env: &Env) -> Result<bool, serde_json::Error> {
731
+ let pred: JsonValue = serde_json::from_str(pred_json)?;
732
+ Ok(every(items, &pred, param, base_env))
733
+ }
734
+
735
+ pub fn some_json(items: &JsValue, pred_json: &str, param: &str, base_env: &Env) -> Result<bool, serde_json::Error> {
736
+ let pred: JsonValue = serde_json::from_str(pred_json)?;
737
+ Ok(some(items, &pred, param, base_env))
738
+ }
739
+
740
+ pub fn find_json(
741
+ items: &JsValue,
742
+ pred_json: &str,
743
+ param: &str,
744
+ forward: bool,
745
+ base_env: &Env,
746
+ ) -> Result<JsValue, serde_json::Error> {
747
+ let pred: JsonValue = serde_json::from_str(pred_json)?;
748
+ Ok(find(items, &pred, param, forward, base_env))
749
+ }
750
+
751
+ pub fn find_index_json(
752
+ items: &JsValue,
753
+ pred_json: &str,
754
+ param: &str,
755
+ forward: bool,
756
+ base_env: &Env,
757
+ ) -> Result<i64, serde_json::Error> {
758
+ let pred: JsonValue = serde_json::from_str(pred_json)?;
759
+ Ok(find_index(items, &pred, param, forward, base_env))
760
+ }
761
+
762
+ pub fn flat_map_json(items: &JsValue, proj_json: &str, param: &str, base_env: &Env) -> Result<Vec<JsValue>, serde_json::Error> {
763
+ let proj: JsonValue = serde_json::from_str(proj_json)?;
764
+ Ok(flat_map(items, &proj, param, base_env))
765
+ }
766
+
767
+ pub fn map_json(items: &JsValue, proj_json: &str, param: &str, base_env: &Env) -> Result<Vec<JsValue>, serde_json::Error> {
768
+ let proj: JsonValue = serde_json::from_str(proj_json)?;
769
+ Ok(map_items(items, &proj, param, base_env))
770
+ }