@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,558 @@
|
|
|
1
|
+
//! Centralized value representation, i64/u64/f64 unification, and
|
|
2
|
+
//! JS-number semantics.
|
|
3
|
+
//!
|
|
4
|
+
//! Port of the numeric primitives scattered across
|
|
5
|
+
//! `packages/adapter-jinja/python/barefootjs/runtime.py` (`_format_js_number`,
|
|
6
|
+
//! `js_number`, `.mod`, `.floor`/`.ceil`/`.round`, `.to_fixed`) and
|
|
7
|
+
//! `packages/adapter-jinja/python/barefootjs/evaluator.py` (`_to_number`,
|
|
8
|
+
//! `_format_number`, `_strict_eq`, `_same_value_zero`, JS `/` and `%`
|
|
9
|
+
//! special-casing). Both `runtime.rs` and `evaluator.rs` route EVERY
|
|
10
|
+
//! number-shaped operation through here so there is exactly one place that
|
|
11
|
+
//! understands number unification and exactly one JS-`Number`-to-string
|
|
12
|
+
//! formatter.
|
|
13
|
+
//!
|
|
14
|
+
//! ## `JsValue`: why not bare `serde_json::Value`
|
|
15
|
+
//!
|
|
16
|
+
//! The design contract describes the evaluator as operating "over
|
|
17
|
+
//! `serde_json::Value`". That is true at the JSON boundary (ParsedExpr
|
|
18
|
+
//! trees parsed from JSON, `bf-render`'s payload vars, `encode_json`'s
|
|
19
|
+
//! output) but CANNOT be literal for values flowing *through* evaluation:
|
|
20
|
+
//! `serde_json::Value::Number` is structurally incapable of holding NaN or
|
|
21
|
+
//! +/-Infinity (`serde_json::Number::from_f64` returns `None` for
|
|
22
|
+
//! non-finite input, and `Value::from(f64::NAN)` silently becomes
|
|
23
|
+
//! `Value::Null` -- JSON has no non-finite numeric literal, by design).
|
|
24
|
+
//! Python's port has no such gap (`float('nan')` is a first-class Python
|
|
25
|
+
//! value that flows through dict/list/env storage unchanged), and the
|
|
26
|
+
//! golden vectors require the SAME fidelity: a division by zero, a failed
|
|
27
|
+
//! `Number("nan")` coercion, or `Infinity - Infinity` inside a `fold`/`sort`
|
|
28
|
+
//! body must survive as a real NaN/Infinity through env storage, array
|
|
29
|
+
//! construction, and comparison -- not collapse to `null` mid-expression.
|
|
30
|
+
//!
|
|
31
|
+
//! `JsValue` closes that gap: it is a `serde_json::Value`-shaped enum whose
|
|
32
|
+
//! `Number` variant is a bare `f64` (so it can hold NaN/Infinity natively),
|
|
33
|
+
//! used as the ONE internal working value type for both `evaluator.rs` and
|
|
34
|
+
//! `runtime.rs`. It converts losslessly to/from `serde_json::Value` on the
|
|
35
|
+
//! way IN (JSON can't spell non-finite literals, so there's nothing to
|
|
36
|
+
//! lose) and lossily (non-finite -> `null`, recursively) on the way OUT via
|
|
37
|
+
//! [`JsValue::to_json`] -- which is exactly `encode_json`'s documented
|
|
38
|
+
//! `_prepare_for_json` contract, so that conversion is implemented ONCE,
|
|
39
|
+
//! here, and reused by `backend_minijinja::encode_json`.
|
|
40
|
+
//!
|
|
41
|
+
//! This is flagged as a considered, necessary deviation from the design
|
|
42
|
+
//! doc's literal wording, not a scope expansion: no new module was added
|
|
43
|
+
//! (this type lives in `num.rs`, the file already chartered to own
|
|
44
|
+
//! "ALL i64/u64/f64 unification"), and every other file's shape is
|
|
45
|
+
//! unchanged.
|
|
46
|
+
//!
|
|
47
|
+
//! ## Divergences that VANISH in this backend
|
|
48
|
+
//!
|
|
49
|
+
//! Two Python-only workarounds disappear here because Rust's `f64` already
|
|
50
|
+
//! implements IEEE-754 semantics natively (Python's `float` does NOT:
|
|
51
|
+
//! `1.0 / 0.0` raises `ZeroDivisionError`, `math.fmod(x, 0)` raises
|
|
52
|
+
//! `ValueError`, `math.floor(nan)` raises `ValueError`):
|
|
53
|
+
//!
|
|
54
|
+
//! * JS `/` needs NO manual zero-divisor special-casing (unlike
|
|
55
|
+
//! `evaluator.py`'s `_binary` `"/"` arm) -- plain `l / r` on `f64`
|
|
56
|
+
//! already yields `+Infinity` / `-Infinity` / `NaN` exactly like JS.
|
|
57
|
+
//! * JS `%` needs NO manual zero-divisor special-casing (unlike
|
|
58
|
+
//! `runtime.py`'s `mod` / `evaluator.py`'s `_binary` `"%"` arm) --
|
|
59
|
+
//! Rust's `%` operator on `f64` IS C `fmod` (remainder with the
|
|
60
|
+
//! dividend's sign), and `x % 0.0` is already `NaN` under IEEE-754.
|
|
61
|
+
//! * `Math.floor` / `Math.ceil` / `Math.round` need NO NaN/Infinity guard
|
|
62
|
+
//! (unlike the Python port's `_is_nan`/`_is_inf` checks, needed only
|
|
63
|
+
//! because `math.floor(float('nan'))` raises in Python) -- `f64::floor`
|
|
64
|
+
//! / `f64::ceil` pass NaN/Infinity through unchanged per IEEE-754.
|
|
65
|
+
//!
|
|
66
|
+
//! See `tests/vector-divergences.json` for where this removes Python-only
|
|
67
|
+
//! divergence declarations (`add/beyond the safe-integer edge...`,
|
|
68
|
+
//! `div/zero divisor yields Infinity`) -- those two keys simply have no
|
|
69
|
+
//! entry in this backend's declaration file.
|
|
70
|
+
|
|
71
|
+
use serde_json::Value as JsonValue;
|
|
72
|
+
use std::collections::BTreeMap;
|
|
73
|
+
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
// The internal JS-value domain. See the module docstring for why this
|
|
76
|
+
// exists instead of using `serde_json::Value` directly for in-flight
|
|
77
|
+
// evaluation results.
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
81
|
+
pub enum JsValue {
|
|
82
|
+
Null,
|
|
83
|
+
Bool(bool),
|
|
84
|
+
/// Always `f64` -- JS has exactly one number type, and unlike
|
|
85
|
+
/// `serde_json::Value::Number` this CAN hold NaN / +Infinity /
|
|
86
|
+
/// -Infinity (see module docstring).
|
|
87
|
+
Number(f64),
|
|
88
|
+
String(String),
|
|
89
|
+
Array(Vec<JsValue>),
|
|
90
|
+
/// `BTreeMap` (not insertion-order) is deliberate: nothing in the
|
|
91
|
+
/// ParsedExpr subset or the runtime helper catalogue depends on JS
|
|
92
|
+
/// object key insertion order, and a sorted map gives `encode_json`
|
|
93
|
+
/// (`bf.json` / `bf-p` / `bf-scope`) its "canonical sorted keys"
|
|
94
|
+
/// contract for free, in one place, rather than re-sorting at the
|
|
95
|
+
/// JSON-encoding boundary.
|
|
96
|
+
Object(BTreeMap<String, JsValue>),
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
impl JsValue {
|
|
100
|
+
pub const fn null() -> JsValue {
|
|
101
|
+
JsValue::Null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
pub fn as_str(&self) -> Option<&str> {
|
|
105
|
+
match self {
|
|
106
|
+
JsValue::String(s) => Some(s.as_str()),
|
|
107
|
+
_ => None,
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
pub fn as_array(&self) -> Option<&[JsValue]> {
|
|
112
|
+
match self {
|
|
113
|
+
JsValue::Array(a) => Some(a.as_slice()),
|
|
114
|
+
_ => None,
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
pub fn as_object(&self) -> Option<&BTreeMap<String, JsValue>> {
|
|
119
|
+
match self {
|
|
120
|
+
JsValue::Object(o) => Some(o),
|
|
121
|
+
_ => None,
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
pub fn as_bool(&self) -> Option<bool> {
|
|
126
|
+
match self {
|
|
127
|
+
JsValue::Bool(b) => Some(*b),
|
|
128
|
+
_ => None,
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// Convert a decoded JSON document into the working value domain.
|
|
133
|
+
/// Lossless: JSON cannot spell a non-finite literal, so there is
|
|
134
|
+
/// nothing to collapse on the way in.
|
|
135
|
+
pub fn from_json(v: &JsonValue) -> JsValue {
|
|
136
|
+
match v {
|
|
137
|
+
JsonValue::Null => JsValue::Null,
|
|
138
|
+
JsonValue::Bool(b) => JsValue::Bool(*b),
|
|
139
|
+
JsonValue::Number(n) => JsValue::Number(n.as_f64().unwrap_or(f64::NAN)),
|
|
140
|
+
JsonValue::String(s) => JsValue::String(s.clone()),
|
|
141
|
+
JsonValue::Array(a) => JsValue::Array(a.iter().map(JsValue::from_json).collect()),
|
|
142
|
+
JsonValue::Object(o) => {
|
|
143
|
+
JsValue::Object(o.iter().map(|(k, v)| (k.clone(), JsValue::from_json(v))).collect())
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Convert to a JSON document. Non-finite numbers become `null`,
|
|
149
|
+
/// recursively, at ANY depth -- this IS `encode_json`'s
|
|
150
|
+
/// `_prepare_for_json` contract (see `backend_minijinja::encode_json`,
|
|
151
|
+
/// which calls this and then `serde_json::to_string`).
|
|
152
|
+
pub fn to_json(&self) -> JsonValue {
|
|
153
|
+
match self {
|
|
154
|
+
JsValue::Null => JsonValue::Null,
|
|
155
|
+
JsValue::Bool(b) => JsonValue::Bool(*b),
|
|
156
|
+
JsValue::Number(n) => number_to_json(*n),
|
|
157
|
+
JsValue::String(s) => JsonValue::String(s.clone()),
|
|
158
|
+
JsValue::Array(a) => JsonValue::Array(a.iter().map(JsValue::to_json).collect()),
|
|
159
|
+
JsValue::Object(o) => {
|
|
160
|
+
JsonValue::Object(o.iter().map(|(k, v)| (k.clone(), v.to_json())).collect())
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/// `f64` -> `serde_json::Value::Number`, matching JS `JSON.stringify`'s
|
|
167
|
+
/// integral-number spelling (`42`, never `42.0`) for the common case
|
|
168
|
+
/// (whole numbers within `i64`/`u64` range spell as a JSON integer;
|
|
169
|
+
/// everything else -- fractional, huge magnitude, or non-finite -- goes
|
|
170
|
+
/// through `Number::from_f64`, with non-finite collapsing to `null`).
|
|
171
|
+
/// `serde_json::Number::from_f64` alone is NOT sufficient here: it always
|
|
172
|
+
/// produces a JSON float token (`42.0`), which is JS-INCORRECT for a whole
|
|
173
|
+
/// number (`JSON.stringify(42) === "42"`, not `"42.0"`).
|
|
174
|
+
fn number_to_json(n: f64) -> JsonValue {
|
|
175
|
+
// `i64` range covers every practical prop/index/count value; `as i64`
|
|
176
|
+
// is exact here because `fract() == 0.0` already guarantees no
|
|
177
|
+
// fractional bits, and -0.0 casts to plain 0.
|
|
178
|
+
if n.is_finite() && n.fract() == 0.0 && n.abs() < 9_223_372_036_854_775_807.0 {
|
|
179
|
+
return JsonValue::Number(serde_json::Number::from(n as i64));
|
|
180
|
+
}
|
|
181
|
+
match serde_json::Number::from_f64(n) {
|
|
182
|
+
Some(num) => JsonValue::Number(num),
|
|
183
|
+
None => JsonValue::Null, // NaN / +-Infinity
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
impl From<f64> for JsValue {
|
|
188
|
+
fn from(n: f64) -> Self {
|
|
189
|
+
JsValue::Number(n)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
impl From<bool> for JsValue {
|
|
194
|
+
fn from(b: bool) -> Self {
|
|
195
|
+
JsValue::Bool(b)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
impl From<String> for JsValue {
|
|
200
|
+
fn from(s: String) -> Self {
|
|
201
|
+
JsValue::String(s)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
impl From<&str> for JsValue {
|
|
206
|
+
fn from(s: &str) -> Self {
|
|
207
|
+
JsValue::String(s.to_string())
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
impl From<Vec<JsValue>> for JsValue {
|
|
212
|
+
fn from(a: Vec<JsValue>) -> Self {
|
|
213
|
+
JsValue::Array(a)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Numeric-string grammar (mirrors Python's `looks_like_number` /
|
|
219
|
+
// `parse_number_literal`, itself a mirror of Perl's
|
|
220
|
+
// `Scalar::Util::looks_like_number`).
|
|
221
|
+
//
|
|
222
|
+
// Rather than hand-rolling the regex `^[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?$`
|
|
223
|
+
// plus `^[+-]?(inf(inity)?|nan)$` (no `regex` crate is available in this
|
|
224
|
+
// crate's dependency set -- see the design doc's fixed dependency list),
|
|
225
|
+
// this delegates to `f64::from_str`, whose documented grammar
|
|
226
|
+
// (`Sign? ('inf' | 'infinity' | 'nan' | Number)` where `Number` is exactly
|
|
227
|
+
// the digit/decimal-point/exponent shape above) is already isomorphic to
|
|
228
|
+
// the Python/Perl grammar. `looks_like_number_rust_grammar_matches_python`
|
|
229
|
+
// below pins the equivalence against representative cases (including the
|
|
230
|
+
// ones the golden vectors exercise) so any future libstd grammar drift is
|
|
231
|
+
// caught here rather than as a mysterious golden-vector failure.
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
/// Mirror of `evaluator.looks_like_number` / `Scalar::Util::looks_like_number`
|
|
235
|
+
/// for a (possibly padded) string.
|
|
236
|
+
pub fn looks_like_number(s: &str) -> bool {
|
|
237
|
+
let t = s.trim();
|
|
238
|
+
if t.is_empty() {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
t.parse::<f64>().is_ok()
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Parse a string already known to satisfy [`looks_like_number`] into an
|
|
245
|
+
/// `f64`. Mirrors `evaluator.parse_number_literal`.
|
|
246
|
+
pub fn parse_number_literal(s: &str) -> f64 {
|
|
247
|
+
s.trim().parse::<f64>().unwrap_or(f64::NAN)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
// JsValue <-> f64 / kind unification.
|
|
252
|
+
// ---------------------------------------------------------------------------
|
|
253
|
+
|
|
254
|
+
pub fn is_number(v: &JsValue) -> bool {
|
|
255
|
+
matches!(v, JsValue::Number(_))
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
pub fn is_string(v: &JsValue) -> bool {
|
|
259
|
+
matches!(v, JsValue::String(_))
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
pub fn is_bool(v: &JsValue) -> bool {
|
|
263
|
+
matches!(v, JsValue::Bool(_))
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
pub fn is_null(v: &JsValue) -> bool {
|
|
267
|
+
matches!(v, JsValue::Null)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// Extract the numeric value of a `JsValue::Number` as `f64`. Non-number
|
|
271
|
+
/// values yield `NaN`.
|
|
272
|
+
pub fn to_f64(v: &JsValue) -> f64 {
|
|
273
|
+
match v {
|
|
274
|
+
JsValue::Number(n) => *n,
|
|
275
|
+
_ => f64::NAN,
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ---------------------------------------------------------------------------
|
|
280
|
+
// JS Number::toString (ECMA-262 7.1.12.1), the "custom formatter" fallback
|
|
281
|
+
// and `js_string`'s number branch both route through this. Port of Python's
|
|
282
|
+
// `_format_js_number` / `_format_number`, made exponent-notation-correct
|
|
283
|
+
// (Rust's `f64::Display`/`ToString` NEVER emits exponential notation, no
|
|
284
|
+
// matter how large or small the magnitude -- the documented trap this
|
|
285
|
+
// module exists to close; Python's `repr(float)` fallback carries the same
|
|
286
|
+
// unresolved-divergence caveat for extreme magnitudes, which this port
|
|
287
|
+
// removes instead of carrying forward).
|
|
288
|
+
//
|
|
289
|
+
// Algorithm: obtain the shortest round-trip significant-digit string via
|
|
290
|
+
// `{:e}` (Rust's `LowerExp`, backed by the same shortest-round-trip
|
|
291
|
+
// decimal-conversion core as `Display`, just normalized to `d.ddddEe`
|
|
292
|
+
// form), then apply the ECMA-262 placement rules verbatim.
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
pub fn format_js_number(n: f64) -> String {
|
|
296
|
+
if n.is_nan() {
|
|
297
|
+
return "NaN".to_string();
|
|
298
|
+
}
|
|
299
|
+
if n.is_infinite() {
|
|
300
|
+
return if n > 0.0 { "Infinity".to_string() } else { "-Infinity".to_string() };
|
|
301
|
+
}
|
|
302
|
+
if n == 0.0 {
|
|
303
|
+
// Normalizes -0.0 to JS's "0" spelling (String(-0) === "0").
|
|
304
|
+
return "0".to_string();
|
|
305
|
+
}
|
|
306
|
+
if n < 0.0 {
|
|
307
|
+
return format!("-{}", format_js_number(-n));
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// n is finite, positive, nonzero. `digits` is the shortest round-trip
|
|
311
|
+
// significant-digit string (k digits, no trailing zeros, first digit
|
|
312
|
+
// nonzero); `n_exp` is the ECMA-262 exponent such that
|
|
313
|
+
// `n == (digits as integer) * 10^(n_exp - k)`.
|
|
314
|
+
let (digits, n_exp) = shortest_digits(n);
|
|
315
|
+
let k = digits.len() as i32;
|
|
316
|
+
|
|
317
|
+
if k <= n_exp && n_exp <= 21 {
|
|
318
|
+
// Integer-valued spelling, zero-padded on the right.
|
|
319
|
+
let mut s = digits;
|
|
320
|
+
s.push_str(&"0".repeat((n_exp - k) as usize));
|
|
321
|
+
s
|
|
322
|
+
} else if 0 < n_exp && n_exp <= 21 {
|
|
323
|
+
// Decimal point lands inside the digit string.
|
|
324
|
+
format!("{}.{}", &digits[..n_exp as usize], &digits[n_exp as usize..])
|
|
325
|
+
} else if -6 < n_exp && n_exp <= 0 {
|
|
326
|
+
// Leading "0." plus zero-padding before the digits.
|
|
327
|
+
format!("0.{}{}", "0".repeat((-n_exp) as usize), digits)
|
|
328
|
+
} else {
|
|
329
|
+
// Exponential notation: d(.ddd)?e[+-]N.
|
|
330
|
+
let exp = n_exp - 1;
|
|
331
|
+
let mantissa = if k == 1 {
|
|
332
|
+
digits
|
|
333
|
+
} else {
|
|
334
|
+
format!("{}.{}", &digits[..1], &digits[1..])
|
|
335
|
+
};
|
|
336
|
+
let sign = if exp >= 0 { "+" } else { "-" };
|
|
337
|
+
format!("{}e{}{}", mantissa, sign, exp.abs())
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/// Returns (shortest round-trip significant digits, ECMA-262 `n` exponent)
|
|
342
|
+
/// for a finite, positive, nonzero `f64`.
|
|
343
|
+
fn shortest_digits(n: f64) -> (String, i32) {
|
|
344
|
+
// Rust's `{:e}` always normalizes to exactly one digit before the
|
|
345
|
+
// decimal point ("d" or "d.ddd") using the shortest round-trip decimal
|
|
346
|
+
// expansion, e.g. `1e2`, `1.23456e2`, `3.0000000000000004e-1`.
|
|
347
|
+
let s = format!("{:e}", n);
|
|
348
|
+
let (mantissa, exp_str) = s.split_once('e').expect("LowerExp always emits 'e'");
|
|
349
|
+
let exp: i32 = exp_str.parse().expect("LowerExp exponent is a plain integer");
|
|
350
|
+
let digits: String = mantissa.chars().filter(|c| *c != '.').collect();
|
|
351
|
+
// value == d.ddd * 10^exp == (digits as integer) * 10^(exp - (k - 1))
|
|
352
|
+
// ECMA form: value == (digits as integer) * 10^(n_exp - k)
|
|
353
|
+
// => n_exp - k == exp - (k - 1) => n_exp == exp + 1
|
|
354
|
+
(digits, exp + 1)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ---------------------------------------------------------------------------
|
|
358
|
+
// Math.floor / Math.ceil / Math.round -- no NaN/Infinity guard needed (see
|
|
359
|
+
// module docstring): `f64::floor`/`f64::ceil` already pass them through.
|
|
360
|
+
// `Math.round` rounds half toward +Infinity (`Math.round(-1.5) === -1`, NOT
|
|
361
|
+
// -2), matching `runtime.py`'s `round` / `evaluator.py`'s `_math_round`.
|
|
362
|
+
// ---------------------------------------------------------------------------
|
|
363
|
+
|
|
364
|
+
pub fn js_floor(n: f64) -> f64 {
|
|
365
|
+
n.floor()
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
pub fn js_ceil(n: f64) -> f64 {
|
|
369
|
+
n.ceil()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
pub fn js_round(n: f64) -> f64 {
|
|
373
|
+
(n + 0.5).floor()
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/// JS `%`: remainder with the dividend's sign. Rust's `%` on `f64` already
|
|
377
|
+
/// implements C `fmod` semantics (IEEE-754 remainder), so this is a
|
|
378
|
+
/// documentation-only wrapper -- see the module docstring for why no
|
|
379
|
+
/// zero-divisor special-casing (needed in the Python port) is needed here.
|
|
380
|
+
pub fn js_mod(a: f64, b: f64) -> f64 {
|
|
381
|
+
a % b
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/// JS `Number.prototype.toFixed(digits)`. Mirrors `runtime.py.to_fixed` /
|
|
385
|
+
/// `bf.go`'s `ToFixed`: JS rounds the scaled value half toward +Infinity
|
|
386
|
+
/// (`(2.5).toFixed(0) === "3"`), not Rust's `format!`'s round-half-to-even,
|
|
387
|
+
/// so the value is pre-rounded via [`js_round`]-equivalent scaling before
|
|
388
|
+
/// formatting.
|
|
389
|
+
pub fn to_fixed(n: f64, digits: i32) -> String {
|
|
390
|
+
if n.is_nan() {
|
|
391
|
+
return "NaN".to_string();
|
|
392
|
+
}
|
|
393
|
+
if n.is_infinite() {
|
|
394
|
+
return if n < 0.0 { "-Infinity".to_string() } else { "Infinity".to_string() };
|
|
395
|
+
}
|
|
396
|
+
let digits = digits.max(0);
|
|
397
|
+
let factor = 10f64.powi(digits);
|
|
398
|
+
let rounded = (n * factor + 0.5).floor();
|
|
399
|
+
format!("{:.*}", digits as usize, rounded / factor)
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ---------------------------------------------------------------------------
|
|
403
|
+
// JS strict equality (`===`) and SameValueZero (`Array.prototype.includes`
|
|
404
|
+
// membership), kind-aware (a JS number never `===` a JS string, even when
|
|
405
|
+
// they "look like" the same value: `2 === "2"` is false). Port of
|
|
406
|
+
// `evaluator._strict_eq` / `evaluator._same_value_zero`; ALSO used by
|
|
407
|
+
// `runtime.rs`'s `index_of` / `last_index_of` (in place of the Python
|
|
408
|
+
// port's native `==`, which the Python module docstring documents as
|
|
409
|
+
// "mostly" JS-strict-equality-equivalent but with an acknowledged,
|
|
410
|
+
// unexercised gap: Python `bool` is an `int` subclass, so `True == 1`,
|
|
411
|
+
// which JS `true === 1` is not). Routing `index_of`/`last_index_of` through
|
|
412
|
+
// this kind-aware `strict_eq` closes that gap entirely rather than
|
|
413
|
+
// reproducing it -- documented in `runtime.rs`.
|
|
414
|
+
// ---------------------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
pub fn strict_eq(l: &JsValue, r: &JsValue) -> bool {
|
|
417
|
+
let (ln, rn) = (is_number(l), is_number(r));
|
|
418
|
+
if ln && rn {
|
|
419
|
+
let (lf, rf) = (to_f64(l), to_f64(r));
|
|
420
|
+
if lf.is_nan() || rf.is_nan() {
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
return lf == rf;
|
|
424
|
+
}
|
|
425
|
+
if ln != rn {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
match (l, r) {
|
|
429
|
+
(JsValue::Null, JsValue::Null) => true,
|
|
430
|
+
(JsValue::Null, _) | (_, JsValue::Null) => false,
|
|
431
|
+
(JsValue::Bool(a), JsValue::Bool(b)) => a == b,
|
|
432
|
+
(JsValue::Bool(_), _) | (_, JsValue::Bool(_)) => false,
|
|
433
|
+
(JsValue::String(a), JsValue::String(b)) => a == b,
|
|
434
|
+
_ => false,
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/// SameValueZero: `===` except `NaN` equals itself (and +0/-0 are not
|
|
439
|
+
/// distinguished, which this domain doesn't track separately either).
|
|
440
|
+
pub fn same_value_zero(l: &JsValue, r: &JsValue) -> bool {
|
|
441
|
+
if is_number(l) && is_number(r) {
|
|
442
|
+
let (lf, rf) = (to_f64(l), to_f64(r));
|
|
443
|
+
if lf.is_nan() && rf.is_nan() {
|
|
444
|
+
return true;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
strict_eq(l, r)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
#[cfg(test)]
|
|
451
|
+
mod tests {
|
|
452
|
+
use super::*;
|
|
453
|
+
|
|
454
|
+
#[test]
|
|
455
|
+
#[allow(clippy::approx_constant)] // literal test data ported from Python, not an approximation of pi
|
|
456
|
+
fn format_js_number_matches_ecma262() {
|
|
457
|
+
assert_eq!(format_js_number(0.0), "0");
|
|
458
|
+
assert_eq!(format_js_number(-0.0), "0");
|
|
459
|
+
assert_eq!(format_js_number(f64::NAN), "NaN");
|
|
460
|
+
assert_eq!(format_js_number(f64::INFINITY), "Infinity");
|
|
461
|
+
assert_eq!(format_js_number(f64::NEG_INFINITY), "-Infinity");
|
|
462
|
+
assert_eq!(format_js_number(42.0), "42");
|
|
463
|
+
assert_eq!(format_js_number(-7.0), "-7");
|
|
464
|
+
assert_eq!(format_js_number(3.14), "3.14");
|
|
465
|
+
assert_eq!(format_js_number(0.5), "0.5");
|
|
466
|
+
assert_eq!(format_js_number(1.0), "1");
|
|
467
|
+
assert_eq!(format_js_number(0.30000000000000004), "0.30000000000000004");
|
|
468
|
+
assert_eq!(format_js_number(9007199254740992.0), "9007199254740992");
|
|
469
|
+
assert_eq!(format_js_number(1e20), "100000000000000000000");
|
|
470
|
+
assert_eq!(format_js_number(1e21), "1e+21");
|
|
471
|
+
assert_eq!(format_js_number(123456789012345680000.0), "123456789012345680000");
|
|
472
|
+
assert_eq!(format_js_number(1.5e21), "1.5e+21");
|
|
473
|
+
assert_eq!(format_js_number(0.0001), "0.0001");
|
|
474
|
+
assert_eq!(format_js_number(0.000001), "0.000001");
|
|
475
|
+
assert_eq!(format_js_number(0.0000001), "1e-7");
|
|
476
|
+
assert_eq!(format_js_number(-0.0000001), "-1e-7");
|
|
477
|
+
assert_eq!(format_js_number(100.0), "100");
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
#[test]
|
|
481
|
+
fn looks_like_number_rust_grammar_matches_python() {
|
|
482
|
+
for s in ["42", "3.14", "-0.5", "1e3", "1E3", "+5", ".5", "5.", " 8 ", "NaN", "nan", "Infinity", "-inf"] {
|
|
483
|
+
assert!(looks_like_number(s), "expected {s:?} to look like a number");
|
|
484
|
+
}
|
|
485
|
+
for s in ["", " ", "not a num", "1,000", "5e", "abc", "1_000", "0x1"] {
|
|
486
|
+
assert!(!looks_like_number(s), "expected {s:?} to NOT look like a number");
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
#[test]
|
|
491
|
+
fn parse_number_literal_trims_and_parses() {
|
|
492
|
+
assert_eq!(parse_number_literal(" 8 "), 8.0);
|
|
493
|
+
assert_eq!(parse_number_literal("1e3"), 1000.0);
|
|
494
|
+
assert_eq!(parse_number_literal("-0.5"), -0.5);
|
|
495
|
+
assert!(parse_number_literal("nan").is_nan());
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
#[test]
|
|
499
|
+
fn js_round_half_toward_positive_infinity() {
|
|
500
|
+
assert_eq!(js_round(3.5), 4.0);
|
|
501
|
+
assert_eq!(js_round(3.4), 3.0);
|
|
502
|
+
assert_eq!(js_round(-1.5), -1.0);
|
|
503
|
+
assert_eq!(js_round(-1.6), -2.0);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
#[test]
|
|
507
|
+
fn js_mod_matches_js_percent() {
|
|
508
|
+
assert_eq!(js_mod(5.0, 3.0), 2.0);
|
|
509
|
+
assert_eq!(js_mod(-5.0, 3.0), -2.0);
|
|
510
|
+
assert!(js_mod(5.0, 0.0).is_nan());
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
#[test]
|
|
514
|
+
#[allow(clippy::approx_constant)] // literal test data ported from Python, not an approximation of pi
|
|
515
|
+
fn to_fixed_rounds_half_up_and_handles_nonfinite() {
|
|
516
|
+
assert_eq!(to_fixed(316.0, 2), "316.00");
|
|
517
|
+
assert_eq!(to_fixed(3.14159, 2), "3.14");
|
|
518
|
+
assert_eq!(to_fixed(2.5, 0), "3");
|
|
519
|
+
assert_eq!(to_fixed(1.005, 2), "1.00");
|
|
520
|
+
assert_eq!(to_fixed(f64::NAN, 2), "NaN");
|
|
521
|
+
assert_eq!(to_fixed(f64::INFINITY, 2), "Infinity");
|
|
522
|
+
assert_eq!(to_fixed(f64::NEG_INFINITY, 2), "-Infinity");
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
#[test]
|
|
526
|
+
fn strict_eq_is_kind_aware() {
|
|
527
|
+
assert!(strict_eq(&JsValue::Number(2.0), &JsValue::Number(2.0)));
|
|
528
|
+
assert!(!strict_eq(&JsValue::Number(2.0), &JsValue::String("2".into())));
|
|
529
|
+
assert!(!strict_eq(&JsValue::Bool(true), &JsValue::Number(1.0)));
|
|
530
|
+
assert!(!strict_eq(&JsValue::Number(f64::NAN), &JsValue::Number(f64::NAN)));
|
|
531
|
+
assert!(same_value_zero(&JsValue::Number(f64::NAN), &JsValue::Number(f64::NAN)));
|
|
532
|
+
assert!(!same_value_zero(&JsValue::Number(2.0), &JsValue::String("2".into())));
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
#[test]
|
|
536
|
+
fn json_round_trip_collapses_non_finite_on_the_way_out_only() {
|
|
537
|
+
let inf = JsValue::Number(f64::INFINITY);
|
|
538
|
+
// Non-finite survives WITHIN the working domain...
|
|
539
|
+
assert!(matches!(inf, JsValue::Number(n) if n.is_infinite()));
|
|
540
|
+
// ...and only collapses to JSON null at the to_json boundary.
|
|
541
|
+
assert_eq!(inf.to_json(), JsonValue::Null);
|
|
542
|
+
|
|
543
|
+
let nested = JsValue::Array(vec![JsValue::Number(f64::NAN), JsValue::Number(1.0)]);
|
|
544
|
+
// `1.0` -> JSON `1`, not `1.0` (matches `JSON.stringify`'s
|
|
545
|
+
// integral-number spelling -- see `number_to_json`).
|
|
546
|
+
assert_eq!(nested.to_json(), serde_json::json!([null, 1]));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
#[test]
|
|
550
|
+
fn number_to_json_matches_js_integral_spelling() {
|
|
551
|
+
assert_eq!(serde_json::to_string(&JsValue::Number(42.0).to_json()).unwrap(), "42");
|
|
552
|
+
assert_eq!(serde_json::to_string(&JsValue::Number(-7.0).to_json()).unwrap(), "-7");
|
|
553
|
+
assert_eq!(serde_json::to_string(&JsValue::Number(0.0).to_json()).unwrap(), "0");
|
|
554
|
+
assert_eq!(serde_json::to_string(&JsValue::Number(-0.0).to_json()).unwrap(), "0");
|
|
555
|
+
assert_eq!(serde_json::to_string(&JsValue::Number(1.5).to_json()).unwrap(), "1.5");
|
|
556
|
+
assert_eq!(serde_json::to_string(&JsValue::Number(0.19999999999999998).to_json()).unwrap(), "0.19999999999999998");
|
|
557
|
+
}
|
|
558
|
+
}
|