ferrovue 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,939 @@
1
+ /* The compiled template's statements: pushes, conditions, lists, child components and slots. */
2
+ import { fail, GenError, INT, rustStr, sameTy, snake, STR } from "./model.js";
3
+ import { ctx } from "./context.js";
4
+ import { markHome } from "./typescript.js";
5
+ import { asF64, boolOf, cond, expr, fieldVal, isObjectCall, known, narrowTo, presence, truthy } from "./expr.js";
6
+ import { atom, bare, CMP, condition, occurrences, operand, OR, strArg, UNARY } from "./parens.js";
7
+ import { Emitter } from "./emitter.js";
8
+ import { IGNORED_PROPS, interpolate, renderAttr, renderAttrs, renderClass, renderDynamicAttr, renderStyle } from "./attrs.js";
9
+ import { routerLink } from "./router.js";
10
+ import { rustTy } from "./rust.js";
11
+ /** One `${...}` inside a pushed template literal. */
12
+ export function slot(s, e, n) {
13
+ // The slot scope id slot content is given, written onto its elements: nothing unless the
14
+ // component it is given to passes one.
15
+ if (n.type === "Identifier" && n.name === "_scopeId") {
16
+ if (s.sid !== null)
17
+ e.stmt(`out.push_str(${s.sid});`);
18
+ return;
19
+ }
20
+ if (n.type === "CallExpression" && n.callee.type === "Identifier") {
21
+ const a = n.arguments;
22
+ switch (n.callee.name) {
23
+ case "_ssrInterpolate":
24
+ interpolate(e, expr(s, a[0]));
25
+ return;
26
+ case "_ssrRenderAttr":
27
+ if (a[0].type !== "StringLiteral")
28
+ fail(s.comp, "attribute names are literal", n);
29
+ renderAttr(e, a[0].value, expr(s, a[1]));
30
+ return;
31
+ // `:hidden`, whose rendering depends on the value's type: Vue cannot decide it at compile time.
32
+ case "_ssrRenderDynamicAttr":
33
+ if (a[0].type !== "StringLiteral")
34
+ fail(s.comp, "attribute names are literal", n);
35
+ renderDynamicAttr(s, e, a[0].value, expr(s, a[1]), n);
36
+ return;
37
+ case "_ssrRenderAttrs":
38
+ if (a.length > 1)
39
+ fail(s.comp, "`ssrRenderAttrs` with a tag argument", n);
40
+ renderAttrs(s, e, a[0]);
41
+ return;
42
+ case "_ssrRenderClass":
43
+ renderClass(s, e, a[0]);
44
+ return;
45
+ case "_ssrRenderStyle":
46
+ renderStyle(s, e, a[0]);
47
+ return;
48
+ }
49
+ }
50
+ /* `v-html`, which Vue compiles to the bare value with an empty-string fallback and no escaping.
51
+ * Only a `TrustedHtml` prop may arrive here; anything else would be a string written raw. */
52
+ if (n.type === "LogicalExpression" && n.operator === "??" && n.right.type === "StringLiteral" && n.right.value === "") {
53
+ const v = expr(s, n.left);
54
+ if (v.ty.k === "html") {
55
+ e.stmt(`fv::trusted_into(out, ${v.code});`);
56
+ return;
57
+ }
58
+ if (v.ty.k === "opt" && v.ty.of.k === "html") {
59
+ e.open(`if let Some(html) = ${v.code}`);
60
+ e.stmt("fv::trusted_into(out, html);");
61
+ e.close();
62
+ return;
63
+ }
64
+ fail(s.comp, "`v-html` renders only a `TrustedHtml` prop (from `ferrovue/types`)", n);
65
+ }
66
+ if (n.type === "ConditionalExpression" && n.consequent.type === "StringLiteral" && n.alternate.type === "StringLiteral") {
67
+ const t = expr(s, n.test);
68
+ const k = known(t);
69
+ if (k !== undefined) {
70
+ e.lit(k ? n.consequent.value : n.alternate.value);
71
+ return;
72
+ }
73
+ e.open(`if ${condition(truthy(t))}`);
74
+ e.lit(n.consequent.value);
75
+ if (n.alternate.value) {
76
+ e.close(" else {");
77
+ e.lit(n.alternate.value);
78
+ }
79
+ e.close();
80
+ return;
81
+ }
82
+ fail(s.comp, "this expression cannot be rendered on the server", n);
83
+ }
84
+ /** `isComment` in `@vue/server-renderer`: a chunk that is only comments and whitespace. */
85
+ export function isComment(text) {
86
+ if (!/^<!--[\s\S]*-->$/.test(text))
87
+ return false;
88
+ return text.length <= 8 || !text.replace(/<!--[^]*?-->/gm, "").trim();
89
+ }
90
+ /** Whether a `_push` argument is content to `ssrRenderSlot`, rather than only comments. */
91
+ export function pushesContent(s, n) {
92
+ if (n.type === "StringLiteral")
93
+ return !isComment(n.value);
94
+ if (n.type === "TemplateLiteral") {
95
+ if (n.expressions.length === 0)
96
+ return !isComment(n.quasis[0].value.cooked);
97
+ if (n.quasis[0].value.cooked.startsWith("<!--")) {
98
+ fail(s.comp, "slot content that starts with a comment holding an interpolation", n);
99
+ }
100
+ return true;
101
+ }
102
+ // A component's render is a buffer, never a comment.
103
+ return true;
104
+ }
105
+ export function push(s, e, n) {
106
+ if (s.fill && pushesContent(s, n))
107
+ e.stmt("filled = true;");
108
+ const text = n.type === "StringLiteral" ? n.value : n.type === "TemplateLiteral" && !n.expressions.length ? n.quasis[0].value.cooked : null;
109
+ if (s.vnode && text === "<!---->") {
110
+ e.lit("<!--v-if-->");
111
+ return;
112
+ }
113
+ if (n.type === "StringLiteral") {
114
+ e.lit(n.value);
115
+ return;
116
+ }
117
+ if (n.type === "TemplateLiteral") {
118
+ n.quasis.forEach((q, i) => {
119
+ e.lit(q.value.cooked);
120
+ if (i < n.expressions.length)
121
+ slot(s, e, n.expressions[i]);
122
+ });
123
+ return;
124
+ }
125
+ if (n.type === "CallExpression" && n.callee.type === "Identifier" && n.callee.name === "_ssrRenderComponent") {
126
+ const target = n.arguments[0];
127
+ const routed = target.type === "Identifier"
128
+ ? s.router.get(target.name)
129
+ : target.type === "MemberExpression" && target.object.name === "$setup"
130
+ ? s.router.get(target.computed ? target.property.value : target.property.name)
131
+ : undefined;
132
+ if (routed === "RouterLink")
133
+ routerLink(s, e, n);
134
+ else if (routed === "RouterView") {
135
+ // vue-router renders the page as its own root, which takes this component's id.
136
+ if (s.comp.scopeId !== null)
137
+ fail(s.comp, "`<RouterView>` in a component with `<style scoped>` gives the page this component's id, which the server's page does not carry", n);
138
+ e.stmt("fv_slots.router_view.render_to(out);");
139
+ }
140
+ else
141
+ renderChild(s, e, n);
142
+ return;
143
+ }
144
+ fail(s.comp, "this cannot be pushed", n);
145
+ }
146
+ /** The scope ids a child's root is handed, as a Rust `&str` (`null` for none), as
147
+ * `renderComponentSubTree` gathers them into its `attrs`: what this component passes on when the
148
+ * child is its root, unless the child sets `inheritAttrs: false`; this component's own id, the id of
149
+ * the instance that created the child's virtual node — slot content's included, which renders as
150
+ * the component that wrote it; and the slot scope id of the slot content the child is rendered in. */
151
+ function childAttrs(s, child, passesAttrs, inSlot, n) {
152
+ const base = passesAttrs && child.inheritAttrs ? s.attrs : null;
153
+ const own = s.comp.scopeId;
154
+ const slotted = inSlot ? s.sid : null;
155
+ let code;
156
+ if (base === null && slotted === null)
157
+ code = own === null ? null : rustStr(` ${own}`);
158
+ else if (own === null && slotted === null)
159
+ code = base;
160
+ else
161
+ code = `&fv::scope_attrs(${base ?? '""'}, ${own === null ? '""' : rustStr(own)}, ${slotted ?? '""'})`;
162
+ if (code !== null && !child.inherits)
163
+ fail(s.comp, `${child.name} is handed scope ids its render does not take`, n);
164
+ return code;
165
+ }
166
+ export function renderChild(s, e, n) {
167
+ const [target, rawProps, slots, , slotScopeId] = n.arguments;
168
+ // The parent's `_attrs`, which it passes on to a child that is its root.
169
+ const isAttrs = (a) => a?.type === "Identifier" && a.name === "_attrs";
170
+ const passesAttrs = isAttrs(rawProps) || (rawProps?.type === "CallExpression" && rawProps.arguments.some(isAttrs));
171
+ let local = null;
172
+ if (target.type === "MemberExpression" && target.object.name === "$setup") {
173
+ local = target.computed ? target.property.value : target.property.name;
174
+ }
175
+ const isSelf = target.type === "Identifier" && target.name === s.selfAlias.name;
176
+ const childName = isSelf ? s.comp.name : local ? s.children.get(local) : undefined;
177
+ const child = childName ? s.components.get(childName) : undefined;
178
+ // Inside a `v-for`, Vue merges a `{ ref_for: true }` marker into the props; it renders nothing.
179
+ let props = rawProps;
180
+ if (props?.type === "CallExpression" && props.callee.type === "Identifier" && props.callee.name === "_mergeProps" &&
181
+ props.arguments.length === 2 && props.arguments[0].type === "ObjectExpression" &&
182
+ props.arguments[0].properties.every((p) => p.type === "ObjectProperty" && IGNORED_PROPS.has(p.key.name ?? p.key.value))) {
183
+ props = props.arguments[1];
184
+ }
185
+ // A component at the root of the template is handed the fallthrough attributes, which an island
186
+ // never has.
187
+ if (props?.type === "CallExpression" && props.callee.type === "Identifier" && props.callee.name === "_mergeProps" &&
188
+ props.arguments.length === 2 && props.arguments[1].type === "Identifier" && props.arguments[1].name === "_attrs") {
189
+ props = props.arguments[0];
190
+ }
191
+ // A child given no props at all: Vue passes `null`, or the empty fallthrough attributes.
192
+ if ((props?.type === "Identifier" && props.name === "_attrs") || props?.type === "NullLiteral" || !props) {
193
+ props = { type: "ObjectExpression", properties: [] };
194
+ }
195
+ if (!child)
196
+ fail(s.comp, "a child component must be an imported island", n);
197
+ if (child.routerView) {
198
+ fail(s.comp, `${child.name} holds \`<RouterView>\`: the server renders it at the top, never as a child`, n);
199
+ }
200
+ // `v-bind="x"`, where `x` is exactly the child's own `Props`: handed over as it is.
201
+ if (props.type !== "ObjectExpression") {
202
+ const v = expr(s, props);
203
+ // The component itself, however the template reached it: its own `Props` struct is the type.
204
+ const own = child.name === s.comp.name && v.ty.k === "struct" && v.ty.name === "Props";
205
+ if (own || (v.ty.k === "child" && v.ty.name === child.name)) {
206
+ callChild(s, e, child, v.code, slots, childAttrs(s, child, passesAttrs, !!slotScopeId, n));
207
+ return;
208
+ }
209
+ fail(s.comp, `child props must be an object literal, or \`v-bind\` of ${child.name}'s own \`Props\``, n);
210
+ }
211
+ const given = new Map();
212
+ for (const p of props.properties) {
213
+ if (p.type !== "ObjectProperty" || p.computed)
214
+ fail(s.comp, "child props hold plain keys", p);
215
+ const key = p.key.type === "Identifier" ? p.key.name : p.key.value;
216
+ // Listeners, `v-model`'s `onUpdate:…` among them, run on the client alone; a model's modifiers
217
+ // change only what an update writes.
218
+ if (IGNORED_PROPS.has(key) || /^on[^a-z]/.test(key))
219
+ continue;
220
+ if (key.endsWith("Modifiers") && child.props.fields.some((f) => f.js === (key === "modelModifiers" ? "modelValue" : key.slice(0, -"Modifiers".length))))
221
+ continue;
222
+ if (!child.props.fields.some((f) => f.js === key)) {
223
+ // A key the child does not declare would fall through onto its root element as an
224
+ // attribute, which the generated child does not render.
225
+ fail(s.comp, `\`${key}\` is not a prop of ${child.name}`, p);
226
+ }
227
+ given.set(key, p.value);
228
+ }
229
+ const inits = child.props.fields.map((f) => {
230
+ const node = given.get(f.js);
231
+ if (!node) {
232
+ if (f.ty.k !== "opt")
233
+ fail(s.comp, `${child.name} requires \`${f.js}\``, n);
234
+ return `${f.rust}: None`;
235
+ }
236
+ // Each side's own types named by the component that declares them, so that a type the parent
237
+ // imports from the child's `.vue` file is the child's type.
238
+ const v = expr(s, node);
239
+ return fieldInit(f.rust, ownInto(s.comp, { ...v, ty: markHome(v.ty, s.comp.name) }, markHome(f.ty, child.name), node));
240
+ });
241
+ callChild(s, e, child, `&super::${child.module}::Props { ${inits.join(", ")} }`, slots, childAttrs(s, child, passesAttrs, !!slotScopeId, n));
242
+ }
243
+ /** `name: value` in a struct literal, or the name alone when the value is a variable of that name. */
244
+ export function fieldInit(name, value) {
245
+ return value === name ? name : `${name}: ${bare(value)}`;
246
+ }
247
+ /** A borrow of an object or a list a template reads. A variable already holds a borrow: a loop's
248
+ * item, a narrowed value, a setup binding, a scoped slot's prop. */
249
+ function borrowed(code) {
250
+ return code.startsWith("&") || /^\w+$/.test(code) || /^fv_sp\d+\.\w+$/.test(code) ? code : `&${operand(code, UNARY)}`;
251
+ }
252
+ /** Whether a component takes a `Slots` argument. */
253
+ export function takesSlots(c) {
254
+ return c.slotNames.length > 0 || c.routerView;
255
+ }
256
+ /** The arguments after `props` that a component's `render` takes. */
257
+ export function extraParams(c) {
258
+ return ((takesSlots(c) ? ", fv_slots: Slots<'_>" : "") +
259
+ (c.usesRoute ? ", fv_route: &fv::Route<'_>" : "") +
260
+ (c.usesStores ? ", fv_stores: &super::stores::Stores<'_>" : "") +
261
+ (c.usesI18n ? ", fv_i18n: &fv::I18n" : "") +
262
+ (c.usesTeleports ? ", fv_teleports: &fv::Teleports" : ""));
263
+ }
264
+ /** `render(out, props[, slots][, route])` for a child, with the slot content this template gives
265
+ * it as closures; `render_scoped`, with the scope ids its root is handed last, for a child that may
266
+ * be handed some. */
267
+ export function callChild(s, e, child, propsCode, slots, attrs) {
268
+ const given = new Map();
269
+ if (slots && slots.type !== "NullLiteral") {
270
+ if (slots.type !== "ObjectExpression")
271
+ fail(s.comp, "slots must be an object literal", slots);
272
+ for (const p of slots.properties) {
273
+ const key = p.key?.type === "Identifier" ? p.key.name : p.key?.value;
274
+ if (key === "_")
275
+ continue;
276
+ if (p.type !== "ObjectProperty" || p.computed)
277
+ fail(s.comp, "slots hold plain keys", p);
278
+ if (!child.slotNames.includes(key))
279
+ fail(s.comp, `${child.name} has no slot \`${key}\``, p);
280
+ given.set(key, p.value);
281
+ }
282
+ }
283
+ const m = `super::${child.module}`;
284
+ const route = (child.usesRoute ? ", fv_route" : "") + (child.usesStores ? ", fv_stores" : "") + (child.usesI18n ? ", fv_i18n" : "") + (child.usesTeleports ? ", fv_teleports" : "") +
285
+ (child.inherits ? `, ${attrs ?? '""'}` : "");
286
+ const render = child.inherits ? "render_scoped" : "render";
287
+ if (!takesSlots(child)) {
288
+ e.stmt(`${m}::${render}(out, ${propsCode}${route});`);
289
+ return;
290
+ }
291
+ e.open(`${m}::${render}(out, ${propsCode}, ${m}::Slots`);
292
+ for (const name of child.slotNames) {
293
+ const field = snake(name);
294
+ const value = given.get(name);
295
+ if (!value) {
296
+ e.stmt(`${field}: None,`);
297
+ continue;
298
+ }
299
+ const { body, param } = slotContent(s, value);
300
+ const shape = child.slotShapes.get(name);
301
+ const takesNone = param?.type === "Identifier" && param.name === "_";
302
+ // The slot scope id the child's outlets give the content, when they may give one.
303
+ const sid = child.passesSlotIds ? `fv_sid${++ctx.narrowCount}` : null;
304
+ const sidParam = sid === null ? "" : `, ${sid}: &str`;
305
+ /** The content's statements, then whether it pushed anything but comments. */
306
+ const content = (inner) => {
307
+ if (staticallyFilled(inner, body)) {
308
+ statements({ ...inner, fill: false }, e, body);
309
+ e.stmt("true");
310
+ }
311
+ else {
312
+ e.stmt("let mut filled = false;");
313
+ statements({ ...inner, fill: true }, e, body);
314
+ e.stmt("filled");
315
+ }
316
+ };
317
+ /** A parameter the content never reads, named so that Rust does not warn of it. */
318
+ const unread = (opened, binding) => {
319
+ if (binding !== null && !e.reads(binding, opened + 1))
320
+ e.replace(opened, `${binding}:`, `_${binding}:`);
321
+ };
322
+ if (shape) {
323
+ // A scoped slot: content given the outlet's props, bound as the parent destructured them.
324
+ const sp = `fv_sp${++ctx.narrowCount}`;
325
+ const ty = { k: "struct", name: shape.name, home: child.name };
326
+ const locals = new Map(s.locals);
327
+ if (param?.type === "ObjectPattern") {
328
+ for (const p of param.properties) {
329
+ if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
330
+ fail(s.comp, "slot props are destructured into plain names, without defaults", p);
331
+ }
332
+ locals.set(p.value.name, fieldVal(s.comp, sp, ty, p.key.name ?? p.key.value, p));
333
+ }
334
+ }
335
+ else if (param?.type === "Identifier" && !takesNone) {
336
+ locals.set(param.name, { code: sp, ty });
337
+ }
338
+ else if (!takesNone)
339
+ fail(s.comp, "slot props are a name or an object pattern", param);
340
+ const life = shape.fields.some((f) => slotFieldBorrows(f.ty)) ? "<'_>" : "";
341
+ e.open(`${field}: Some(&|out: &mut String, ${sp}: &${m}::${shape.name}${life}${sidParam}| -> bool`);
342
+ const opened = e.lines.length - 1;
343
+ content({ ...s, locals, sid });
344
+ unread(opened, sp);
345
+ unread(opened, sid);
346
+ e.close("),");
347
+ continue;
348
+ }
349
+ if (!takesNone)
350
+ fail(s.comp, `\`<slot${name === "default" ? "" : ` name="${name}"`}>\` in ${child.name} passes no props`, param);
351
+ const inner = { ...s, sid };
352
+ if (sid !== null) {
353
+ e.open(`${field}: Some(fv::Slot::slotted(&|out: &mut String${sidParam}| -> bool`);
354
+ const opened = e.lines.length - 1;
355
+ content(inner);
356
+ unread(opened, sid);
357
+ e.close(")),");
358
+ }
359
+ else if (staticallyFilled(inner, body)) {
360
+ e.open(`${field}: Some(fv::Slot::new(&|out: &mut String|`);
361
+ statements({ ...inner, fill: false }, e, body);
362
+ e.close(")),");
363
+ }
364
+ else {
365
+ e.open(`${field}: Some(fv::Slot::markup(&|out: &mut String| -> bool`);
366
+ e.stmt("let mut filled = false;");
367
+ statements({ ...inner, fill: true }, e, body);
368
+ e.stmt("filled");
369
+ e.close(")),");
370
+ }
371
+ }
372
+ e.close(`${route});`);
373
+ }
374
+ /** The statements a parent's slot content pushes: the `if (_push)` half of its `_withCtx`. */
375
+ export function slotBody(s, value) {
376
+ return slotContent(s, value).body;
377
+ }
378
+ /** A parent's slot content: what it pushes, and the parameter its scoped slot props are bound to
379
+ * (`_` when it takes none). */
380
+ export function slotContent(s, value) {
381
+ const fn = value.type === "CallExpression" && value.callee.type === "Identifier" && value.callee.name === "_withCtx"
382
+ ? value.arguments[0]
383
+ : null;
384
+ if (fn?.type !== "ArrowFunctionExpression" || fn.body.type !== "BlockStatement") {
385
+ fail(s.comp, "unexpected slot content", value);
386
+ }
387
+ const branch = fn.body.body.length === 1 ? fn.body.body[0] : null;
388
+ if (branch?.type !== "IfStatement" || branch.test.type !== "Identifier" || branch.test.name !== "_push") {
389
+ fail(s.comp, "unexpected slot content", fn);
390
+ }
391
+ const body = branch.consequent.type === "BlockStatement" ? branch.consequent.body : [branch.consequent];
392
+ return { body, param: fn.params[0] };
393
+ }
394
+ /** Whether slot content always pushes something that is not a comment: a push of content outside
395
+ * any `if` or loop. Then nothing has to be decided at run time. */
396
+ export function staticallyFilled(s, body) {
397
+ return body.some((st) => st.type === "ExpressionStatement" && st.expression.type === "CallExpression" &&
398
+ st.expression.callee.type === "Identifier" && st.expression.callee.name === "_push" &&
399
+ pushesContent(s, st.expression.arguments[0]));
400
+ }
401
+ /** `RowSlotProps`, `RowSlot`: the names of a scoped slot's props struct and content type. */
402
+ export function slotTypeName(slotName, suffix) {
403
+ return slotName.split(/[-_]/).map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join("") + suffix;
404
+ }
405
+ /** Whether a scoped slot prop's Rust type borrows, and so needs the props' lifetime `'v`. */
406
+ export function slotFieldBorrows(ty) {
407
+ const copied = (t) => t.k === "int" || t.k === "float" || t.k === "bool";
408
+ return !(copied(ty) || (ty.k === "opt" && copied(ty.of)));
409
+ }
410
+ /** A scoped slot prop's Rust type: a scalar copied, anything else borrowed from the render for `'v`. */
411
+ export function slotFieldTy(ty, comp) {
412
+ const owned = (t) => rustTy(t, comp).replace(/'a\b/g, "'v");
413
+ switch (ty.k) {
414
+ case "int":
415
+ return "i64";
416
+ case "float":
417
+ return "f64";
418
+ case "bool":
419
+ return "bool";
420
+ case "str":
421
+ return "&'v str";
422
+ case "list":
423
+ return `&'v [${owned(ty.of)}]`;
424
+ case "struct":
425
+ case "child":
426
+ case "html":
427
+ return `&'v ${owned(ty)}`;
428
+ case "opt":
429
+ return `Option<${slotFieldTy(ty.of, comp)}>`;
430
+ default:
431
+ throw new GenError(`no Rust type for a slot prop of type ${JSON.stringify(ty)}`);
432
+ }
433
+ }
434
+ /** A value an outlet passes, as the scoped slot prop field holds it. */
435
+ export function slotFieldValue(s, v, n) {
436
+ switch (v.ty.k) {
437
+ case "str":
438
+ return strArg(v.code);
439
+ case "int":
440
+ case "float":
441
+ case "bool":
442
+ return bare(v.code);
443
+ case "list":
444
+ // An array literal is a Rust array of `&str`, and a computed list holds its items in a form
445
+ // of its own: neither is the list a slot prop borrows.
446
+ if (v.code.startsWith("["))
447
+ fail(s.comp, "a slot prop is not an array literal: pass a list the component holds", n);
448
+ if (v.iter !== undefined)
449
+ fail(s.comp, "a slot prop is not a computed list: pass a list the component holds", n);
450
+ return borrowed(v.code);
451
+ case "struct":
452
+ case "child":
453
+ case "html":
454
+ return borrowed(v.code);
455
+ case "opt":
456
+ if (v.ty.of.k === "list")
457
+ return `${atom(v.code)}.map(|v| &v[..])`;
458
+ if (v.ty.of.k === "opt" || v.ty.of.k === "undef")
459
+ break;
460
+ return v.code;
461
+ }
462
+ return fail(s.comp, "a slot prop is a string, a number, a boolean, an object or a list", n);
463
+ }
464
+ /** `<slot name="x" :prop="…">fallback</slot>`: `ssrRenderSlot`, with the props a scoped slot passes. */
465
+ export function slotOutlet(s, e, c) {
466
+ const [, nameNode, slotProps, fallback] = c.arguments;
467
+ if (nameNode?.type !== "StringLiteral")
468
+ fail(s.comp, "a slot's name is literal", c);
469
+ if (slotProps?.type !== "ObjectExpression")
470
+ fail(s.comp, "a slot's props are attributes or a `v-bind` object literal", c);
471
+ const slotName = nameNode.value;
472
+ const field = `fv_slots.${snake(slotName)}`;
473
+ const outlet = `\`<slot${slotName === "default" ? "" : ` name="${slotName}"`}>\``;
474
+ let fn = "fv::slot_into";
475
+ let args = field;
476
+ if (slotProps.properties.length) {
477
+ const fields = [];
478
+ const values = [];
479
+ for (const p of slotProps.properties) {
480
+ if (p.type !== "ObjectProperty" || p.computed)
481
+ fail(s.comp, "slot props hold plain names", p);
482
+ const key = p.key.name ?? p.key.value;
483
+ if (!/^[A-Za-z_$][\w$]*$/.test(key))
484
+ fail(s.comp, `slot prop \`${key}\` is not a plain name; write it in camelCase`, p);
485
+ const v = expr(s, p.value);
486
+ fields.push({ js: key, rust: snake(key), ty: v.ty });
487
+ values.push(fieldInit(snake(key), slotFieldValue(s, v, p.value)));
488
+ }
489
+ const name = slotTypeName(slotName, "SlotProps");
490
+ const shape = { name, fields, slot: true };
491
+ const had = s.comp.slotShapes.get(slotName);
492
+ if (had && JSON.stringify(had.fields) !== JSON.stringify(fields)) {
493
+ fail(s.comp, `every ${outlet} passes the same props, of the same types`, c);
494
+ }
495
+ if (!had) {
496
+ if (s.comp.structs.has(name) && !s.comp.structs.get(name).slot)
497
+ fail(s.comp, `\`${name}\` names this slot's props; rename the interface`, c);
498
+ s.comp.slotShapes.set(slotName, shape);
499
+ s.comp.structs.set(name, shape);
500
+ }
501
+ fn = "fv::scoped_slot_into";
502
+ args = `${field}, &${name} { ${values.join(", ")} }`;
503
+ }
504
+ else if (s.comp.slotShapes.has(slotName)) {
505
+ fail(s.comp, `every ${outlet} passes the same props, of the same types`, c);
506
+ }
507
+ // The slot scope id: `"data-v-…-s"` from a component with `:slotted()` styles, followed inside
508
+ // slot content by the id that content was given, or that id alone.
509
+ const id = c.arguments[6];
510
+ let slotted = null;
511
+ if (id?.type === "StringLiteral")
512
+ slotted = rustStr(id.value);
513
+ else if (id?.type === "BinaryExpression" && id.operator === "+" && id.left.type === "StringLiteral" && id.right.type === "Identifier" && id.right.name === "_scopeId") {
514
+ slotted = s.sid === null ? rustStr(id.left.value) : `&[${rustStr(id.left.value)}, ${s.sid}].concat()`;
515
+ }
516
+ else if (id?.type === "Identifier" && id.name === "_scopeId")
517
+ slotted = s.sid;
518
+ else if (id && id.type !== "NullLiteral")
519
+ fail(s.comp, "unexpected slot scope id", id);
520
+ if (s.comp.passesSlotIds) {
521
+ fn = fn === "fv::slot_into" ? "fv::slot_into_slotted" : "fv::scoped_slot_into_slotted";
522
+ args += `, ${slotted ?? '""'}`;
523
+ }
524
+ else if (slotted !== null)
525
+ fail(s.comp, "a slot scope id passed to content that does not take one", c);
526
+ const call = (rest) => (s.fill ? `if ${fn}(out, ${args}, ${rest}) { filled = true; }` : `${fn}(out, ${args}, ${rest});`);
527
+ if (fallback?.type === "NullLiteral" || !fallback) {
528
+ e.stmt(call("None"));
529
+ return;
530
+ }
531
+ if (fallback.type !== "ArrowFunctionExpression" || fallback.body.type !== "BlockStatement") {
532
+ fail(s.comp, "unexpected slot fallback", fallback);
533
+ }
534
+ // The fallback writes to the same buffer as the outlet, so inside slot content it fills it too.
535
+ e.open(`${s.fill ? "if " : ""}${fn}(out, ${args}, Some(&mut |out: &mut String|`);
536
+ statements(s, e, fallback.body.body);
537
+ e.close(s.fill ? ")) { filled = true; }" : "));");
538
+ }
539
+ /** A value into a child's props field, whose strings are `Cow`s. */
540
+ export function ownInto(comp, v, want, node) {
541
+ if (want.k === "str") {
542
+ if (v.ty.k !== "str")
543
+ fail(comp, "a string prop needs a string", node);
544
+ return `std::borrow::Cow::Borrowed(${bare(v.code)})`;
545
+ }
546
+ if (want.k === "opt" && want.of.k === "str") {
547
+ if (v.ty.k === "undef")
548
+ return "None";
549
+ if (v.ty.k === "str")
550
+ return `Some(std::borrow::Cow::Borrowed(${bare(v.code)}))`;
551
+ if (v.ty.k === "opt" && v.ty.of.k === "str")
552
+ return `${atom(v.code)}.map(std::borrow::Cow::Borrowed)`;
553
+ }
554
+ if (sameTy(v.ty, want) && (want.k === "int" || want.k === "float" || want.k === "bool"))
555
+ return v.code;
556
+ if (want.k === "opt" && sameTy(v.ty, want.of) && (v.ty.k === "int" || v.ty.k === "float" || v.ty.k === "bool"))
557
+ return `Some(${bare(v.code)})`;
558
+ // An integer handed to a prop that takes a fraction.
559
+ if (want.k === "float" && v.ty.k === "int")
560
+ return asF64(v);
561
+ if (want.k === "opt" && want.of.k === "float" && v.ty.k === "int")
562
+ return `Some(${asF64(v)})`;
563
+ // A computed list: its items collected into the child's own list, objects cloned.
564
+ if (v.iter !== undefined && v.ty.k === "list" && want.k === "list" && sameTy(v.ty.of, want.of)) {
565
+ return want.of.k === "struct" || want.of.k === "child" ? `${v.iter}.cloned().collect()` : `${v.iter}.collect()`;
566
+ }
567
+ // An object, a list of objects, a record, or an optional one, of the type the child declares:
568
+ // cloned, which copies its strings only where they are owned.
569
+ const objecty = (t) => t.k === "struct" || t.k === "child" || t.k === "record" || ((t.k === "list" || t.k === "opt") && objecty(t.of));
570
+ if (objecty(want) && sameTy(v.ty, want))
571
+ return `${atom(v.code)}.${v.ty.k === "opt" ? "cloned" : "to_owned"}()`;
572
+ if (want.k === "opt" && objecty(want.of) && sameTy(v.ty, want.of))
573
+ return `Some(${atom(v.code)}.to_owned())`;
574
+ if (objecty(want) && v.ty.k === want.k && JSON.stringify(v.ty).includes('"struct"')) {
575
+ fail(comp, `a ${JSON.stringify(v.ty)} where the child takes a ${JSON.stringify(want)}: two components share a type only when both import it from one \`.ts\` file`, node);
576
+ }
577
+ // A list of strings or numbers: each item borrowed, or copied, into the child's own list.
578
+ if (want.k === "list" && v.ty.k === "list" && v.ty.of.k === "undef")
579
+ return "Vec::new()";
580
+ if (want.k === "opt" && want.of.k === "list" && v.ty.k === "list")
581
+ return `Some(${ownInto(comp, v, want.of, node)})`;
582
+ if (want.k === "list" && v.ty.k === "list" && sameTy(v.ty.of, want.of)) {
583
+ if (want.of.k === "str")
584
+ return `${atom(v.code)}.iter().map(|v| std::borrow::Cow::Borrowed(&**v)).collect()`;
585
+ if (want.of.k === "int" || want.of.k === "float" || want.of.k === "bool")
586
+ return `${atom(v.code)}.to_vec()`;
587
+ }
588
+ return fail(comp, `a ${JSON.stringify(v.ty)} into a ${JSON.stringify(want)} prop`, node);
589
+ }
590
+ export function statements(s, e, body) {
591
+ for (const st of body) {
592
+ if (st.type === "ExpressionStatement" && st.expression.type === "CallExpression") {
593
+ const c = st.expression;
594
+ const callee = c.callee.type === "Identifier" ? c.callee.name : null;
595
+ if (callee === "_push") {
596
+ push(s, e, c.arguments[0]);
597
+ continue;
598
+ }
599
+ if (callee === "_ssrRenderList") {
600
+ list(s, e, c);
601
+ continue;
602
+ }
603
+ if (callee === "_ssrRenderSlot") {
604
+ slotOutlet(s, e, c);
605
+ continue;
606
+ }
607
+ // `<Suspense>`: its default content, rendered in place — ferrovue renders nothing async.
608
+ if (callee === "_ssrRenderSuspense") {
609
+ const def = c.arguments[1]?.properties?.find((p) => (p.key?.name ?? p.key?.value) === "default");
610
+ if (!def)
611
+ e.lit("<!---->");
612
+ else if (def.value.type === "ArrowFunctionExpression" && def.value.body.type === "BlockStatement")
613
+ statements(s, e, def.value.body.body);
614
+ else
615
+ fail(s.comp, "unexpected `<Suspense>` content", c);
616
+ continue;
617
+ }
618
+ // `<Teleport>`: markers here, the content in its target's buffer, which the page writes.
619
+ if (callee === "_ssrRenderTeleport") {
620
+ const [, content, target, disabled] = c.arguments;
621
+ if (s.fill)
622
+ fail(s.comp, "a `<Teleport>` in slot content whose emptiness is decided at run time", st);
623
+ const to = expr(s, target);
624
+ if (to.ty.k !== "str")
625
+ fail(s.comp, "a `<Teleport>`'s `to` is a string", target);
626
+ const off = disabled ? bare(cond(s, disabled)) : "false";
627
+ if (content?.type !== "ArrowFunctionExpression" || content.body.type !== "BlockStatement")
628
+ fail(s.comp, "unexpected `<Teleport>` content", st);
629
+ e.open(`fv::teleport_into(out, fv_teleports, ${strArg(to.code)}, ${off}, &|out: &mut String|`);
630
+ statements(s, e, content.body.body);
631
+ e.close(");");
632
+ continue;
633
+ }
634
+ if (callee === "_ssrRenderVNode") {
635
+ fail(s.comp, "`<component :is>` chooses its component at run time; write the choices out with `v-if`", st);
636
+ }
637
+ }
638
+ /* `const _component_X = _resolveComponent("X", true)`: a component that uses itself, which is
639
+ * how a tree renders. Any other component resolved by name is one this compiler cannot see. */
640
+ if (st.type === "VariableDeclaration" && st.declarations.length === 1) {
641
+ const d = st.declarations[0];
642
+ const init = d.init;
643
+ if (init?.type === "CallExpression" && init.callee.type === "Identifier" &&
644
+ init.callee.name === "_resolveComponent" && init.arguments[0]?.type === "StringLiteral" &&
645
+ init.arguments[0].value === s.comp.name && init.arguments[1]?.type === "BooleanLiteral") {
646
+ s.selfAlias.name = d.id.name;
647
+ continue;
648
+ }
649
+ const routed = init?.type === "CallExpression" && init.callee.type === "Identifier" &&
650
+ init.callee.name === "_resolveComponent" && init.arguments.length === 1 &&
651
+ init.arguments[0]?.type === "StringLiteral" ? init.arguments[0].value : null;
652
+ if (routed === "RouterLink" || routed === "RouterView") {
653
+ s.router.set(d.id.name, routed);
654
+ continue;
655
+ }
656
+ // `const _directive_focus = _resolveDirective("focus")`: a globally registered directive.
657
+ if (init?.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "_resolveDirective" && init.arguments[0]?.type === "StringLiteral") {
658
+ s.directives.set(d.id.name, init.arguments[0].value);
659
+ continue;
660
+ }
661
+ fail(s.comp, "a component the template resolves by name must be imported, or be this one", st);
662
+ }
663
+ if (st.type === "IfStatement") {
664
+ const branch = (b) => (b.type === "BlockStatement" ? b.body : [b]);
665
+ /* `a && b && …`, where a leading operand is an optional value: present for the whole branch,
666
+ * so it is bound and narrowed there, as TypeScript narrows it — a Rust let-chain, which is why
667
+ * generated code needs edition 2024. */
668
+ if (st.test.type === "LogicalExpression" && st.test.operator === "&&") {
669
+ const operands = [];
670
+ const flatten = (n) => {
671
+ if (n.type === "LogicalExpression" && n.operator === "&&") {
672
+ flatten(n.left);
673
+ flatten(n.right);
674
+ }
675
+ else
676
+ operands.push(n);
677
+ };
678
+ flatten(st.test);
679
+ const narrowed = new Map(s.narrowed);
680
+ const bound = new Map();
681
+ const parts = [];
682
+ for (const op of operands) {
683
+ const inner = { ...s, narrowed };
684
+ const p = presence(inner, op);
685
+ if (p && !p.negated) {
686
+ const name = `n${++ctx.narrowCount}`;
687
+ parts.push(p.pattern(name));
688
+ narrowed.set(p.path, { code: name, ty: p.of });
689
+ bound.set(name, p);
690
+ }
691
+ else {
692
+ parts.push(operand(cond(inner, op), CMP));
693
+ }
694
+ }
695
+ // A condition known now: a `false` decides the branch, a `true` adds nothing.
696
+ if (parts.includes("false")) {
697
+ if (st.alternate)
698
+ statements(s, e, branch(st.alternate));
699
+ continue;
700
+ }
701
+ if (parts.includes("true"))
702
+ parts.splice(0, parts.length, ...parts.filter((p) => p !== "true"));
703
+ if (narrowed.size > s.narrowed.size) {
704
+ e.open(`if ${parts.join(" && ")}`);
705
+ const at = e.lines.length - 1;
706
+ statements({ ...s, narrowed }, e, branch(st.consequent));
707
+ // A value the branch never reads is only tested for presence.
708
+ for (const [name, p] of bound) {
709
+ // Read by the branch, or by a later condition of the same chain, which binds it once.
710
+ const inHead = occurrences(e.lines[at], name) > 1;
711
+ if (!inHead && !e.reads(name, at + 1))
712
+ e.replace(at, p.pattern(name), operand(p.present, CMP));
713
+ }
714
+ if (st.alternate) {
715
+ e.close(" else {");
716
+ statements(s, e, branch(st.alternate));
717
+ }
718
+ e.close();
719
+ continue;
720
+ }
721
+ }
722
+ const compound = (st.test.type === "LogicalExpression" && st.test.operator !== "??") ||
723
+ (st.test.type === "UnaryExpression" && st.test.operator === "!");
724
+ const t = compound ? boolOf(cond(s, st.test)) : expr(s, st.test);
725
+ const k = known(t);
726
+ if (k !== undefined) {
727
+ const taken = k ? st.consequent : st.alternate;
728
+ if (taken)
729
+ statements(s, e, branch(taken));
730
+ continue;
731
+ }
732
+ /* An optional value tested for presence is narrowed inside the branch, as TypeScript narrows
733
+ * it: bound by `if let`, so `user.name` under `v-if="user"` reads the bound value. A negated
734
+ * test — `!user`, `user === undefined` — narrows the `v-else` instead, written first. */
735
+ const p = presence(s, st.test);
736
+ if (p && (!p.negated || st.alternate)) {
737
+ const name = `n${++ctx.narrowCount}`;
738
+ e.open(`if ${p.pattern(name)}`);
739
+ const at = e.lines.length - 1;
740
+ const [present, absent] = p.negated ? [st.alternate, st.consequent] : [st.consequent, st.alternate];
741
+ statements(narrowTo(s, p, name), e, branch(present));
742
+ if (!e.reads(name, at + 1))
743
+ e.replace(at, p.pattern(name), condition(p.present));
744
+ if (absent) {
745
+ e.close(" else {");
746
+ statements(s, e, branch(absent));
747
+ }
748
+ e.close();
749
+ continue;
750
+ }
751
+ else {
752
+ e.open(`if ${condition(cond(s, st.test))}`);
753
+ statements(s, e, branch(st.consequent));
754
+ }
755
+ if (st.alternate) {
756
+ e.close(" else {");
757
+ statements(s, e, branch(st.alternate));
758
+ }
759
+ e.close();
760
+ continue;
761
+ }
762
+ fail(s.comp, `\`${st.type}\` in the compiled template`, st);
763
+ }
764
+ }
765
+ export function list(s, e, c) {
766
+ const fn = c.arguments[1];
767
+ if (fn.type !== "ArrowFunctionExpression" || fn.body.type !== "BlockStatement") {
768
+ fail(s.comp, "unexpected `ssrRenderList` callback", c);
769
+ }
770
+ // `([key, value], i) in Object.entries(r)`, which walks the record as `(value, key, i) in r` does.
771
+ if (isObjectCall(c.arguments[0], "entries")) {
772
+ const [pair, index] = fn.params;
773
+ const r = c.arguments[0].arguments.length === 1 ? expr(s, c.arguments[0].arguments[0]) : null;
774
+ if (r?.ty.k !== "record")
775
+ return fail(s.comp, "`Object.entries()` takes a `Record<string, T>`", c.arguments[0]);
776
+ if (pair?.type !== "ArrayPattern" || pair.elements.length > 2 || pair.elements.some((x) => x?.type !== "Identifier")) {
777
+ return fail(s.comp, "a `v-for` over `Object.entries()` names its items `[key, value]`", pair ?? c);
778
+ }
779
+ recordLoop(s, e, r, fn, pair.elements[1], pair.elements[0], index);
780
+ return;
781
+ }
782
+ const src = expr(s, c.arguments[0]);
783
+ if (src.ty.k === "record") {
784
+ const [value, key, index] = fn.params;
785
+ recordLoop(s, e, src, fn, value, key, index);
786
+ return;
787
+ }
788
+ const [item, index] = fn.params;
789
+ const idx = index ? snake(index.name) : null;
790
+ // The item's name in Rust: its own when it is a plain name, a placeholder when it is destructured.
791
+ const itemName = item.type === "Identifier" ? snake(item.name) : `fv_item${++ctx.narrowCount}`;
792
+ const inner = new Map(s.locals);
793
+ let of;
794
+ // What the loop walks, and what its head binds each item to.
795
+ let walk;
796
+ let bound;
797
+ let itemLet = -1;
798
+ const loop = (it, i) => (i ? `for (${i}, ${it}) in ${atom(walk)}.enumerate()` : `for ${it} in ${walk}`);
799
+ if (src.ty.k === "list" && src.iter !== undefined) {
800
+ // A computed list: its items come as they are — a string as a `Cow`, read as a `&str`.
801
+ of = src.ty.of;
802
+ walk = src.iter;
803
+ bound = of.k === "str" ? `${itemName}_cow` : itemName;
804
+ e.open(loop(bound, idx));
805
+ if (of.k === "str") {
806
+ e.stmt(`let ${itemName}: &str = &${bound};`);
807
+ itemLet = e.lines.length - 1;
808
+ }
809
+ }
810
+ else if (src.ty.k === "int") {
811
+ // `v-for="n in 5"`: 1 to 5, as `renderList` counts a number.
812
+ of = INT;
813
+ walk = `1..=${operand(src.code, OR)}`;
814
+ bound = itemName;
815
+ e.open(loop(bound, idx));
816
+ }
817
+ else if (src.ty.k === "list") {
818
+ of = src.ty.of;
819
+ if (of.k === "undef")
820
+ fail(s.comp, "`v-for` over an empty array literal", c);
821
+ const itemCode = `${itemName}_ref`;
822
+ walk = `${atom(src.code)}.iter()`;
823
+ bound = itemCode;
824
+ e.open(loop(bound, idx));
825
+ if (of.k === "str")
826
+ e.stmt(`let ${itemName}: &str = ${itemCode};`);
827
+ // An object — a struct, or another component's props — is borrowed; only a scalar is copied.
828
+ else if (of.k === "struct" || of.k === "child")
829
+ e.stmt(`let ${itemName} = ${itemCode};`);
830
+ else
831
+ e.stmt(`let ${itemName} = *${itemName}_ref;`);
832
+ itemLet = e.lines.length - 1;
833
+ }
834
+ else {
835
+ return fail(s.comp, "`v-for` walks an array, or counts to a number", c);
836
+ }
837
+ let idxLet = -1;
838
+ if (idx) {
839
+ e.stmt(`let ${idx} = ${idx} as i64;`);
840
+ idxLet = e.lines.length - 1;
841
+ }
842
+ const bodyFrom = e.lines.length;
843
+ if (item.type === "Identifier")
844
+ inner.set(item.name, { code: itemName, ty: of, ...(src.lone ? { lone: true } : {}) });
845
+ else if (item.type === "ObjectPattern") {
846
+ // `v-for="{ name, id: key } in items"`: each name is that field of the item.
847
+ for (const p of item.properties) {
848
+ if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
849
+ fail(s.comp, "a destructured `v-for` item binds plain names, without defaults", p);
850
+ }
851
+ inner.set(p.value.name, fieldVal(s.comp, itemName, of, p.key.name ?? p.key.value, p));
852
+ }
853
+ }
854
+ else
855
+ fail(s.comp, "a `v-for` item is a name or an object pattern", item);
856
+ if (index)
857
+ inner.set(index.name, { code: idx, ty: INT });
858
+ const before = e.literalBytes;
859
+ const fromProps = src.ty.k === "list" && src.iter === undefined && /^\(?props\./.test(src.code);
860
+ statements({ ...s, locals: inner, loop: fromProps ? { item: itemName, over: src.code } : undefined }, e, fn.body.body);
861
+ // An item or index the body never reads is not bound, and an index never counted: the loop only
862
+ // counts.
863
+ const unusedItem = !e.reads(itemName, bodyFrom);
864
+ const unusedIdx = idx !== null && !e.reads(idx, bodyFrom);
865
+ const head = bodyFrom - 1 - (idx ? 1 : 0) - (itemLet >= 0 ? 1 : 0);
866
+ e.replace(head, loop(bound, idx), loop(unusedItem ? "_" : bound, unusedIdx ? null : idx));
867
+ // Removed from the last line up, so the earlier indices stay right.
868
+ if (unusedIdx)
869
+ e.lines.splice(idxLet, 1);
870
+ if (unusedItem && itemLet >= 0)
871
+ e.lines.splice(itemLet, 1);
872
+ e.close();
873
+ // The body's markup is written once per item, not once.
874
+ const body = e.literalBytes - before;
875
+ e.literalBytes = before;
876
+ // Counted up front only when the list is reachable from there: a list of the props, or a list in
877
+ // each item of one, counted over all of them. One a `v-if` narrowed, or a loop deeper down, exists
878
+ // only inside its block, and the reservation is an estimate either way.
879
+ if (body > 0 && fromProps)
880
+ e.perItem.push(`${body} * ${atom(src.code)}.len()`);
881
+ else if (body > 0 && src.ty.k === "list" && s.loop && src.code.startsWith(`${s.loop.item}.`)) {
882
+ const outer = s.loop;
883
+ e.perItem.push(`${body} * ${atom(outer.over)}.iter().map(|${outer.item}| ${src.code}.len()).sum::<usize>()`);
884
+ }
885
+ }
886
+ /** `v-for="(value, key, index) in r"` over a record: its entries in JavaScript's order of keys,
887
+ * which is the order `renderList` walks `Object.keys`. */
888
+ function recordLoop(s, e, r, fn, value, key, index) {
889
+ if (r.ty.k !== "record")
890
+ return;
891
+ const of = r.ty.of;
892
+ for (const p of [key, index])
893
+ if (p && p.type !== "Identifier")
894
+ fail(s.comp, "a record's key and index in `v-for` are plain names", p);
895
+ const n = ++ctx.narrowCount;
896
+ const v = value?.type === "Identifier" ? snake(value.name) : `fv_value${n}`;
897
+ const k = key ? snake(key.name) : `fv_key${n}`;
898
+ const i = index ? snake(index.name) : `fv_index${n}`;
899
+ e.open(`for (${i}, (${k}, ${v}_ref)) in ${atom(r.code)}.iter().enumerate()`);
900
+ const head = e.lines.length - 1;
901
+ const indent = /^ */.exec(e.lines[head])[0];
902
+ const inner = new Map(s.locals);
903
+ // A value is read as a list item is: a string as a `&str`, a number copied, an object borrowed.
904
+ e.stmt(of.k === "str" ? `let ${v}: &str = ${v}_ref;` : of.k === "int" || of.k === "float" || of.k === "bool" ? `let ${v} = *${v}_ref;` : `let ${v} = ${v}_ref;`);
905
+ e.stmt(`let ${i} = ${i} as i64;`);
906
+ const bodyFrom = e.lines.length;
907
+ if (value?.type === "Identifier")
908
+ inner.set(value.name, { code: v, ty: of });
909
+ else if (value?.type === "ObjectPattern" && (of.k === "struct" || of.k === "child")) {
910
+ for (const p of value.properties) {
911
+ if (p.type !== "ObjectProperty" || p.computed || p.value.type !== "Identifier") {
912
+ fail(s.comp, "a destructured `v-for` item binds plain names, without defaults", p);
913
+ }
914
+ inner.set(p.value.name, fieldVal(s.comp, v, of, p.key.name ?? p.key.value, p));
915
+ }
916
+ }
917
+ else if (value)
918
+ fail(s.comp, "a `v-for` item is a name, or an object pattern of an object", value);
919
+ if (key)
920
+ inner.set(key.name, { code: k, ty: STR });
921
+ if (index)
922
+ inner.set(index.name, { code: i, ty: INT });
923
+ const before = e.literalBytes;
924
+ statements({ ...s, locals: inner }, e, fn.body.body);
925
+ // Whatever the body never reads is not bound, from the last line up.
926
+ const reads = (name) => e.reads(name, bodyFrom);
927
+ const [readsV, readsK, readsI] = [reads(v), reads(k), reads(i)];
928
+ if (!readsI)
929
+ e.lines.splice(head + 2, 1);
930
+ if (!readsV)
931
+ e.lines.splice(head + 1, 1);
932
+ const pair = `(${readsK ? k : "_"}, ${readsV ? `${v}_ref` : "_"})`;
933
+ e.lines[head] = `${indent}for ${readsI ? `(${i}, ${pair}) in ${atom(r.code)}.iter().enumerate()` : `${pair} in ${atom(r.code)}.iter()`} {`;
934
+ e.close();
935
+ const body = e.literalBytes - before;
936
+ e.literalBytes = before;
937
+ if (body > 0 && /^\(?props\./.test(r.code))
938
+ e.perItem.push(`${body} * ${atom(r.code)}.len()`);
939
+ }