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.
Files changed (85) hide show
  1. package/README.md +32 -13
  2. package/dist/attrs.d.ts +2 -52
  3. package/dist/attrs.js +18 -448
  4. package/dist/calls.d.ts +4 -0
  5. package/dist/calls.js +219 -0
  6. package/dist/children.d.ts +9 -0
  7. package/dist/children.js +273 -0
  8. package/dist/classes.d.ts +13 -0
  9. package/dist/classes.js +176 -0
  10. package/dist/cli.js +0 -17
  11. package/dist/client.js +0 -12
  12. package/dist/compiler.js +25 -90
  13. package/dist/component.js +10 -86
  14. package/dist/context.d.ts +4 -66
  15. package/dist/context.js +2 -33
  16. package/dist/diff.d.ts +0 -3
  17. package/dist/diff.js +0 -9
  18. package/dist/emitter.d.ts +0 -8
  19. package/dist/emitter.js +0 -12
  20. package/dist/expr.d.ts +0 -70
  21. package/dist/expr.js +27 -890
  22. package/dist/fallthrough.d.ts +9 -0
  23. package/dist/fallthrough.js +78 -0
  24. package/dist/fixture.d.ts +2 -1
  25. package/dist/fixture.js +37 -22
  26. package/dist/index.d.ts +2 -0
  27. package/dist/index.js +1 -0
  28. package/dist/islands.d.ts +1 -0
  29. package/dist/islands.js +1 -0
  30. package/dist/lists.d.ts +0 -11
  31. package/dist/lists.js +4 -39
  32. package/dist/loops.d.ts +3 -0
  33. package/dist/loops.js +164 -0
  34. package/dist/model.d.ts +14 -143
  35. package/dist/model.js +27 -12
  36. package/dist/narrowing.d.ts +19 -0
  37. package/dist/narrowing.js +126 -0
  38. package/dist/numbers.d.ts +8 -0
  39. package/dist/numbers.js +73 -0
  40. package/dist/parens.d.ts +0 -27
  41. package/dist/parens.js +0 -44
  42. package/dist/plugin.d.ts +84 -0
  43. package/dist/plugin.js +26 -0
  44. package/dist/plugins/i18n.d.ts +23 -0
  45. package/dist/{i18n.js → plugins/i18n.js} +76 -19
  46. package/dist/plugins/index.d.ts +2 -0
  47. package/dist/plugins/index.js +8 -0
  48. package/dist/plugins/router-link.d.ts +8 -0
  49. package/dist/{router.js → plugins/router-link.js} +19 -61
  50. package/dist/plugins/router.d.ts +42 -0
  51. package/dist/plugins/router.js +256 -0
  52. package/dist/plugins/scoped.d.ts +12 -0
  53. package/dist/plugins/scoped.js +125 -0
  54. package/dist/plugins/shared-types.d.ts +2 -0
  55. package/dist/plugins/shared-types.js +7 -0
  56. package/dist/plugins/stores.d.ts +37 -0
  57. package/dist/plugins/stores.js +377 -0
  58. package/dist/plugins/teleport.d.ts +5 -0
  59. package/dist/plugins/teleport.js +55 -0
  60. package/dist/rust.d.ts +1 -7
  61. package/dist/rust.js +38 -160
  62. package/dist/script.d.ts +0 -6
  63. package/dist/script.js +13 -128
  64. package/dist/slots.d.ts +13 -0
  65. package/dist/slots.js +160 -0
  66. package/dist/strings.d.ts +13 -0
  67. package/dist/strings.js +208 -0
  68. package/dist/styles.d.ts +14 -0
  69. package/dist/styles.js +181 -0
  70. package/dist/template.d.ts +1 -42
  71. package/dist/template.js +14 -738
  72. package/dist/testing.d.ts +1 -0
  73. package/dist/testing.js +1 -8
  74. package/dist/types.d.ts +2 -0
  75. package/dist/typescript.d.ts +0 -13
  76. package/dist/typescript.js +8 -41
  77. package/dist/vite.d.ts +1 -0
  78. package/dist/vite.js +1 -23
  79. package/package.json +7 -2
  80. package/dist/i18n.d.ts +0 -27
  81. package/dist/router.d.ts +0 -17
  82. package/dist/scoped.d.ts +0 -24
  83. package/dist/scoped.js +0 -177
  84. package/dist/stores.d.ts +0 -6
  85. 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 { CONFIG_FILE, ctx } from "./context.js";
2
+ import { ctx } from "./context.js";
4
3
  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
- // 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
- if (base.ty.k === "params" || base.ty.k === "queryobj")
703
- return routeField(s, base, n.property.value, n);
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
- if (n.object.name === "_ctx" && prop === "$route")
724
- return theRoute(s, n);
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
- if (base.ty.k === "route" || base.ty.k === "params" || base.ty.k === "queryobj")
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
- // A query value falls back for `undefined` and `null`, and stays a query value: an array
765
- // given more than once stays an array.
766
- if (a.ty.k === "query") {
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
- // `typeof route.query.q === "string"`: a single value, not `null`, absent or repeated.
839
- const tested = typeofString(n);
840
- if (tested) {
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
- // A query value equals a string only when it is that single value.
855
- if (a.ty.k === "query" && b.ty.k === "str")
856
- eq = `${atom(a.code)}.is(${strArg(b.code)})`;
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
- // Two string literals are compared now.
876
- else if (a.ty.k === "str" && b.ty.k === "str" && known(a) !== undefined && known(b) !== undefined)
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
- }