@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.
- package/README.md +194 -0
- package/dist/adapter/analysis/component-tree.d.ts +26 -0
- package/dist/adapter/analysis/component-tree.d.ts.map +1 -0
- package/dist/adapter/boolean-result.d.ts +85 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +107 -0
- package/dist/adapter/emit-context.d.ts.map +1 -0
- package/dist/adapter/expr/array-method.d.ts +75 -0
- package/dist/adapter/expr/array-method.d.ts.map +1 -0
- package/dist/adapter/expr/emitters.d.ts +143 -0
- package/dist/adapter/expr/emitters.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +6 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/index.js +189091 -0
- package/dist/adapter/lib/constants.d.ts +25 -0
- package/dist/adapter/lib/constants.d.ts.map +1 -0
- package/dist/adapter/lib/ir-scope.d.ts +50 -0
- package/dist/adapter/lib/ir-scope.d.ts.map +1 -0
- package/dist/adapter/lib/minijinja-naming.d.ts +64 -0
- package/dist/adapter/lib/minijinja-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +32 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +84 -0
- package/dist/adapter/memo/seed.d.ts.map +1 -0
- package/dist/adapter/minijinja-adapter.d.ts +421 -0
- package/dist/adapter/minijinja-adapter.d.ts.map +1 -0
- package/dist/adapter/props/prop-classes.d.ts +33 -0
- package/dist/adapter/props/prop-classes.d.ts.map +1 -0
- package/dist/adapter/spread/spread-codegen.d.ts +63 -0
- package/dist/adapter/spread/spread-codegen.d.ts.map +1 -0
- package/dist/adapter/value/parsed-literal.d.ts +28 -0
- package/dist/adapter/value/parsed-literal.d.ts.map +1 -0
- package/dist/build.d.ts +29 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +189111 -0
- package/dist/conformance-pins.d.ts +13 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +189112 -0
- package/package.json +67 -0
- package/runtime/Cargo.lock +124 -0
- package/runtime/Cargo.toml +21 -0
- package/runtime/src/backend_minijinja.rs +176 -0
- package/runtime/src/bin/bf-render.rs +147 -0
- package/runtime/src/evaluator.rs +770 -0
- package/runtime/src/lib.rs +19 -0
- package/runtime/src/manifest.rs +258 -0
- package/runtime/src/num.rs +558 -0
- package/runtime/src/runtime.rs +1548 -0
- package/runtime/src/search_params.rs +173 -0
- package/runtime/tests/eval_vectors.rs +94 -0
- package/runtime/tests/evaluator.rs +407 -0
- package/runtime/tests/helper_vectors.rs +348 -0
- package/runtime/tests/manifest.rs +169 -0
- package/runtime/tests/omit.rs +79 -0
- package/runtime/tests/props_attr.rs +75 -0
- package/runtime/tests/query.rs +50 -0
- package/runtime/tests/render_child.rs +210 -0
- package/runtime/tests/search_params.rs +68 -0
- package/runtime/tests/spread_attrs.rs +94 -0
- package/runtime/tests/template_primitives.rs +376 -0
- package/runtime/tests/vector-divergences.json +33 -0
- package/src/__tests__/minijinja-adapter-unit.test.ts +392 -0
- package/src/__tests__/minijinja-adapter.test.ts +58 -0
- package/src/__tests__/minijinja-counter.test.ts +61 -0
- package/src/__tests__/minijinja-query-href.test.ts +101 -0
- package/src/__tests__/minijinja-spread-attrs.test.ts +227 -0
- package/src/adapter/analysis/component-tree.ts +119 -0
- package/src/adapter/boolean-result.ts +177 -0
- package/src/adapter/emit-context.ts +119 -0
- package/src/adapter/expr/array-method.ts +346 -0
- package/src/adapter/expr/emitters.ts +608 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/lib/constants.ts +37 -0
- package/src/adapter/lib/ir-scope.ts +95 -0
- package/src/adapter/lib/minijinja-naming.ts +85 -0
- package/src/adapter/lib/types.ts +35 -0
- package/src/adapter/memo/seed.ts +135 -0
- package/src/adapter/minijinja-adapter.ts +1796 -0
- package/src/adapter/props/prop-classes.ts +65 -0
- package/src/adapter/spread/spread-codegen.ts +168 -0
- package/src/adapter/value/parsed-literal.ts +76 -0
- package/src/build.ts +38 -0
- package/src/conformance-pins.ts +101 -0
- package/src/index.ts +12 -0
- 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
|
+
}
|