@barefootjs/jinja 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 +66 -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 +84 -0
- package/dist/adapter/boolean-result.d.ts.map +1 -0
- package/dist/adapter/emit-context.d.ts +106 -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/expr/operand.d.ts +25 -0
- package/dist/adapter/expr/operand.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 +189097 -0
- package/dist/adapter/jinja-adapter.d.ts +394 -0
- package/dist/adapter/jinja-adapter.d.ts.map +1 -0
- package/dist/adapter/lib/constants.d.ts +21 -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/jinja-naming.d.ts +77 -0
- package/dist/adapter/lib/jinja-naming.d.ts.map +1 -0
- package/dist/adapter/lib/types.d.ts +27 -0
- package/dist/adapter/lib/types.d.ts.map +1 -0
- package/dist/adapter/memo/seed.d.ts +81 -0
- package/dist/adapter/memo/seed.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 +61 -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 +28 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +189117 -0
- package/dist/conformance-pins.d.ts +12 -0
- package/dist/conformance-pins.d.ts.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +189118 -0
- package/package.json +66 -0
- package/python/VERSION +1 -0
- package/python/barefootjs/__init__.py +57 -0
- package/python/barefootjs/backend_jinja.py +125 -0
- package/python/barefootjs/evaluator.py +787 -0
- package/python/barefootjs/runtime.py +1334 -0
- package/python/barefootjs/search_params.py +89 -0
- package/python/pyproject.toml +32 -0
- package/python/tests/__init__.py +0 -0
- package/python/tests/test_eval_vectors.py +88 -0
- package/python/tests/test_evaluator.py +406 -0
- package/python/tests/test_helper_vectors.py +288 -0
- package/python/tests/test_omit.py +62 -0
- package/python/tests/test_props_attr.py +54 -0
- package/python/tests/test_query.py +41 -0
- package/python/tests/test_render.py +102 -0
- package/python/tests/test_render_child.py +96 -0
- package/python/tests/test_search_params.py +50 -0
- package/python/tests/test_spread_attrs.py +86 -0
- package/python/tests/test_template_primitives.py +347 -0
- package/python/tests/vector-divergences.json +42 -0
- package/src/__tests__/jinja-adapter-unit.test.ts +390 -0
- package/src/__tests__/jinja-adapter.test.ts +53 -0
- package/src/__tests__/jinja-counter.test.ts +62 -0
- package/src/__tests__/jinja-query-href.test.ts +99 -0
- package/src/__tests__/jinja-spread-attrs.test.ts +225 -0
- package/src/adapter/analysis/component-tree.ts +119 -0
- package/src/adapter/boolean-result.ts +176 -0
- package/src/adapter/emit-context.ts +118 -0
- package/src/adapter/expr/array-method.ts +346 -0
- package/src/adapter/expr/emitters.ts +608 -0
- package/src/adapter/expr/operand.ts +35 -0
- package/src/adapter/index.ts +6 -0
- package/src/adapter/jinja-adapter.ts +1747 -0
- package/src/adapter/lib/constants.ts +33 -0
- package/src/adapter/lib/ir-scope.ts +95 -0
- package/src/adapter/lib/jinja-naming.ts +114 -0
- package/src/adapter/lib/types.ts +30 -0
- package/src/adapter/memo/seed.ts +132 -0
- package/src/adapter/props/prop-classes.ts +65 -0
- package/src/adapter/spread/spread-codegen.ts +166 -0
- package/src/adapter/value/parsed-literal.ts +76 -0
- package/src/build.ts +37 -0
- package/src/conformance-pins.ts +101 -0
- package/src/index.ts +9 -0
- package/src/test-render.ts +714 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
"""Python port of packages/adapter-perl/lib/BarefootJS/Evaluator.pm.
|
|
2
|
+
|
|
3
|
+
Lightweight evaluator for the pure ``ParsedExpr`` subset, scoped to
|
|
4
|
+
higher-order callback bodies (reduce / sort / map / filter / find
|
|
5
|
+
``(...) => expr``) -- issue #2018. Templates cannot carry a lambda in
|
|
6
|
+
expression position, which is why the adapters historically special-cased
|
|
7
|
+
these callbacks into fixed shapes (bf_sort's comparator catalogue,
|
|
8
|
+
bf_reduce's +/* fold). Instead, the callback BODY rides as a pure
|
|
9
|
+
``ParsedExpr`` subtree (the structured IR the compiler already produces) and
|
|
10
|
+
is evaluated here against an environment (``{acc, item, ...captured free
|
|
11
|
+
vars}``).
|
|
12
|
+
|
|
13
|
+
ONE shared implementation across all backends (mirrors the Perl module,
|
|
14
|
+
which is shared by the Mojo + Xslate Perl backends), living alongside
|
|
15
|
+
``search_params.py`` in the engine-agnostic core. The accepted subset and
|
|
16
|
+
its semantics are documented in ``spec/compiler.md`` ("ParsedExpr Evaluator
|
|
17
|
+
Semantics") and pinned isomorphically by the cross-language golden vectors
|
|
18
|
+
(``packages/adapter-tests/vectors/eval-vectors.json``), shared with
|
|
19
|
+
the Go evaluator (bf.go) and the Perl evaluator -- same input -> same output.
|
|
20
|
+
|
|
21
|
+
The coercion below is JS-faithful (ToNumber / ToString / ToBoolean, strict
|
|
22
|
+
equality) and deliberately distinct from the divergent ``bf.string`` /
|
|
23
|
+
``bf.number`` helpers in ``runtime.py``, so the contract is unambiguous and
|
|
24
|
+
every template adapter stays byte-equal with each other and with Go.
|
|
25
|
+
|
|
26
|
+
Unlike the Perl port, this module does NOT need a separate SV-flag trick
|
|
27
|
+
(``B::svref_2object`` in Evaluator.pm) to tell the JS *string* "10" from the
|
|
28
|
+
JS *number* 10: Python's ``json.loads`` already decodes a JSON string to
|
|
29
|
+
``str`` and a JSON number to ``int``/``float``, so the type distinction is
|
|
30
|
+
native. ``bool`` is checked before the general numeric branch everywhere
|
|
31
|
+
below because Python's ``bool`` is a subclass of ``int``.
|
|
32
|
+
|
|
33
|
+
This module intentionally does NOT import ``runtime.py`` (and vice versa is
|
|
34
|
+
fine -- ``runtime.py`` imports this module for the ``*_eval`` helpers), to
|
|
35
|
+
mirror the Perl structure: ``Evaluator.pm`` is a standalone unit with no
|
|
36
|
+
dependency on ``BarefootJS.pm``.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import functools
|
|
42
|
+
import json as _json
|
|
43
|
+
import math
|
|
44
|
+
import re
|
|
45
|
+
from typing import Any
|
|
46
|
+
|
|
47
|
+
# ---------------------------------------------------------------------------
|
|
48
|
+
# JS numeric-string grammar (mirrors Perl's Scalar::Util::looks_like_number).
|
|
49
|
+
# Exposed (not underscore-prefixed) so runtime.py's JS-`Number()`-diverging
|
|
50
|
+
# `number()` helper can reuse the exact same "does this string look like a
|
|
51
|
+
# number" classifier -- the two modules apply different coercion RULES
|
|
52
|
+
# (Evaluator is JS-faithful; runtime.number() deliberately returns NaN for
|
|
53
|
+
# undef/empty-string) but share the same underlying string grammar, just as
|
|
54
|
+
# both Perl modules share one imported `Scalar::Util::looks_like_number`.
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
_NUM_RE = re.compile(r"^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$")
|
|
57
|
+
_INF_NAN_RE = re.compile(r"^[+-]?(inf(inity)?|nan)$", re.IGNORECASE)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def looks_like_number(s: str) -> bool:
|
|
61
|
+
"""Mirror Scalar::Util::looks_like_number for a (possibly padded) string."""
|
|
62
|
+
t = s.strip()
|
|
63
|
+
if t == "":
|
|
64
|
+
return False
|
|
65
|
+
return bool(_NUM_RE.match(t) or _INF_NAN_RE.match(t))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def parse_number_literal(s: str) -> float:
|
|
69
|
+
"""Parse a string already known to satisfy `looks_like_number` into a float."""
|
|
70
|
+
t = s.strip()
|
|
71
|
+
if _INF_NAN_RE.match(t):
|
|
72
|
+
low = t.lower()
|
|
73
|
+
if "nan" in low:
|
|
74
|
+
return float("nan")
|
|
75
|
+
return float("-inf") if low.startswith("-") else float("inf")
|
|
76
|
+
return float(t)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# evaluate(node, env): evaluate a decoded ParsedExpr node (a dict keyed by
|
|
81
|
+
# `kind`) against the environment dict, returning a Python value (float, str,
|
|
82
|
+
# bool, None for null, list, dict). The matching JSON entry point is
|
|
83
|
+
# eval_json() below.
|
|
84
|
+
# ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def evaluate(node: Any, env: dict) -> Any:
|
|
88
|
+
if not isinstance(node, dict):
|
|
89
|
+
return None
|
|
90
|
+
kind = node.get("kind") or ""
|
|
91
|
+
|
|
92
|
+
if kind == "literal":
|
|
93
|
+
return node.get("value")
|
|
94
|
+
if kind == "identifier":
|
|
95
|
+
return env.get(node.get("name"))
|
|
96
|
+
if kind == "binary":
|
|
97
|
+
return _binary(
|
|
98
|
+
node.get("op"),
|
|
99
|
+
evaluate(node.get("left"), env),
|
|
100
|
+
evaluate(node.get("right"), env),
|
|
101
|
+
)
|
|
102
|
+
if kind == "unary":
|
|
103
|
+
return _unary(node.get("op"), evaluate(node.get("argument"), env))
|
|
104
|
+
if kind == "logical":
|
|
105
|
+
op = node.get("op")
|
|
106
|
+
left = evaluate(node.get("left"), env)
|
|
107
|
+
if op == "&&":
|
|
108
|
+
return evaluate(node.get("right"), env) if _truthy(left) else left
|
|
109
|
+
if op == "||":
|
|
110
|
+
return left if _truthy(left) else evaluate(node.get("right"), env)
|
|
111
|
+
# `??`
|
|
112
|
+
return left if left is not None else evaluate(node.get("right"), env)
|
|
113
|
+
if kind == "conditional":
|
|
114
|
+
return (
|
|
115
|
+
evaluate(node.get("consequent"), env)
|
|
116
|
+
if _truthy(evaluate(node.get("test"), env))
|
|
117
|
+
else evaluate(node.get("alternate"), env)
|
|
118
|
+
)
|
|
119
|
+
if kind == "member":
|
|
120
|
+
return _read_property(evaluate(node.get("object"), env), node.get("property"))
|
|
121
|
+
if kind == "index-access":
|
|
122
|
+
return _read_index(
|
|
123
|
+
evaluate(node.get("object"), env), evaluate(node.get("index"), env)
|
|
124
|
+
)
|
|
125
|
+
if kind == "call":
|
|
126
|
+
nested = _array_callback_call(node)
|
|
127
|
+
if nested is not None:
|
|
128
|
+
method, object_node, arrow_node = nested
|
|
129
|
+
return _array_callback(method, object_node, arrow_node, env)
|
|
130
|
+
name = _builtin_name(node.get("callee"))
|
|
131
|
+
if not name:
|
|
132
|
+
return None
|
|
133
|
+
args = [evaluate(a, env) for a in node.get("args") or []]
|
|
134
|
+
return _call_builtin(name, args)
|
|
135
|
+
if kind == "template-literal":
|
|
136
|
+
out = []
|
|
137
|
+
for p in node.get("parts") or []:
|
|
138
|
+
if (p.get("type") or "") == "string":
|
|
139
|
+
out.append(p.get("value") or "")
|
|
140
|
+
else:
|
|
141
|
+
out.append(_to_string(evaluate(p.get("expr"), env)))
|
|
142
|
+
return "".join(out)
|
|
143
|
+
if kind == "array-literal":
|
|
144
|
+
return [evaluate(e, env) for e in node.get("elements") or []]
|
|
145
|
+
if kind == "object-literal":
|
|
146
|
+
out: dict = {}
|
|
147
|
+
for prop in node.get("properties") or []:
|
|
148
|
+
out[prop.get("key")] = evaluate(prop.get("value"), env)
|
|
149
|
+
return out
|
|
150
|
+
if kind == "array-method":
|
|
151
|
+
method = node.get("method") or ""
|
|
152
|
+
if method == "includes":
|
|
153
|
+
args = node.get("args") or []
|
|
154
|
+
if len(args) == 1:
|
|
155
|
+
# `.includes(x)` (#2075) -- the one `array-method` in the
|
|
156
|
+
# evaluator subset, shared between `Array.prototype.includes`
|
|
157
|
+
# (SameValueZero membership) and `String.prototype.includes`
|
|
158
|
+
# (substring search), matching the receiver-type dispatch the SSR
|
|
159
|
+
# template lowering does at runtime (`bf.includes`). Mirrors the
|
|
160
|
+
# JS reference's `includes()` (eval-reference.ts) and the Perl
|
|
161
|
+
# port's identical `array-method`/`includes` arm
|
|
162
|
+
# (Evaluator.pm).
|
|
163
|
+
obj = evaluate(node.get("object"), env)
|
|
164
|
+
needle = evaluate(args[0], env)
|
|
165
|
+
if isinstance(obj, list):
|
|
166
|
+
return _bool(any(_same_value_zero(el, needle) for el in obj))
|
|
167
|
+
if _is_string(obj):
|
|
168
|
+
return _bool(_to_string(needle) in obj)
|
|
169
|
+
# Any other receiver is not a JS `.includes` target -- degrade
|
|
170
|
+
# to false rather than raising, mirroring the reference.
|
|
171
|
+
return _bool(False)
|
|
172
|
+
elif method == "join":
|
|
173
|
+
# `.join(sep?)` (#2094): a plain `array-method` node alongside
|
|
174
|
+
# `includes` above. JS semantics: default separator is `,`; a
|
|
175
|
+
# `null`/`undefined` element joins as `''` -- NOT the string
|
|
176
|
+
# "null" that a bare `_to_string(None)` ToString call would
|
|
177
|
+
# produce (see `_to_string`'s docstring-equivalent note above),
|
|
178
|
+
# so that special case is applied here, before stringifying,
|
|
179
|
+
# rather than inside `_to_string` itself.
|
|
180
|
+
args = node.get("args") or []
|
|
181
|
+
sep = _to_string(evaluate(args[0], env)) if len(args) >= 1 else ","
|
|
182
|
+
obj = evaluate(node.get("object"), env)
|
|
183
|
+
arr = obj if isinstance(obj, list) else []
|
|
184
|
+
return sep.join("" if el is None else _to_string(el) for el in arr)
|
|
185
|
+
|
|
186
|
+
# arrow-fn / higher-order / unsupported array-method: a callback body
|
|
187
|
+
# containing these is refused upstream (BF101); never reached here.
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def eval_json(json_str: str, env: dict) -> Any:
|
|
192
|
+
"""Decode a ParsedExpr JSON string and evaluate it. Mirrors the Go EvalExpr
|
|
193
|
+
entry point and Perl's eval_json."""
|
|
194
|
+
return evaluate(_json.loads(json_str), env)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------------------
|
|
198
|
+
# JS value classification.
|
|
199
|
+
# ---------------------------------------------------------------------------
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _is_string(v: Any) -> bool:
|
|
203
|
+
return isinstance(v, str)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _is_number(v: Any) -> bool:
|
|
207
|
+
return isinstance(v, (int, float)) and not isinstance(v, bool)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _nan() -> float:
|
|
211
|
+
return float("nan")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# ---------------------------------------------------------------------------
|
|
215
|
+
# JS coercion primitives (ToNumber / ToString / ToBoolean).
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _to_number(v: Any) -> float:
|
|
220
|
+
if v is None:
|
|
221
|
+
return 0.0
|
|
222
|
+
if isinstance(v, bool):
|
|
223
|
+
return 1.0 if v else 0.0
|
|
224
|
+
if _is_number(v):
|
|
225
|
+
return float(v)
|
|
226
|
+
if isinstance(v, str):
|
|
227
|
+
t = v.strip()
|
|
228
|
+
if t == "":
|
|
229
|
+
return 0.0
|
|
230
|
+
return parse_number_literal(t) if looks_like_number(t) else _nan()
|
|
231
|
+
return _nan() # list / dict
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _format_number(n: float) -> str:
|
|
235
|
+
# Shortest round-trip repr with JS's integral-float spelling (`1.0` ->
|
|
236
|
+
# `"1"`). Python's `repr(float)` has been shortest-round-trip since 3.1,
|
|
237
|
+
# matching JS engines' digit sequences for the common range; very
|
|
238
|
+
# large/small magnitudes can diverge in exponent-notation formatting --
|
|
239
|
+
# the same documented divergence region the Go runtime's evalToString
|
|
240
|
+
# carries (see bf.go / eval.go), not re-solved here either.
|
|
241
|
+
if n == 0:
|
|
242
|
+
return "0"
|
|
243
|
+
if n == int(n) and abs(n) < 1e21:
|
|
244
|
+
return str(int(n))
|
|
245
|
+
return repr(n)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _to_string(v: Any) -> str:
|
|
249
|
+
if v is None:
|
|
250
|
+
return "null"
|
|
251
|
+
if isinstance(v, bool):
|
|
252
|
+
return "true" if v else "false"
|
|
253
|
+
if _is_number(v):
|
|
254
|
+
n = float(v)
|
|
255
|
+
if n != n:
|
|
256
|
+
return "NaN"
|
|
257
|
+
if n == float("inf"):
|
|
258
|
+
return "Infinity"
|
|
259
|
+
if n == float("-inf"):
|
|
260
|
+
return "-Infinity"
|
|
261
|
+
return _format_number(n)
|
|
262
|
+
return str(v)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def _truthy(v: Any) -> bool:
|
|
266
|
+
if v is None:
|
|
267
|
+
return False
|
|
268
|
+
if isinstance(v, bool):
|
|
269
|
+
return v
|
|
270
|
+
if isinstance(v, (int, float)):
|
|
271
|
+
n = float(v)
|
|
272
|
+
return n == n and n != 0 # nonzero and not NaN
|
|
273
|
+
if isinstance(v, str):
|
|
274
|
+
return v != "" # incl. the truthy "0"
|
|
275
|
+
return True # arrays / objects are always truthy in JS
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _bool(t: Any) -> bool:
|
|
279
|
+
return bool(t)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
# Operators
|
|
284
|
+
# ---------------------------------------------------------------------------
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _binary(op: str, l: Any, r: Any) -> Any:
|
|
288
|
+
if op == "+":
|
|
289
|
+
# JS `+`: string concatenation once either operand is a string,
|
|
290
|
+
# numeric addition otherwise.
|
|
291
|
+
if _is_string(l) or _is_string(r):
|
|
292
|
+
return _to_string(l) + _to_string(r)
|
|
293
|
+
return _to_number(l) + _to_number(r)
|
|
294
|
+
if op == "-":
|
|
295
|
+
return _to_number(l) - _to_number(r)
|
|
296
|
+
if op == "*":
|
|
297
|
+
return _to_number(l) * _to_number(r)
|
|
298
|
+
if op == "/":
|
|
299
|
+
ln, rn = _to_number(l), _to_number(r)
|
|
300
|
+
if rn == 0:
|
|
301
|
+
if ln == 0 or ln != ln:
|
|
302
|
+
return _nan()
|
|
303
|
+
return float("inf") if ln > 0 else float("-inf")
|
|
304
|
+
return ln / rn
|
|
305
|
+
if op == "%":
|
|
306
|
+
rn = _to_number(r)
|
|
307
|
+
if rn == 0:
|
|
308
|
+
return _nan()
|
|
309
|
+
return math.fmod(_to_number(l), rn)
|
|
310
|
+
if op in ("<", "<=", ">", ">="):
|
|
311
|
+
return _relational(op, l, r)
|
|
312
|
+
if op == "===":
|
|
313
|
+
return _bool(_strict_eq(l, r))
|
|
314
|
+
if op == "!==":
|
|
315
|
+
return _bool(not _strict_eq(l, r))
|
|
316
|
+
# Loose equality / bitwise / shift are out of the subset.
|
|
317
|
+
return None
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _relational(op: str, l: Any, r: Any) -> bool:
|
|
321
|
+
# JS Abstract Relational Comparison: both strings -> compare by code
|
|
322
|
+
# unit; otherwise coerce both to numbers (a NaN operand makes it false).
|
|
323
|
+
if _is_string(l) and _is_string(r):
|
|
324
|
+
c = -1 if l < r else (1 if l > r else 0)
|
|
325
|
+
else:
|
|
326
|
+
ln, rn = _to_number(l), _to_number(r)
|
|
327
|
+
if ln != ln or rn != rn: # NaN -> false
|
|
328
|
+
return _bool(False)
|
|
329
|
+
c = -1 if ln < rn else (1 if ln > rn else 0)
|
|
330
|
+
if op == "<":
|
|
331
|
+
return _bool(c < 0)
|
|
332
|
+
if op == "<=":
|
|
333
|
+
return _bool(c <= 0)
|
|
334
|
+
if op == ">":
|
|
335
|
+
return _bool(c > 0)
|
|
336
|
+
if op == ">=":
|
|
337
|
+
return _bool(c >= 0)
|
|
338
|
+
return _bool(False)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _strict_eq(l: Any, r: Any) -> bool:
|
|
342
|
+
# Strict `===`: equal JS type and value, no coercion.
|
|
343
|
+
ln, rn = _is_number(l), _is_number(r)
|
|
344
|
+
if ln and rn:
|
|
345
|
+
lf, rf = float(l), float(r)
|
|
346
|
+
if lf != lf or rf != rf: # NaN
|
|
347
|
+
return False
|
|
348
|
+
return lf == rf
|
|
349
|
+
if ln != rn: # one numeric, one not
|
|
350
|
+
return False
|
|
351
|
+
if l is None:
|
|
352
|
+
return r is None
|
|
353
|
+
if r is None:
|
|
354
|
+
return False
|
|
355
|
+
lb, rb = isinstance(l, bool), isinstance(r, bool)
|
|
356
|
+
if lb or rb:
|
|
357
|
+
if not (lb and rb):
|
|
358
|
+
return False
|
|
359
|
+
return bool(l) == bool(r)
|
|
360
|
+
if _is_string(l) and _is_string(r):
|
|
361
|
+
return l == r
|
|
362
|
+
return False
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _same_value_zero(l: Any, r: Any) -> bool:
|
|
366
|
+
"""`Array.prototype.includes` membership test -- `===` except `NaN`
|
|
367
|
+
equals itself (and +0/-0 are not distinguished, which the JSON-decoded
|
|
368
|
+
values here can't represent anyway). Reuses `_strict_eq`'s type/value
|
|
369
|
+
rules and only special-cases the two-NaN case that `_strict_eq`
|
|
370
|
+
(deliberately, for `===`) reports as unequal."""
|
|
371
|
+
if _is_number(l) and _is_number(r):
|
|
372
|
+
lf, rf = float(l), float(r)
|
|
373
|
+
if lf != lf and rf != rf: # NaN sameValueZero NaN
|
|
374
|
+
return True
|
|
375
|
+
return _strict_eq(l, r)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def _unary(op: str, v: Any) -> Any:
|
|
379
|
+
if op == "!":
|
|
380
|
+
return _bool(not _truthy(v))
|
|
381
|
+
if op == "-":
|
|
382
|
+
return -_to_number(v)
|
|
383
|
+
if op == "+":
|
|
384
|
+
return _to_number(v)
|
|
385
|
+
return None
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
# ---------------------------------------------------------------------------
|
|
389
|
+
# Nested `.map`/`.filter` callback calls (#2094) -- widens the evaluator's
|
|
390
|
+
# "call" case to also execute a `.map(cb)` / `.filter(cb)` call nested INSIDE
|
|
391
|
+
# a callback body it's already given, e.g. a `.flatMap(p => p.tags.map(t =>
|
|
392
|
+
# '#'+t))` projection whose body itself contains a `.map`. Everything else
|
|
393
|
+
# nested (`.some`/`.find`/`.every`/`.sort`/`.reduce`/`.flat`/`.flatMap`,
|
|
394
|
+
# standalone arrows) stays refused -- that gating is the compiler's job
|
|
395
|
+
# upstream (`serializeParsedExpr`) and is not touched here. Mirrors Go's
|
|
396
|
+
# `evalArrayCallbackCall` / `evalArrayCallback`.
|
|
397
|
+
# ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def _array_callback_call(node: dict) -> tuple[str, Any, dict] | None:
|
|
401
|
+
"""Recognise a `call` node shaped like a nested `.map(cb)` / `.filter(cb)`
|
|
402
|
+
call: `callee` is a non-computed `member` node whose `property` is `map`
|
|
403
|
+
or `filter`, and the first (only) arg is an `arrow` node. Returns
|
|
404
|
+
`(method, object_node, arrow_node)` or `None` when the node doesn't match
|
|
405
|
+
(the caller then falls through to the ordinary builtin-name dispatch)."""
|
|
406
|
+
callee = node.get("callee")
|
|
407
|
+
if not isinstance(callee, dict) or (callee.get("kind") or "") != "member":
|
|
408
|
+
return None
|
|
409
|
+
if callee.get("computed"):
|
|
410
|
+
return None
|
|
411
|
+
prop = callee.get("property") or ""
|
|
412
|
+
if prop not in ("map", "filter"):
|
|
413
|
+
return None
|
|
414
|
+
raw_args = node.get("args") or []
|
|
415
|
+
if not raw_args:
|
|
416
|
+
return None
|
|
417
|
+
arrow_node = raw_args[0]
|
|
418
|
+
if not isinstance(arrow_node, dict) or (arrow_node.get("kind") or "") != "arrow":
|
|
419
|
+
return None
|
|
420
|
+
return prop, callee.get("object"), arrow_node
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _array_callback(method: str, object_node: Any, arrow_node: dict, env: dict) -> Any:
|
|
424
|
+
"""Evaluate a nested `.map(cb)` / `.filter(cb)` call recognised by
|
|
425
|
+
`_array_callback_call`. `params` (plain strings, e.g. `["t"]` or
|
|
426
|
+
`["t", "i"]`) bind the 1st param to the element and the 2nd (if present)
|
|
427
|
+
to the integer index, in a COPY of the parent env (a fresh child scope
|
|
428
|
+
per call -- never mutating the parent dict in place across sibling
|
|
429
|
+
iterations, the classic Python gotcha)."""
|
|
430
|
+
obj = evaluate(object_node, env)
|
|
431
|
+
# Non-list receiver -> None, matching every other runtime evaluator
|
|
432
|
+
# (Go nil / Perl undef / Rust Null / PHP null / Ruby nil) so "missing"
|
|
433
|
+
# stays distinguishable from "empty array" downstream.
|
|
434
|
+
if not isinstance(obj, list):
|
|
435
|
+
return None
|
|
436
|
+
arr = obj
|
|
437
|
+
params = [p for p in (arrow_node.get("params") or [])]
|
|
438
|
+
body = arrow_node.get("body")
|
|
439
|
+
|
|
440
|
+
def call_cb(item: Any, index: int) -> Any:
|
|
441
|
+
inner = dict(env)
|
|
442
|
+
if len(params) > 0:
|
|
443
|
+
inner[params[0]] = item
|
|
444
|
+
if len(params) > 1:
|
|
445
|
+
inner[params[1]] = index
|
|
446
|
+
return evaluate(body, inner)
|
|
447
|
+
|
|
448
|
+
if method == "map":
|
|
449
|
+
return [call_cb(item, i) for i, item in enumerate(arr)]
|
|
450
|
+
return [item for i, item in enumerate(arr) if _truthy(call_cb(item, i))]
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
# ---------------------------------------------------------------------------
|
|
454
|
+
# Built-in calls (the deterministic allowlist). Locale-sensitive builtins
|
|
455
|
+
# (localeCompare) are deliberately excluded to keep the backends isomorphic.
|
|
456
|
+
# ---------------------------------------------------------------------------
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _builtin_name(callee: Any) -> str:
|
|
460
|
+
"""Resolve a `call` callee to its builtin name (e.g. "Math.max"), or ''
|
|
461
|
+
when the callee is not an allowlisted builtin reference."""
|
|
462
|
+
if not isinstance(callee, dict):
|
|
463
|
+
return ""
|
|
464
|
+
kind = callee.get("kind") or ""
|
|
465
|
+
if kind == "identifier":
|
|
466
|
+
return callee.get("name") or ""
|
|
467
|
+
if kind == "member" and not callee.get("computed"):
|
|
468
|
+
obj = callee.get("object")
|
|
469
|
+
if not isinstance(obj, dict) or (obj.get("kind") or "") != "identifier":
|
|
470
|
+
return ""
|
|
471
|
+
return f"{obj.get('name') or ''}.{callee.get('property') or ''}"
|
|
472
|
+
return ""
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _safe_floor(n: float) -> float:
|
|
476
|
+
if n != n or n in (float("inf"), float("-inf")):
|
|
477
|
+
return n
|
|
478
|
+
return float(math.floor(n))
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _safe_ceil(n: float) -> float:
|
|
482
|
+
if n != n or n in (float("inf"), float("-inf")):
|
|
483
|
+
return n
|
|
484
|
+
return float(math.ceil(n))
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _math_round(n: float) -> float:
|
|
488
|
+
# Half rounds toward +Infinity (JS Math.round: 2.5 -> 3, -2.5 -> -2),
|
|
489
|
+
# matching runtime.py's `round` helper rather than half-away-from-zero.
|
|
490
|
+
if n != n or n in (float("inf"), float("-inf")):
|
|
491
|
+
return n
|
|
492
|
+
return float(math.floor(n + 0.5))
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _call_builtin(name: str, args: list) -> Any:
|
|
496
|
+
def arg(i: int) -> Any:
|
|
497
|
+
return args[i] if i < len(args) else None
|
|
498
|
+
|
|
499
|
+
if name == "Math.max":
|
|
500
|
+
m = float("-inf") # JS Math.max() with no args is -Infinity
|
|
501
|
+
for a in args:
|
|
502
|
+
n = _to_number(a)
|
|
503
|
+
if n != n: # any NaN argument -> NaN (JS / Go / Perl)
|
|
504
|
+
return n
|
|
505
|
+
if n > m:
|
|
506
|
+
m = n
|
|
507
|
+
return m
|
|
508
|
+
if name == "Math.min":
|
|
509
|
+
m = float("inf") # JS Math.min() with no args is +Infinity
|
|
510
|
+
for a in args:
|
|
511
|
+
n = _to_number(a)
|
|
512
|
+
if n != n:
|
|
513
|
+
return n
|
|
514
|
+
if n < m:
|
|
515
|
+
m = n
|
|
516
|
+
return m
|
|
517
|
+
if name == "Math.abs":
|
|
518
|
+
return abs(_to_number(arg(0)))
|
|
519
|
+
if name == "Math.floor":
|
|
520
|
+
return _safe_floor(_to_number(arg(0)))
|
|
521
|
+
if name == "Math.ceil":
|
|
522
|
+
return _safe_ceil(_to_number(arg(0)))
|
|
523
|
+
if name == "Math.round":
|
|
524
|
+
return _math_round(_to_number(arg(0)))
|
|
525
|
+
if name == "String":
|
|
526
|
+
return _to_string(arg(0))
|
|
527
|
+
if name == "Number":
|
|
528
|
+
return _to_number(arg(0))
|
|
529
|
+
if name == "Boolean":
|
|
530
|
+
return _bool(_truthy(arg(0)))
|
|
531
|
+
# Any other callee is outside the subset (refused upstream).
|
|
532
|
+
return None
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
# ---------------------------------------------------------------------------
|
|
536
|
+
# Member / index access
|
|
537
|
+
# ---------------------------------------------------------------------------
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _read_property(obj: Any, key: Any) -> Any:
|
|
541
|
+
if obj is None:
|
|
542
|
+
return None
|
|
543
|
+
if isinstance(obj, dict):
|
|
544
|
+
return obj.get(key)
|
|
545
|
+
if isinstance(obj, list):
|
|
546
|
+
return len(obj) if key == "length" else None
|
|
547
|
+
if isinstance(obj, str) and key == "length":
|
|
548
|
+
# `.length` is a string property only -- a numeric scalar (123) has
|
|
549
|
+
# no `.length` in the subset (JS `(123).length` is undefined ->
|
|
550
|
+
# null); matches the Go/Perl evaluators (numbers fall through here).
|
|
551
|
+
return len(obj)
|
|
552
|
+
return None
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _read_index(obj: Any, index: Any) -> Any:
|
|
556
|
+
if isinstance(obj, list):
|
|
557
|
+
f = _to_number(index)
|
|
558
|
+
if f != f or f in (float("inf"), float("-inf")):
|
|
559
|
+
return None
|
|
560
|
+
i = int(f)
|
|
561
|
+
if i != f or i < 0 or i >= len(obj):
|
|
562
|
+
return None
|
|
563
|
+
return obj[i]
|
|
564
|
+
if isinstance(obj, dict):
|
|
565
|
+
return obj.get(_to_string(index))
|
|
566
|
+
return None
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
# ---------------------------------------------------------------------------
|
|
570
|
+
# Evaluator-driven higher-order folds (the generalization of bf_reduce /
|
|
571
|
+
# bf_sort onto the evaluator).
|
|
572
|
+
# ---------------------------------------------------------------------------
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def fold(
|
|
576
|
+
items: Any,
|
|
577
|
+
body: Any,
|
|
578
|
+
acc_name: str,
|
|
579
|
+
item_name: str,
|
|
580
|
+
init: Any,
|
|
581
|
+
direction: str = "left",
|
|
582
|
+
base_env: dict | None = None,
|
|
583
|
+
) -> Any:
|
|
584
|
+
"""Fold a list into a value via the evaluator. `body` is a pure ParsedExpr
|
|
585
|
+
node evaluated against `{acc_name: acc, item_name: item}` plus the
|
|
586
|
+
captured free vars in `base_env` per element; `init` seeds the
|
|
587
|
+
accumulator and `direction` is "left" (reduce) or "right" (reduceRight).
|
|
588
|
+
Generalizes bf_reduce -- any reducer body, not just the +/* arithmetic
|
|
589
|
+
catalogue, and acc may appear anywhere. Mirrors Go's FoldEval."""
|
|
590
|
+
arr = list(items) if isinstance(items, list) else []
|
|
591
|
+
if direction == "right":
|
|
592
|
+
arr = list(reversed(arr))
|
|
593
|
+
env = dict(base_env) if base_env else {}
|
|
594
|
+
acc = init
|
|
595
|
+
for item in arr:
|
|
596
|
+
env[acc_name] = acc
|
|
597
|
+
env[item_name] = item
|
|
598
|
+
acc = evaluate(body, env)
|
|
599
|
+
return acc
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def sort_by(
|
|
603
|
+
items: Any,
|
|
604
|
+
cmp: Any,
|
|
605
|
+
param_a: str,
|
|
606
|
+
param_b: str,
|
|
607
|
+
base_env: dict | None = None,
|
|
608
|
+
) -> list:
|
|
609
|
+
"""Return a new list ordered by a ParsedExpr comparator `cmp` evaluated
|
|
610
|
+
against `{param_a: a, param_b: b}` plus the captured free vars in
|
|
611
|
+
`base_env`, to a number (negative / zero / positive, like a JS
|
|
612
|
+
comparator). Generalizes bf_sort -- any comparator body. Non-mutating.
|
|
613
|
+
Stable: Python's `sorted` carries a formal stability guarantee, so
|
|
614
|
+
(unlike the Perl port) no explicit original-index tie-break decoration
|
|
615
|
+
is needed. Mirrors Go's SortEval."""
|
|
616
|
+
if not isinstance(items, list):
|
|
617
|
+
return []
|
|
618
|
+
env = dict(base_env) if base_env else {}
|
|
619
|
+
|
|
620
|
+
def cmp_fn(a: Any, b: Any) -> int:
|
|
621
|
+
env[param_a] = a
|
|
622
|
+
env[param_b] = b
|
|
623
|
+
c = _to_number(evaluate(cmp, env))
|
|
624
|
+
# NaN comparator result -> keep order (matches JS + the Go/Perl sign
|
|
625
|
+
# test, which treat a NaN comparator result as "no reordering").
|
|
626
|
+
if c != c:
|
|
627
|
+
return 0
|
|
628
|
+
return -1 if c < 0 else (1 if c > 0 else 0)
|
|
629
|
+
|
|
630
|
+
return sorted(items, key=functools.cmp_to_key(cmp_fn))
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
def fold_json(
|
|
634
|
+
items: Any,
|
|
635
|
+
body_json: str,
|
|
636
|
+
acc_name: str,
|
|
637
|
+
item_name: str,
|
|
638
|
+
init: Any,
|
|
639
|
+
direction: str = "left",
|
|
640
|
+
base_env: dict | None = None,
|
|
641
|
+
) -> Any:
|
|
642
|
+
return fold(items, _json.loads(body_json), acc_name, item_name, init, direction, base_env)
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def sort_by_json(
|
|
646
|
+
items: Any,
|
|
647
|
+
cmp_json: str,
|
|
648
|
+
param_a: str,
|
|
649
|
+
param_b: str,
|
|
650
|
+
base_env: dict | None = None,
|
|
651
|
+
) -> list:
|
|
652
|
+
return sort_by(items, _json.loads(cmp_json), param_a, param_b, base_env)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
# ---------------------------------------------------------------------------
|
|
656
|
+
# Higher-order predicates (#2018, P2) -- the generalization of bf_filter /
|
|
657
|
+
# bf_find / bf_find_index / bf_every / bf_some onto the evaluator. Each
|
|
658
|
+
# mirrors the corresponding Go helper (FilterEval / EveryEval / SomeEval /
|
|
659
|
+
# FindEval / FindIndexEval).
|
|
660
|
+
# ---------------------------------------------------------------------------
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def filter(items: Any, pred: Any, param: str, base_env: dict | None = None) -> list:
|
|
664
|
+
if not isinstance(items, list):
|
|
665
|
+
return []
|
|
666
|
+
env = dict(base_env) if base_env else {}
|
|
667
|
+
out = []
|
|
668
|
+
for item in items:
|
|
669
|
+
env[param] = item
|
|
670
|
+
if _truthy(evaluate(pred, env)):
|
|
671
|
+
out.append(item)
|
|
672
|
+
return out
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
def every(items: Any, pred: Any, param: str, base_env: dict | None = None) -> bool:
|
|
676
|
+
arr = items if isinstance(items, list) else []
|
|
677
|
+
env = dict(base_env) if base_env else {}
|
|
678
|
+
for item in arr:
|
|
679
|
+
env[param] = item
|
|
680
|
+
if not _truthy(evaluate(pred, env)):
|
|
681
|
+
return False
|
|
682
|
+
return True
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def some(items: Any, pred: Any, param: str, base_env: dict | None = None) -> bool:
|
|
686
|
+
arr = items if isinstance(items, list) else []
|
|
687
|
+
env = dict(base_env) if base_env else {}
|
|
688
|
+
for item in arr:
|
|
689
|
+
env[param] = item
|
|
690
|
+
if _truthy(evaluate(pred, env)):
|
|
691
|
+
return True
|
|
692
|
+
return False
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
def find(
|
|
696
|
+
items: Any, pred: Any, param: str, forward: bool = True, base_env: dict | None = None
|
|
697
|
+
) -> Any:
|
|
698
|
+
arr = items if isinstance(items, list) else []
|
|
699
|
+
if not forward:
|
|
700
|
+
arr = list(reversed(arr))
|
|
701
|
+
env = dict(base_env) if base_env else {}
|
|
702
|
+
for item in arr:
|
|
703
|
+
env[param] = item
|
|
704
|
+
if _truthy(evaluate(pred, env)):
|
|
705
|
+
return item
|
|
706
|
+
return None
|
|
707
|
+
|
|
708
|
+
|
|
709
|
+
def find_index(
|
|
710
|
+
items: Any, pred: Any, param: str, forward: bool = True, base_env: dict | None = None
|
|
711
|
+
) -> int:
|
|
712
|
+
arr = items if isinstance(items, list) else []
|
|
713
|
+
env = dict(base_env) if base_env else {}
|
|
714
|
+
idxs = range(len(arr)) if forward else range(len(arr) - 1, -1, -1)
|
|
715
|
+
for i in idxs:
|
|
716
|
+
env[param] = arr[i]
|
|
717
|
+
if _truthy(evaluate(pred, env)):
|
|
718
|
+
return i
|
|
719
|
+
return -1
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
def flat_map(items: Any, proj: Any, param: str, base_env: dict | None = None) -> list:
|
|
723
|
+
arr = items if isinstance(items, list) else []
|
|
724
|
+
env = dict(base_env) if base_env else {}
|
|
725
|
+
out = []
|
|
726
|
+
for item in arr:
|
|
727
|
+
env[param] = item
|
|
728
|
+
v = evaluate(proj, env)
|
|
729
|
+
if isinstance(v, list):
|
|
730
|
+
out.extend(v)
|
|
731
|
+
else:
|
|
732
|
+
out.append(v)
|
|
733
|
+
return out
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def map_items(items: Any, proj: Any, param: str, base_env: dict | None = None) -> list:
|
|
737
|
+
"""Value-producing `.map(cb)` (#2073): project each element through
|
|
738
|
+
`proj`, one result per element (no flatten). Named `map_items` (not
|
|
739
|
+
`map`) so the Python builtin stays unshadowed -- mirrors the Perl port's
|
|
740
|
+
`map_items` naming (Perl's rename was to keep its own `map` builtin
|
|
741
|
+
unshadowed; Python has the identical concern for the `map` builtin)."""
|
|
742
|
+
arr = items if isinstance(items, list) else []
|
|
743
|
+
env = dict(base_env) if base_env else {}
|
|
744
|
+
out = []
|
|
745
|
+
for item in arr:
|
|
746
|
+
env[param] = item
|
|
747
|
+
out.append(evaluate(proj, env))
|
|
748
|
+
return out
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
# ---------------------------------------------------------------------------
|
|
752
|
+
# JSON-string seams -- the adapters emit `bf.filter_eval(recv, '<json>', ...)`;
|
|
753
|
+
# the predicate body arrives as a JSON string here, decoded then handed to
|
|
754
|
+
# the helper above (mirroring fold_json / sort_by_json).
|
|
755
|
+
# ---------------------------------------------------------------------------
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def filter_json(items: Any, pred_json: str, param: str, base_env: dict | None = None) -> list:
|
|
759
|
+
return filter(items, _json.loads(pred_json), param, base_env)
|
|
760
|
+
|
|
761
|
+
|
|
762
|
+
def every_json(items: Any, pred_json: str, param: str, base_env: dict | None = None) -> bool:
|
|
763
|
+
return every(items, _json.loads(pred_json), param, base_env)
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def some_json(items: Any, pred_json: str, param: str, base_env: dict | None = None) -> bool:
|
|
767
|
+
return some(items, _json.loads(pred_json), param, base_env)
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
def find_json(
|
|
771
|
+
items: Any, pred_json: str, param: str, forward: bool = True, base_env: dict | None = None
|
|
772
|
+
) -> Any:
|
|
773
|
+
return find(items, _json.loads(pred_json), param, forward, base_env)
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
def find_index_json(
|
|
777
|
+
items: Any, pred_json: str, param: str, forward: bool = True, base_env: dict | None = None
|
|
778
|
+
) -> int:
|
|
779
|
+
return find_index(items, _json.loads(pred_json), param, forward, base_env)
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def flat_map_json(items: Any, proj_json: str, param: str, base_env: dict | None = None) -> list:
|
|
783
|
+
return flat_map(items, _json.loads(proj_json), param, base_env)
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
def map_json(items: Any, proj_json: str, param: str, base_env: dict | None = None) -> list:
|
|
787
|
+
return map_items(items, _json.loads(proj_json), param, base_env)
|