ferrovue 0.1.0 → 0.2.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 +206 -20
- package/dist/attrs.d.ts +51 -0
- package/dist/attrs.js +488 -0
- package/dist/cli.js +68 -10
- package/dist/client.d.ts +27 -0
- package/dist/client.js +44 -0
- package/dist/compiler.d.ts +16 -53
- package/dist/compiler.js +87 -3178
- package/dist/component.d.ts +6 -0
- package/dist/component.js +251 -0
- package/dist/context.d.ts +151 -0
- package/dist/context.js +84 -0
- package/dist/emitter.d.ts +23 -0
- package/dist/emitter.js +58 -0
- package/dist/expr.d.ts +76 -0
- package/dist/expr.js +1267 -0
- package/dist/fixture.d.ts +38 -0
- package/dist/fixture.js +78 -0
- package/dist/i18n.d.ts +27 -0
- package/dist/i18n.js +175 -0
- package/dist/lists.d.ts +19 -0
- package/dist/lists.js +253 -0
- package/dist/model.d.ts +248 -0
- package/dist/model.js +102 -0
- package/dist/parens.d.ts +51 -0
- package/dist/parens.js +239 -0
- package/dist/router.d.ts +17 -0
- package/dist/router.js +272 -0
- package/dist/rust.d.ts +14 -0
- package/dist/rust.js +496 -0
- package/dist/scoped.d.ts +10 -0
- package/dist/scoped.js +90 -0
- package/dist/script.d.ts +16 -0
- package/dist/script.js +316 -0
- package/dist/stores.d.ts +6 -0
- package/dist/stores.js +227 -0
- package/dist/template.d.ts +45 -0
- package/dist/template.js +939 -0
- package/dist/testing.d.ts +2 -22
- package/dist/testing.js +11 -55
- package/dist/types.d.ts +1 -0
- package/dist/typescript.d.ts +31 -0
- package/dist/typescript.js +310 -0
- package/dist/vite.d.ts +23 -0
- package/dist/vite.js +114 -0
- package/package.json +26 -8
package/dist/expr.js
ADDED
|
@@ -0,0 +1,1267 @@
|
|
|
1
|
+
/* Expressions translated to Rust, each with the type it evaluates to. */
|
|
2
|
+
import { BOOL, fail, FLOAT, GenError, INT, opt, rustStr, sameTy, snake, STR, UNDEF } from "./model.js";
|
|
3
|
+
import { CONFIG_FILE, ctx } from "./context.js";
|
|
4
|
+
import { lookupStruct, markHome } from "./typescript.js";
|
|
5
|
+
import { translate } from "./i18n.js";
|
|
6
|
+
import { AS, atom, bare, binary, condition, enclosed, FLIPPED, ifElse, logical, negate, not, occurrences, operand, receiver, strArg, UNARY } from "./parens.js";
|
|
7
|
+
import { collected, computed, computedListMethod, items, listMethod, objectCall } from "./lists.js";
|
|
8
|
+
/** The refusal for a string that may hold half of a surrogate pair, used where that half matters. */
|
|
9
|
+
export function lonely(what) {
|
|
10
|
+
return `${what} with a string that may hold half of a surrogate pair (cut by \`slice\`, \`substring\`, \`at\`, \`charAt\`, \`split("")\` and the like): JavaScript keeps the half, which can match, order or join with another where ferrovue holds U+FFFD`;
|
|
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. */
|
|
138
|
+
export function describeTy(ty) {
|
|
139
|
+
switch (ty.k) {
|
|
140
|
+
case "str":
|
|
141
|
+
return "a string";
|
|
142
|
+
case "int":
|
|
143
|
+
return "a number";
|
|
144
|
+
case "float":
|
|
145
|
+
return "a number";
|
|
146
|
+
case "bool":
|
|
147
|
+
return "a boolean";
|
|
148
|
+
case "list":
|
|
149
|
+
return "a list";
|
|
150
|
+
case "record":
|
|
151
|
+
return "a record";
|
|
152
|
+
case "struct":
|
|
153
|
+
case "child":
|
|
154
|
+
return "an object";
|
|
155
|
+
case "opt":
|
|
156
|
+
return `an optional ${describeTy(ty.of).replace(/^an? /, "")}`;
|
|
157
|
+
case "undef":
|
|
158
|
+
return "`undefined`";
|
|
159
|
+
case "query":
|
|
160
|
+
return "a query value";
|
|
161
|
+
default:
|
|
162
|
+
return "a value of another kind";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export function fieldVal(comp, base, ty, js, node) {
|
|
166
|
+
if (ty.k !== "struct")
|
|
167
|
+
fail(comp, `\`.${js}\` on a value that is not an object`, node);
|
|
168
|
+
const { st, owner } = lookupStruct(comp, ty);
|
|
169
|
+
const found = st?.fields.find((x) => x.js === js);
|
|
170
|
+
if (!found)
|
|
171
|
+
fail(comp, `\`${ty.name}\` has no field \`${js}\``, node);
|
|
172
|
+
// A field of a type another component declares names that component's types.
|
|
173
|
+
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
|
+
if (st.slot)
|
|
176
|
+
return { code: `${base}.${f.rust}`, ty: f.ty };
|
|
177
|
+
const place = `${base}.${f.rust}`;
|
|
178
|
+
if (f.dflt !== undefined && f.ty.k === "opt") {
|
|
179
|
+
const of = f.ty.of;
|
|
180
|
+
if (of.k === "str")
|
|
181
|
+
return { code: `${place}.as_deref().unwrap_or(${f.dflt})`, ty: STR };
|
|
182
|
+
if (of.k === "list")
|
|
183
|
+
return { code: `${place}.as_deref().unwrap_or(${f.dflt})`, ty: of };
|
|
184
|
+
return { code: `${place}.unwrap_or(${f.dflt})`, ty: of };
|
|
185
|
+
}
|
|
186
|
+
switch (f.ty.k) {
|
|
187
|
+
case "str":
|
|
188
|
+
return { code: `&*${place}`, ty: STR };
|
|
189
|
+
case "html":
|
|
190
|
+
case "child":
|
|
191
|
+
return { code: `&${place}`, ty: f.ty };
|
|
192
|
+
case "opt":
|
|
193
|
+
if (f.ty.of.k === "html")
|
|
194
|
+
return { code: `${place}.as_ref()`, ty: f.ty };
|
|
195
|
+
if (f.ty.of.k === "str")
|
|
196
|
+
return { code: `${place}.as_deref()`, ty: f.ty };
|
|
197
|
+
if (f.ty.of.k === "int" || f.ty.of.k === "float" || f.ty.of.k === "bool")
|
|
198
|
+
return { code: place, ty: f.ty };
|
|
199
|
+
// An object or a list, borrowed: `Option<&T>`, which a `v-if` narrows to the `&T`.
|
|
200
|
+
return { code: `${place}.as_ref()`, ty: f.ty };
|
|
201
|
+
default:
|
|
202
|
+
return { code: place, ty: f.ty };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
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
|
+
export function coerce(comp, v, want, node) {
|
|
246
|
+
if (sameTy(v.ty, want))
|
|
247
|
+
return v.code;
|
|
248
|
+
if (want.k === "opt" && v.ty.k === "undef")
|
|
249
|
+
return "None";
|
|
250
|
+
if (want.k === "opt" && sameTy(v.ty, want.of))
|
|
251
|
+
return `Some(${bare(v.code)})`;
|
|
252
|
+
// An integer is a number too, where a fraction is expected.
|
|
253
|
+
if (want.k === "float" && v.ty.k === "int")
|
|
254
|
+
return asF64(v);
|
|
255
|
+
if (want.k === "opt" && want.of.k === "float" && v.ty.k === "int")
|
|
256
|
+
return `Some(${asF64(v)})`;
|
|
257
|
+
return fail(comp, `a ${JSON.stringify(v.ty)} where ${JSON.stringify(want)} is expected`, node);
|
|
258
|
+
}
|
|
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
|
+
export function childOf(name) {
|
|
351
|
+
const c = ctx.components.get(name);
|
|
352
|
+
if (!c)
|
|
353
|
+
throw new GenError(`\`Props\` is imported from ${name}.vue, which is not among the components compiled`);
|
|
354
|
+
return c;
|
|
355
|
+
}
|
|
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
|
+
export function expr(s, n) {
|
|
526
|
+
const comp = s.comp;
|
|
527
|
+
const path = pathOf(n);
|
|
528
|
+
if (path !== null) {
|
|
529
|
+
const narrowed = s.narrowed.get(path);
|
|
530
|
+
if (narrowed)
|
|
531
|
+
return narrowed;
|
|
532
|
+
}
|
|
533
|
+
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
|
+
if (isObjectCall(n.object, "entries") && n.object.arguments.length === 1) {
|
|
536
|
+
const r = expr(s, n.object.arguments[0]);
|
|
537
|
+
if (r.ty.k === "record")
|
|
538
|
+
return { code: `${atom(r.code)}.len() as i64`, ty: INT };
|
|
539
|
+
}
|
|
540
|
+
const base = expr(s, n.object);
|
|
541
|
+
if (base.ty.k === "list" && base.iter !== undefined) {
|
|
542
|
+
// A list split out is a `Vec` already, whose length is known.
|
|
543
|
+
const vec = /^(.*)\.into_iter\(\)$/s.exec(base.iter)?.[1];
|
|
544
|
+
// Nor does a `map` at the end change how many there are.
|
|
545
|
+
if (vec === undefined) {
|
|
546
|
+
const at = base.iter.lastIndexOf(".map(");
|
|
547
|
+
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
|
+
const held = /^(.*)\.iter\(\)$/s.exec(mapped)?.[1];
|
|
550
|
+
return { code: held !== undefined ? `${atom(held)}.len() as i64` : `${atom(mapped)}.count() as i64`, ty: INT };
|
|
551
|
+
}
|
|
552
|
+
return { code: vec !== undefined ? `${atom(vec)}.len() as i64` : `${atom(base.iter)}.count() as i64`, ty: INT };
|
|
553
|
+
}
|
|
554
|
+
if (base.ty.k === "list")
|
|
555
|
+
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
|
+
if (base.ty.k === "str")
|
|
558
|
+
return { code: `fv::js_length(${strArg(base.code)})`, ty: INT };
|
|
559
|
+
}
|
|
560
|
+
if (n.type === "BinaryExpression" && n.operator === "+") {
|
|
561
|
+
const a = expr(s, n.left);
|
|
562
|
+
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
|
+
const joinable = (t) => t.k === "str" || isNumber(t);
|
|
566
|
+
if ((a.ty.k === "str" || b.ty.k === "str") && joinable(a.ty) && joinable(b.ty)) {
|
|
567
|
+
meet(comp, a, b, "`+`", n, "join");
|
|
568
|
+
// A number is written as JavaScript writes it, rounded beyond 2⁵³.
|
|
569
|
+
return formatted(comp, [a, b], n, "`+`");
|
|
570
|
+
}
|
|
571
|
+
// Two numbers: arithmetic, as `n + 1` in a template means.
|
|
572
|
+
if (isNumber(a.ty) && isNumber(b.ty))
|
|
573
|
+
return arithmetic(a, "+", b, a.ty.k === "int" && b.ty.k === "int");
|
|
574
|
+
return fail(comp, "`+` joins a string to a string or a number, or adds two numbers", n);
|
|
575
|
+
}
|
|
576
|
+
if (n.type === "BinaryExpression" && ["-", "*", "%", "/", "<", ">", "<=", ">="].includes(n.operator)) {
|
|
577
|
+
const a = expr(s, n.left);
|
|
578
|
+
const b = expr(s, n.right);
|
|
579
|
+
const comparison = ["<", ">", "<=", ">="].includes(n.operator);
|
|
580
|
+
// Two strings, ordered as JavaScript orders them: by UTF-16 code unit.
|
|
581
|
+
if (comparison && a.ty.k === "str" && b.ty.k === "str") {
|
|
582
|
+
meet(comp, a, b, `\`${n.operator}\``, n, "order");
|
|
583
|
+
const is = { "<": "lt", ">": "gt", "<=": "le", ">=": "ge" }[n.operator];
|
|
584
|
+
return { code: `fv::js_cmp(${strArg(a.code)}, ${strArg(b.code)}).is_${is}()`, ty: BOOL };
|
|
585
|
+
}
|
|
586
|
+
if (!isNumber(a.ty) || !isNumber(b.ty)) {
|
|
587
|
+
const why = a.ty.k === "opt" || b.ty.k === "opt"
|
|
588
|
+
? "narrow an optional one with `v-if` first"
|
|
589
|
+
: `the other is ${describeTy(isNumber(a.ty) ? b.ty : a.ty)}`;
|
|
590
|
+
const between = comparison ? "two numbers, or two strings," : "two numbers";
|
|
591
|
+
return fail(comp, `\`${n.operator}\` is supported between ${between} that are present: ${why}`, n);
|
|
592
|
+
}
|
|
593
|
+
if (["<", ">", "<=", ">="].includes(n.operator))
|
|
594
|
+
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
|
+
const intDivisor = n.right.type === "NumericLiteral" && Number.isInteger(n.right.value) && n.right.value !== 0;
|
|
598
|
+
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
|
+
return arithmetic(a, n.operator, b, int);
|
|
601
|
+
}
|
|
602
|
+
switch (n.type) {
|
|
603
|
+
case "TemplateLiteral": {
|
|
604
|
+
// `${a}-${b}`: each part written as JavaScript writes it into a string.
|
|
605
|
+
const parts = [];
|
|
606
|
+
n.quasis.forEach((q, i) => {
|
|
607
|
+
parts.push(q.value.cooked);
|
|
608
|
+
if (i < n.expressions.length)
|
|
609
|
+
parts.push(expr(s, n.expressions[i]));
|
|
610
|
+
});
|
|
611
|
+
return formatted(comp, parts, n, "a template literal");
|
|
612
|
+
}
|
|
613
|
+
case "ArrayExpression": {
|
|
614
|
+
// A list of literals or values of one scalar type, as a Rust array.
|
|
615
|
+
if (n.elements.length === 0)
|
|
616
|
+
return { code: "[]", ty: { k: "list", of: UNDEF } };
|
|
617
|
+
const values = n.elements.map((el) => {
|
|
618
|
+
if (!el || el.type === "SpreadElement")
|
|
619
|
+
fail(comp, "an array literal holds plain values", n);
|
|
620
|
+
return expr(s, el);
|
|
621
|
+
});
|
|
622
|
+
const of = values[0].ty;
|
|
623
|
+
if (!(of.k === "str" || of.k === "int" || of.k === "float" || of.k === "bool") || values.some((v) => !sameTy(v.ty, of))) {
|
|
624
|
+
fail(comp, "an array literal holds strings, numbers or booleans, all of one type", n);
|
|
625
|
+
}
|
|
626
|
+
return { code: `[${values.map((v) => bare(v.code)).join(", ")}]`, ty: { k: "list", of }, ...loneOf(...values) };
|
|
627
|
+
}
|
|
628
|
+
case "OptionalMemberExpression": {
|
|
629
|
+
// `a?.b`: the field of an optional object when it is present.
|
|
630
|
+
if (n.computed)
|
|
631
|
+
return fail(comp, "computed member access", n);
|
|
632
|
+
const base = expr(s, n.object);
|
|
633
|
+
if (base.ty.k !== "opt")
|
|
634
|
+
return fieldVal(comp, base.code, base.ty, n.property.name, n);
|
|
635
|
+
const f = fieldVal(comp, "v", base.ty.of, n.property.name, n);
|
|
636
|
+
if (f.ty.k === "opt")
|
|
637
|
+
return { code: `${atom(base.code)}.and_then(|v| ${f.code})`, ty: f.ty };
|
|
638
|
+
return { code: `${atom(base.code)}.map(|v| ${f.code})`, ty: opt(f.ty) };
|
|
639
|
+
}
|
|
640
|
+
case "StringLiteral":
|
|
641
|
+
return { code: rustStr(n.value), ty: STR };
|
|
642
|
+
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
|
+
return numberVal(n.value, Number.isSafeInteger(n.value));
|
|
646
|
+
case "BooleanLiteral":
|
|
647
|
+
return { code: String(n.value), ty: BOOL, konst: n.value };
|
|
648
|
+
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
|
+
return fail(comp, "`null`: compare with `undefined`, which is what an absent prop is", n);
|
|
652
|
+
case "Identifier": {
|
|
653
|
+
if (n.name === "undefined")
|
|
654
|
+
return { code: "None", ty: UNDEF };
|
|
655
|
+
const local = s.locals.get(n.name) ?? s.setup.get(n.name);
|
|
656
|
+
if (local)
|
|
657
|
+
return local;
|
|
658
|
+
if (s.clientOnly.has(n.name)) {
|
|
659
|
+
return fail(comp, `\`${n.name}\` is set up in a way the server cannot evaluate: ${s.clientOnly.get(n.name)}`, n);
|
|
660
|
+
}
|
|
661
|
+
if (n.name === s.propsIdent)
|
|
662
|
+
return { code: "props", ty: { k: "struct", name: "Props" } };
|
|
663
|
+
return fail(comp, `\`${n.name}\` is not available when rendering on the server`, n);
|
|
664
|
+
}
|
|
665
|
+
case "MemberExpression": {
|
|
666
|
+
// `count.value` in the script: the ref's value, which is what the binding already reads.
|
|
667
|
+
if (!n.computed && n.property.name === "value" && n.object.type === "Identifier" && s.refs.has(n.object.name) && !s.locals.has(n.object.name)) {
|
|
668
|
+
return expr(s, n.object);
|
|
669
|
+
}
|
|
670
|
+
if (n.computed) {
|
|
671
|
+
if (n.object.type === "Identifier" && n.object.name === "$setup" && n.property.type === "StringLiteral") {
|
|
672
|
+
return expr(s, { ...n.property, type: "Identifier", name: n.property.value });
|
|
673
|
+
}
|
|
674
|
+
if (n.property.type === "StringLiteral") {
|
|
675
|
+
const base = expr(s, n.object);
|
|
676
|
+
if (base.ty.k === "params" || base.ty.k === "queryobj")
|
|
677
|
+
return routeField(s, base, n.property.value, n);
|
|
678
|
+
}
|
|
679
|
+
return fail(comp, "computed member access", n);
|
|
680
|
+
}
|
|
681
|
+
const prop = n.property.name;
|
|
682
|
+
// `$slots.side`: whether the parent gave that slot any content.
|
|
683
|
+
const slotsObject = (n.object.type === "Identifier" && n.object.name === "$slots") ||
|
|
684
|
+
(n.object.type === "MemberExpression" && !n.object.computed && n.object.object.type === "Identifier" &&
|
|
685
|
+
n.object.object.name === "_ctx" && n.object.property.name === "$slots");
|
|
686
|
+
if (slotsObject) {
|
|
687
|
+
if (!comp.slotNames.includes(prop))
|
|
688
|
+
fail(comp, `\`$slots.${prop}\` names a slot this template does not render`, n);
|
|
689
|
+
return { code: `fv_slots.${snake(prop)}.is_some()`, ty: BOOL };
|
|
690
|
+
}
|
|
691
|
+
if (n.object.type === "Identifier") {
|
|
692
|
+
switch (n.object.name) {
|
|
693
|
+
case "$props":
|
|
694
|
+
return fieldVal(comp, "props", { k: "struct", name: "Props" }, prop, n);
|
|
695
|
+
case "$setup":
|
|
696
|
+
case "_ctx": {
|
|
697
|
+
if (n.object.name === "_ctx" && prop === "$route")
|
|
698
|
+
return theRoute(s, n);
|
|
699
|
+
const v = s.setup.get(prop);
|
|
700
|
+
if (v)
|
|
701
|
+
return v;
|
|
702
|
+
if (s.clientOnly.has(prop)) {
|
|
703
|
+
return fail(comp, `\`${prop}\` is set up in a way the server cannot evaluate: ${s.clientOnly.get(prop)}`, n);
|
|
704
|
+
}
|
|
705
|
+
if (n.object.name === "_ctx" && comp.props.fields.some((f) => f.js === prop)) {
|
|
706
|
+
return fieldVal(comp, "props", { k: "struct", name: "Props" }, prop, n);
|
|
707
|
+
}
|
|
708
|
+
return fail(comp, `\`${prop}\` is not available when rendering on the server`, n);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const base = expr(s, n.object);
|
|
713
|
+
if (base.ty.k === "route" || base.ty.k === "params" || base.ty.k === "queryobj")
|
|
714
|
+
return routeField(s, base, prop, n);
|
|
715
|
+
return storeGetter(s, base, prop, n) ?? fieldVal(comp, base.code, base.ty, prop, n);
|
|
716
|
+
}
|
|
717
|
+
case "CallExpression":
|
|
718
|
+
return call(s, n);
|
|
719
|
+
case "LogicalExpression": {
|
|
720
|
+
// `x !== undefined && …` and `!x || …` / `x === undefined || …`: booleans, with `x` present on
|
|
721
|
+
// the right.
|
|
722
|
+
if (n.operator === "&&" || n.operator === "||") {
|
|
723
|
+
const p = presence(s, n.left);
|
|
724
|
+
if (p && p.negated === (n.operator === "||") && (p.negated || !p.truthy)) {
|
|
725
|
+
let right;
|
|
726
|
+
const code = narrowing(s, p, (inner) => (right = expr(inner, n.right)).code, () => String(n.operator === "||"));
|
|
727
|
+
if (right?.ty.k === "bool")
|
|
728
|
+
return { code, ty: BOOL };
|
|
729
|
+
return fail(comp, `\`${n.operator}\` is supported between booleans only`, n);
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
const a = expr(s, n.left);
|
|
733
|
+
const b = expr(s, n.right);
|
|
734
|
+
if (n.operator === "??") {
|
|
735
|
+
// A query value falls back for `undefined` and `null`, and stays a query value: an array
|
|
736
|
+
// given more than once stays an array.
|
|
737
|
+
if (a.ty.k === "query") {
|
|
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
|
+
}
|
|
742
|
+
if (a.ty.k !== "opt")
|
|
743
|
+
return a;
|
|
744
|
+
if (b.ty.k === "undef")
|
|
745
|
+
return a;
|
|
746
|
+
if (b.iter !== undefined)
|
|
747
|
+
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
|
+
if (a.held !== undefined && b.ty.k === "str")
|
|
750
|
+
return { code: `&*${atom(a.held)}.unwrap_or(${asCow(b)})`, ty: STR, ...loneOf(a, b) };
|
|
751
|
+
if (a.ty.of.k === "str" && b.ty.k === "str" && isTemporary(b)) {
|
|
752
|
+
return { code: `&*${atom(a.code)}.map(std::borrow::Cow::<str>::Borrowed).unwrap_or(${asCow(b)})`, ty: STR, ...loneOf(a, b) };
|
|
753
|
+
}
|
|
754
|
+
if (sameTy(a.ty.of, b.ty))
|
|
755
|
+
return { code: `${atom(a.code)}.unwrap_or(${bare(b.code)})`, ty: b.ty, ...loneOf(a, b) };
|
|
756
|
+
if (sameTy(a.ty, b.ty))
|
|
757
|
+
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
|
+
if (a.ty.of.k === "float" && b.ty.k === "int")
|
|
760
|
+
return { code: `${atom(a.code)}.unwrap_or(${bare(asF64(b))})`, ty: FLOAT };
|
|
761
|
+
if (a.ty.of.k === "int" && b.ty.k === "float")
|
|
762
|
+
return { code: `${atom(a.code)}.map(|v| v as f64).unwrap_or(${bare(b.code)})`, ty: FLOAT };
|
|
763
|
+
return fail(comp, "`??` between different types", n);
|
|
764
|
+
}
|
|
765
|
+
if (n.operator === "||") {
|
|
766
|
+
if (a.ty.k === "bool" && b.ty.k === "bool")
|
|
767
|
+
return boolOf(logical(a.code, "||", b.code));
|
|
768
|
+
if (b.ty.k === "undef") {
|
|
769
|
+
// `x || undefined`: the value when it is truthy, nothing otherwise.
|
|
770
|
+
const inner = a.ty.k === "opt" ? a.ty.of : a.ty;
|
|
771
|
+
const src = a.ty.k === "opt" ? atom(a.code) : `Some(${bare(a.code)})`;
|
|
772
|
+
return { code: `${src}.filter(|v| ${truthy({ code: "*v", ty: inner })})`, ty: opt(inner) };
|
|
773
|
+
}
|
|
774
|
+
if (a.ty.k === "str" && b.ty.k === "str" && known(a) !== undefined)
|
|
775
|
+
return known(a) ? a : b;
|
|
776
|
+
if (a.ty.k === "str" && b.ty.k === "str" && (isTemporary(a) || isTemporary(b))) {
|
|
777
|
+
return { code: `&*{ let a = ${asCow(a)}; if !a.is_empty() { a } else { ${asCow(b)} } }`, ty: STR, ...loneOf(a, b) };
|
|
778
|
+
}
|
|
779
|
+
if (sameTy(a.ty, b.ty) && (a.ty.k === "str" || isNumber(a.ty))) {
|
|
780
|
+
const k = known(a);
|
|
781
|
+
if (k !== undefined)
|
|
782
|
+
return k ? a : b;
|
|
783
|
+
const otherwise = a.ty.k === "str" ? strArg(b.code) : bare(b.code);
|
|
784
|
+
return { code: `{ let a = ${bare(a.code)}; if ${truthy({ code: "a", ty: a.ty })} { a } else { ${otherwise} } }`, ty: a.ty, ...loneOf(a, b) };
|
|
785
|
+
}
|
|
786
|
+
return fail(comp, "`||` between these types", n);
|
|
787
|
+
}
|
|
788
|
+
if (n.operator === "&&" && a.ty.k === "bool" && b.ty.k === "bool")
|
|
789
|
+
return boolOf(logical(a.code, "&&", b.code));
|
|
790
|
+
return fail(comp, `\`${n.operator}\` is supported between booleans only`, n);
|
|
791
|
+
}
|
|
792
|
+
case "UnaryExpression":
|
|
793
|
+
if (n.operator === "!") {
|
|
794
|
+
return negatedOrder(s, n.argument) ?? boolOf(not(truthy(expr(s, n.argument))));
|
|
795
|
+
}
|
|
796
|
+
if (n.operator === "-") {
|
|
797
|
+
const a = expr(s, n.argument);
|
|
798
|
+
if (isNumber(a.ty) && a.num !== undefined)
|
|
799
|
+
return numberVal(-a.num, a.ty.k === "int");
|
|
800
|
+
if (a.ty.k === "int")
|
|
801
|
+
return intFromF64(negate(asF64(a)));
|
|
802
|
+
if (a.ty.k === "float")
|
|
803
|
+
return { code: negate(a.code), ty: FLOAT };
|
|
804
|
+
}
|
|
805
|
+
return fail(comp, `unary \`${n.operator}\``, n);
|
|
806
|
+
case "BinaryExpression": {
|
|
807
|
+
if (n.operator !== "===" && n.operator !== "!==")
|
|
808
|
+
fail(comp, `\`${n.operator}\``, n);
|
|
809
|
+
const a = expr(s, n.left);
|
|
810
|
+
const b = expr(s, n.right);
|
|
811
|
+
let eq;
|
|
812
|
+
const scalar = (t) => t.k === "str" || t.k === "int" || t.k === "bool";
|
|
813
|
+
const strish = (t) => t.k === "str" || (t.k === "opt" && t.of.k === "str");
|
|
814
|
+
if (strish(a.ty) && strish(b.ty))
|
|
815
|
+
meet(comp, a, b, `\`${n.operator}\``, n, "equal");
|
|
816
|
+
// A query value equals a string only when it is that single value.
|
|
817
|
+
if (a.ty.k === "query" && b.ty.k === "str")
|
|
818
|
+
eq = `${atom(a.code)}.is(${strArg(b.code)})`;
|
|
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()`;
|
|
825
|
+
else if (b.ty.k === "undef" && a.ty.k === "opt")
|
|
826
|
+
eq = `${atom(a.code)}.is_none()`;
|
|
827
|
+
else if (a.ty.k === "undef" && b.ty.k === "opt")
|
|
828
|
+
eq = `${atom(b.code)}.is_none()`;
|
|
829
|
+
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
|
+
const same = a.ty.k === b.ty.k;
|
|
832
|
+
const konst = n.operator === "===" ? same : !same;
|
|
833
|
+
return { code: String(konst), ty: BOOL, konst };
|
|
834
|
+
}
|
|
835
|
+
else if (isNumber(a.ty) && isNumber(b.ty))
|
|
836
|
+
eq = compare(a, "==", b).code;
|
|
837
|
+
// Two string literals are compared now.
|
|
838
|
+
else if (a.ty.k === "str" && b.ty.k === "str" && known(a) !== undefined && known(b) !== undefined)
|
|
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
|
+
}
|
|
844
|
+
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
|
+
const [k, other] = a.konst !== undefined ? [a.konst, b.code] : [b.konst, a.code];
|
|
847
|
+
eq = k ? other : not(other);
|
|
848
|
+
}
|
|
849
|
+
else if (sameTy(a.ty, b.ty) && (scalar(a.ty) || (a.ty.k === "opt" && scalar(a.ty.of)))) {
|
|
850
|
+
eq = binary(a.code, "==", b.code);
|
|
851
|
+
}
|
|
852
|
+
else if (a.ty.k === "opt" && sameTy(a.ty.of, b.ty) && scalar(b.ty))
|
|
853
|
+
eq = binary(a.code, "==", `Some(${bare(b.code)})`);
|
|
854
|
+
else if (b.ty.k === "opt" && sameTy(b.ty.of, a.ty) && scalar(a.ty))
|
|
855
|
+
eq = binary(`Some(${bare(a.code)})`, "==", b.code);
|
|
856
|
+
else
|
|
857
|
+
return fail(comp, "`===` between these types", n);
|
|
858
|
+
return boolOf(n.operator === "===" ? eq : not(eq));
|
|
859
|
+
}
|
|
860
|
+
case "ConditionalExpression": {
|
|
861
|
+
const t = expr(s, n.test);
|
|
862
|
+
const k = known(t);
|
|
863
|
+
if (k !== undefined)
|
|
864
|
+
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
|
+
const p = presence(s, n.test);
|
|
867
|
+
const name = p ? `n${++ctx.narrowCount}` : "";
|
|
868
|
+
const a = expr(p && !p.negated ? narrowTo(s, p, name) : s, n.consequent);
|
|
869
|
+
const b = expr(p && p.negated ? narrowTo(s, p, name) : s, n.alternate);
|
|
870
|
+
// Two branches alike are one value, whatever the test.
|
|
871
|
+
if (sameTy(a.ty, b.ty) && a.code === b.code && (!name || !occurrences(a.code, name)))
|
|
872
|
+
return a;
|
|
873
|
+
// A string in the second branch is coerced to the `&str` of the first.
|
|
874
|
+
const second = (code) => (a.ty.k === "str" ? strArg(code) : bare(code));
|
|
875
|
+
const choose = (yes, no) => {
|
|
876
|
+
if (!p)
|
|
877
|
+
return choice(truthy(t), bare(yes), second(no));
|
|
878
|
+
const [present, absent] = p.negated ? [bare(no), second(yes)] : [bare(yes), second(no)];
|
|
879
|
+
// `x ? x : undefined` is the `Option` itself.
|
|
880
|
+
if (present === `Some(${name})` && absent === "None")
|
|
881
|
+
return p.option;
|
|
882
|
+
// `x ? x : y`: the value, or the fallback.
|
|
883
|
+
if (present === name)
|
|
884
|
+
return `${atom(p.option)}.unwrap_or(${absent})`;
|
|
885
|
+
if (occurrences(present, name))
|
|
886
|
+
return `if ${p.pattern(name)} { ${present} } else { ${absent} }`;
|
|
887
|
+
return choice(p.present, present, absent);
|
|
888
|
+
};
|
|
889
|
+
const lone = loneOf(a, b);
|
|
890
|
+
if (sameTy(a.ty, b.ty) && a.ty.k === "str" && (isTemporary(a) || isTemporary(b))) {
|
|
891
|
+
return { code: `&*(${choose(asCow(a), asCow(b))})`, ty: STR, ...lone };
|
|
892
|
+
}
|
|
893
|
+
// Two lists, one of them computed: both branches collect their items, to have one type.
|
|
894
|
+
if (sameTy(a.ty, b.ty) && a.ty.k === "list" && (a.iter !== undefined || b.iter !== undefined)) {
|
|
895
|
+
const both = `(${choose(collected(a), collected(b))})`;
|
|
896
|
+
return { code: both, ty: a.ty, iter: `${both}.into_iter()`, ...lone };
|
|
897
|
+
}
|
|
898
|
+
const optStr = (v) => v.ty.k === "undef" || (v.ty.k === "opt" && v.ty.of.k === "str") || v.ty.k === "str";
|
|
899
|
+
if (sameTy(a.ty, b.ty) && a.ty.k !== "opt")
|
|
900
|
+
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
|
+
if (isNumber(a.ty) && isNumber(b.ty))
|
|
903
|
+
return { code: choose(asF64(a), asF64(b)), ty: FLOAT };
|
|
904
|
+
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
|
+
const ty = opt(a.ty.k === "undef" || b.ty.k === "opt" ? b.ty : a.ty);
|
|
906
|
+
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
|
+
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
|
+
const both = atom(choose(held(a), held(b)));
|
|
910
|
+
return { code: `${both}.as_deref()`, ty, held: both, ...lone };
|
|
911
|
+
}
|
|
912
|
+
if (sameTy(a.ty, b.ty))
|
|
913
|
+
return { code: choose(a.code, b.code), ty: a.ty, ...lone };
|
|
914
|
+
return { code: choose(coerce(comp, a, ty, n), coerce(comp, b, ty, n)), ty, ...lone };
|
|
915
|
+
}
|
|
916
|
+
return fail(comp, "the two branches of `?:` differ in type", n);
|
|
917
|
+
}
|
|
918
|
+
default:
|
|
919
|
+
return fail(comp, `\`${n.type}\` is not supported in an island template`, n);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
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
|
+
}
|