ferrovue 0.2.0 → 0.4.0

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