ferrovue 0.2.0 → 0.4.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 +103 -15
- package/dist/attrs.d.ts +8 -42
- package/dist/attrs.js +148 -297
- package/dist/calls.d.ts +4 -0
- package/dist/calls.js +219 -0
- package/dist/children.d.ts +9 -0
- package/dist/children.js +273 -0
- package/dist/classes.d.ts +13 -0
- package/dist/classes.js +176 -0
- package/dist/cli.js +121 -27
- package/dist/client.d.ts +11 -2
- package/dist/client.js +45 -9
- package/dist/compiler.d.ts +4 -0
- package/dist/compiler.js +37 -90
- package/dist/component.js +19 -82
- package/dist/context.d.ts +6 -68
- package/dist/context.js +25 -38
- package/dist/diff.d.ts +1 -0
- package/dist/diff.js +68 -0
- package/dist/emitter.d.ts +0 -8
- package/dist/emitter.js +0 -12
- package/dist/expr.d.ts +0 -70
- package/dist/expr.js +30 -855
- package/dist/fallthrough.d.ts +9 -0
- package/dist/fallthrough.js +78 -0
- package/dist/fixture.d.ts +2 -1
- package/dist/fixture.js +37 -22
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/islands.d.ts +6 -0
- package/dist/islands.js +5 -0
- package/dist/lists.d.ts +0 -11
- package/dist/lists.js +4 -39
- package/dist/loops.d.ts +3 -0
- package/dist/loops.js +164 -0
- package/dist/model.d.ts +22 -130
- package/dist/model.js +37 -9
- package/dist/narrowing.d.ts +19 -0
- package/dist/narrowing.js +126 -0
- package/dist/numbers.d.ts +8 -0
- package/dist/numbers.js +73 -0
- package/dist/parens.d.ts +0 -27
- package/dist/parens.js +0 -44
- package/dist/plugin.d.ts +84 -0
- package/dist/plugin.js +26 -0
- package/dist/plugins/i18n.d.ts +23 -0
- package/dist/{i18n.js → plugins/i18n.js} +76 -19
- package/dist/plugins/index.d.ts +2 -0
- package/dist/plugins/index.js +8 -0
- package/dist/plugins/router-link.d.ts +8 -0
- package/dist/{router.js → plugins/router-link.js} +65 -57
- package/dist/plugins/router.d.ts +42 -0
- package/dist/plugins/router.js +256 -0
- package/dist/plugins/scoped.d.ts +12 -0
- package/dist/{scoped.js → plugins/scoped.js} +55 -20
- package/dist/plugins/shared-types.d.ts +2 -0
- package/dist/plugins/shared-types.js +7 -0
- package/dist/plugins/stores.d.ts +37 -0
- package/dist/plugins/stores.js +377 -0
- package/dist/plugins/teleport.d.ts +5 -0
- package/dist/plugins/teleport.js +55 -0
- package/dist/rust.d.ts +2 -5
- package/dist/rust.js +66 -161
- package/dist/script.d.ts +0 -6
- package/dist/script.js +22 -121
- package/dist/slots.d.ts +13 -0
- package/dist/slots.js +160 -0
- package/dist/strings.d.ts +13 -0
- package/dist/strings.js +208 -0
- package/dist/styles.d.ts +14 -0
- package/dist/styles.js +181 -0
- package/dist/template.d.ts +2 -40
- package/dist/template.js +40 -691
- package/dist/testing.d.ts +1 -0
- package/dist/testing.js +1 -8
- package/dist/types.d.ts +2 -0
- package/dist/typescript.d.ts +0 -13
- package/dist/typescript.js +8 -41
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +35 -14
- package/package.json +11 -2
- package/dist/i18n.d.ts +0 -27
- package/dist/router.d.ts +0 -17
- package/dist/scoped.d.ts +0 -10
- package/dist/stores.d.ts +0 -6
- package/dist/stores.js +0 -227
package/dist/expr.js
CHANGED
|
@@ -1,140 +1,13 @@
|
|
|
1
|
-
/* Expressions translated to Rust, each with the type it evaluates to. */
|
|
2
1
|
import { BOOL, fail, FLOAT, GenError, INT, opt, rustStr, sameTy, snake, STR, UNDEF } from "./model.js";
|
|
3
|
-
import {
|
|
2
|
+
import { ctx } from "./context.js";
|
|
4
3
|
import { lookupStruct, markHome } from "./typescript.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { collected
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
12
|
-
/** Two strings meeting, where a half of a surrogate pair — which ferrovue holds as U+FFFD — would
|
|
13
|
-
* change the result:
|
|
14
|
-
* - `equal`: two halves may differ, and a half never equals a literal U+FFFD;
|
|
15
|
-
* - `order`: a half orders as a surrogate, below U+E000, where U+FFFD orders above — the same only
|
|
16
|
-
* against a literal of characters below U+D800, which both order after;
|
|
17
|
-
* - `search`: a half searched for (`b`) matches half of a whole pair in `a`;
|
|
18
|
-
* - `join`: two halves side by side join into one character.
|
|
19
|
-
* A string holding U+FFFD in the data still equals, or is found in, a half: README, "Strings". */
|
|
20
|
-
export function meet(comp, a, b, what, n, how) {
|
|
21
|
-
const literal = (v) => v.code.startsWith('"');
|
|
22
|
-
const fffd = (v) => literal(v) && v.code.includes("\u{FFFD}");
|
|
23
|
-
const low = (v) => literal(v) && !/[\u{D800}-\u{10FFFF}]/u.test(v.code);
|
|
24
|
-
const refused = how === "order" ? (a.lone && !low(b)) || (b.lone && !low(a))
|
|
25
|
-
: how === "join" ? a.lone && b.lone
|
|
26
|
-
: how === "search" ? b.lone || (a.lone && fffd(b))
|
|
27
|
-
: (a.lone && b.lone) || (a.lone && fffd(b)) || (b.lone && fffd(a));
|
|
28
|
-
if (refused)
|
|
29
|
-
fail(comp, lonely(what), n);
|
|
30
|
-
}
|
|
31
|
-
/** `lone` for a value built from these: whether any of them may hold half of a surrogate pair. */
|
|
32
|
-
function loneOf(...vs) {
|
|
33
|
-
return vs.some((v) => v?.lone) ? { lone: true } : {};
|
|
34
|
-
}
|
|
35
|
-
/** Whether a string is a non-empty literal: a separator that always parts what it joins. */
|
|
36
|
-
function nonEmptyLiteral(v) {
|
|
37
|
-
return /^"[^"]/.test(v.code);
|
|
38
|
-
}
|
|
39
|
-
/** Whether a string expression borrows from a temporary it builds — \`&*format!(…)\`, a call that
|
|
40
|
-
* returns a \`String\` — rather than from the props or the state. Such a borrow ends with the block
|
|
41
|
-
* it is in, so a branch of \`?:\` or \`||\` must hand back the string itself. */
|
|
42
|
-
export function isTemporary(v) {
|
|
43
|
-
if (v.ty.k !== "str")
|
|
44
|
-
return false;
|
|
45
|
-
// A trim borrows from its argument, so it is a temporary when that is.
|
|
46
|
-
const trimmed = /^fv::js_trim(?:_start|_end)?\(([^]*)\)$/.exec(v.code);
|
|
47
|
-
if (trimmed?.[1] !== undefined)
|
|
48
|
-
return isTemporary({ code: trimmed[1].replace(/^&(?!\*)/, "&*"), ty: v.ty });
|
|
49
|
-
// A borrow of anything but a field: a string built by \`format!\` or a call, or a \`Cow\` made of
|
|
50
|
-
// the branches of \`?:\` or \`||\`.
|
|
51
|
-
// So is an optional string with such a fallback: `x.unwrap_or(&*format!(…))`.
|
|
52
|
-
return /^&\*(?![\w.]+$)/.test(v.code) || /\.unwrap_or\(&\*(?![\w.]+\)$)/.test(v.code);
|
|
53
|
-
}
|
|
54
|
-
/** A string as a \`Cow\`: borrowed when it lives on, owned when it is a temporary. The \`Cow\` is
|
|
55
|
-
* itself a temporary of the whole statement, so the \`&str\` taken from it lives to its end. A
|
|
56
|
-
* \`String\` built for the purpose is moved into it, and one that is already a \`Cow\` — a branch
|
|
57
|
-
* of \`?:\` or \`||\` — is taken as it is. */
|
|
58
|
-
export function asCow(v) {
|
|
59
|
-
if (!isTemporary(v))
|
|
60
|
-
return `std::borrow::Cow::<str>::Borrowed(${bare(v.code)})`;
|
|
61
|
-
const built = /^&\*(.*)$/s.exec(v.code)?.[1];
|
|
62
|
-
if (built === undefined)
|
|
63
|
-
return `std::borrow::Cow::<str>::Owned(${atom(v.code)}.to_owned())`;
|
|
64
|
-
if (enclosed(built))
|
|
65
|
-
return bare(built);
|
|
66
|
-
// A routine's `Cow` may borrow a string built in the same branch, which ends with it: owned.
|
|
67
|
-
if (yieldsCow(built))
|
|
68
|
-
return built.startsWith("fv::") ? `std::borrow::Cow::<str>::Owned(${built}.into_owned())` : bare(built);
|
|
69
|
-
// A routine that hands back part of the string it is given, as a `&str`: copied out.
|
|
70
|
-
if (/^fv::js_(?:char_at|trim|trim_start|trim_end)\(/.test(built))
|
|
71
|
-
return `std::borrow::Cow::<str>::Owned(${built}.to_owned())`;
|
|
72
|
-
return `std::borrow::Cow::<str>::Owned(${built})`;
|
|
73
|
-
}
|
|
74
|
-
/** Whether built code is a `Cow<str>` already: a runtime routine that borrows when it can
|
|
75
|
-
* (`js_slice`, `js_replace`, `js_pad_start`, …), or a fallback taken from one (`.unwrap_or(Cow…)`). */
|
|
76
|
-
export function yieldsCow(code) {
|
|
77
|
-
return /^fv::js_(?:slice|substring|replace|replace_all|pad_start|pad_end)\(/.test(code) || /\.unwrap_or\(std::borrow::Cow::<str>::\w+\(.*\)\)$/s.test(code);
|
|
78
|
-
}
|
|
79
|
-
/** Whether a value is a JavaScript number: an integer or a fraction. */
|
|
80
|
-
export function isNumber(ty) {
|
|
81
|
-
return ty.k === "int" || ty.k === "float";
|
|
82
|
-
}
|
|
83
|
-
/** A number as an `f64`, as JavaScript holds every number. */
|
|
84
|
-
export function asF64(v) {
|
|
85
|
-
if (v.f64 !== undefined)
|
|
86
|
-
return v.f64;
|
|
87
|
-
return v.ty.k === "float" ? v.code : `${operand(v.code, AS)} as f64`;
|
|
88
|
-
}
|
|
89
|
-
/** An integer computed on doubles, as JavaScript computes it: the \`i64\` and the double it came from. */
|
|
90
|
-
function intFromF64(f64) {
|
|
91
|
-
return { code: `${operand(f64, AS)} as i64`, ty: INT, f64 };
|
|
92
|
-
}
|
|
93
|
-
/** A double as a Rust literal: JavaScript's own spelling, which Rust reads back to the same double. */
|
|
94
|
-
function floatLiteral(x) {
|
|
95
|
-
if (Number.isNaN(x))
|
|
96
|
-
return "f64::NAN";
|
|
97
|
-
if (!Number.isFinite(x))
|
|
98
|
-
return x > 0 ? "f64::INFINITY" : "f64::NEG_INFINITY";
|
|
99
|
-
const spelled = String(Math.abs(x)).replace("e+", "e");
|
|
100
|
-
return `${x < 0 || Object.is(x, -0) ? "-" : ""}${/[.e]/.test(spelled) ? spelled : `${spelled}.0`}f64`;
|
|
101
|
-
}
|
|
102
|
-
/** A number known at generation time — a literal, or literals JavaScript computed — as an integer
|
|
103
|
-
* when it is one of the operations that keep integers, a fraction otherwise. */
|
|
104
|
-
function numberVal(x, int) {
|
|
105
|
-
if (!int)
|
|
106
|
-
return { code: floatLiteral(x), ty: FLOAT, num: x };
|
|
107
|
-
if (!Number.isSafeInteger(x))
|
|
108
|
-
return { ...intFromF64(floatLiteral(x)), num: x };
|
|
109
|
-
// `-0` is the integer 0, and the double it came from keeps its sign.
|
|
110
|
-
return { code: `${x === 0 ? 0 : x}i64`, ty: INT, f64: floatLiteral(x), num: x };
|
|
111
|
-
}
|
|
112
|
-
/** `a op b` on two numbers JavaScript computes now, as it would at run time. */
|
|
113
|
-
function fold(a, op, b) {
|
|
114
|
-
switch (op) {
|
|
115
|
-
case "+":
|
|
116
|
-
return a + b;
|
|
117
|
-
case "-":
|
|
118
|
-
return a - b;
|
|
119
|
-
case "*":
|
|
120
|
-
return a * b;
|
|
121
|
-
case "/":
|
|
122
|
-
return a / b;
|
|
123
|
-
case "%":
|
|
124
|
-
return a % b;
|
|
125
|
-
case "<":
|
|
126
|
-
return a < b;
|
|
127
|
-
case ">":
|
|
128
|
-
return a > b;
|
|
129
|
-
case "<=":
|
|
130
|
-
return a <= b;
|
|
131
|
-
case ">=":
|
|
132
|
-
return a >= b;
|
|
133
|
-
default:
|
|
134
|
-
return a === b;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
/** A type as an error names it. */
|
|
4
|
+
import { claim } from "./plugin.js";
|
|
5
|
+
import { atom, bare, binary, enclosed, logical, negate, not, occurrences, strArg } from "./parens.js";
|
|
6
|
+
import { collected } from "./lists.js";
|
|
7
|
+
import { asCow, formatted, isTemporary, loneOf, meet, stringsEqual } from "./strings.js";
|
|
8
|
+
import { arithmetic, asF64, compare, intFromF64, isNumber, negatedOrder, numberVal } from "./numbers.js";
|
|
9
|
+
import { boolOf, choice, known, narrowing, narrowTo, pathOf, presence, truthy } from "./narrowing.js";
|
|
10
|
+
import { call, isObjectCall } from "./calls.js";
|
|
138
11
|
export function describeTy(ty) {
|
|
139
12
|
switch (ty.k) {
|
|
140
13
|
case "str":
|
|
@@ -156,10 +29,8 @@ export function describeTy(ty) {
|
|
|
156
29
|
return `an optional ${describeTy(ty.of).replace(/^an? /, "")}`;
|
|
157
30
|
case "undef":
|
|
158
31
|
return "`undefined`";
|
|
159
|
-
case "query":
|
|
160
|
-
return "a query value";
|
|
161
32
|
default:
|
|
162
|
-
return "a value of another kind";
|
|
33
|
+
return claim((p) => p.values?.describe?.(ty)) ?? "a value of another kind";
|
|
163
34
|
}
|
|
164
35
|
}
|
|
165
36
|
export function fieldVal(comp, base, ty, js, node) {
|
|
@@ -169,9 +40,7 @@ export function fieldVal(comp, base, ty, js, node) {
|
|
|
169
40
|
const found = st?.fields.find((x) => x.js === js);
|
|
170
41
|
if (!found)
|
|
171
42
|
fail(comp, `\`${ty.name}\` has no field \`${js}\``, node);
|
|
172
|
-
// A field of a type another component declares names that component's types.
|
|
173
43
|
const f = owner === comp ? found : { ...found, ty: markHome(found.ty, owner.name) };
|
|
174
|
-
// A scoped slot's props already hold borrows and copies: each field is read as it is.
|
|
175
44
|
if (st.slot)
|
|
176
45
|
return { code: `${base}.${f.rust}`, ty: f.ty };
|
|
177
46
|
const place = `${base}.${f.rust}`;
|
|
@@ -196,52 +65,11 @@ export function fieldVal(comp, base, ty, js, node) {
|
|
|
196
65
|
return { code: `${place}.as_deref()`, ty: f.ty };
|
|
197
66
|
if (f.ty.of.k === "int" || f.ty.of.k === "float" || f.ty.of.k === "bool")
|
|
198
67
|
return { code: place, ty: f.ty };
|
|
199
|
-
// An object or a list, borrowed: `Option<&T>`, which a `v-if` narrows to the `&T`.
|
|
200
68
|
return { code: `${place}.as_ref()`, ty: f.ty };
|
|
201
69
|
default:
|
|
202
70
|
return { code: place, ty: f.ty };
|
|
203
71
|
}
|
|
204
72
|
}
|
|
205
|
-
/** A value's truthiness when it is known at generation time: a boolean constant, a string literal,
|
|
206
|
-
* a number JavaScript computed. */
|
|
207
|
-
export function known(v) {
|
|
208
|
-
if (v.konst !== undefined)
|
|
209
|
-
return v.konst;
|
|
210
|
-
if (v.ty.k === "str" && /^"(?:[^"\\]|\\.)*"$/.test(v.code))
|
|
211
|
-
return v.code !== '""';
|
|
212
|
-
if (v.num !== undefined)
|
|
213
|
-
return v.num !== 0 && !Number.isNaN(v.num);
|
|
214
|
-
return undefined;
|
|
215
|
-
}
|
|
216
|
-
export function truthy(v) {
|
|
217
|
-
const k = known(v);
|
|
218
|
-
if (k !== undefined)
|
|
219
|
-
return String(k);
|
|
220
|
-
// Of a value one of two branches gives, each branch's own truthiness.
|
|
221
|
-
const branches = v.ty.k === "str" || isNumber(v.ty) ? ifElse(v.code) : null;
|
|
222
|
-
if (branches)
|
|
223
|
-
return choice(branches.test, truthy({ code: branches.yes, ty: v.ty }), truthy({ code: branches.no, ty: v.ty }));
|
|
224
|
-
switch (v.ty.k) {
|
|
225
|
-
case "str":
|
|
226
|
-
return `!${receiver(v.code)}.is_empty()`;
|
|
227
|
-
case "int":
|
|
228
|
-
return binary(v.code, "!=", "0");
|
|
229
|
-
// JavaScript's falsy numbers: 0, -0 and NaN.
|
|
230
|
-
case "float":
|
|
231
|
-
return `${binary(v.code, "!=", "0.0")} && !${receiver(v.code)}.is_nan()`;
|
|
232
|
-
case "bool":
|
|
233
|
-
return v.code;
|
|
234
|
-
case "undef":
|
|
235
|
-
return "false";
|
|
236
|
-
case "query":
|
|
237
|
-
return `${atom(v.code)}.truthy()`;
|
|
238
|
-
case "opt":
|
|
239
|
-
return `${atom(v.code)}.is_some_and(|v| ${truthy({ code: "v", ty: v.ty.of })})`;
|
|
240
|
-
default:
|
|
241
|
-
return "true";
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
/** A value passed where `want` is expected: a present value into an optional slot is wrapped. */
|
|
245
73
|
export function coerce(comp, v, want, node) {
|
|
246
74
|
if (sameTy(v.ty, want))
|
|
247
75
|
return v.code;
|
|
@@ -249,279 +77,18 @@ export function coerce(comp, v, want, node) {
|
|
|
249
77
|
return "None";
|
|
250
78
|
if (want.k === "opt" && sameTy(v.ty, want.of))
|
|
251
79
|
return `Some(${bare(v.code)})`;
|
|
252
|
-
// An integer is a number too, where a fraction is expected.
|
|
253
80
|
if (want.k === "float" && v.ty.k === "int")
|
|
254
81
|
return asF64(v);
|
|
255
82
|
if (want.k === "opt" && want.of.k === "float" && v.ty.k === "int")
|
|
256
83
|
return `Some(${asF64(v)})`;
|
|
257
84
|
return fail(comp, `a ${JSON.stringify(v.ty)} where ${JSON.stringify(want)} is expected`, node);
|
|
258
85
|
}
|
|
259
|
-
/** The JavaScript path an expression names — `$props.user`, `item.author.name` — or `null` when it
|
|
260
|
-
* is not a plain chain of names. */
|
|
261
|
-
export function pathOf(n) {
|
|
262
|
-
if (n.type === "Identifier")
|
|
263
|
-
return n.name;
|
|
264
|
-
if (n.type !== "MemberExpression")
|
|
265
|
-
return null;
|
|
266
|
-
const base = pathOf(n.object);
|
|
267
|
-
if (base === null)
|
|
268
|
-
return null;
|
|
269
|
-
if (!n.computed && n.property.type === "Identifier")
|
|
270
|
-
return `${base}.${n.property.name}`;
|
|
271
|
-
if (n.computed && n.property.type === "StringLiteral")
|
|
272
|
-
return `${base}.${n.property.value}`;
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
export function presence(s, n) {
|
|
276
|
-
let target = n;
|
|
277
|
-
let negated = false;
|
|
278
|
-
let byTruth = true;
|
|
279
|
-
if (n.type === "UnaryExpression" && n.operator === "!") {
|
|
280
|
-
target = n.argument;
|
|
281
|
-
negated = true;
|
|
282
|
-
}
|
|
283
|
-
else if (n.type === "BinaryExpression" && (n.operator === "===" || n.operator === "!==")) {
|
|
284
|
-
const isUndef = (m) => m.type === "Identifier" && m.name === "undefined";
|
|
285
|
-
if (isUndef(n.right))
|
|
286
|
-
target = n.left;
|
|
287
|
-
else if (isUndef(n.left))
|
|
288
|
-
target = n.right;
|
|
289
|
-
else
|
|
290
|
-
return null;
|
|
291
|
-
negated = n.operator === "===";
|
|
292
|
-
byTruth = false;
|
|
293
|
-
}
|
|
294
|
-
const path = pathOf(target);
|
|
295
|
-
if (path === null)
|
|
296
|
-
return null;
|
|
297
|
-
const v = expr(s, target);
|
|
298
|
-
if (v.ty.k !== "opt")
|
|
299
|
-
return null;
|
|
300
|
-
const of = v.ty.of;
|
|
301
|
-
// A truthiness test keeps JavaScript's: an empty string, a 0 and `false` are not taken — while an
|
|
302
|
-
// object and a list, empty or not, always are.
|
|
303
|
-
const scalar = of.k === "str" || of.k === "int" || of.k === "float" || of.k === "bool";
|
|
304
|
-
const filtered = byTruth && scalar;
|
|
305
|
-
const option = filtered ? `${atom(v.code)}.filter(|v| ${truthy({ code: "*v", ty: of })})` : v.code;
|
|
306
|
-
const present = filtered ? truthy(v) : `${atom(v.code)}.is_some()`;
|
|
307
|
-
return { path, of, negated, truthy: byTruth, option, present, pattern: (name) => `let Some(${name}) = ${option}` };
|
|
308
|
-
}
|
|
309
|
-
/** The scope with `p`'s value present, bound as `name`. */
|
|
310
|
-
export function narrowTo(s, p, name) {
|
|
311
|
-
return { ...s, narrowed: new Map(s.narrowed).set(p.path, { code: name, ty: p.of }) };
|
|
312
|
-
}
|
|
313
|
-
/** `if let Some(name) = … { then } else { otherwise }`, each branch translated in its own scope —
|
|
314
|
-
* the value present in one of them — with only the test for a branch that never reads it. */
|
|
315
|
-
function narrowing(s, p, present, absent) {
|
|
316
|
-
const name = `n${++ctx.narrowCount}`;
|
|
317
|
-
const then = bare(present(narrowTo(s, p, name)));
|
|
318
|
-
const otherwise = bare(absent(s));
|
|
319
|
-
if (occurrences(then, name))
|
|
320
|
-
return `if ${p.pattern(name)} { ${then} } else { ${otherwise} }`;
|
|
321
|
-
return choice(p.present, then, otherwise);
|
|
322
|
-
}
|
|
323
|
-
/** `if test { yes } else { no }`, or less when the branches allow: one value when they are the same,
|
|
324
|
-
* the test itself when they are `true` and `false`. A test that binds a value keeps its branches. */
|
|
325
|
-
function choice(test, yes, no) {
|
|
326
|
-
const binds = test.startsWith("let ");
|
|
327
|
-
if (yes === no && !binds)
|
|
328
|
-
return yes;
|
|
329
|
-
if (yes === "true" && no === "false" && !binds)
|
|
330
|
-
return test;
|
|
331
|
-
if (yes === "false" && no === "true" && !binds)
|
|
332
|
-
return not(test);
|
|
333
|
-
return `if ${condition(test)} { ${yes} } else { ${no} }`;
|
|
334
|
-
}
|
|
335
|
-
/* A test, as a Rust boolean. Only its truthiness is used, so `||`, `&&` and `!` combine the truthiness
|
|
336
|
-
* of their operands whatever their types — where as a VALUE `a || b` is `a` or `b`, and stays held to
|
|
337
|
-
* the stricter rules `expr` applies. `x && …` and `!x || …` read `x` present on the right. */
|
|
338
|
-
export function cond(s, n) {
|
|
339
|
-
if (n.type === "LogicalExpression" && (n.operator === "||" || n.operator === "&&")) {
|
|
340
|
-
const p = presence(s, n.left);
|
|
341
|
-
if (p && p.negated === (n.operator === "||")) {
|
|
342
|
-
return narrowing(s, p, (inner) => cond(inner, n.right), () => String(n.operator === "||"));
|
|
343
|
-
}
|
|
344
|
-
return logical(cond(s, n.left), n.operator, cond(s, n.right));
|
|
345
|
-
}
|
|
346
|
-
if (n.type === "UnaryExpression" && n.operator === "!")
|
|
347
|
-
return negatedOrder(s, n.argument)?.code ?? not(cond(s, n.argument));
|
|
348
|
-
return truthy(expr(s, n));
|
|
349
|
-
}
|
|
350
86
|
export function childOf(name) {
|
|
351
87
|
const c = ctx.components.get(name);
|
|
352
88
|
if (!c)
|
|
353
89
|
throw new GenError(`\`Props\` is imported from ${name}.vue, which is not among the components compiled`);
|
|
354
90
|
return c;
|
|
355
91
|
}
|
|
356
|
-
/** The reader's route, which the component then takes. */
|
|
357
|
-
export function theRoute(s, n) {
|
|
358
|
-
if (!ctx.routes)
|
|
359
|
-
fail(s.comp, `reading the route needs \`routes\` in ${CONFIG_FILE}`, n);
|
|
360
|
-
s.comp.readsRoute = true;
|
|
361
|
-
return { code: "fv_route", ty: { k: "route" } };
|
|
362
|
-
}
|
|
363
|
-
/** A field of the route: what `useRoute()` gives that the server knows as vue-router does. */
|
|
364
|
-
export function routeField(s, base, prop, n) {
|
|
365
|
-
if (base.ty.k === "params") {
|
|
366
|
-
// Every parameter is a string; absent when the route has none of that name.
|
|
367
|
-
return { code: `fv_route.param(${rustStr(prop)})`, ty: opt(STR) };
|
|
368
|
-
}
|
|
369
|
-
if (base.ty.k === "queryobj")
|
|
370
|
-
return { code: `fv_route.query(${rustStr(prop)})`, ty: { k: "query" } };
|
|
371
|
-
switch (prop) {
|
|
372
|
-
case "path":
|
|
373
|
-
return { code: "fv_route.path()", ty: STR };
|
|
374
|
-
case "hash":
|
|
375
|
-
return { code: "fv_route.hash()", ty: STR };
|
|
376
|
-
case "name":
|
|
377
|
-
return { code: "fv_route.name()", ty: opt(STR) };
|
|
378
|
-
case "params":
|
|
379
|
-
return { code: "fv_route", ty: { k: "params" } };
|
|
380
|
-
case "query":
|
|
381
|
-
return { code: "fv_route", ty: { k: "queryobj" } };
|
|
382
|
-
case "fullPath":
|
|
383
|
-
return { code: "fv_route.full_path()", ty: STR };
|
|
384
|
-
}
|
|
385
|
-
return fail(s.comp, `\`route.${prop}\` is not available on the server: \`path\`, \`fullPath\`, \`hash\`, \`name\`, \`params\` and \`query\` are`, n);
|
|
386
|
-
}
|
|
387
|
-
/** The getters being translated, so that two reading each other is an error, not a loop. */
|
|
388
|
-
const inProgress = new Set();
|
|
389
|
-
/** Whether an expression names \`name\` as an identifier. */
|
|
390
|
-
function mentions(n, name) {
|
|
391
|
-
if (!n || typeof n !== "object")
|
|
392
|
-
return false;
|
|
393
|
-
if (n.type === "Identifier" && n.name === name)
|
|
394
|
-
return true;
|
|
395
|
-
return Object.entries(n).some(([k, v]) => k !== "loc" && k !== "__fv" && (Array.isArray(v) ? v.some((x) => mentions(x, name)) : typeof v === "object" && mentions(v, name)));
|
|
396
|
-
}
|
|
397
|
-
/** A Pinia getter read from a store's state, `prefs.doubled`: its expression, translated with the
|
|
398
|
-
* state it takes bound to that state. `null` when `name` is not a getter of that store. */
|
|
399
|
-
export function storeGetter(s, base, name, node) {
|
|
400
|
-
if (base.ty.k !== "struct" || !base.ty.store)
|
|
401
|
-
return null;
|
|
402
|
-
const stateName = base.ty.name;
|
|
403
|
-
const store = [...ctx.stores.values()].find((st) => st.state === stateName);
|
|
404
|
-
const g = store?.getters.get(name);
|
|
405
|
-
if (!g)
|
|
406
|
-
return null;
|
|
407
|
-
const where = `getter \`${name}\` in ${g.file}`;
|
|
408
|
-
if (!g.body)
|
|
409
|
-
fail(s.comp, `${where} returns a single expression`, node);
|
|
410
|
-
const uses = (n, type) => !!n && typeof n === "object" && (n.type === type || Object.entries(n).some(([k, v]) => k !== "loc" && (Array.isArray(v) ? v.some((x) => uses(x, type)) : typeof v === "object" && uses(v, type))));
|
|
411
|
-
if (uses(g.body, "ThisExpression"))
|
|
412
|
-
fail(s.comp, `${where} reads \`this\`; read the state through the getter's parameter`, node);
|
|
413
|
-
if (g.body.type === "ArrowFunctionExpression" || g.body.type === "FunctionExpression") {
|
|
414
|
-
fail(s.comp, `${where} returns a function, which takes arguments only the client passes`, node);
|
|
415
|
-
}
|
|
416
|
-
const locals = new Map();
|
|
417
|
-
if (g.param)
|
|
418
|
-
locals.set(g.param, { code: base.code, ty: base.ty });
|
|
419
|
-
// A setup store's computed reads the state's refs, and the other getters, by name and `.value`.
|
|
420
|
-
const setup = new Map();
|
|
421
|
-
const refs = new Set();
|
|
422
|
-
if (g.setup) {
|
|
423
|
-
const fields = ctx.storeStructs.get(stateName)?.fields ?? [];
|
|
424
|
-
for (const f of fields) {
|
|
425
|
-
setup.set(f.js, fieldVal(s.comp, base.code, base.ty, f.js, node));
|
|
426
|
-
refs.add(f.js);
|
|
427
|
-
}
|
|
428
|
-
for (const other of store.getters.keys()) {
|
|
429
|
-
if (other === name || !mentions(g.body, other))
|
|
430
|
-
continue;
|
|
431
|
-
if (inProgress.has(`${stateName}.${other}`))
|
|
432
|
-
fail(s.comp, `${where} and \`${other}\` read each other`, node);
|
|
433
|
-
inProgress.add(`${stateName}.${name}`);
|
|
434
|
-
try {
|
|
435
|
-
setup.set(other, storeGetter(s, base, other, node));
|
|
436
|
-
}
|
|
437
|
-
finally {
|
|
438
|
-
inProgress.delete(`${stateName}.${name}`);
|
|
439
|
-
}
|
|
440
|
-
refs.add(other);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
try {
|
|
444
|
-
return expr({ ...s, locals, narrowed: new Map(), setup, refs, propsIdent: null }, g.body);
|
|
445
|
-
}
|
|
446
|
-
catch (e) {
|
|
447
|
-
if (e instanceof GenError)
|
|
448
|
-
throw new GenError(`${s.comp.file}: ${where}: ${e.message.replace(/^[^:]*: /, "")}`);
|
|
449
|
-
throw e;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
/** `a op b` on two numbers, on doubles as JavaScript computes it — now, when both are known. `int`
|
|
453
|
-
* keeps the result an integer. */
|
|
454
|
-
function arithmetic(a, op, b, int) {
|
|
455
|
-
if (a.num !== undefined && b.num !== undefined)
|
|
456
|
-
return numberVal(fold(a.num, op, b.num), int);
|
|
457
|
-
const f64 = binary(asF64(a), op, asF64(b));
|
|
458
|
-
return int ? intFromF64(f64) : { code: f64, ty: FLOAT };
|
|
459
|
-
}
|
|
460
|
-
/** A comparison of two numbers, as doubles. */
|
|
461
|
-
function compare(a, op, b) {
|
|
462
|
-
if (a.num !== undefined && b.num !== undefined) {
|
|
463
|
-
const konst = fold(a.num, op, b.num);
|
|
464
|
-
return { code: String(konst), ty: BOOL, konst };
|
|
465
|
-
}
|
|
466
|
-
return { code: binary(asF64(a), op, asF64(b)), ty: BOOL };
|
|
467
|
-
}
|
|
468
|
-
/** `!(a < b)` of two integers: `a >= b`, as neither is ever NaN. `null` for anything else. */
|
|
469
|
-
function negatedOrder(s, n) {
|
|
470
|
-
if (n.type !== "BinaryExpression" || !(n.operator in FLIPPED))
|
|
471
|
-
return null;
|
|
472
|
-
const a = expr(s, n.left);
|
|
473
|
-
const b = expr(s, n.right);
|
|
474
|
-
return a.ty.k === "int" && b.ty.k === "int" ? compare(a, FLIPPED[n.operator], b) : null;
|
|
475
|
-
}
|
|
476
|
-
/** A Rust string literal's text. */
|
|
477
|
-
export function unquote(literal) {
|
|
478
|
-
const escapes = { n: "\n", r: "\r", t: "\t" };
|
|
479
|
-
return literal.slice(1, -1).replace(/\\(?:u\{([0-9a-f]+)\}|(.))/g, (_, hex, c) => (hex ? String.fromCodePoint(parseInt(hex, 16)) : (escapes[c] ?? c)));
|
|
480
|
-
}
|
|
481
|
-
/** Literal text and values joined into one string, each value written as JavaScript writes it into
|
|
482
|
-
* a string: `format!`, with string literals among the values part of its text, and a string another
|
|
483
|
-
* `format!` built part of its text and arguments. */
|
|
484
|
-
function formatted(comp, parts, n, what) {
|
|
485
|
-
let fmt = "";
|
|
486
|
-
const args = [];
|
|
487
|
-
// The value before, when nothing lies between it and the next: two halves of a pair would join.
|
|
488
|
-
let before;
|
|
489
|
-
const lone = loneOf(...parts.map((p) => (typeof p === "string" ? undefined : p)));
|
|
490
|
-
for (const part of parts) {
|
|
491
|
-
if (typeof part === "string" || (part.ty.k === "str" && known(part) !== undefined)) {
|
|
492
|
-
const text = typeof part === "string" ? part : unquote(part.code);
|
|
493
|
-
if (text !== "")
|
|
494
|
-
before = undefined;
|
|
495
|
-
fmt += text.replace(/[{}]/g, (c) => c + c);
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
498
|
-
if (before)
|
|
499
|
-
meet(comp, before, part, what, n, "join");
|
|
500
|
-
before = part;
|
|
501
|
-
if (part.format) {
|
|
502
|
-
fmt += part.format.text;
|
|
503
|
-
args.push(...part.format.args);
|
|
504
|
-
continue;
|
|
505
|
-
}
|
|
506
|
-
// A string written by `Display` is written as it is: `fv::Js(n)` rather than its `to_string()`.
|
|
507
|
-
if (part.ty.k === "str")
|
|
508
|
-
args.push(receiver(part.code).replace(/^(fv::Js\(.*\))\.to_string\(\)$/s, "$1"));
|
|
509
|
-
else if (isNumber(part.ty))
|
|
510
|
-
args.push(`fv::Js(${bare(part.code)})`);
|
|
511
|
-
else if (part.ty.k === "bool")
|
|
512
|
-
args.push(`if ${condition(part.code)} { "true" } else { "false" }`);
|
|
513
|
-
else
|
|
514
|
-
fail(comp, "a template literal interpolates strings, numbers and booleans that are present", n);
|
|
515
|
-
fmt += "{}";
|
|
516
|
-
}
|
|
517
|
-
if (!args.length)
|
|
518
|
-
return { code: rustStr(fmt.replace(/\{\{|\}\}/g, (c) => c[0])), ty: STR };
|
|
519
|
-
// One string and nothing else is that string.
|
|
520
|
-
const one = parts.filter((p) => (typeof p === "string" ? p !== "" : p.code !== '""'));
|
|
521
|
-
if (fmt === "{}" && one.length === 1 && typeof one[0] !== "string" && one[0].ty.k === "str")
|
|
522
|
-
return one[0];
|
|
523
|
-
return { code: `&*format!(${rustStr(fmt)}, ${args.join(", ")})`, ty: STR, format: { text: fmt, args }, ...lone };
|
|
524
|
-
}
|
|
525
92
|
export function expr(s, n) {
|
|
526
93
|
const comp = s.comp;
|
|
527
94
|
const path = pathOf(n);
|
|
@@ -531,7 +98,6 @@ export function expr(s, n) {
|
|
|
531
98
|
return narrowed;
|
|
532
99
|
}
|
|
533
100
|
if (n.type === "MemberExpression" && !n.computed && n.property.type === "Identifier" && n.property.name === "length") {
|
|
534
|
-
// `Object.entries(r).length`: as many as the record has keys.
|
|
535
101
|
if (isObjectCall(n.object, "entries") && n.object.arguments.length === 1) {
|
|
536
102
|
const r = expr(s, n.object.arguments[0]);
|
|
537
103
|
if (r.ty.k === "record")
|
|
@@ -539,13 +105,10 @@ export function expr(s, n) {
|
|
|
539
105
|
}
|
|
540
106
|
const base = expr(s, n.object);
|
|
541
107
|
if (base.ty.k === "list" && base.iter !== undefined) {
|
|
542
|
-
// A list split out is a `Vec` already, whose length is known.
|
|
543
108
|
const vec = /^(.*)\.into_iter\(\)$/s.exec(base.iter)?.[1];
|
|
544
|
-
// Nor does a `map` at the end change how many there are.
|
|
545
109
|
if (vec === undefined) {
|
|
546
110
|
const at = base.iter.lastIndexOf(".map(");
|
|
547
111
|
const mapped = at > 0 && enclosed(base.iter.slice(at + 4)) ? base.iter.slice(0, at) : base.iter;
|
|
548
|
-
// What is left may walk a `Vec` that is held, whose length is known.
|
|
549
112
|
const held = /^(.*)\.iter\(\)$/s.exec(mapped)?.[1];
|
|
550
113
|
return { code: held !== undefined ? `${atom(held)}.len() as i64` : `${atom(mapped)}.count() as i64`, ty: INT };
|
|
551
114
|
}
|
|
@@ -553,22 +116,17 @@ export function expr(s, n) {
|
|
|
553
116
|
}
|
|
554
117
|
if (base.ty.k === "list")
|
|
555
118
|
return { code: `${atom(base.code)}.len() as i64`, ty: INT };
|
|
556
|
-
// A JavaScript string's length counts UTF-16 code units, not the bytes Rust's `len` counts.
|
|
557
119
|
if (base.ty.k === "str")
|
|
558
120
|
return { code: `fv::js_length(${strArg(base.code)})`, ty: INT };
|
|
559
121
|
}
|
|
560
122
|
if (n.type === "BinaryExpression" && n.operator === "+") {
|
|
561
123
|
const a = expr(s, n.left);
|
|
562
124
|
const b = expr(s, n.right);
|
|
563
|
-
// JavaScript's `+` concatenates as soon as one side is a string, writing a number in decimal —
|
|
564
|
-
// which is what `{}` does with an `i64`. Two numbers would be arithmetic, which is not here.
|
|
565
125
|
const joinable = (t) => t.k === "str" || isNumber(t);
|
|
566
126
|
if ((a.ty.k === "str" || b.ty.k === "str") && joinable(a.ty) && joinable(b.ty)) {
|
|
567
127
|
meet(comp, a, b, "`+`", n, "join");
|
|
568
|
-
// A number is written as JavaScript writes it, rounded beyond 2⁵³.
|
|
569
128
|
return formatted(comp, [a, b], n, "`+`");
|
|
570
129
|
}
|
|
571
|
-
// Two numbers: arithmetic, as `n + 1` in a template means.
|
|
572
130
|
if (isNumber(a.ty) && isNumber(b.ty))
|
|
573
131
|
return arithmetic(a, "+", b, a.ty.k === "int" && b.ty.k === "int");
|
|
574
132
|
return fail(comp, "`+` joins a string to a string or a number, or adds two numbers", n);
|
|
@@ -577,7 +135,6 @@ export function expr(s, n) {
|
|
|
577
135
|
const a = expr(s, n.left);
|
|
578
136
|
const b = expr(s, n.right);
|
|
579
137
|
const comparison = ["<", ">", "<=", ">="].includes(n.operator);
|
|
580
|
-
// Two strings, ordered as JavaScript orders them: by UTF-16 code unit.
|
|
581
138
|
if (comparison && a.ty.k === "str" && b.ty.k === "str") {
|
|
582
139
|
meet(comp, a, b, `\`${n.operator}\``, n, "order");
|
|
583
140
|
const is = { "<": "lt", ">": "gt", "<=": "le", ">=": "ge" }[n.operator];
|
|
@@ -592,16 +149,12 @@ export function expr(s, n) {
|
|
|
592
149
|
}
|
|
593
150
|
if (["<", ">", "<=", ">="].includes(n.operator))
|
|
594
151
|
return compare(a, n.operator, b);
|
|
595
|
-
// Two integers stay integers — but for `/`, which gives a fraction in JavaScript, and a `%` whose
|
|
596
|
-
// divisor may be zero, which gives NaN.
|
|
597
152
|
const intDivisor = n.right.type === "NumericLiteral" && Number.isInteger(n.right.value) && n.right.value !== 0;
|
|
598
153
|
const int = a.ty.k === "int" && b.ty.k === "int" && n.operator !== "/" && (n.operator !== "%" || intDivisor);
|
|
599
|
-
// Doubles, as JavaScript computes: Rust's `%` on `f64` is JavaScript's, and `/` by zero is ±∞.
|
|
600
154
|
return arithmetic(a, n.operator, b, int);
|
|
601
155
|
}
|
|
602
156
|
switch (n.type) {
|
|
603
157
|
case "TemplateLiteral": {
|
|
604
|
-
// `${a}-${b}`: each part written as JavaScript writes it into a string.
|
|
605
158
|
const parts = [];
|
|
606
159
|
n.quasis.forEach((q, i) => {
|
|
607
160
|
parts.push(q.value.cooked);
|
|
@@ -611,7 +164,6 @@ export function expr(s, n) {
|
|
|
611
164
|
return formatted(comp, parts, n, "a template literal");
|
|
612
165
|
}
|
|
613
166
|
case "ArrayExpression": {
|
|
614
|
-
// A list of literals or values of one scalar type, as a Rust array.
|
|
615
167
|
if (n.elements.length === 0)
|
|
616
168
|
return { code: "[]", ty: { k: "list", of: UNDEF } };
|
|
617
169
|
const values = n.elements.map((el) => {
|
|
@@ -626,7 +178,6 @@ export function expr(s, n) {
|
|
|
626
178
|
return { code: `[${values.map((v) => bare(v.code)).join(", ")}]`, ty: { k: "list", of }, ...loneOf(...values) };
|
|
627
179
|
}
|
|
628
180
|
case "OptionalMemberExpression": {
|
|
629
|
-
// `a?.b`: the field of an optional object when it is present.
|
|
630
181
|
if (n.computed)
|
|
631
182
|
return fail(comp, "computed member access", n);
|
|
632
183
|
const base = expr(s, n.object);
|
|
@@ -640,14 +191,10 @@ export function expr(s, n) {
|
|
|
640
191
|
case "StringLiteral":
|
|
641
192
|
return { code: rustStr(n.value), ty: STR };
|
|
642
193
|
case "NumericLiteral":
|
|
643
|
-
// JavaScript's own spelling of the number is a valid Rust float literal: `0.5`, `1e-7`. An
|
|
644
|
-
// integer beyond what a double holds exactly is a double too, as it is in JavaScript.
|
|
645
194
|
return numberVal(n.value, Number.isSafeInteger(n.value));
|
|
646
195
|
case "BooleanLiteral":
|
|
647
196
|
return { code: String(n.value), ty: BOOL, konst: n.value };
|
|
648
197
|
case "NullLiteral":
|
|
649
|
-
// An absent prop is `undefined`, and `null` is a different value: `x === null` is false for
|
|
650
|
-
// it in Vue and would be `is_none()` here.
|
|
651
198
|
return fail(comp, "`null`: compare with `undefined`, which is what an absent prop is", n);
|
|
652
199
|
case "Identifier": {
|
|
653
200
|
if (n.name === "undefined")
|
|
@@ -663,7 +210,6 @@ export function expr(s, n) {
|
|
|
663
210
|
return fail(comp, `\`${n.name}\` is not available when rendering on the server`, n);
|
|
664
211
|
}
|
|
665
212
|
case "MemberExpression": {
|
|
666
|
-
// `count.value` in the script: the ref's value, which is what the binding already reads.
|
|
667
213
|
if (!n.computed && n.property.name === "value" && n.object.type === "Identifier" && s.refs.has(n.object.name) && !s.locals.has(n.object.name)) {
|
|
668
214
|
return expr(s, n.object);
|
|
669
215
|
}
|
|
@@ -673,13 +219,13 @@ export function expr(s, n) {
|
|
|
673
219
|
}
|
|
674
220
|
if (n.property.type === "StringLiteral") {
|
|
675
221
|
const base = expr(s, n.object);
|
|
676
|
-
|
|
677
|
-
|
|
222
|
+
const own = claim((p) => p.member?.(s, base, n.property.value, n, true));
|
|
223
|
+
if (own)
|
|
224
|
+
return own;
|
|
678
225
|
}
|
|
679
226
|
return fail(comp, "computed member access", n);
|
|
680
227
|
}
|
|
681
228
|
const prop = n.property.name;
|
|
682
|
-
// `$slots.side`: whether the parent gave that slot any content.
|
|
683
229
|
const slotsObject = (n.object.type === "Identifier" && n.object.name === "$slots") ||
|
|
684
230
|
(n.object.type === "MemberExpression" && !n.object.computed && n.object.object.type === "Identifier" &&
|
|
685
231
|
n.object.object.name === "_ctx" && n.object.property.name === "$slots");
|
|
@@ -694,8 +240,12 @@ export function expr(s, n) {
|
|
|
694
240
|
return fieldVal(comp, "props", { k: "struct", name: "Props" }, prop, n);
|
|
695
241
|
case "$setup":
|
|
696
242
|
case "_ctx": {
|
|
697
|
-
|
|
698
|
-
|
|
243
|
+
const global = n.object.name === "_ctx" ? claim((p) => p.global?.(s, prop, n)) : undefined;
|
|
244
|
+
if (global)
|
|
245
|
+
return global;
|
|
246
|
+
if (n.object.name === "_ctx" && prop === "$attrs") {
|
|
247
|
+
return fail(comp, "`$attrs` is bound whole, with `v-bind=\"$attrs\"`; a value read from it has no type: declare it as a prop", n);
|
|
248
|
+
}
|
|
699
249
|
const v = s.setup.get(prop);
|
|
700
250
|
if (v)
|
|
701
251
|
return v;
|
|
@@ -710,15 +260,11 @@ export function expr(s, n) {
|
|
|
710
260
|
}
|
|
711
261
|
}
|
|
712
262
|
const base = expr(s, n.object);
|
|
713
|
-
|
|
714
|
-
return routeField(s, base, prop, n);
|
|
715
|
-
return storeGetter(s, base, prop, n) ?? fieldVal(comp, base.code, base.ty, prop, n);
|
|
263
|
+
return claim((p) => p.member?.(s, base, prop, n, false)) ?? fieldVal(comp, base.code, base.ty, prop, n);
|
|
716
264
|
}
|
|
717
265
|
case "CallExpression":
|
|
718
266
|
return call(s, n);
|
|
719
267
|
case "LogicalExpression": {
|
|
720
|
-
// `x !== undefined && …` and `!x || …` / `x === undefined || …`: booleans, with `x` present on
|
|
721
|
-
// the right.
|
|
722
268
|
if (n.operator === "&&" || n.operator === "||") {
|
|
723
269
|
const p = presence(s, n.left);
|
|
724
270
|
if (p && p.negated === (n.operator === "||") && (p.negated || !p.truthy)) {
|
|
@@ -732,20 +278,15 @@ export function expr(s, n) {
|
|
|
732
278
|
const a = expr(s, n.left);
|
|
733
279
|
const b = expr(s, n.right);
|
|
734
280
|
if (n.operator === "??") {
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
if (b.ty.k !== "str")
|
|
739
|
-
fail(comp, "`??` after a query value takes a string", n);
|
|
740
|
-
return { code: `${atom(a.code)}.or(${bare(b.code)})`, ty: a.ty };
|
|
741
|
-
}
|
|
281
|
+
const own = claim((p) => p.values?.nullish?.(s, a, b, n));
|
|
282
|
+
if (own)
|
|
283
|
+
return own;
|
|
742
284
|
if (a.ty.k !== "opt")
|
|
743
285
|
return a;
|
|
744
286
|
if (b.ty.k === "undef")
|
|
745
287
|
return a;
|
|
746
288
|
if (b.iter !== undefined)
|
|
747
289
|
fail(comp, "`??` falling back to a computed list", n);
|
|
748
|
-
// A string a temporary owns: the result is one too, borrowed from the `Cow` either side gives.
|
|
749
290
|
if (a.held !== undefined && b.ty.k === "str")
|
|
750
291
|
return { code: `&*${atom(a.held)}.unwrap_or(${asCow(b)})`, ty: STR, ...loneOf(a, b) };
|
|
751
292
|
if (a.ty.of.k === "str" && b.ty.k === "str" && isTemporary(b)) {
|
|
@@ -755,7 +296,6 @@ export function expr(s, n) {
|
|
|
755
296
|
return { code: `${atom(a.code)}.unwrap_or(${bare(b.code)})`, ty: b.ty, ...loneOf(a, b) };
|
|
756
297
|
if (sameTy(a.ty, b.ty))
|
|
757
298
|
return { code: `${atom(a.code)}.or(${bare(b.code)})`, ty: a.ty, ...loneOf(a, b) };
|
|
758
|
-
// An integer and a fraction: both numbers in JavaScript, so a fraction here.
|
|
759
299
|
if (a.ty.of.k === "float" && b.ty.k === "int")
|
|
760
300
|
return { code: `${atom(a.code)}.unwrap_or(${bare(asF64(b))})`, ty: FLOAT };
|
|
761
301
|
if (a.ty.of.k === "int" && b.ty.k === "float")
|
|
@@ -766,7 +306,6 @@ export function expr(s, n) {
|
|
|
766
306
|
if (a.ty.k === "bool" && b.ty.k === "bool")
|
|
767
307
|
return boolOf(logical(a.code, "||", b.code));
|
|
768
308
|
if (b.ty.k === "undef") {
|
|
769
|
-
// `x || undefined`: the value when it is truthy, nothing otherwise.
|
|
770
309
|
const inner = a.ty.k === "opt" ? a.ty.of : a.ty;
|
|
771
310
|
const src = a.ty.k === "opt" ? atom(a.code) : `Some(${bare(a.code)})`;
|
|
772
311
|
return { code: `${src}.filter(|v| ${truthy({ code: "*v", ty: inner })})`, ty: opt(inner) };
|
|
@@ -806,6 +345,9 @@ export function expr(s, n) {
|
|
|
806
345
|
case "BinaryExpression": {
|
|
807
346
|
if (n.operator !== "===" && n.operator !== "!==")
|
|
808
347
|
fail(comp, `\`${n.operator}\``, n);
|
|
348
|
+
const whole = claim((p) => p.equality?.(s, n));
|
|
349
|
+
if (whole)
|
|
350
|
+
return whole;
|
|
809
351
|
const a = expr(s, n.left);
|
|
810
352
|
const b = expr(s, n.right);
|
|
811
353
|
let eq;
|
|
@@ -813,36 +355,23 @@ export function expr(s, n) {
|
|
|
813
355
|
const strish = (t) => t.k === "str" || (t.k === "opt" && t.of.k === "str");
|
|
814
356
|
if (strish(a.ty) && strish(b.ty))
|
|
815
357
|
meet(comp, a, b, `\`${n.operator}\``, n, "equal");
|
|
816
|
-
|
|
817
|
-
if (
|
|
818
|
-
eq =
|
|
819
|
-
else if (b.ty.k === "query" && a.ty.k === "str")
|
|
820
|
-
eq = `${atom(b.code)}.is(${strArg(a.code)})`;
|
|
821
|
-
else if (a.ty.k === "query" && b.ty.k === "undef")
|
|
822
|
-
eq = `${atom(a.code)}.is_undefined()`;
|
|
823
|
-
else if (b.ty.k === "query" && a.ty.k === "undef")
|
|
824
|
-
eq = `${atom(b.code)}.is_undefined()`;
|
|
358
|
+
const own = claim((p) => p.values?.equals?.(a, b));
|
|
359
|
+
if (own !== undefined)
|
|
360
|
+
eq = own;
|
|
825
361
|
else if (b.ty.k === "undef" && a.ty.k === "opt")
|
|
826
362
|
eq = `${atom(a.code)}.is_none()`;
|
|
827
363
|
else if (a.ty.k === "undef" && b.ty.k === "opt")
|
|
828
364
|
eq = `${atom(b.code)}.is_none()`;
|
|
829
365
|
else if (a.ty.k === "undef" || b.ty.k === "undef") {
|
|
830
|
-
// A value that is always present — or narrowed to present — is never \`undefined\`.
|
|
831
366
|
const same = a.ty.k === b.ty.k;
|
|
832
367
|
const konst = n.operator === "===" ? same : !same;
|
|
833
368
|
return { code: String(konst), ty: BOOL, konst };
|
|
834
369
|
}
|
|
835
370
|
else if (isNumber(a.ty) && isNumber(b.ty))
|
|
836
371
|
eq = compare(a, "==", b).code;
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
eq = String(unquote(a.code) === unquote(b.code));
|
|
840
|
-
else if (a.ty.k === "str" && b.ty.k === "str" && (a.code === '""' || b.code === '""')) {
|
|
841
|
-
// A string compared with the empty one: whether it is empty.
|
|
842
|
-
eq = `${receiver(a.code === '""' ? b.code : a.code)}.is_empty()`;
|
|
843
|
-
}
|
|
372
|
+
else if (a.ty.k === "str" && b.ty.k === "str")
|
|
373
|
+
eq = stringsEqual(a, b);
|
|
844
374
|
else if (a.ty.k === "bool" && b.ty.k === "bool" && (a.konst !== undefined || b.konst !== undefined)) {
|
|
845
|
-
// A boolean compared with `true` is itself, with `false` its negation.
|
|
846
375
|
const [k, other] = a.konst !== undefined ? [a.konst, b.code] : [b.konst, a.code];
|
|
847
376
|
eq = k ? other : not(other);
|
|
848
377
|
}
|
|
@@ -862,24 +391,19 @@ export function expr(s, n) {
|
|
|
862
391
|
const k = known(t);
|
|
863
392
|
if (k !== undefined)
|
|
864
393
|
return expr(s, k ? n.consequent : n.alternate);
|
|
865
|
-
// A test for presence narrows the branch where the value is present, as TypeScript does.
|
|
866
394
|
const p = presence(s, n.test);
|
|
867
395
|
const name = p ? `n${++ctx.narrowCount}` : "";
|
|
868
396
|
const a = expr(p && !p.negated ? narrowTo(s, p, name) : s, n.consequent);
|
|
869
397
|
const b = expr(p && p.negated ? narrowTo(s, p, name) : s, n.alternate);
|
|
870
|
-
// Two branches alike are one value, whatever the test.
|
|
871
398
|
if (sameTy(a.ty, b.ty) && a.code === b.code && (!name || !occurrences(a.code, name)))
|
|
872
399
|
return a;
|
|
873
|
-
// A string in the second branch is coerced to the `&str` of the first.
|
|
874
400
|
const second = (code) => (a.ty.k === "str" ? strArg(code) : bare(code));
|
|
875
401
|
const choose = (yes, no) => {
|
|
876
402
|
if (!p)
|
|
877
403
|
return choice(truthy(t), bare(yes), second(no));
|
|
878
404
|
const [present, absent] = p.negated ? [bare(no), second(yes)] : [bare(yes), second(no)];
|
|
879
|
-
// `x ? x : undefined` is the `Option` itself.
|
|
880
405
|
if (present === `Some(${name})` && absent === "None")
|
|
881
406
|
return p.option;
|
|
882
|
-
// `x ? x : y`: the value, or the fallback.
|
|
883
407
|
if (present === name)
|
|
884
408
|
return `${atom(p.option)}.unwrap_or(${absent})`;
|
|
885
409
|
if (occurrences(present, name))
|
|
@@ -890,7 +414,6 @@ export function expr(s, n) {
|
|
|
890
414
|
if (sameTy(a.ty, b.ty) && a.ty.k === "str" && (isTemporary(a) || isTemporary(b))) {
|
|
891
415
|
return { code: `&*(${choose(asCow(a), asCow(b))})`, ty: STR, ...lone };
|
|
892
416
|
}
|
|
893
|
-
// Two lists, one of them computed: both branches collect their items, to have one type.
|
|
894
417
|
if (sameTy(a.ty, b.ty) && a.ty.k === "list" && (a.iter !== undefined || b.iter !== undefined)) {
|
|
895
418
|
const both = `(${choose(collected(a), collected(b))})`;
|
|
896
419
|
return { code: both, ty: a.ty, iter: `${both}.into_iter()`, ...lone };
|
|
@@ -898,13 +421,11 @@ export function expr(s, n) {
|
|
|
898
421
|
const optStr = (v) => v.ty.k === "undef" || (v.ty.k === "opt" && v.ty.of.k === "str") || v.ty.k === "str";
|
|
899
422
|
if (sameTy(a.ty, b.ty) && a.ty.k !== "opt")
|
|
900
423
|
return { code: choose(a.code, b.code), ty: a.ty, ...lone };
|
|
901
|
-
// An integer and a fraction: both numbers in JavaScript, so a fraction here.
|
|
902
424
|
if (isNumber(a.ty) && isNumber(b.ty))
|
|
903
425
|
return { code: choose(asF64(a), asF64(b)), ty: FLOAT };
|
|
904
426
|
if (sameTy(a.ty, b.ty) || a.ty.k === "undef" || b.ty.k === "undef" || (a.ty.k === "opt" && sameTy(a.ty.of, b.ty)) || (b.ty.k === "opt" && sameTy(b.ty.of, a.ty))) {
|
|
905
427
|
const ty = opt(a.ty.k === "undef" || b.ty.k === "opt" ? b.ty : a.ty);
|
|
906
428
|
if (optStr(a) && optStr(b) && (isTemporary(a) || isTemporary(b) || a.held !== undefined || b.held !== undefined)) {
|
|
907
|
-
// A string built in a branch, held by an `Option<Cow>` the statement keeps.
|
|
908
429
|
const held = (v) => v.held ?? (v.ty.k === "undef" ? "None" : v.ty.k === "opt" ? `${atom(v.code)}.map(std::borrow::Cow::<str>::Borrowed)` : `Some(${asCow(v)})`);
|
|
909
430
|
const both = atom(choose(held(a), held(b)));
|
|
910
431
|
return { code: `${both}.as_deref()`, ty, held: both, ...lone };
|
|
@@ -919,349 +440,3 @@ export function expr(s, n) {
|
|
|
919
440
|
return fail(comp, `\`${n.type}\` is not supported in an island template`, n);
|
|
920
441
|
}
|
|
921
442
|
}
|
|
922
|
-
/** A boolean computed at run time, or known now when it folded to a constant. */
|
|
923
|
-
export function boolOf(code) {
|
|
924
|
-
return code === "true" || code === "false" ? { code, ty: BOOL, konst: code === "true" } : { code, ty: BOOL };
|
|
925
|
-
}
|
|
926
|
-
export function call(s, n) {
|
|
927
|
-
const comp = s.comp;
|
|
928
|
-
const callee = n.callee;
|
|
929
|
-
const args = n.arguments;
|
|
930
|
-
if (callee.type === "Identifier") {
|
|
931
|
-
switch (callee.name) {
|
|
932
|
-
case "_ssrLooseEqual": {
|
|
933
|
-
const a = expr(s, args[0]);
|
|
934
|
-
const b = expr(s, args[1]);
|
|
935
|
-
if (a.ty.k === "str" && b.ty.k === "str") {
|
|
936
|
-
meet(comp, a, b, "`v-model`", n, "equal");
|
|
937
|
-
return boolOf(binary(a.code, "==", b.code));
|
|
938
|
-
}
|
|
939
|
-
return fail(comp, "`v-model` comparison between these types", n);
|
|
940
|
-
}
|
|
941
|
-
case "_ssrIncludeBooleanAttr": {
|
|
942
|
-
// `!!value || value === ""`: every string is included, empty or not.
|
|
943
|
-
const a = expr(s, args[0]);
|
|
944
|
-
if (a.konst !== undefined)
|
|
945
|
-
return a;
|
|
946
|
-
if (a.ty.k === "str")
|
|
947
|
-
return { code: "true", ty: BOOL, konst: true };
|
|
948
|
-
if (a.ty.k === "opt" && a.ty.of.k === "str")
|
|
949
|
-
return { code: `${atom(a.code)}.is_some()`, ty: BOOL };
|
|
950
|
-
return boolOf(truthy(a));
|
|
951
|
-
}
|
|
952
|
-
case "_ssrLooseContain":
|
|
953
|
-
return fail(comp, "`v-model` over an array", n);
|
|
954
|
-
}
|
|
955
|
-
if (s.i18nT.has(callee.name))
|
|
956
|
-
return translate(s, args, n);
|
|
957
|
-
const helper = s.helpers.get(callee.name);
|
|
958
|
-
if (helper)
|
|
959
|
-
return helperCall(s, helper, args, n);
|
|
960
|
-
if (callee.name === "String" && args.length === 1) {
|
|
961
|
-
const a = expr(s, args[0]);
|
|
962
|
-
if (a.ty.k === "str")
|
|
963
|
-
return a;
|
|
964
|
-
if (isNumber(a.ty))
|
|
965
|
-
return { code: `&*fv::Js(${bare(a.code)}).to_string()`, ty: STR };
|
|
966
|
-
if (a.ty.k === "bool")
|
|
967
|
-
return { code: `if ${condition(a.code)} { "true" } else { "false" }`, ty: STR };
|
|
968
|
-
}
|
|
969
|
-
// Strings read as numbers, as JavaScript reads them: a fraction, `NaN` when there is no number.
|
|
970
|
-
if (callee.name === "Number" && args.length === 1) {
|
|
971
|
-
const a = expr(s, args[0]);
|
|
972
|
-
if (a.ty.k === "str")
|
|
973
|
-
return { code: `fv::js_number(${strArg(a.code)})`, ty: FLOAT };
|
|
974
|
-
if (isNumber(a.ty))
|
|
975
|
-
return a;
|
|
976
|
-
if (a.ty.k === "bool")
|
|
977
|
-
return { code: `i64::from(${bare(a.code)})`, ty: INT };
|
|
978
|
-
return fail(comp, "`Number()` takes a string, a number or a boolean that is present", n);
|
|
979
|
-
}
|
|
980
|
-
if ((callee.name === "parseInt" && (args.length === 1 || args.length === 2)) || (callee.name === "parseFloat" && args.length === 1)) {
|
|
981
|
-
const a = expr(s, args[0]);
|
|
982
|
-
// Not a number: `parseInt` reads it as a string, so `parseInt(0.0000005)` is 5.
|
|
983
|
-
if (a.ty.k !== "str")
|
|
984
|
-
fail(comp, `\`${callee.name}()\` takes a string`, args[0]);
|
|
985
|
-
if (callee.name === "parseFloat")
|
|
986
|
-
return { code: `fv::js_parse_float(${strArg(a.code)})`, ty: FLOAT };
|
|
987
|
-
const radix = args[1];
|
|
988
|
-
if (radix && !(radix.type === "NumericLiteral" && (radix.value === 10 || radix.value === 16))) {
|
|
989
|
-
fail(comp, "`parseInt()` takes a radix of 10 or 16, written as a literal", radix);
|
|
990
|
-
}
|
|
991
|
-
return { code: `fv::js_parse_int(${strArg(a.code)}, ${radix ? radix.value : 0})`, ty: FLOAT };
|
|
992
|
-
}
|
|
993
|
-
return fail(comp, `\`${callee.name}()\` is not available when rendering on the server`, n);
|
|
994
|
-
}
|
|
995
|
-
if (callee.type === "MemberExpression" && !callee.computed) {
|
|
996
|
-
const method = callee.property.name;
|
|
997
|
-
if (callee.object.type === "Identifier" && callee.object.name === "Array" && method === "isArray") {
|
|
998
|
-
const a = expr(s, args[0]);
|
|
999
|
-
if (a.ty.k === "query")
|
|
1000
|
-
return { code: `${atom(a.code)}.is_array()`, ty: BOOL };
|
|
1001
|
-
const is = a.ty.k === "list";
|
|
1002
|
-
return { code: String(is), ty: BOOL, konst: is };
|
|
1003
|
-
}
|
|
1004
|
-
if (callee.object.type === "Identifier" && callee.object.name === "Object")
|
|
1005
|
-
return objectCall(s, method, args, n);
|
|
1006
|
-
if (callee.object.type === "Identifier" && callee.object.name === "JSON" && method === "stringify" && args.length === 1) {
|
|
1007
|
-
const v = expr(s, args[0]);
|
|
1008
|
-
// A boolean is one of two literals, which need no `String`.
|
|
1009
|
-
if (v.ty.k === "bool")
|
|
1010
|
-
return { code: `if ${condition(v.code)} { "true" } else { "false" }`, ty: STR };
|
|
1011
|
-
return { code: `&*${json(s, v, args[0])}`, ty: STR };
|
|
1012
|
-
}
|
|
1013
|
-
// `$t(…)` in the template, and \`t(…)\` from \`useI18n()\`.
|
|
1014
|
-
if (callee.object.type === "Identifier" && callee.object.name === "_ctx" && method === "$t")
|
|
1015
|
-
return translate(s, args, n);
|
|
1016
|
-
if (callee.object.type === "Identifier" && callee.object.name === "$setup" && s.i18nT.has(method))
|
|
1017
|
-
return translate(s, args, n);
|
|
1018
|
-
if (callee.object.type === "Identifier" && (callee.object.name === "$setup" || callee.object.name === "_ctx")) {
|
|
1019
|
-
const helper = s.helpers.get(method);
|
|
1020
|
-
if (helper)
|
|
1021
|
-
return helperCall(s, helper, args, n);
|
|
1022
|
-
}
|
|
1023
|
-
// `Math`, on numbers, as JavaScript computes it.
|
|
1024
|
-
if (callee.object.type === "Identifier" && callee.object.name === "Math") {
|
|
1025
|
-
const vals = args.map((a) => expr(s, a));
|
|
1026
|
-
if (vals.length && vals.every((v) => isNumber(v.ty))) {
|
|
1027
|
-
const ints = vals.every((v) => v.ty.k === "int");
|
|
1028
|
-
if ((method === "max" || method === "min") && vals.length >= 2) {
|
|
1029
|
-
if (ints)
|
|
1030
|
-
return { code: vals.slice(1).reduce((acc, v) => `${atom(acc)}.${method}(${bare(v.code)})`, vals[0].code), ty: INT };
|
|
1031
|
-
return { code: vals.slice(1).reduce((acc, v) => `fv::js_${method}(${bare(acc)}, ${bare(asF64(v))})`, asF64(vals[0])), ty: FLOAT };
|
|
1032
|
-
}
|
|
1033
|
-
if (vals.length === 1) {
|
|
1034
|
-
const x = vals[0];
|
|
1035
|
-
if (method === "abs")
|
|
1036
|
-
return ints ? intFromF64(`${receiver(asF64(x))}.abs()`) : { code: `${receiver(x.code)}.abs()`, ty: FLOAT };
|
|
1037
|
-
if (method === "round")
|
|
1038
|
-
return { code: `fv::js_round(${bare(asF64(x))})`, ty: FLOAT };
|
|
1039
|
-
if (method === "floor" || method === "ceil" || method === "trunc")
|
|
1040
|
-
return { code: `${receiver(asF64(x))}.${method}()`, ty: FLOAT };
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
return fail(comp, `\`Math.${method}()\` is supported on numbers as \`max\`, \`min\`, \`abs\`, \`round\`, \`floor\`, \`ceil\` and \`trunc\``, n);
|
|
1044
|
-
}
|
|
1045
|
-
const target = expr(s, callee.object);
|
|
1046
|
-
if (target.ty.k === "str") {
|
|
1047
|
-
const lone = loneOf(target);
|
|
1048
|
-
switch (args.length === 0 ? method : "") {
|
|
1049
|
-
case "trim":
|
|
1050
|
-
return { code: `fv::js_trim(${strArg(target.code)})`, ty: STR, ...lone };
|
|
1051
|
-
case "trimStart":
|
|
1052
|
-
return { code: `fv::js_trim_start(${strArg(target.code)})`, ty: STR, ...lone };
|
|
1053
|
-
case "trimEnd":
|
|
1054
|
-
return { code: `fv::js_trim_end(${strArg(target.code)})`, ty: STR, ...lone };
|
|
1055
|
-
// Unicode's default case mappings, which JavaScript and Rust both apply, final sigma included.
|
|
1056
|
-
case "toUpperCase":
|
|
1057
|
-
return { code: `&*${receiver(target.code)}.to_uppercase()`, ty: STR, ...lone };
|
|
1058
|
-
case "toLowerCase":
|
|
1059
|
-
return { code: `&*${receiver(target.code)}.to_lowercase()`, ty: STR, ...lone };
|
|
1060
|
-
case "toString":
|
|
1061
|
-
return target;
|
|
1062
|
-
}
|
|
1063
|
-
const m = stringMethod(s, target, method, args, n);
|
|
1064
|
-
if (m)
|
|
1065
|
-
return m;
|
|
1066
|
-
}
|
|
1067
|
-
if (isNumber(target.ty) && method === "toFixed" && args.length <= 1) {
|
|
1068
|
-
// The digits are a literal, as they almost always are: 0 to 100.
|
|
1069
|
-
const d = args.length ? args[0] : { type: "NumericLiteral", value: 0 };
|
|
1070
|
-
if (d.type !== "NumericLiteral" || !Number.isInteger(d.value) || d.value < 0 || d.value > 100) {
|
|
1071
|
-
fail(comp, "`.toFixed()` takes a literal number of digits, from 0 to 100", n);
|
|
1072
|
-
}
|
|
1073
|
-
return { code: `&*fv::js_to_fixed(${bare(asF64(target))}, ${d.value})`, ty: STR };
|
|
1074
|
-
}
|
|
1075
|
-
if (isNumber(target.ty) && method === "toString" && args.length === 0) {
|
|
1076
|
-
return { code: `&*fv::Js(${bare(target.code)}).to_string()`, ty: STR };
|
|
1077
|
-
}
|
|
1078
|
-
if (target.ty.k === "list") {
|
|
1079
|
-
// A list the props hold, of strings or integers, is searched and joined in place below.
|
|
1080
|
-
const inPlace = target.iter === undefined && !target.lone && (target.ty.of.k === "str" || target.ty.of.k === "int");
|
|
1081
|
-
const listed = listMethod(s, target, method, args, n) ?? (inPlace ? null : computedListMethod(s, target, method, args, n));
|
|
1082
|
-
if (listed)
|
|
1083
|
-
return listed;
|
|
1084
|
-
}
|
|
1085
|
-
if (target.ty.k === "list" && (target.ty.of.k === "str" || target.ty.of.k === "int")) {
|
|
1086
|
-
const of = target.ty.of;
|
|
1087
|
-
if (method === "includes" && args.length === 1) {
|
|
1088
|
-
const v = expr(s, args[0]);
|
|
1089
|
-
if (!sameTy(v.ty, of))
|
|
1090
|
-
fail(comp, "`.includes()` looks for a value of the list's own type", args[0]);
|
|
1091
|
-
if (of.k === "int")
|
|
1092
|
-
return { code: `${atom(target.code)}.contains(&${operand(v.code, UNARY)})`, ty: BOOL };
|
|
1093
|
-
const test = v.code === '""' ? "v.is_empty()" : binary("&**v", "==", v.code);
|
|
1094
|
-
return { code: `${atom(target.code)}.iter().any(|v| ${test})`, ty: BOOL };
|
|
1095
|
-
}
|
|
1096
|
-
if (method === "join" && args.length <= 1) {
|
|
1097
|
-
// JavaScript joins with a comma when given no separator.
|
|
1098
|
-
const sep = args.length ? expr(s, args[0]) : { code: '","', ty: STR };
|
|
1099
|
-
if (sep.ty.k !== "str")
|
|
1100
|
-
fail(comp, "`.join()` takes a string", args[0]);
|
|
1101
|
-
const each = of.k === "str" ? `${atom(target.code)}.iter().map(|v| &**v)` : `${atom(target.code)}.iter().map(|v| fv::Js(*v).to_string())`;
|
|
1102
|
-
return { code: `&*${each}.collect::<Vec<_>>().join(${strArg(sep.code)})`, ty: STR, ...loneOf(sep) };
|
|
1103
|
-
}
|
|
1104
|
-
}
|
|
1105
|
-
return fail(comp, `\`.${method}()\` is not supported`, n);
|
|
1106
|
-
}
|
|
1107
|
-
return fail(comp, "this call is not supported", n);
|
|
1108
|
-
}
|
|
1109
|
-
/** Whether `n` is `Object.<method>(…)`. */
|
|
1110
|
-
export function isObjectCall(n, method) {
|
|
1111
|
-
return (n?.type === "CallExpression" && n.callee.type === "MemberExpression" && !n.callee.computed &&
|
|
1112
|
-
n.callee.object.type === "Identifier" && n.callee.object.name === "Object" && n.callee.property.name === method);
|
|
1113
|
-
}
|
|
1114
|
-
/** A string method beyond trimming and case mapping, or `null` when `method` is none of them.
|
|
1115
|
-
* Indices and lengths count UTF-16 code units, as JavaScript's do; a result that may hold half of a
|
|
1116
|
-
* surrogate pair is marked `lone`. */
|
|
1117
|
-
function stringMethod(s, target, method, args, n) {
|
|
1118
|
-
const comp = s.comp;
|
|
1119
|
-
// As an argument, where a `&str` parameter coerces the borrow.
|
|
1120
|
-
const t = strArg(target.code);
|
|
1121
|
-
const arity = (least, most) => {
|
|
1122
|
-
if (args.length < least || args.length > most) {
|
|
1123
|
-
fail(comp, `\`.${method}()\` takes ${least === most ? least : `${least} to ${most}`} argument${most === 1 ? "" : "s"} here`, n);
|
|
1124
|
-
}
|
|
1125
|
-
};
|
|
1126
|
-
const str = (i) => {
|
|
1127
|
-
if (args[i]?.type === "RegExpLiteral")
|
|
1128
|
-
fail(comp, `\`.${method}()\` with a regular expression, which the server does not run: give it a string`, args[i]);
|
|
1129
|
-
const v = expr(s, args[i]);
|
|
1130
|
-
if (v.ty.k !== "str")
|
|
1131
|
-
fail(comp, `\`.${method}()\` takes a string that is present`, args[i]);
|
|
1132
|
-
return v;
|
|
1133
|
-
};
|
|
1134
|
-
/** A number argument as an `f64`; `fallback` when it is left out or `undefined`. */
|
|
1135
|
-
const num = (i, fallback) => {
|
|
1136
|
-
if (args[i] === undefined)
|
|
1137
|
-
return fallback;
|
|
1138
|
-
const v = expr(s, args[i]);
|
|
1139
|
-
if (v.ty.k === "undef")
|
|
1140
|
-
return fallback;
|
|
1141
|
-
if (!isNumber(v.ty))
|
|
1142
|
-
fail(comp, `\`.${method}()\` takes a number that is present`, args[i]);
|
|
1143
|
-
return asF64(v);
|
|
1144
|
-
};
|
|
1145
|
-
const cut = { lone: true };
|
|
1146
|
-
switch (method) {
|
|
1147
|
-
case "includes":
|
|
1148
|
-
case "startsWith":
|
|
1149
|
-
case "endsWith": {
|
|
1150
|
-
// Without a position: `includes(x, 3)` would start the search part way.
|
|
1151
|
-
arity(1, 1);
|
|
1152
|
-
const x = str(0);
|
|
1153
|
-
meet(comp, target, x, `\`.${method}()\``, n, "search");
|
|
1154
|
-
const rust = { includes: "contains", startsWith: "starts_with", endsWith: "ends_with" }[method];
|
|
1155
|
-
return { code: `${receiver(target.code)}.${rust}(${bare(x.code)})`, ty: BOOL };
|
|
1156
|
-
}
|
|
1157
|
-
case "indexOf":
|
|
1158
|
-
case "lastIndexOf": {
|
|
1159
|
-
arity(1, 1);
|
|
1160
|
-
const x = str(0);
|
|
1161
|
-
meet(comp, target, x, `\`.${method}()\``, n, "search");
|
|
1162
|
-
return { code: `fv::js_${method === "indexOf" ? "index_of" : "last_index_of"}(${t}, ${strArg(x.code)})`, ty: INT };
|
|
1163
|
-
}
|
|
1164
|
-
case "slice":
|
|
1165
|
-
case "substring": {
|
|
1166
|
-
arity(0, 2);
|
|
1167
|
-
const end = num(1, "");
|
|
1168
|
-
return { code: `&*fv::js_${method}(${t}, ${num(0, "0.0")}, ${end ? `Some(${end})` : "None"})`, ty: STR, ...cut };
|
|
1169
|
-
}
|
|
1170
|
-
case "at": {
|
|
1171
|
-
arity(1, 1);
|
|
1172
|
-
const at = `fv::js_at(${t}, ${num(0, "0.0")})`;
|
|
1173
|
-
if (!isTemporary(target))
|
|
1174
|
-
return { code: at, ty: opt(STR), ...cut };
|
|
1175
|
-
// Of a string built here, the character is copied out, to outlive it.
|
|
1176
|
-
const held = `${at}.map(|v| std::borrow::Cow::<str>::Owned(v.to_owned()))`;
|
|
1177
|
-
return { code: `${atom(held)}.as_deref()`, ty: opt(STR), held, ...cut };
|
|
1178
|
-
}
|
|
1179
|
-
case "charAt":
|
|
1180
|
-
arity(0, 1);
|
|
1181
|
-
// It borrows from the string, so it is a temporary when the string is.
|
|
1182
|
-
return { code: `${isTemporary(target) ? "&*" : ""}fv::js_char_at(${t}, ${num(0, "0.0")})`, ty: STR, ...cut };
|
|
1183
|
-
case "split": {
|
|
1184
|
-
// Without a limit, and with a string separator: `split()` alone gives the whole string.
|
|
1185
|
-
arity(1, 1);
|
|
1186
|
-
const sep = str(0);
|
|
1187
|
-
meet(comp, target, sep, "`.split()`", n, "search");
|
|
1188
|
-
// An empty separator cuts between code units, so a pair in two.
|
|
1189
|
-
return computed(`fv::js_split(${t}, ${strArg(sep.code)}).into_iter()`, STR, target.lone || !nonEmptyLiteral(sep));
|
|
1190
|
-
}
|
|
1191
|
-
case "replace":
|
|
1192
|
-
case "replaceAll": {
|
|
1193
|
-
arity(2, 2);
|
|
1194
|
-
if (args[1].type === "ArrowFunctionExpression" || args[1].type === "FunctionExpression") {
|
|
1195
|
-
fail(comp, `\`.${method}()\` with a function, which the server does not run: give it a string`, args[1]);
|
|
1196
|
-
}
|
|
1197
|
-
const [pattern, replacement] = [str(0), str(1)];
|
|
1198
|
-
meet(comp, target, pattern, `\`.${method}()\``, n, "search");
|
|
1199
|
-
meet(comp, target, replacement, `\`.${method}()\``, n, "join");
|
|
1200
|
-
// `replaceAll("", …)` matches between code units, which cuts every pair in two.
|
|
1201
|
-
const lone = target.lone || replacement.lone || (method === "replaceAll" && !nonEmptyLiteral(pattern));
|
|
1202
|
-
return { code: `&*fv::js_${method === "replace" ? "replace" : "replace_all"}(${t}, ${strArg(pattern.code)}, ${strArg(replacement.code)})`, ty: STR, ...(lone ? cut : {}) };
|
|
1203
|
-
}
|
|
1204
|
-
case "padStart":
|
|
1205
|
-
case "padEnd": {
|
|
1206
|
-
arity(1, 2);
|
|
1207
|
-
const fill = args.length > 1 ? str(1) : { code: '" "', ty: STR };
|
|
1208
|
-
// The fill repeats itself, and is cut short where a pair may be: before a string starting
|
|
1209
|
-
// with a half, the two would join.
|
|
1210
|
-
const whole = fill.code.startsWith('"') && !/[\u{10000}-\u{10FFFF}]/u.test(fill.code);
|
|
1211
|
-
if (fill.lone || (method === "padStart" && target.lone && !whole))
|
|
1212
|
-
fail(comp, lonely(`\`.${method}()\``), n);
|
|
1213
|
-
return { code: `&*fv::js_${method === "padStart" ? "pad_start" : "pad_end"}(${t}, ${num(0, "0.0")}, ${strArg(fill.code)})`, ty: STR, ...(target.lone || !whole ? cut : {}) };
|
|
1214
|
-
}
|
|
1215
|
-
case "repeat": {
|
|
1216
|
-
arity(1, 1);
|
|
1217
|
-
// JavaScript throws a `RangeError` for a negative or infinite count.
|
|
1218
|
-
const c = args[0];
|
|
1219
|
-
if ((c.type === "UnaryExpression" && c.operator === "-" && c.argument.type === "NumericLiteral" && c.argument.value > 0) || (c.type === "Identifier" && c.name === "Infinity")) {
|
|
1220
|
-
fail(comp, "`.repeat()` with a negative or infinite count, which throws a `RangeError`", c);
|
|
1221
|
-
}
|
|
1222
|
-
// Repeated, a string ending with one half and starting with the other joins them.
|
|
1223
|
-
if (target.lone)
|
|
1224
|
-
fail(comp, lonely("`.repeat()`"), n);
|
|
1225
|
-
return { code: `&*fv::js_repeat(${t}, ${num(0, "0.0")})`, ty: STR };
|
|
1226
|
-
}
|
|
1227
|
-
case "toLocaleUpperCase":
|
|
1228
|
-
case "toLocaleLowerCase":
|
|
1229
|
-
return fail(comp, `\`.${method}()\` maps case by the locale the server runs in, which the browser need not share: use \`.${method.replace("Locale", "")}()\``, n);
|
|
1230
|
-
}
|
|
1231
|
-
return null;
|
|
1232
|
-
}
|
|
1233
|
-
/** `JSON.stringify(v)` as a Rust `String`: of a string, a number, a boolean, or a list of those. */
|
|
1234
|
-
function json(s, v, n) {
|
|
1235
|
-
// A half of a pair is written as an escape, `"\ud83e"`, which ferrovue cannot know to write.
|
|
1236
|
-
if (v.lone)
|
|
1237
|
-
fail(s.comp, lonely("`JSON.stringify()`"), n);
|
|
1238
|
-
const one = (code, ty) => {
|
|
1239
|
-
switch (ty.k) {
|
|
1240
|
-
case "str":
|
|
1241
|
-
return `fv::js_json_string(${code === "v" ? "&v" : strArg(code)})`;
|
|
1242
|
-
case "int":
|
|
1243
|
-
return `fv::Js(${bare(code)}).to_string()`;
|
|
1244
|
-
case "float":
|
|
1245
|
-
return `fv::js_json_number(${bare(code)})`;
|
|
1246
|
-
case "bool":
|
|
1247
|
-
return `(if ${condition(code)} { "true" } else { "false" }).to_owned()`;
|
|
1248
|
-
default:
|
|
1249
|
-
return fail(s.comp, "`JSON.stringify()` of a string, a number, a boolean or a list of those, present", n);
|
|
1250
|
-
}
|
|
1251
|
-
};
|
|
1252
|
-
if (v.ty.k === "list") {
|
|
1253
|
-
// A routine of the item alone is mapped as itself, without a closure around it.
|
|
1254
|
-
const each = one("v", v.ty.of);
|
|
1255
|
-
const f = /^([\w:]+)\(v\)$/.exec(each)?.[1];
|
|
1256
|
-
return `format!("[{}]", ${items(v)}.map(${f ?? `|v| ${each}`}).collect::<Vec<_>>().join(","))`;
|
|
1257
|
-
}
|
|
1258
|
-
return one(v.code, v.ty);
|
|
1259
|
-
}
|
|
1260
|
-
export function helperCall(s, name, args, n) {
|
|
1261
|
-
const h = ctx.helpers[name];
|
|
1262
|
-
if (args.length !== h.params.length)
|
|
1263
|
-
fail(s.comp, `\`${name}\` takes ${h.params.length} argument(s)`, n);
|
|
1264
|
-
const code = args.map((a, i) => bare(coerce(s.comp, expr(s, a), h.params[i], a))).join(", ");
|
|
1265
|
-
s.helperBytes.n += h.maxLen;
|
|
1266
|
-
return { code: `${h.rust}(${code})`, ty: h.ret };
|
|
1267
|
-
}
|