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