svelte-shaker 0.15.1 → 0.15.3
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/dist/analyze.js +39 -9
- package/dist/eval.d.ts +30 -0
- package/dist/eval.js +55 -3
- package/dist/ir.d.ts +14 -0
- package/dist/ir.js +16 -0
- package/dist/mono.d.ts +1 -1
- package/dist/mono.js +3 -0
- package/dist/parse.d.ts +7 -0
- package/dist/svelte_shaker_engine_bg.wasm +0 -0
- package/dist/transform.js +21 -0
- package/package.json +1 -1
package/dist/analyze.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { parseCached, parseModuleProgram, walk, } from './parse.js';
|
|
2
|
-
import { emptyPlan, } from './ir.js';
|
|
2
|
+
import { emptyPlan, isFoldableValue, } from './ir.js';
|
|
3
3
|
import { computeDeadSpans, inSpans } from './dead.js';
|
|
4
|
-
import { evaluate, setVar } from './eval.js';
|
|
4
|
+
import { evaluate, literalValue, setVar, unwrapTsAssertions } from './eval.js';
|
|
5
5
|
const isSvelte = (source) => source.endsWith('.svelte');
|
|
6
6
|
/** Floor for the fixpoint iteration bound (see {@link fixpointIterationBound}). */
|
|
7
7
|
const MIN_FIXPOINT_ITERATIONS = 10;
|
|
@@ -856,6 +856,11 @@ function computeScriptConstEnv(ast, instance, moduleScript, propsDeclaration, wr
|
|
|
856
856
|
* rune call — is evaluated verbatim, so a non-value rune simply falls to unknown.
|
|
857
857
|
*/
|
|
858
858
|
function evalDeclaratorValue(init, env) {
|
|
859
|
+
// `const x = $state(0) as T` wraps the rune in a `TSAsExpression`; erase the
|
|
860
|
+
// (runtime-transparent) assertion first so the `$state`/`$state.raw` unwrap
|
|
861
|
+
// below and `evaluate` both see the real initializer, same as a parser that
|
|
862
|
+
// strips the assertion would.
|
|
863
|
+
init = unwrapTsAssertions(init) ?? undefined;
|
|
859
864
|
if (isStateRuneCall(init)) {
|
|
860
865
|
const arg = init?.arguments?.[0];
|
|
861
866
|
if (arg == null)
|
|
@@ -1081,9 +1086,13 @@ function collectTemplateBindings(ast, instance, propsDeclaration) {
|
|
|
1081
1086
|
function collectWrittenNames(node, out) {
|
|
1082
1087
|
if (node.type === 'AssignmentExpression')
|
|
1083
1088
|
addPatternNames(node.left, out);
|
|
1084
|
-
else if (node.type === 'UpdateExpression'
|
|
1085
|
-
|
|
1086
|
-
|
|
1089
|
+
else if (node.type === 'UpdateExpression') {
|
|
1090
|
+
// `x!++` keeps the `!` as a `TSNonNullExpression` around the target (only a
|
|
1091
|
+
// bare `x = …` LHS is normalized to the identifier), so read through it or the
|
|
1092
|
+
// write goes uncounted and the name is wrongly admitted as a constant.
|
|
1093
|
+
const arg = unwrapTsAssertions(node.argument);
|
|
1094
|
+
if (arg?.type === 'Identifier' && arg.name)
|
|
1095
|
+
out.add(arg.name);
|
|
1087
1096
|
}
|
|
1088
1097
|
}
|
|
1089
1098
|
/**
|
|
@@ -1091,6 +1100,11 @@ function collectWrittenNames(node, out) {
|
|
|
1091
1100
|
* Handles bare identifiers, object/array destructuring, defaults and rest.
|
|
1092
1101
|
*/
|
|
1093
1102
|
function addPatternNames(pattern, out) {
|
|
1103
|
+
// A non-null-asserted assignment target keeps its `TSNonNullExpression` wrapper
|
|
1104
|
+
// in every position but a bare `x = …` LHS — `x! += 1`, `[x!] = a`, `({k: x!} =
|
|
1105
|
+
// o)` — so peel it here (the one choke point every pattern position recurses
|
|
1106
|
+
// through) to count the write against the bare name.
|
|
1107
|
+
pattern = unwrapTsAssertions(pattern) ?? undefined;
|
|
1094
1108
|
if (!pattern)
|
|
1095
1109
|
return;
|
|
1096
1110
|
switch (pattern.type) {
|
|
@@ -1467,8 +1481,14 @@ function literalAttrValue(value) {
|
|
|
1467
1481
|
const part = parts[0];
|
|
1468
1482
|
if (part.type === 'Text')
|
|
1469
1483
|
return { known: true, value: (part.data ?? part.raw ?? '') };
|
|
1470
|
-
if (part.type === 'ExpressionTag'
|
|
1471
|
-
|
|
1484
|
+
if (part.type === 'ExpressionTag') {
|
|
1485
|
+
// Recognize `prop={'x' as const}` as the literal it wraps, so the write is
|
|
1486
|
+
// classified NON-dynamic identically to a parser that strips the assertion.
|
|
1487
|
+
// Both paths already fold the value (via the expr fallback), but the
|
|
1488
|
+
// `dynamic` flag itself must match — mono's `specializableShape` reads it.
|
|
1489
|
+
const expr = unwrapTsAssertions(part.expression);
|
|
1490
|
+
if (expr?.type === 'Literal')
|
|
1491
|
+
return literalValue(expr);
|
|
1472
1492
|
}
|
|
1473
1493
|
return { known: false };
|
|
1474
1494
|
}
|
|
@@ -1529,7 +1549,9 @@ function buildPlan(model, u, ownerEnv) {
|
|
|
1529
1549
|
continue;
|
|
1530
1550
|
// constant fold: a clean singleton value set is the foldable case.
|
|
1531
1551
|
if (set.values.length === 1) {
|
|
1532
|
-
|
|
1552
|
+
const only = set.values[0];
|
|
1553
|
+
if (isFoldableValue(only))
|
|
1554
|
+
plan.constFold.set(decl.name, only);
|
|
1533
1555
|
continue;
|
|
1534
1556
|
}
|
|
1535
1557
|
// value-set narrowing: >= 2 distinct literals with no dynamic/⊤ contribution is a fully
|
|
@@ -1608,10 +1630,18 @@ function valueSetFor(decl, sites, ownerEnv) {
|
|
|
1608
1630
|
return { values, dynamic, top };
|
|
1609
1631
|
}
|
|
1610
1632
|
function literalDefault(expr) {
|
|
1633
|
+
// A default like `= 500 as const` reaches here as a `TSAsExpression`; the
|
|
1634
|
+
// assertion erases at runtime, so read through it to the bare default value.
|
|
1635
|
+
expr = unwrapTsAssertions(expr) ?? undefined;
|
|
1611
1636
|
if (!expr)
|
|
1612
1637
|
return { known: true, value: undefined }; // omitted default -> undefined
|
|
1638
|
+
// `literalValue` is the same gate `literalAttrValue` and `evaluate` use: a
|
|
1639
|
+
// BigInt/RegExp default (no faithful source form) or a `null` that is really
|
|
1640
|
+
// `1e999` surviving JSON transport must stay UNKNOWN here too, or a call site
|
|
1641
|
+
// that merely omits the prop reintroduces the crash/misfold this default path
|
|
1642
|
+
// exists to prevent.
|
|
1613
1643
|
if (expr.type === 'Literal')
|
|
1614
|
-
return
|
|
1644
|
+
return literalValue(expr);
|
|
1615
1645
|
if (expr.type === 'Identifier' && expr.name === 'undefined')
|
|
1616
1646
|
return { known: true, value: undefined };
|
|
1617
1647
|
return { known: false };
|
package/dist/eval.d.ts
CHANGED
|
@@ -6,6 +6,21 @@ export type EvalResult = {
|
|
|
6
6
|
} | {
|
|
7
7
|
known: false;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* Strip TypeScript assertion wrappers — `x as T` (`TSAsExpression`), `x!`
|
|
11
|
+
* (`TSNonNullExpression`), `x satisfies T` (`TSSatisfiesExpression`) — down to
|
|
12
|
+
* the runtime expression they wrap, recursing so `('a' as const)!` becomes `'a'`.
|
|
13
|
+
*
|
|
14
|
+
* These are compile-time-only type operators: they are erased before any code
|
|
15
|
+
* runs and do not touch the operand's value, so interpreting the operand IS
|
|
16
|
+
* interpreting the whole expression (docs/ARCHITECTURE.md §2.2 — the value the
|
|
17
|
+
* expression abstracts to is exactly the operand's). The engine runs on the
|
|
18
|
+
* `lang="ts"` source with types preserved (§6.3), so svelte/compiler hands it
|
|
19
|
+
* these nodes verbatim; a parser that erases them (rsvelte today) yields the bare
|
|
20
|
+
* operand instead. Unwrapping here folds both ASTs identically — the
|
|
21
|
+
* parser-neutrality contract every value-interpreting entry point relies on.
|
|
22
|
+
*/
|
|
23
|
+
export declare function unwrapTsAssertions(node: AnyNode | null | undefined): AnyNode | null | undefined;
|
|
9
24
|
/**
|
|
10
25
|
* A deliberately tiny, total constant evaluator over an ESTree expression,
|
|
11
26
|
* given an environment of statically-known identifiers. It never throws and
|
|
@@ -16,6 +31,21 @@ export type EvalResult = {
|
|
|
16
31
|
* unknown on non-distributive ops), just without the interprocedural lattice.
|
|
17
32
|
*/
|
|
18
33
|
export declare function evaluate(node: AnyNode | null | undefined, env: ReadonlyMap<string, Literal>): EvalResult;
|
|
34
|
+
/**
|
|
35
|
+
* The `Literal` value of an ESTree `Literal` node, or unknown when the node
|
|
36
|
+
* carries a value outside that union. This is the ONLY door through which a
|
|
37
|
+
* parsed value enters the engine, so it is where the union is enforced:
|
|
38
|
+
*
|
|
39
|
+
* - **RegExp / BigInt literals** (`/x/g`, `1n`) hold a `RegExp` / `bigint` in
|
|
40
|
+
* `value` — no faithful literal source form, and `JSON.stringify` throws on
|
|
41
|
+
* the latter and flattens the former to `{}`. Both carry a discriminator.
|
|
42
|
+
* - **`value: null`** is ambiguous: an AST that reached us as JSON (the rsvelte
|
|
43
|
+
* parser crosses a WASM boundary that way) cannot represent `Infinity`, so
|
|
44
|
+
* `1e999` arrives indistinguishable from a genuine `null` except by `raw`.
|
|
45
|
+
*
|
|
46
|
+
* Anything unproven stays unknown, which costs one fold — never a wrong one.
|
|
47
|
+
*/
|
|
48
|
+
export declare function literalValue(node: AnyNode): EvalResult;
|
|
19
49
|
/**
|
|
20
50
|
* Sound set-aware predicate. `constEnv` holds props collapsed to a single
|
|
21
51
|
* literal (`constFold`); `setEnv` holds props whose reachable value set is known
|
package/dist/eval.js
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
const UNKNOWN = { known: false };
|
|
2
|
+
/**
|
|
3
|
+
* Strip TypeScript assertion wrappers — `x as T` (`TSAsExpression`), `x!`
|
|
4
|
+
* (`TSNonNullExpression`), `x satisfies T` (`TSSatisfiesExpression`) — down to
|
|
5
|
+
* the runtime expression they wrap, recursing so `('a' as const)!` becomes `'a'`.
|
|
6
|
+
*
|
|
7
|
+
* These are compile-time-only type operators: they are erased before any code
|
|
8
|
+
* runs and do not touch the operand's value, so interpreting the operand IS
|
|
9
|
+
* interpreting the whole expression (docs/ARCHITECTURE.md §2.2 — the value the
|
|
10
|
+
* expression abstracts to is exactly the operand's). The engine runs on the
|
|
11
|
+
* `lang="ts"` source with types preserved (§6.3), so svelte/compiler hands it
|
|
12
|
+
* these nodes verbatim; a parser that erases them (rsvelte today) yields the bare
|
|
13
|
+
* operand instead. Unwrapping here folds both ASTs identically — the
|
|
14
|
+
* parser-neutrality contract every value-interpreting entry point relies on.
|
|
15
|
+
*/
|
|
16
|
+
export function unwrapTsAssertions(node) {
|
|
17
|
+
let current = node;
|
|
18
|
+
while (current?.type === 'TSAsExpression' ||
|
|
19
|
+
current?.type === 'TSNonNullExpression' ||
|
|
20
|
+
current?.type === 'TSSatisfiesExpression') {
|
|
21
|
+
current = current.expression;
|
|
22
|
+
}
|
|
23
|
+
return current;
|
|
24
|
+
}
|
|
2
25
|
/**
|
|
3
26
|
* A deliberately tiny, total constant evaluator over an ESTree expression,
|
|
4
27
|
* given an environment of statically-known identifiers. It never throws and
|
|
@@ -9,11 +32,12 @@ const UNKNOWN = { known: false };
|
|
|
9
32
|
* unknown on non-distributive ops), just without the interprocedural lattice.
|
|
10
33
|
*/
|
|
11
34
|
export function evaluate(node, env) {
|
|
35
|
+
node = unwrapTsAssertions(node);
|
|
12
36
|
if (!node)
|
|
13
37
|
return UNKNOWN;
|
|
14
38
|
switch (node.type) {
|
|
15
39
|
case 'Literal':
|
|
16
|
-
return
|
|
40
|
+
return literalValue(node);
|
|
17
41
|
case 'Identifier': {
|
|
18
42
|
const name = node.name ?? '';
|
|
19
43
|
if (name === 'undefined')
|
|
@@ -108,6 +132,30 @@ export function evaluate(node, env) {
|
|
|
108
132
|
return UNKNOWN;
|
|
109
133
|
}
|
|
110
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* The `Literal` value of an ESTree `Literal` node, or unknown when the node
|
|
137
|
+
* carries a value outside that union. This is the ONLY door through which a
|
|
138
|
+
* parsed value enters the engine, so it is where the union is enforced:
|
|
139
|
+
*
|
|
140
|
+
* - **RegExp / BigInt literals** (`/x/g`, `1n`) hold a `RegExp` / `bigint` in
|
|
141
|
+
* `value` — no faithful literal source form, and `JSON.stringify` throws on
|
|
142
|
+
* the latter and flattens the former to `{}`. Both carry a discriminator.
|
|
143
|
+
* - **`value: null`** is ambiguous: an AST that reached us as JSON (the rsvelte
|
|
144
|
+
* parser crosses a WASM boundary that way) cannot represent `Infinity`, so
|
|
145
|
+
* `1e999` arrives indistinguishable from a genuine `null` except by `raw`.
|
|
146
|
+
*
|
|
147
|
+
* Anything unproven stays unknown, which costs one fold — never a wrong one.
|
|
148
|
+
*/
|
|
149
|
+
export function literalValue(node) {
|
|
150
|
+
if (node.regex !== undefined || node.bigint !== undefined)
|
|
151
|
+
return UNKNOWN;
|
|
152
|
+
const v = node.value;
|
|
153
|
+
if (v === null)
|
|
154
|
+
return node.raw === 'null' ? { known: true, value: null } : UNKNOWN;
|
|
155
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean')
|
|
156
|
+
return { known: true, value: v };
|
|
157
|
+
return UNKNOWN;
|
|
158
|
+
}
|
|
111
159
|
/**
|
|
112
160
|
* Sound set-aware predicate. `constEnv` holds props collapsed to a single
|
|
113
161
|
* literal (`constFold`); `setEnv` holds props whose reachable value set is known
|
|
@@ -126,6 +174,7 @@ export function evaluateWithSets(node, constEnv, setEnv) {
|
|
|
126
174
|
}
|
|
127
175
|
/** Evaluate a boolean condition to a Kleene truth over the value sets. */
|
|
128
176
|
function evalTri(node, constEnv, setEnv) {
|
|
177
|
+
node = unwrapTsAssertions(node);
|
|
129
178
|
if (!node)
|
|
130
179
|
return 'unknown';
|
|
131
180
|
switch (node.type) {
|
|
@@ -193,8 +242,11 @@ function equalityTri(left, right, constEnv, setEnv, loose) {
|
|
|
193
242
|
* set pass-through (analyze.ts) all decide it through this, so they stay in lockstep.
|
|
194
243
|
*/
|
|
195
244
|
export function setVar(node, setEnv) {
|
|
196
|
-
|
|
197
|
-
|
|
245
|
+
// A `variant as const` / `variant!` reference is still a bare read of `variant`
|
|
246
|
+
// (the assertion erases at runtime), so it narrows like the identifier it wraps.
|
|
247
|
+
const bare = unwrapTsAssertions(node);
|
|
248
|
+
if (bare?.type === 'Identifier' && bare.name && setEnv.has(bare.name))
|
|
249
|
+
return setEnv.get(bare.name);
|
|
198
250
|
return null;
|
|
199
251
|
}
|
|
200
252
|
/**
|
package/dist/ir.d.ts
CHANGED
|
@@ -131,4 +131,18 @@ export interface ComponentPlan {
|
|
|
131
131
|
*/
|
|
132
132
|
valueSets: Map<string, PropValueSet>;
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Whether a proven value may be SUBSTITUTED into the residual as a constant.
|
|
136
|
+
*
|
|
137
|
+
* Every member of {@link Literal} has a faithful source form except `-0`: the
|
|
138
|
+
* Svelte compiler constant-folds the expression it is spliced into and loses the
|
|
139
|
+
* sign of zero there (`{1 / n}` renders `-Infinity` with `n = -0` at runtime,
|
|
140
|
+
* but the folded `{1 / (-0)}` compiles to a literal `Infinity`). Emitting it
|
|
141
|
+
* would therefore change what renders even though the value we proved is right,
|
|
142
|
+
* so a `-0`-valued prop is simply left alone — one missed fold, no risk.
|
|
143
|
+
*
|
|
144
|
+
* Narrowing is unaffected: a narrowed prop stays dynamic and is never
|
|
145
|
+
* substituted, and the comparisons it feeds use real JS semantics.
|
|
146
|
+
*/
|
|
147
|
+
export declare function isFoldableValue(value: Literal): boolean;
|
|
134
148
|
export declare function emptyPlan(id: ComponentId): ComponentPlan;
|
package/dist/ir.js
CHANGED
|
@@ -3,6 +3,22 @@
|
|
|
3
3
|
// See docs/ARCHITECTURE.md §5.1. This is the M0 (walking-skeleton) subset:
|
|
4
4
|
// only the pieces basic1 exercises, but shaped so later levels slot in.
|
|
5
5
|
// ----------------------------------------------------------------------
|
|
6
|
+
/**
|
|
7
|
+
* Whether a proven value may be SUBSTITUTED into the residual as a constant.
|
|
8
|
+
*
|
|
9
|
+
* Every member of {@link Literal} has a faithful source form except `-0`: the
|
|
10
|
+
* Svelte compiler constant-folds the expression it is spliced into and loses the
|
|
11
|
+
* sign of zero there (`{1 / n}` renders `-Infinity` with `n = -0` at runtime,
|
|
12
|
+
* but the folded `{1 / (-0)}` compiles to a literal `Infinity`). Emitting it
|
|
13
|
+
* would therefore change what renders even though the value we proved is right,
|
|
14
|
+
* so a `-0`-valued prop is simply left alone — one missed fold, no risk.
|
|
15
|
+
*
|
|
16
|
+
* Narrowing is unaffected: a narrowed prop stays dynamic and is never
|
|
17
|
+
* substituted, and the comparisons it feeds use real JS semantics.
|
|
18
|
+
*/
|
|
19
|
+
export function isFoldableValue(value) {
|
|
20
|
+
return !Object.is(value, -0);
|
|
21
|
+
}
|
|
6
22
|
export function emptyPlan(id) {
|
|
7
23
|
return {
|
|
8
24
|
id,
|
package/dist/mono.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type FileModel } from './analyze.js';
|
|
2
2
|
import { type AnyNode } from './parse.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type ComponentId, type ComponentPlan, type Literal } from './ir.js';
|
|
4
4
|
/** Tuning knobs for monomorphization (docs §8.1, §13.2). All have sound defaults. */
|
|
5
5
|
export interface MonomorphizeOptions {
|
|
6
6
|
/** Master switch. Default OFF — every existing behavior is unchanged. */
|
package/dist/mono.js
CHANGED
|
@@ -60,6 +60,7 @@ import { inSpans } from './dead.js';
|
|
|
60
60
|
import { shakeBody } from './transform.js';
|
|
61
61
|
import { readCallSite, deadSpansForPlans, isFoldBlockedName, } from './analyze.js';
|
|
62
62
|
import { parseSvelte, walk } from './parse.js';
|
|
63
|
+
import { isFoldableValue } from './ir.js';
|
|
63
64
|
export const DEFAULT_MONO_OPTIONS = {
|
|
64
65
|
enabled: false,
|
|
65
66
|
maxVariants: 8,
|
|
@@ -418,6 +419,8 @@ function specializableShape(node, child, plan) {
|
|
|
418
419
|
// override — exactly the analysis's "safely explicit" condition.
|
|
419
420
|
if (explicit.dynamic || !explicit.afterLastSpread)
|
|
420
421
|
continue;
|
|
422
|
+
if (!isFoldableValue(explicit.value))
|
|
423
|
+
continue; // no safe source form — exactly as constant fold
|
|
421
424
|
shape.set(name, explicit.value);
|
|
422
425
|
}
|
|
423
426
|
return shape;
|
package/dist/parse.d.ts
CHANGED
|
@@ -75,6 +75,13 @@ export interface AnyNode {
|
|
|
75
75
|
kind?: string | undefined;
|
|
76
76
|
/** ObjectExpression `Property` shorthand-method flag (`{ m() {} }`). */
|
|
77
77
|
method?: boolean | undefined;
|
|
78
|
+
/** ESTree `Literal` discriminators, present only on a RegExp (`/x/g`) or
|
|
79
|
+
* BigInt (`1n`) literal — neither of which carries a foldable `value`. */
|
|
80
|
+
regex?: {
|
|
81
|
+
pattern: string;
|
|
82
|
+
flags: string;
|
|
83
|
+
} | undefined;
|
|
84
|
+
bigint?: string | undefined;
|
|
78
85
|
value?: unknown;
|
|
79
86
|
}
|
|
80
87
|
export interface Root extends AnyNode {
|
|
Binary file
|
package/dist/transform.js
CHANGED
|
@@ -1014,8 +1014,29 @@ function setDefault(map, key) {
|
|
|
1014
1014
|
function isSpace(ch) {
|
|
1015
1015
|
return ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r';
|
|
1016
1016
|
}
|
|
1017
|
+
/** Source text for a folded value, faithful for every member of {@link Literal}
|
|
1018
|
+
* (the union {@link evaluate} admits). Always an expression, so it drops into
|
|
1019
|
+
* both substitution positions unchanged. */
|
|
1017
1020
|
function literalSource(value) {
|
|
1018
1021
|
if (value === undefined)
|
|
1019
1022
|
return 'undefined';
|
|
1023
|
+
if (typeof value === 'number')
|
|
1024
|
+
return numberSource(value);
|
|
1025
|
+
return JSON.stringify(value);
|
|
1026
|
+
}
|
|
1027
|
+
/**
|
|
1028
|
+
* `JSON.stringify` flattens `Infinity`/`-Infinity`/`NaN` to `null`, so those get
|
|
1029
|
+
* an explicit form. Written as arithmetic rather than the `Infinity`/`NaN`
|
|
1030
|
+
* globals because the substituted text lands in the CALLEE's scope, where a
|
|
1031
|
+
* local of either name would silently capture it; `(0/0)` cannot be shadowed.
|
|
1032
|
+
* (`-0`, the fourth lossy case, never reaches here — see `isFoldableValue`.)
|
|
1033
|
+
*/
|
|
1034
|
+
function numberSource(value) {
|
|
1035
|
+
if (Number.isNaN(value))
|
|
1036
|
+
return '(0/0)';
|
|
1037
|
+
if (value === Infinity)
|
|
1038
|
+
return '(1/0)';
|
|
1039
|
+
if (value === -Infinity)
|
|
1040
|
+
return '(-1/0)';
|
|
1020
1041
|
return JSON.stringify(value);
|
|
1021
1042
|
}
|