ferrovue 0.7.2 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/lists.js CHANGED
@@ -1,11 +1,11 @@
1
- import { BOOL, fail, FLOAT, INT, opt, sameTy, STR } from "./model.js";
1
+ import { BOOL, fail, FLOAT, INT, opt, sameTy, STR, UNDEF } from "./model.js";
2
2
  import { ctx } from "./context.js";
3
- import { expr, fieldVal } from "./expr.js";
3
+ import { describeTy, expr, fieldVal } from "./expr.js";
4
4
  import { cond } from "./narrowing.js";
5
5
  import { localTy } from "./rust.js";
6
6
  import { asF64, isNumber } from "./numbers.js";
7
7
  import { asCow, isTemporary, lonely, meet, yieldsCow } from "./strings.js";
8
- import { atom, bare, binary, operand, strArg, UNARY } from "./parens.js";
8
+ import { atom, bare, binary, enclosed, operand, strArg, UNARY } from "./parens.js";
9
9
  const COW = "std::borrow::Cow::<str>";
10
10
  function itemsTy(s, v, n) {
11
11
  if (v.ty.k !== "list")
@@ -86,7 +86,7 @@ export function arrow(s, fn, list, of, byRef, method, body) {
86
86
  locals.set(index.name, { code: i, ty: INT });
87
87
  bound.push(index.name);
88
88
  }
89
- const narrowed = new Map([...s.narrowed].filter(([path]) => !bound.some((b) => path === b || path.startsWith(`${b}.`))));
89
+ const narrowed = new Map([...s.narrowed].filter(([path]) => !bound.some((b) => path === b || path.startsWith(`${b}.`) || path.startsWith(`${b}[`))));
90
90
  const code = body({ ...s, locals, narrowed });
91
91
  const reads = (name) => new RegExp(`\\b${name}\\b`).test(code);
92
92
  const pName = reads(p) ? p : "_";
@@ -162,8 +162,10 @@ function present(target) {
162
162
  */
163
163
  export function presentLiteral(s, n) {
164
164
  const comp = s.comp;
165
+ if (n.elements.some((el) => el?.type === "SpreadElement"))
166
+ return null;
165
167
  const values = n.elements.map((el) => {
166
- if (!el || el.type === "SpreadElement")
168
+ if (!el)
167
169
  fail(comp, "FV0610", "an array literal holds plain values", n);
168
170
  return expr(s, el);
169
171
  });
@@ -180,9 +182,396 @@ export function presentLiteral(s, n) {
180
182
  const flat = `[${options.join(", ")}].into_iter().flatten()`;
181
183
  return computed(of.k === "str" ? `${flat}.map(${COW}::Borrowed)` : flat, of, kept.some((v) => v.lone));
182
184
  }
185
+ function backwards(target) {
186
+ return target.iter === undefined ? items(target) : `${target.iter}.collect::<Vec<_>>().into_iter()`;
187
+ }
188
+ /**
189
+ * The item of `target` that `fromList` (given the list) or `fromIter` (given its items) picks, as
190
+ * an optional value: `undefined` where the list has no item there.
191
+ */
192
+ export function picked(s, target, n, fromList, fromIter) {
193
+ const of = target.ty.k === "list" ? target.ty.of : target.ty;
194
+ const lone = target.lone ? { lone: true } : {};
195
+ if (target.iter !== undefined) {
196
+ const got = fromIter(target.iter);
197
+ if (of.k === "str")
198
+ return { code: `${atom(got)}.as_deref()`, ty: opt(STR), held: `${got}.map(|v| ${COW}::Owned(v.into_owned()))`, ...lone };
199
+ if (["int", "float", "bool", "struct", "child"].includes(of.k))
200
+ return { code: got, ty: opt(of) };
201
+ }
202
+ else {
203
+ const got = fromList(atom(target.code));
204
+ const inner = of.k === "opt" ? of.of : of;
205
+ const flat = of.k === "opt";
206
+ switch (inner.k) {
207
+ case "str":
208
+ return { code: flat ? `${got}.and_then(|v| v.as_deref())` : `${got}.map(|v| &**v)`, ty: opt(of), ...lone };
209
+ case "int":
210
+ case "float":
211
+ case "bool":
212
+ return { code: flat ? `${got}.and_then(|v| *v)` : `${got}.copied()`, ty: opt(of) };
213
+ case "struct":
214
+ case "child":
215
+ return { code: flat ? `${got}.and_then(|v| v.as_ref())` : got, ty: opt(of) };
216
+ default:
217
+ break;
218
+ }
219
+ }
220
+ return fail(s.comp, "FV0804", "an item is read from a list of strings, numbers, booleans or objects, each of which may be absent", n);
221
+ }
222
+ const RISKS_NEGATIVE_ZERO = /-[\w(]|\s[*%]\s/;
223
+ function ownedString(v, acc) {
224
+ if (v.code === `&*${acc}`)
225
+ return acc;
226
+ if (v.code === '""')
227
+ return "String::new()";
228
+ const built = /^&\*(format!\(.*\))$/s.exec(v.code)?.[1];
229
+ if (built !== undefined && enclosed(built.slice("format!".length)))
230
+ return built;
231
+ return `String::from(${bare(v.code)})`;
232
+ }
233
+ function reduced(s, target, method, args, n) {
234
+ const comp = s.comp;
235
+ if (args.length < 2) {
236
+ fail(comp, "FV0828", `\`.${method}()\` takes an initial value after its function: without one, JavaScript throws a \`TypeError\` on an empty list`, n);
237
+ }
238
+ if (args.length > 2)
239
+ fail(comp, "FV0829", `\`.${method}()\` takes a function and an initial value`, n);
240
+ const of = itemsTy(s, target, n);
241
+ const fn = args[0];
242
+ if (fn?.type !== "ArrowFunctionExpression" || fn.async || fn.params.length < 2 || fn.params.length > 3 || fn.params[0].type !== "Identifier") {
243
+ fail(comp, "FV0829", `\`.${method}()\` takes an arrow function of the accumulator, the item and its index: \`(acc, x) => …\` or \`(acc, x, i) => …\``, fn);
244
+ }
245
+ const accName = fn.params[0].name;
246
+ const init = expr(s, args[1]);
247
+ if (!["str", "int", "float", "bool"].includes(init.ty.k)) {
248
+ fail(comp, "FV0829", `\`.${method}()\` accumulates a string, a number or a boolean that is present, and its initial value is ${describeTy(init.ty)}`, args[1]);
249
+ }
250
+ let accTy = init.ty.k === "int" && init.f64 !== undefined && RISKS_NEGATIVE_ZERO.test(init.f64) ? FLOAT : init.ty;
251
+ let lone = !!init.lone;
252
+ const item = { ...fn, params: fn.params.slice(1) };
253
+ const narrowed = new Map([...s.narrowed].filter(([path]) => path !== accName && !path.startsWith(`${accName}.`) && !path.startsWith(`${accName}[`)));
254
+ for (;;) {
255
+ const acc = `fv_acc${++ctx.narrowCount}`;
256
+ const accVal = accTy.k === "str" ? { code: `&*${acc}`, ty: STR, ...(lone ? { lone } : {}) } : { code: acc, ty: accTy };
257
+ const outer = { ...s, locals: new Map(s.locals).set(accName, accVal), narrowed };
258
+ const seen = { wider: false, loner: false };
259
+ const { closure, enumerate } = arrow(outer, item, target, of, false, method, (inner) => {
260
+ const out = expr(inner, fn.body);
261
+ const changes = `the function gives ${describeTy(out.ty)} where the accumulator, from its initial value, is ${describeTy(accTy)}`;
262
+ switch (accTy.k) {
263
+ case "str":
264
+ if (out.ty.k !== "str")
265
+ fail(comp, "FV0829", `\`.${method}()\` keeps one type of accumulator, and ${changes}`, fn.body);
266
+ if (out.lone && !lone)
267
+ seen.loner = true;
268
+ return ownedString(out, acc);
269
+ case "int":
270
+ if (out.ty.k === "float" || (out.ty.k === "int" && out.f64 !== undefined && RISKS_NEGATIVE_ZERO.test(out.f64)))
271
+ seen.wider = true;
272
+ else if (out.ty.k !== "int")
273
+ fail(comp, "FV0829", `\`.${method}()\` keeps one type of accumulator, and ${changes}`, fn.body);
274
+ return bare(out.code);
275
+ case "float":
276
+ if (!isNumber(out.ty))
277
+ fail(comp, "FV0829", `\`.${method}()\` keeps one type of accumulator, and ${changes}`, fn.body);
278
+ return bare(asF64(out));
279
+ default:
280
+ if (out.ty.k !== "bool")
281
+ fail(comp, "FV0829", `\`.${method}()\` keeps one type of accumulator, and ${changes}`, fn.body);
282
+ return bare(out.code);
283
+ }
284
+ });
285
+ if (seen.wider) {
286
+ accTy = FLOAT;
287
+ continue;
288
+ }
289
+ if (seen.loner) {
290
+ lone = true;
291
+ continue;
292
+ }
293
+ const start = accTy.k === "str" ? ownedString(init, "") : accTy.k === "float" ? bare(asF64(init)) : bare(init.code);
294
+ const reads = new RegExp(`\\b${acc}\\b`).test(closure);
295
+ const step = closure.replace(/^\|/, `|${reads ? acc : "_"}, `);
296
+ const from = method === "reduce" ? `${items(target)}${enumerate ? ".enumerate()" : ""}` : `${backwards(target)}${enumerate ? ".enumerate()" : ""}.rev()`;
297
+ const folded = `fv::js_reduce(${from}, ${start}, ${step})`;
298
+ if (accTy.k === "str")
299
+ return { code: `&*${folded}`, ty: STR, ...(lone ? { lone } : {}) };
300
+ return { code: folded, ty: accTy };
301
+ }
302
+ }
303
+ function keyPath(node, param) {
304
+ while (node?.type === "ParenthesizedExpression")
305
+ node = node.expression;
306
+ if (node?.type === "Identifier")
307
+ return node.name === param ? "" : null;
308
+ if (node?.type === "MemberExpression" && !node.computed && node.property.type === "Identifier") {
309
+ const base = keyPath(node.object, param);
310
+ return base === null ? null : `${base}.${node.property.name}`;
311
+ }
312
+ return null;
313
+ }
314
+ function literalNumber(node) {
315
+ while (node?.type === "ParenthesizedExpression")
316
+ node = node.expression;
317
+ if (node?.type === "NumericLiteral")
318
+ return node.value;
319
+ if (node?.type === "UnaryExpression" && (node.operator === "-" || node.operator === "+")) {
320
+ const x = literalNumber(node.argument);
321
+ return x === null ? null : node.operator === "-" ? -x : x;
322
+ }
323
+ return null;
324
+ }
325
+ function comparatorShape(fn) {
326
+ const [a, b] = fn.params.map((p) => p.name);
327
+ let body = fn.body;
328
+ while (body?.type === "ParenthesizedExpression")
329
+ body = body.expression;
330
+ if (body?.type === "BinaryExpression" && body.operator === "-") {
331
+ const [la, rb, lb, ra] = [keyPath(body.left, a), keyPath(body.right, b), keyPath(body.left, b), keyPath(body.right, a)];
332
+ if (la !== null && la === rb)
333
+ return { path: la, ascending: true, subtracted: true };
334
+ if (lb !== null && lb === ra)
335
+ return { path: lb, ascending: false, subtracted: true };
336
+ return null;
337
+ }
338
+ let path;
339
+ const test = (node, order) => {
340
+ while (node?.type === "ParenthesizedExpression")
341
+ node = node.expression;
342
+ if (node?.type !== "BinaryExpression" || !["<", ">", "<=", ">=", "===", "!=="].includes(node.operator))
343
+ return null;
344
+ let rel;
345
+ const [la, rb, lb, ra] = [keyPath(node.left, a), keyPath(node.right, b), keyPath(node.left, b), keyPath(node.right, a)];
346
+ if (la !== null && la === rb)
347
+ rel = order;
348
+ else if (lb !== null && lb === ra)
349
+ rel = -order;
350
+ else
351
+ return null;
352
+ const p = la !== null && la === rb ? la : lb;
353
+ if (path !== undefined && path !== p)
354
+ return null;
355
+ path = p;
356
+ switch (node.operator) {
357
+ case "<":
358
+ return rel < 0;
359
+ case ">":
360
+ return rel > 0;
361
+ case "<=":
362
+ return rel <= 0;
363
+ case ">=":
364
+ return rel >= 0;
365
+ case "===":
366
+ return rel === 0;
367
+ default:
368
+ return rel !== 0;
369
+ }
370
+ };
371
+ const result = (node, order) => {
372
+ while (node?.type === "ParenthesizedExpression")
373
+ node = node.expression;
374
+ if (node?.type === "ConditionalExpression") {
375
+ const t = test(node.test, order);
376
+ return t === null ? null : result(t ? node.consequent : node.alternate, order);
377
+ }
378
+ return literalNumber(node);
379
+ };
380
+ const [less, equal, greater] = [result(body, -1), result(body, 0), result(body, 1)];
381
+ if (less === null || equal === null || greater === null || [less, equal, greater].some(Number.isNaN))
382
+ return null;
383
+ if (Math.sign(equal) !== 0 || Math.sign(less) !== -Math.sign(greater))
384
+ return null;
385
+ if (path === undefined || less === 0)
386
+ return { path: "", ascending: null, subtracted: false };
387
+ return { path: path ?? "", ascending: less < 0, subtracted: false };
388
+ }
389
+ function sortedBy(s, target, of, fn) {
390
+ const comp = s.comp;
391
+ const shapes = "`(a, b) => a - b`, `(a, b) => a.k - b.k` over integers, or `?:` over `<` and `>` of integers or strings giving -1, 0 and 1";
392
+ if (fn?.type !== "ArrowFunctionExpression" || fn.async || fn.params.length !== 2 || fn.params.some((p) => p.type !== "Identifier") || fn.body.type === "BlockStatement") {
393
+ fail(comp, "FV0830", `\`.toSorted()\` takes a comparator of a shape the server orders by, as JavaScript does: ${shapes}`, fn);
394
+ }
395
+ const k = ++ctx.narrowCount;
396
+ const [pa, pb] = [`fv_l${k}`, `fv_r${k}`];
397
+ const locals = new Map(s.locals).set(fn.params[0].name, itemVal(of, pa, true, target.lone)).set(fn.params[1].name, itemVal(of, pb, true, target.lone));
398
+ const bound = fn.params.map((p) => p.name);
399
+ const narrowed = new Map([...s.narrowed].filter(([path]) => !bound.some((b) => path === b || path.startsWith(`${b}.`) || path.startsWith(`${b}[`))));
400
+ const inner = { ...s, locals, narrowed };
401
+ const shape = comparatorShape(fn);
402
+ if (shape === null) {
403
+ expr(inner, fn.body);
404
+ fail(comp, "FV0830", `\`.toSorted()\` takes a comparator of a shape the server orders by, as JavaScript does: ${shapes}`, fn.body);
405
+ }
406
+ if (shape.ascending === null)
407
+ return computed(items(target), of, target.lone);
408
+ const key = (name) => {
409
+ let node = { type: "Identifier", name, loc: fn.body.loc, start: fn.body.start, end: fn.body.end };
410
+ for (const field of shape.path.split(".").slice(1))
411
+ node = { type: "MemberExpression", computed: false, object: node, property: { type: "Identifier", name: field }, loc: fn.body.loc, start: fn.body.start, end: fn.body.end };
412
+ return expr(inner, node);
413
+ };
414
+ const [ka, kb] = [key(fn.params[0].name), key(fn.params[1].name)];
415
+ const [x, y] = shape.ascending ? [ka, kb] : [kb, ka];
416
+ let order;
417
+ if (ka.ty.k === "int")
418
+ order = `fv::js_order(${bare(asF64(x))}, ${bare(asF64(y))})`;
419
+ else if (ka.ty.k === "str" && !shape.subtracted) {
420
+ if (ka.lone)
421
+ fail(comp, "FV0706", lonely("`.toSorted()`"), fn.body);
422
+ order = `fv::js_cmp(${strArg(x.code)}, ${strArg(y.code)})`;
423
+ }
424
+ else if (ka.ty.k === "float") {
425
+ return fail(comp, "FV0830", "`.toSorted()` by a `Float`, which may be `NaN`: JavaScript then takes the comparator as inconsistent and each engine orders the items its own way. Sort by an integer or a string", fn.body);
426
+ }
427
+ else {
428
+ return fail(comp, "FV0830", `\`.toSorted()\` orders by ${shape.subtracted ? "integers" : "integers or strings"}, and this key is ${describeTy(ka.ty)}`, fn.body);
429
+ }
430
+ const reads = (name) => new RegExp(`\\b${name}\\b`).test(order);
431
+ return computed(`fv::js_sorted(${items(target)}, |${reads(pa) ? pa : "_"}, ${reads(pb) ? pb : "_"}| ${order})`, of, target.lone);
432
+ }
433
+ function sorted(s, target, args, n) {
434
+ const comp = s.comp;
435
+ const of = itemsTy(s, target, n);
436
+ if (args.length > 1)
437
+ fail(comp, "FV0830", "`.toSorted()` takes one comparator, or none", n);
438
+ if (args.length === 1 && !(args[0].type === "Identifier" && args[0].name === "undefined"))
439
+ return sortedBy(s, target, of, args[0]);
440
+ const it = items(target);
441
+ switch (of.k) {
442
+ case "str":
443
+ if (target.lone)
444
+ fail(comp, "FV0706", lonely("`.toSorted()`"), n);
445
+ return computed(`fv::js_sorted(${it}, |a, b| fv::js_cmp(a, b))`, of);
446
+ case "int":
447
+ case "float":
448
+ return computed(`fv::js_sorted_by_string(${it}, |v| fv::Js(*v).to_string())`, of);
449
+ case "bool":
450
+ return computed(`fv::js_sorted(${it}, bool::cmp)`, of);
451
+ default:
452
+ return fail(comp, "FV0831", "`.toSorted()` without a comparator orders objects by `String(x)`, which is `[object Object]` for each: give it a comparator, `(a, b) => a.k - b.k`", n);
453
+ }
454
+ }
455
+ /** An array literal holding spread lists (`[...a, x, ...b]`): the items of each part in turn, as a computed list. */
456
+ export function spreadLiteral(s, n) {
457
+ const comp = s.comp;
458
+ const parts = n.elements.map((el) => {
459
+ if (!el)
460
+ fail(comp, "FV0610", "an array literal holds plain values, without holes", n);
461
+ if (el.type !== "SpreadElement")
462
+ return { one: expr(s, el), node: el };
463
+ const list = expr(s, el.argument);
464
+ if (list.ty.k !== "list")
465
+ fail(comp, "FV0833", `\`...\` in an array literal spreads a list, and this is ${describeTy(list.ty)}`, el);
466
+ return { list, node: el };
467
+ }).filter((p) => p.list === undefined || !untypedEmpty(p.list));
468
+ const tys = parts.map((p) => (p.list ? p.list.ty.of : p.one.ty));
469
+ if (tys.length === 0)
470
+ return { code: "[]", ty: { k: "list", of: UNDEF } };
471
+ const own = tys[0];
472
+ const numbers = tys.every(isNumber);
473
+ const scalar = ["str", "int", "float", "bool"].includes(own.k);
474
+ const objects = (own.k === "struct" || own.k === "child") && parts.every((p) => p.list !== undefined);
475
+ if ((!scalar && !objects) || (!numbers && tys.some((t) => !sameTy(t, own)))) {
476
+ const bad = parts.find((_, i) => !sameTy(tys[i], own)) ?? parts[0];
477
+ fail(comp, "FV0833", "an array literal spreads lists of one item type, strings, numbers, booleans or objects, and holds plain values of that type beside them (numbers whether `number` or `Float`)", bad.node);
478
+ }
479
+ const of = numbers && tys.some((t) => t.k === "float") ? FLOAT : own;
480
+ const pieces = [];
481
+ let plain = [];
482
+ const flush = () => {
483
+ if (plain.length)
484
+ pieces.push(`[${plain.join(", ")}]${pieces.length ? "" : ".into_iter()"}`);
485
+ plain = [];
486
+ };
487
+ for (const p of parts) {
488
+ if (p.list) {
489
+ flush();
490
+ pieces.push(of.k === "float" && p.list.ty.k === "list" && p.list.ty.of.k === "int" ? `${items(p.list)}.map(|v| v as f64)` : items(p.list));
491
+ }
492
+ else {
493
+ const v = p.one;
494
+ plain.push(of.k === "str" ? asCow(v) : of.k === "float" ? bare(asF64(v)) : bare(v.code));
495
+ }
496
+ }
497
+ flush();
498
+ const lone = parts.some((p) => (p.list ?? p.one).lone);
499
+ return computed(pieces.slice(1).reduce((acc, piece) => `${acc}.chain(${piece})`, pieces[0]), of, lone);
500
+ }
501
+ /** `Math.max(...xs)` and `Math.min(...xs)`: every number of the parts folded from `-Infinity` or `Infinity`, as JavaScript folds them. */
502
+ export function spreadExtreme(s, method, args) {
503
+ const pieces = args.map((a) => {
504
+ if (a?.type === "SpreadElement") {
505
+ const list = expr(s, a.argument);
506
+ if (list.ty.k !== "list" || (!isNumber(list.ty.of) && !untypedEmpty(list)))
507
+ fail(s.comp, "FV0704", `\`Math.${method}(...xs)\` spreads a list of numbers, and this is ${list.ty.k === "list" ? "a list of other values" : describeTy(list.ty)}`, a);
508
+ if (untypedEmpty(list))
509
+ return null;
510
+ return list.ty.of.k === "int" ? `${items(list)}.map(|v| v as f64)` : items(list);
511
+ }
512
+ const v = expr(s, a);
513
+ if (!isNumber(v.ty))
514
+ fail(s.comp, "FV0704", `\`Math.${method}()\` takes numbers that are present`, a);
515
+ return `std::iter::once(${bare(asF64(v))})`;
516
+ }).filter((p) => p !== null);
517
+ const start = method === "max" ? "f64::NEG_INFINITY" : "f64::INFINITY";
518
+ if (pieces.length === 0)
519
+ return { code: start, ty: FLOAT, num: method === "max" ? -Infinity : Infinity };
520
+ const all = pieces.slice(1).reduce((acc, piece) => `${acc}.chain(${piece})`, pieces[0]);
521
+ return { code: `${all}.fold(${start}, fv::js_${method})`, ty: FLOAT };
522
+ }
183
523
  export function listMethod(s, target, method, args, n) {
184
524
  const comp = s.comp;
185
- if (!["filter", "map", "some", "every", "find", "findIndex", "slice"].includes(method))
525
+ if (method === "reduce" || method === "reduceRight")
526
+ return reduced(s, target, method, args, n);
527
+ if (method === "toReversed") {
528
+ if (args.length !== 0)
529
+ fail(comp, "FV0602", "`.toReversed()` takes no arguments", n);
530
+ const of = itemsTy(s, target, n);
531
+ const held = target.iter !== undefined && /^[\w.]+\.iter\(\)(?:\.copied\(\)|\.map\(\|v\| std::borrow::Cow::<str>::Borrowed\(&\*\*v\)\))?$/.test(target.iter);
532
+ return computed(`${held ? target.iter : backwards(target)}.rev()`, of, target.lone);
533
+ }
534
+ if (method === "toSorted")
535
+ return sorted(s, target, args, n);
536
+ if (method === "sort" || method === "reverse") {
537
+ fail(comp, "FV0832", `\`.${method}()\` changes the list in place, which a render must not do to its props or state: write \`.${method === "sort" ? "toSorted" : "toReversed"}()\`, or \`[...list].${method}(…)\``, n);
538
+ }
539
+ if (method === "at") {
540
+ if (args.length !== 1)
541
+ fail(comp, "FV0825", "`.at()` takes one index", n);
542
+ const i = expr(s, args[0]);
543
+ if (!isNumber(i.ty))
544
+ fail(comp, "FV0825", `\`.at()\` takes a number that is present, and this index is ${describeTy(i.ty)}`, args[0]);
545
+ const f = bare(asF64(i));
546
+ return picked(s, target, n, (list) => `fv::js_item_at(${list}.iter(), ${f})`, (it) => `fv::js_item_at(${it}, ${f})`);
547
+ }
548
+ if (method === "indexOf" || method === "lastIndexOf") {
549
+ if (args.length !== 1)
550
+ fail(comp, "FV0826", `\`.${method}()\` takes one value to look for, without a position to start from`, n);
551
+ const of = target.ty.k === "list" ? target.ty.of : target.ty;
552
+ const x = expr(s, args[0]);
553
+ const v = `fv_x${++ctx.narrowCount}`;
554
+ const test = sameItem(s, target, of, x, v, "FV0826", method, args[0]);
555
+ const it = items(target);
556
+ const found = method === "indexOf" ? `${it}.position(|${v}| ${test})` : `${backwards(target)}.rposition(|${v}| ${test})`;
557
+ return { code: `${found}.map_or(-1, |i| i as i64)`, ty: INT };
558
+ }
559
+ if (method === "concat") {
560
+ const own = itemsTy(s, target, n);
561
+ const parts = args.map((a) => {
562
+ const part = a?.type === "SpreadElement" ? null : expr(s, a);
563
+ const numbers = part?.ty.k === "list" && isNumber(part.ty.of) && isNumber(own);
564
+ if (part?.ty.k !== "list" || (!untypedEmpty(part) && !sameTy(part.ty.of, own) && !numbers)) {
565
+ fail(comp, "FV0827", `\`.concat()\` joins lists of the list's own item type, ${describeTy(own)}`, a);
566
+ }
567
+ return part;
568
+ }).filter((p) => !untypedEmpty(p));
569
+ const of = [target, ...parts].some((p) => p.ty.k === "list" && p.ty.of.k === "float") ? FLOAT : own;
570
+ const each = (p) => (of.k === "float" && p.ty.k === "list" && p.ty.of.k === "int" ? `${items(p)}.map(|v| v as f64)` : items(p));
571
+ const joined = parts.reduce((acc, p) => `${acc}.chain(${each(p)})`, each(target));
572
+ return computed(joined, of, [target, ...parts].some((p) => p.lone));
573
+ }
574
+ if (!["filter", "map", "some", "every", "find", "findIndex", "findLast", "findLastIndex", "slice"].includes(method))
186
575
  return null;
187
576
  if (args.length === 1 && isBoolean(s, args[0])) {
188
577
  const kept = method === "filter" ? present(target) : null;
@@ -231,9 +620,12 @@ export function listMethod(s, target, method, args, n) {
231
620
  const { closure, enumerate } = arrow(s, fn, target, of, false, method, (inner) => cond(inner, fn.body));
232
621
  return { code: `${it}${enumerate ? ".enumerate()" : ""}.${method === "some" ? "any" : "all"}(${closure})`, ty: BOOL };
233
622
  }
234
- case "find": {
623
+ case "find":
624
+ case "findLast": {
235
625
  const { closure, enumerate } = arrow(s, fn, target, of, true, method, (inner) => cond(inner, fn.body));
236
- const found = enumerate ? `${it}.enumerate().find(${closure}).map(|(_, v)| v)` : `${it}.find(${closure})`;
626
+ const from = method === "find" ? it : backwards(target);
627
+ const rev = method === "find" ? "" : ".rev()";
628
+ const found = enumerate ? `${from}.enumerate()${rev}.find(${closure}).map(|(_, v)| v)` : `${from}${rev}.find(${closure})`;
237
629
  const lone = target.lone ? { lone: true } : {};
238
630
  if (of.k === "str")
239
631
  return { code: `${atom(found)}.as_deref()`, ty: opt(STR), held: `${found}.map(|v| ${COW}::Owned(v.into_owned()))`, ...lone };
@@ -241,7 +633,8 @@ export function listMethod(s, target, method, args, n) {
241
633
  }
242
634
  default: {
243
635
  const { closure, enumerate } = arrow(s, fn, target, of, false, method, (inner) => cond(inner, fn.body));
244
- return { code: `${it}${enumerate ? ".enumerate()" : ""}.position(${closure}).map_or(-1, |i| i as i64)`, ty: INT };
636
+ const [from, search] = method === "findIndex" ? [it, "position"] : [backwards(target), "rposition"];
637
+ return { code: `${from}${enumerate ? ".enumerate()" : ""}.${search}(${closure}).map_or(-1, |i| i as i64)`, ty: INT };
245
638
  }
246
639
  }
247
640
  }
@@ -263,27 +656,31 @@ export function objectCall(s, method, args, n) {
263
656
  return fail(comp, "FV0816", "`Object.entries()` is supported as the source of a `v-for`: `([key, value], i) in Object.entries(r)`", n);
264
657
  return fail(comp, "FV0817", `\`Object.${method}()\` is not supported: \`keys\`, \`values\` and \`entries\` of a record are`, n);
265
658
  }
659
+ function sameItem(s, target, of, x, v, code, method, n) {
660
+ const numbers = isNumber(x.ty) && isNumber(of);
661
+ if ((!sameTy(x.ty, of) && !numbers) || !["str", "int", "float", "bool"].includes(of.k)) {
662
+ fail(s.comp, code, `\`.${method}()\` looks for a value of the list's own type, in a list of strings, numbers or booleans`, n);
663
+ }
664
+ if (of.k === "str")
665
+ meet(s.comp, target, x, `\`.${method}()\``, n, "equal");
666
+ if (of.k === "str")
667
+ return `*${v} == ${x.code.startsWith("&*") ? `*${x.code.slice(2)}` : `*${operand(x.code, UNARY)}`}`;
668
+ if (of.k === "float" && x.ty.k === "int")
669
+ return binary(v, "==", asF64(x));
670
+ if (of.k === "int" && x.ty.k === "float")
671
+ return binary(`${v} as f64`, "==", x.code);
672
+ return of.k === "bool" ? binary(v, "==", x.code) : `${v} == ${x.code}`;
673
+ }
266
674
  export function computedListMethod(s, target, method, args, n) {
267
675
  if (target.ty.k !== "list")
268
676
  return null;
269
677
  const of = target.ty.of;
270
678
  if (method === "includes" && args.length === 1) {
271
679
  const x = expr(s, args[0]);
272
- const numbers = isNumber(x.ty) && isNumber(of);
273
- if ((!sameTy(x.ty, of) && !numbers) || !["str", "int", "float", "bool"].includes(of.k))
274
- fail(s.comp, "FV0801", "`.includes()` looks for a value of the list's own type", args[0]);
275
- if (of.k === "str")
276
- meet(s.comp, target, x, "`.includes()`", n, "equal");
277
- const test = of.k === "str"
278
- ? `*v == ${x.code.startsWith("&*") ? `*${x.code.slice(2)}` : `*${operand(x.code, UNARY)}`}`
279
- : of.k === "float" && x.ty.k === "int"
280
- ? binary("v", "==", asF64(x))
281
- : of.k === "float"
282
- ? `v == ${x.code} || (v.is_nan() && ${atom(x.code)}.is_nan())`
283
- : of.k === "int" && x.ty.k === "float"
284
- ? binary("v as f64", "==", x.code)
285
- : `v == ${x.code}`;
286
- return { code: `${items(target)}.any(|v| ${test})`, ty: BOOL };
680
+ const v = `fv_x${++ctx.narrowCount}`;
681
+ const test = sameItem(s, target, of, x, v, "FV0801", "includes", args[0]);
682
+ const nan = of.k === "float" && x.ty.k === "float" ? ` || (${v}.is_nan() && ${atom(x.code)}.is_nan())` : "";
683
+ return { code: `${items(target)}.any(|${v}| ${test}${nan})`, ty: BOOL };
287
684
  }
288
685
  if (method === "join" && args.length <= 1) {
289
686
  if (!["str", "int", "float", "bool"].includes(of.k))
package/dist/model.d.ts CHANGED
@@ -116,6 +116,7 @@ export interface Component {
116
116
  };
117
117
  templateAst?: N;
118
118
  attrsDropped?: N;
119
+ propsAt?: N;
119
120
  cssVarsAt?: N[];
120
121
  props: Struct;
121
122
  structs: Map<string, Struct>;
@@ -10,6 +10,15 @@ export interface NullTest {
10
10
  }
11
11
  export declare function nullTest(n: N): NullTest | null;
12
12
  export declare function checkNullTest(s: Scope, t: NullTest, v: Val): void;
13
+ /** `typeof target === literal`, `!==`, `==` or `!=`, either way round. */
14
+ export interface TypeofCompare {
15
+ target: N;
16
+ literal: string;
17
+ equal: boolean;
18
+ }
19
+ export declare function typeofCompare(n: N): TypeofCompare | null;
20
+ export declare function typeofVal(s: Scope, n: N): Val;
21
+ export declare function typeofTest(s: Scope, t: TypeofCompare, n: N): Val;
13
22
  export interface Presence {
14
23
  path: string;
15
24
  pattern: (name: string) => string;