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/README.md +154 -691
- package/dist/calls.d.ts +4 -0
- package/dist/calls.js +146 -13
- package/dist/children.js +8 -0
- package/dist/client.d.ts +7 -0
- package/dist/client.js +25 -3
- package/dist/compiler.js +5 -2
- package/dist/component.js +3 -2
- package/dist/context.d.ts +7 -0
- package/dist/context.js +4 -1
- package/dist/errors.d.ts +116 -5
- package/dist/errors.js +81 -6
- package/dist/expr.js +143 -30
- package/dist/fixture.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/lists.d.ts +9 -0
- package/dist/lists.js +421 -24
- package/dist/model.d.ts +1 -0
- package/dist/narrowing.d.ts +9 -0
- package/dist/narrowing.js +98 -9
- package/dist/numbers.d.ts +4 -0
- package/dist/numbers.js +61 -2
- package/dist/parens.js +7 -0
- package/dist/plugin.d.ts +5 -0
- package/dist/plugins/dates.d.ts +25 -0
- package/dist/plugins/dates.js +224 -0
- package/dist/plugins/i18n.js +16 -14
- package/dist/plugins/index.js +2 -1
- package/dist/plugins/router.js +8 -5
- package/dist/rust.d.ts +1 -1
- package/dist/rust.js +22 -9
- package/dist/schema.js +1 -0
- package/dist/script.d.ts +1 -0
- package/dist/script.js +41 -32
- package/dist/strings.d.ts +9 -1
- package/dist/strings.js +113 -23
- package/dist/styles.d.ts +3 -0
- package/dist/styles.js +22 -0
- package/dist/typescript.d.ts +12 -4
- package/dist/typescript.js +50 -12
- package/package.json +20 -20
- package/schema.json +6 -0
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
|
|
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 (
|
|
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
|
|
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
|
-
|
|
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
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
package/dist/narrowing.d.ts
CHANGED
|
@@ -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;
|