ferrovue 0.8.0 → 0.10.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 +162 -697
- package/dist/args.js +1 -1
- package/dist/attrs.d.ts +2 -0
- package/dist/attrs.js +12 -4
- package/dist/blocks.d.ts +32 -0
- package/dist/blocks.js +476 -0
- package/dist/calls.d.ts +4 -0
- package/dist/calls.js +152 -15
- package/dist/children.js +18 -3
- package/dist/cli.js +2 -6
- package/dist/client-render.d.ts +4 -3
- package/dist/client-render.js +17 -4
- package/dist/client.d.ts +19 -0
- package/dist/client.js +51 -25
- package/dist/compiler.d.ts +14 -4
- package/dist/compiler.js +187 -35
- package/dist/component.js +12 -5
- package/dist/conformance.d.ts +1 -1
- package/dist/constants.d.ts +26 -3
- package/dist/constants.js +176 -20
- package/dist/context.d.ts +22 -1
- package/dist/context.js +12 -3
- package/dist/diagnostics.d.ts +11 -1
- package/dist/diagnostics.js +33 -4
- package/dist/dynamic.js +7 -2
- package/dist/editor.d.ts +2 -2
- package/dist/editor.js +14 -9
- package/dist/emitter.d.ts +1 -0
- package/dist/emitter.js +12 -0
- package/dist/errors.d.ts +519 -13
- package/dist/errors.js +1440 -322
- package/dist/expr.d.ts +11 -0
- package/dist/expr.js +273 -59
- package/dist/fixture.d.ts +4 -1
- package/dist/fixture.js +11 -3
- package/dist/functions.d.ts +14 -0
- package/dist/functions.js +172 -0
- package/dist/hydration.js +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/inputs.d.ts +2 -2
- package/dist/inputs.js +1 -1
- package/dist/lists.d.ts +14 -1
- package/dist/lists.js +581 -40
- package/dist/loads.d.ts +27 -0
- package/dist/loads.js +81 -0
- package/dist/loops.js +10 -3
- package/dist/model.d.ts +51 -3
- package/dist/model.js +162 -6
- package/dist/narrowing.d.ts +17 -0
- package/dist/narrowing.js +125 -12
- package/dist/numbers.d.ts +4 -0
- package/dist/numbers.js +53 -1
- package/dist/page-json.d.ts +1 -0
- package/dist/page-json.js +14 -0
- package/dist/plugin.d.ts +5 -1
- package/dist/plugins/client-only.js +1 -1
- package/dist/plugins/dates.d.ts +6 -0
- package/dist/plugins/dates.js +81 -5
- package/dist/plugins/head.js +2 -1
- package/dist/plugins/index.js +2 -1
- package/dist/plugins/loaders.d.ts +35 -0
- package/dist/plugins/loaders.js +349 -0
- package/dist/plugins/provide.js +5 -4
- package/dist/plugins/router-link.js +2 -2
- package/dist/plugins/router.js +4 -2
- package/dist/plugins/stores.js +7 -7
- package/dist/plugins/twins.js +1 -1
- package/dist/resolve.d.ts +32 -0
- package/dist/resolve.js +246 -0
- package/dist/rust.d.ts +1 -1
- package/dist/rust.js +39 -9
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +1 -0
- package/dist/script.d.ts +1 -1
- package/dist/script.js +64 -54
- package/dist/settle.js +8 -0
- package/dist/strings.d.ts +9 -1
- package/dist/strings.js +112 -22
- package/dist/template.js +162 -137
- package/dist/typescript.d.ts +12 -5
- package/dist/typescript.js +64 -34
- package/dist/vite.js +3 -3
- package/dist/volar.js +1 -1
- package/package.json +1 -1
- package/schema.json +6 -0
package/dist/args.js
CHANGED
|
@@ -60,7 +60,7 @@ export function parseArgs(argv, flags = FLAGS, commands = COMMANDS) {
|
|
|
60
60
|
return { error: `option '${name}' takes no value` };
|
|
61
61
|
}
|
|
62
62
|
if (flag.deprecated) {
|
|
63
|
-
args.warnings.push(new GenError("FV1118", `'${name}' is deprecated since ${flag.deprecated.since}, and goes in
|
|
63
|
+
args.warnings.push(new GenError("FV1118", `'${name}' is deprecated since ${flag.deprecated.since}, and goes in a later minor release: use ${flag.deprecated.use}`));
|
|
64
64
|
}
|
|
65
65
|
args.options.set(longName(flag), value);
|
|
66
66
|
args.spelt.set(longName(flag), name);
|
package/dist/attrs.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export declare function interpolate(s: Scope, e: Emitter, v: Val, n: N): void;
|
|
|
5
5
|
export declare function attrValue(e: Emitter, v: Val): void;
|
|
6
6
|
export declare function renderAttr(s: Scope, e: Emitter, key: string, v: Val, n: N): void;
|
|
7
7
|
export declare function keepsAttrCase(tag: string): boolean;
|
|
8
|
+
/** The value bound to the attribute \`key\`, shown as text unless the attribute is a boolean one. */
|
|
9
|
+
export declare function attrShown(s: Scope, key: string, n: N, tag?: string | null): Val;
|
|
8
10
|
export declare function renderDynamicAttr(s: Scope, e: Emitter, key: string, v: Val, n: N, tag?: string | null): void;
|
|
9
11
|
export declare const IGNORED_PROPS: Set<string>;
|
|
10
12
|
export declare function directiveProps(s: Scope, n: N): boolean;
|
package/dist/attrs.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { escapeHtml, hyphenate, isBooleanAttr, isSSRSafeAttrName, isSVGTag, propsToAttrMap } from "@vue/shared";
|
|
2
2
|
import { fail, nothing, GenError, rustStr } from "./model.js";
|
|
3
3
|
import { CONFIG_FILE, ctx } from "./context.js";
|
|
4
|
-
import { describeTy, expr } from "./expr.js";
|
|
4
|
+
import { describeTy, expr, shown } from "./expr.js";
|
|
5
5
|
import { known, truthy } from "./narrowing.js";
|
|
6
6
|
import { asCow, isTemporary, unquote } from "./strings.js";
|
|
7
7
|
import { atom, bare, condition, receiver, strArg } from "./parens.js";
|
|
@@ -78,8 +78,16 @@ export function renderAttr(s, e, key, v, n) {
|
|
|
78
78
|
export function keepsAttrCase(tag) {
|
|
79
79
|
return tag.indexOf("-") > 0 || isSVGTag(tag);
|
|
80
80
|
}
|
|
81
|
+
function attrName(key, tag) {
|
|
82
|
+
return tag !== null && keepsAttrCase(tag) ? key : (propsToAttrMap[key] ?? key.toLowerCase());
|
|
83
|
+
}
|
|
84
|
+
/** The value bound to the attribute \`key\`, shown as text unless the attribute is a boolean one. */
|
|
85
|
+
export function attrShown(s, key, n, tag = null) {
|
|
86
|
+
const name = attrName(key, tag);
|
|
87
|
+
return isBooleanAttr(name) || name === "hidden" ? expr(s, n) : shown(s, n, "attr");
|
|
88
|
+
}
|
|
81
89
|
export function renderDynamicAttr(s, e, key, v, n, tag = null) {
|
|
82
|
-
const name =
|
|
90
|
+
const name = attrName(key, tag);
|
|
83
91
|
if (!isSSRSafeAttrName(name))
|
|
84
92
|
fail(s.comp, "FV0404", `unsafe attribute name \`${name}\``, n);
|
|
85
93
|
renderable(s, key, v, n);
|
|
@@ -259,7 +267,7 @@ function objectAttrs(s, e, n, merged) {
|
|
|
259
267
|
e.lit(`"`);
|
|
260
268
|
}
|
|
261
269
|
else {
|
|
262
|
-
renderDynamicAttr(s, e, key,
|
|
270
|
+
renderDynamicAttr(s, e, key, attrShown(s, key, p.value), p);
|
|
263
271
|
}
|
|
264
272
|
}
|
|
265
273
|
}
|
|
@@ -318,7 +326,7 @@ export function attrOf(s, key, n, side) {
|
|
|
318
326
|
return styleAttr(s, n);
|
|
319
327
|
if (n.type === "NullLiteral")
|
|
320
328
|
return "fv::Attr::Undefined";
|
|
321
|
-
return valueAttr(s,
|
|
329
|
+
return valueAttr(s, attrShown(s, key, n), n);
|
|
322
330
|
}
|
|
323
331
|
export function valueAttr(s, v, n) {
|
|
324
332
|
switch (v.ty.k) {
|
package/dist/blocks.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type Component, type N, type Scope, type Val } from "./model.ts";
|
|
2
|
+
/** Runtime calls that panic where JavaScript throws, or user code that may. */
|
|
3
|
+
export declare const PANICS: RegExp;
|
|
4
|
+
export declare function panicky(code: string): boolean;
|
|
5
|
+
/** How a value is held in a Rust `let`: what the `let` is given, how the name reads back, and
|
|
6
|
+
* whether the `let` owns what a read borrows (a `String`, a `Cow`, a `Vec`). */
|
|
7
|
+
export declare function bindingOf(v: Val): {
|
|
8
|
+
init: string;
|
|
9
|
+
read: (at: string) => Val;
|
|
10
|
+
owned: boolean;
|
|
11
|
+
};
|
|
12
|
+
/** How many times a body's statements may be translated again after an `if` or a `case` that
|
|
13
|
+
* falls off its end: each such path continues with the statements after it, translated on its own,
|
|
14
|
+
* so sequential `switch`es of `break`ing cases multiply them. */
|
|
15
|
+
export declare const PATH_BUDGET = 4096;
|
|
16
|
+
/** The expression a function's body computes: the expression of an arrow function, or a block of
|
|
17
|
+
* statements in the subset ferrovue translates, written as one expression. */
|
|
18
|
+
export declare function bodyExpr(comp: Component, fn: N): N;
|
|
19
|
+
/** `p` (a name, or an object or array pattern of names) bound to `value` around `body`. */
|
|
20
|
+
export declare function destructured(comp: Component, p: N, value: N, body: N): N;
|
|
21
|
+
/** `fn` with an expression for its body, where it is an arrow or function expression with a block. */
|
|
22
|
+
export declare function expressionBodied(comp: Component, fn: N): N;
|
|
23
|
+
/** `const name = init` around `body`: the value `body` computes with `name` bound once, as a Rust
|
|
24
|
+
* `let` in a block that hands out nothing borrowed from it. */
|
|
25
|
+
export declare function letIn(s: Scope, n: N): Val;
|
|
26
|
+
/** `v` bound once to `name` for `body`, which is given the scope that reads it. */
|
|
27
|
+
export declare function bound(s: Scope, local: string, v: Val, then: (inner: Scope) => Val): Val;
|
|
28
|
+
/** Whether a desugared `if` whose test is `a && b` or `a || b` should be split, so that a side
|
|
29
|
+
* that narrows a value narrows what follows it, as TypeScript narrows it. */
|
|
30
|
+
export declare function splits(s: Scope, test: N): boolean;
|
|
31
|
+
/** `a && b ? x : y` as `a ? (b ? x : y) : y`, and `a || b ? x : y` as `a ? x : (b ? x : y)`. */
|
|
32
|
+
export declare function split(n: N): N;
|
package/dist/blocks.js
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { fail, GenError, nothing } from "./model.js";
|
|
2
|
+
import { ctx } from "./context.js";
|
|
3
|
+
import { expr, typedBy } from "./expr.js";
|
|
4
|
+
import { collected, heldList } from "./lists.js";
|
|
5
|
+
import { atom, bare, occurrences, operand, UNARY } from "./parens.js";
|
|
6
|
+
import { presence } from "./narrowing.js";
|
|
7
|
+
import { asCow, heldStr, isTemporary } from "./strings.js";
|
|
8
|
+
import { intFromF64 } from "./numbers.js";
|
|
9
|
+
const COW = "std::borrow::Cow::<str>";
|
|
10
|
+
/** Runtime calls that panic where JavaScript throws, or user code that may. */
|
|
11
|
+
export const PANICS = /\bfv::js_(?:repeat|pad_start|pad_end|date_parse|date_utc|to_iso_string)\(|\bfv_i18n\.t\(/;
|
|
12
|
+
export function panicky(code) {
|
|
13
|
+
return PANICS.test(code) || Object.values(ctx.helpers).some((h) => code.includes(`${h.rust}(`));
|
|
14
|
+
}
|
|
15
|
+
/** How a value is held in a Rust `let`: what the `let` is given, how the name reads back, and
|
|
16
|
+
* whether the `let` owns what a read borrows (a `String`, a `Cow`, a `Vec`). */
|
|
17
|
+
export function bindingOf(v) {
|
|
18
|
+
const lone = v.lone ? { lone: true } : {};
|
|
19
|
+
if (v.slice)
|
|
20
|
+
return { init: bare(v.code), read: (at) => ({ code: at, ty: v.ty, slice: true, ...lone }), owned: false };
|
|
21
|
+
if (v.ty.k === "list" && v.iter !== undefined)
|
|
22
|
+
return { init: collected(v), read: (at) => heldList(at, v.ty.k === "list" ? v.ty.of : v.ty, v.lone), owned: true };
|
|
23
|
+
if (v.held !== undefined)
|
|
24
|
+
return { init: v.held, read: (at) => ({ code: `${at}.as_deref()`, ty: v.ty, ...lone }), owned: true };
|
|
25
|
+
if (v.ty.k === "int" && v.f64 !== undefined && (v.code.endsWith(" as i64") || Object.is(v.num, -0))) {
|
|
26
|
+
return {
|
|
27
|
+
init: bare(v.f64),
|
|
28
|
+
read: (at) => ({ ...intFromF64(at), ...lone, ...(v.num !== undefined ? { num: v.num } : {}), ...(v.konst !== undefined ? { konst: v.konst } : {}) }),
|
|
29
|
+
owned: false,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (isTemporary(v))
|
|
33
|
+
return { init: heldStr(v), read: (at) => ({ code: `&*${at}`, ty: v.ty, ...lone }), owned: true };
|
|
34
|
+
const place = v.ty.k === "list" || v.ty.k === "record" || v.ty.k === "struct" || v.ty.k === "child";
|
|
35
|
+
return {
|
|
36
|
+
init: place ? `&${operand(v.code, UNARY)}` : bare(v.code),
|
|
37
|
+
read: (at) => ({ code: at, ty: v.ty, ...lone, ...(v.konst !== undefined ? { konst: v.konst } : {}) }),
|
|
38
|
+
owned: false,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const SKIP = new Set(["loc", "start", "end", "extra", "range", "typeAnnotation", "typeParameters", "returnType", "leadingComments", "trailingComments", "innerComments", "__fv", "label"]);
|
|
42
|
+
let renames = 0;
|
|
43
|
+
function spot(from) {
|
|
44
|
+
return { loc: from?.loc, start: from?.start, end: from?.end, __fv: from?.__fv };
|
|
45
|
+
}
|
|
46
|
+
function declared(st) {
|
|
47
|
+
if (st?.type === "VariableDeclaration")
|
|
48
|
+
return st.declarations.flatMap((d) => patternNames(d.id));
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
function patternNames(p) {
|
|
52
|
+
switch (p?.type) {
|
|
53
|
+
case "Identifier":
|
|
54
|
+
return [p.name];
|
|
55
|
+
case "ObjectPattern":
|
|
56
|
+
return p.properties.flatMap((q) => patternNames(q.type === "RestElement" ? q.argument : q.value));
|
|
57
|
+
case "ArrayPattern":
|
|
58
|
+
return p.elements.flatMap((q) => (q ? patternNames(q) : []));
|
|
59
|
+
case "AssignmentPattern":
|
|
60
|
+
return patternNames(p.left);
|
|
61
|
+
case "RestElement":
|
|
62
|
+
return patternNames(p.argument);
|
|
63
|
+
default:
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
class Renamer {
|
|
68
|
+
comp;
|
|
69
|
+
constructor(comp) {
|
|
70
|
+
this.comp = comp;
|
|
71
|
+
}
|
|
72
|
+
statements(sts, outer) {
|
|
73
|
+
const env = new Map(outer);
|
|
74
|
+
for (const st of sts)
|
|
75
|
+
for (const name of declared(st))
|
|
76
|
+
env.set(name, null);
|
|
77
|
+
return sts.map((st) => {
|
|
78
|
+
if (st.type !== "VariableDeclaration")
|
|
79
|
+
return this.node(st, env);
|
|
80
|
+
const declarations = st.declarations.map((d) => {
|
|
81
|
+
const init = this.node(d.init, env);
|
|
82
|
+
return { ...d, init, id: this.pattern(d.id, env) };
|
|
83
|
+
});
|
|
84
|
+
return { ...st, declarations };
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
pattern(p, env) {
|
|
88
|
+
switch (p?.type) {
|
|
89
|
+
case "Identifier": {
|
|
90
|
+
const to = `${p.name}#${++renames}`;
|
|
91
|
+
env.set(p.name, to);
|
|
92
|
+
return { ...p, name: to, fvName: p.name };
|
|
93
|
+
}
|
|
94
|
+
case "ObjectPattern":
|
|
95
|
+
return {
|
|
96
|
+
...p,
|
|
97
|
+
properties: p.properties.map((q) => q.type === "RestElement" ? { ...q, argument: this.pattern(q.argument, env) } : { ...q, key: q.computed ? this.node(q.key, env) : q.key, value: this.pattern(q.value, env), shorthand: false }),
|
|
98
|
+
};
|
|
99
|
+
case "ArrayPattern":
|
|
100
|
+
return { ...p, elements: p.elements.map((q) => (q ? this.pattern(q, env) : q)) };
|
|
101
|
+
case "AssignmentPattern":
|
|
102
|
+
return { ...p, right: this.node(p.right, env), left: this.pattern(p.left, env) };
|
|
103
|
+
case "RestElement":
|
|
104
|
+
return { ...p, argument: this.pattern(p.argument, env) };
|
|
105
|
+
default:
|
|
106
|
+
return p;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
node(n, env) {
|
|
110
|
+
if (!n || typeof n !== "object")
|
|
111
|
+
return n;
|
|
112
|
+
if (Array.isArray(n))
|
|
113
|
+
return n.map((x) => this.node(x, env));
|
|
114
|
+
switch (n.type) {
|
|
115
|
+
case "Identifier": {
|
|
116
|
+
const to = env.get(n.name);
|
|
117
|
+
if (to === null)
|
|
118
|
+
fail(this.comp, "FV1901", `\`${n.name}\` is read before its declaration, where JavaScript throws a \`ReferenceError\``, n);
|
|
119
|
+
return to === undefined ? n : { ...n, name: to, fvName: n.name };
|
|
120
|
+
}
|
|
121
|
+
case "MemberExpression":
|
|
122
|
+
case "OptionalMemberExpression":
|
|
123
|
+
return { ...n, object: this.node(n.object, env), property: n.computed ? this.node(n.property, env) : n.property };
|
|
124
|
+
case "ObjectProperty":
|
|
125
|
+
return { ...n, key: n.computed ? this.node(n.key, env) : n.key, value: this.node(n.value, env), shorthand: false };
|
|
126
|
+
case "ArrowFunctionExpression":
|
|
127
|
+
case "FunctionExpression":
|
|
128
|
+
case "FunctionDeclaration":
|
|
129
|
+
case "ObjectMethod": {
|
|
130
|
+
const inner = new Map(env);
|
|
131
|
+
for (const p of n.params)
|
|
132
|
+
for (const name of patternNames(p))
|
|
133
|
+
inner.delete(name);
|
|
134
|
+
if (n.type === "FunctionExpression" && n.id)
|
|
135
|
+
inner.delete(n.id.name);
|
|
136
|
+
const body = n.body.type === "BlockStatement" ? { ...n.body, body: this.statements(n.body.body, inner) } : this.node(n.body, inner);
|
|
137
|
+
return { ...n, body };
|
|
138
|
+
}
|
|
139
|
+
case "BlockStatement":
|
|
140
|
+
return { ...n, body: this.statements(n.body, env) };
|
|
141
|
+
case "SwitchStatement":
|
|
142
|
+
return { ...n, discriminant: this.node(n.discriminant, env), cases: n.cases.map((c) => ({ ...c, test: this.node(c.test, env), consequent: this.statements(c.consequent, env) })) };
|
|
143
|
+
}
|
|
144
|
+
const copy = { ...n };
|
|
145
|
+
for (const key of Object.keys(n)) {
|
|
146
|
+
if (SKIP.has(key))
|
|
147
|
+
continue;
|
|
148
|
+
const v = n[key];
|
|
149
|
+
if (v && typeof v === "object")
|
|
150
|
+
copy[key] = this.node(v, env);
|
|
151
|
+
}
|
|
152
|
+
return copy;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
const LOOPS = {
|
|
156
|
+
ForStatement: "a `for` loop",
|
|
157
|
+
ForOfStatement: "a `for…of` loop",
|
|
158
|
+
ForInStatement: "a `for…in` loop",
|
|
159
|
+
WhileStatement: "a `while` loop",
|
|
160
|
+
DoWhileStatement: "a `do…while` loop",
|
|
161
|
+
};
|
|
162
|
+
const OTHERS = {
|
|
163
|
+
TryStatement: "`try`, which only catches what the body throws",
|
|
164
|
+
ThrowStatement: "`throw`, which ends the render with an error",
|
|
165
|
+
LabeledStatement: "a labelled statement",
|
|
166
|
+
ContinueStatement: "`continue`",
|
|
167
|
+
FunctionDeclaration: "a function declared inside it",
|
|
168
|
+
ClassDeclaration: "a class declared inside it",
|
|
169
|
+
DebuggerStatement: "`debugger`",
|
|
170
|
+
WithStatement: "`with`",
|
|
171
|
+
};
|
|
172
|
+
const SUBSET = "it holds `const` (and `let` never assigned again), `if` / `else`, `switch` and `return`";
|
|
173
|
+
function assignments(comp, body) {
|
|
174
|
+
const lets = new Set();
|
|
175
|
+
const visit = (n) => {
|
|
176
|
+
if (!n || typeof n !== "object")
|
|
177
|
+
return;
|
|
178
|
+
if (Array.isArray(n)) {
|
|
179
|
+
n.forEach(visit);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
if (n.type === "VariableDeclaration" && n.kind === "let")
|
|
183
|
+
for (const d of n.declarations)
|
|
184
|
+
for (const name of patternNames(d.id))
|
|
185
|
+
lets.add(name);
|
|
186
|
+
if (n.type === "AssignmentExpression" || n.type === "UpdateExpression") {
|
|
187
|
+
const target = n.type === "AssignmentExpression" ? n.left : n.argument;
|
|
188
|
+
const shown = `\`${n.operator}\``;
|
|
189
|
+
if (target.type === "Identifier" && lets.has(target.name)) {
|
|
190
|
+
fail(comp, "FV1902", `\`let ${target.name}\` is assigned again with ${shown}, which a function's body does not translate: give each value a \`const\` of its own`, n);
|
|
191
|
+
}
|
|
192
|
+
fail(comp, "FV1902", `an assignment (${shown}) changes a value, which a function's body does not translate: give each value a \`const\` of its own`, n);
|
|
193
|
+
}
|
|
194
|
+
for (const key of Object.keys(n))
|
|
195
|
+
if (!SKIP.has(key) && n[key] && typeof n[key] === "object")
|
|
196
|
+
visit(n[key]);
|
|
197
|
+
};
|
|
198
|
+
visit(body);
|
|
199
|
+
}
|
|
200
|
+
/** How many times a body's statements may be translated again after an `if` or a `case` that
|
|
201
|
+
* falls off its end: each such path continues with the statements after it, translated on its own,
|
|
202
|
+
* so sequential `switch`es of `break`ing cases multiply them. */
|
|
203
|
+
export const PATH_BUDGET = 4096;
|
|
204
|
+
class Desugar {
|
|
205
|
+
comp;
|
|
206
|
+
paths = 0;
|
|
207
|
+
constructor(comp) {
|
|
208
|
+
this.comp = comp;
|
|
209
|
+
}
|
|
210
|
+
run(sts, k, brk, from) {
|
|
211
|
+
for (let i = 0; i < sts.length; i++) {
|
|
212
|
+
const st = sts[i];
|
|
213
|
+
const rest = sts.slice(i + 1);
|
|
214
|
+
const after = rest.length ? { sts: rest, next: k, brk } : k;
|
|
215
|
+
switch (st.type) {
|
|
216
|
+
case "EmptyStatement":
|
|
217
|
+
continue;
|
|
218
|
+
case "ReturnStatement":
|
|
219
|
+
if (!st.argument)
|
|
220
|
+
fail(this.comp, "FV1903", "`return` without a value returns `undefined`: return the value the function gives on this path", st);
|
|
221
|
+
this.dead(rest, "`return`");
|
|
222
|
+
return st.argument;
|
|
223
|
+
case "BreakStatement":
|
|
224
|
+
if (st.label || brk === undefined)
|
|
225
|
+
fail(this.comp, "FV1901", "`break` outside a `switch`'s case, which only a loop could take", st);
|
|
226
|
+
this.dead(rest, "`break`");
|
|
227
|
+
return this.go(brk, st);
|
|
228
|
+
case "BlockStatement":
|
|
229
|
+
return this.run(st.body, after, brk, st);
|
|
230
|
+
case "IfStatement": {
|
|
231
|
+
const yes = this.run(this.branch(st.consequent), after, brk, st);
|
|
232
|
+
const no = st.alternate ? this.run(this.branch(st.alternate), after, brk, st) : this.go(after, st);
|
|
233
|
+
return { type: "ConditionalExpression", test: st.test, consequent: yes, alternate: no, fvIf: true, ...spot(st) };
|
|
234
|
+
}
|
|
235
|
+
case "SwitchStatement":
|
|
236
|
+
return this.switched(st, after);
|
|
237
|
+
case "VariableDeclaration": {
|
|
238
|
+
if (st.kind !== "const" && st.kind !== "let")
|
|
239
|
+
fail(this.comp, "FV1901", `a function's body may not hold \`${st.kind}\`: declare the value with \`const\``, st);
|
|
240
|
+
const body = this.run(rest, k, brk, st);
|
|
241
|
+
return st.declarations.reduceRight((inner, d) => this.bind(d, inner), body);
|
|
242
|
+
}
|
|
243
|
+
case "ExpressionStatement":
|
|
244
|
+
return fail(this.comp, "FV1901", `a function's body may not hold an expression statement (\`${st.expression.type}\`), whose value is dropped and which could only change something: ${SUBSET}`, st);
|
|
245
|
+
default: {
|
|
246
|
+
const what = LOOPS[st.type] ?? OTHERS[st.type] ?? `\`${st.type}\``;
|
|
247
|
+
const why = LOOPS[st.type] ? `: ${SUBSET}; compute a list with \`.map()\`, \`.filter()\` or \`.reduce()\`` : `: ${SUBSET}`;
|
|
248
|
+
return fail(this.comp, "FV1901", `a function's body may not hold ${what}${why}`, st);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return this.go(k, from);
|
|
253
|
+
}
|
|
254
|
+
branch(st) {
|
|
255
|
+
return st.type === "BlockStatement" ? st.body : [st];
|
|
256
|
+
}
|
|
257
|
+
dead(rest, after) {
|
|
258
|
+
const first = rest.find((r) => r.type !== "EmptyStatement");
|
|
259
|
+
if (first)
|
|
260
|
+
fail(this.comp, "FV1904", `code after ${after}, which never runs: remove it`, first);
|
|
261
|
+
}
|
|
262
|
+
go(k, from) {
|
|
263
|
+
if (k === null) {
|
|
264
|
+
return fail(this.comp, "FV1903", "the function can reach its end here without a `return`, and JavaScript then returns `undefined`: return a value on every path", from);
|
|
265
|
+
}
|
|
266
|
+
if (k.falls)
|
|
267
|
+
fail(this.comp, "FV1904", "this case falls through into the next one, which runs its statements too: end it with `return` or `break`", k.falls);
|
|
268
|
+
if (k.sts.length && ++this.paths > PATH_BUDGET) {
|
|
269
|
+
fail(this.comp, "FV1905", `the body's \`if\`s and \`switch\`es continue with the statements after them on more than ${PATH_BUDGET} paths, each translated on its own: end the cases in \`return\`, or split the body into \`computed\`s`, from);
|
|
270
|
+
}
|
|
271
|
+
return this.run(k.sts, k.next, k.brk, from);
|
|
272
|
+
}
|
|
273
|
+
bind(d, body) {
|
|
274
|
+
if (!d.init)
|
|
275
|
+
fail(this.comp, "FV1901", `\`${d.id.fvName ?? d.id.name ?? "a pattern"}\` is declared without a value: give it one where it is declared`, d);
|
|
276
|
+
const typed = d.id.typeAnnotation?.typeAnnotation;
|
|
277
|
+
const init = typed ? typedBy(d.init, typed) : d.init;
|
|
278
|
+
return this.pattern(d.id, init, body, d);
|
|
279
|
+
}
|
|
280
|
+
pattern(p, value, body, d) {
|
|
281
|
+
switch (p.type) {
|
|
282
|
+
case "Identifier":
|
|
283
|
+
return { type: "FvLet", name: p.name, js: p.fvName ?? p.name, init: value, body, ...spot(d) };
|
|
284
|
+
case "ObjectPattern":
|
|
285
|
+
case "ArrayPattern": {
|
|
286
|
+
const tmp = `fv#${++renames}`;
|
|
287
|
+
const ref = () => ({ type: "Identifier", name: tmp, ...spot(value) });
|
|
288
|
+
const parts = [];
|
|
289
|
+
if (p.type === "ObjectPattern") {
|
|
290
|
+
for (const q of p.properties) {
|
|
291
|
+
if (q.type === "RestElement" || q.computed)
|
|
292
|
+
fail(this.comp, "FV1901", "a destructuring pattern names each field it reads, without `...rest` or a computed key", q);
|
|
293
|
+
const key = q.key.type === "Identifier" ? q.key.name : String(q.key.value);
|
|
294
|
+
parts.push([q.value, { type: "MemberExpression", computed: false, object: ref(), property: { type: "Identifier", name: key, ...spot(q.key) }, ...spot(q) }]);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
p.elements.forEach((q, i) => {
|
|
299
|
+
if (!q)
|
|
300
|
+
return;
|
|
301
|
+
if (q.type === "RestElement")
|
|
302
|
+
fail(this.comp, "FV1901", "a destructuring pattern names each item it reads, without `...rest`", q);
|
|
303
|
+
parts.push([q, { type: "MemberExpression", computed: true, object: ref(), property: { type: "NumericLiteral", value: i, ...spot(q) }, ...spot(q) }]);
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
for (const [q] of parts)
|
|
307
|
+
if (q.type === "AssignmentPattern")
|
|
308
|
+
fail(this.comp, "FV1901", "a default in a destructuring pattern, which applies to `undefined` alone: write `?? fallback` where it is read", q);
|
|
309
|
+
const inner = parts.reduceRight((acc, [q, v]) => this.pattern(q, v, acc, d), body);
|
|
310
|
+
return { type: "FvLet", name: tmp, js: "the destructured value", init: value, body: inner, ...spot(d) };
|
|
311
|
+
}
|
|
312
|
+
default:
|
|
313
|
+
return fail(this.comp, "FV1901", "a declaration binds a name, or an object or array pattern of names", p);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
switched(st, after) {
|
|
317
|
+
const d = `switch#${++renames}`;
|
|
318
|
+
const subject = () => ({ type: "Identifier", name: d, ...spot(st.discriminant) });
|
|
319
|
+
const groups = [];
|
|
320
|
+
let open = null;
|
|
321
|
+
for (const c of st.cases) {
|
|
322
|
+
if (!open) {
|
|
323
|
+
open = { tests: [], body: [], dflt: false, at: c };
|
|
324
|
+
groups.push(open);
|
|
325
|
+
}
|
|
326
|
+
if (c.test)
|
|
327
|
+
open.tests.push(c.test);
|
|
328
|
+
else
|
|
329
|
+
open.dflt = true;
|
|
330
|
+
const lexical = c.consequent.find((x) => x.type === "VariableDeclaration" || x.type === "FunctionDeclaration" || x.type === "ClassDeclaration");
|
|
331
|
+
if (lexical)
|
|
332
|
+
fail(this.comp, "FV1904", "a declaration directly in a `case`, which the whole `switch` shares: wrap the case's statements in `{ … }`", lexical);
|
|
333
|
+
if (c.consequent.some((x) => x.type !== "EmptyStatement")) {
|
|
334
|
+
open.body = c.consequent;
|
|
335
|
+
open = null;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const brk = after;
|
|
339
|
+
const bodyOf = (g, i) => {
|
|
340
|
+
const last = i === groups.length - 1;
|
|
341
|
+
const k = last ? after : { sts: [], next: null, brk: undefined, falls: g.body.at(-1) ?? g.at };
|
|
342
|
+
if (!g.body.length)
|
|
343
|
+
return last ? this.go(after, g.at) : this.go(k, g.at);
|
|
344
|
+
return this.run(g.body, k, brk, g.body.at(-1));
|
|
345
|
+
};
|
|
346
|
+
const fallback = groups.findIndex((g) => g.dflt);
|
|
347
|
+
let chain = fallback >= 0 ? bodyOf(groups[fallback], fallback) : this.go(after, st);
|
|
348
|
+
for (let i = groups.length - 1; i >= 0; i--) {
|
|
349
|
+
const g = groups[i];
|
|
350
|
+
if (i === fallback)
|
|
351
|
+
continue;
|
|
352
|
+
const test = g.tests
|
|
353
|
+
.map((t) => ({ type: "BinaryExpression", operator: "===", left: subject(), right: t, ...spot(t) }))
|
|
354
|
+
.reduce((a, b) => ({ type: "LogicalExpression", operator: "||", left: a, right: b, ...spot(b) }));
|
|
355
|
+
chain = { type: "ConditionalExpression", test, consequent: bodyOf(g, i), alternate: chain, ...spot(g.at) };
|
|
356
|
+
}
|
|
357
|
+
return { type: "FvLet", name: d, js: "the `switch`'s value", init: st.discriminant, body: chain, ...spot(st) };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const desugared = new WeakMap();
|
|
361
|
+
/** The expression a function's body computes: the expression of an arrow function, or a block of
|
|
362
|
+
* statements in the subset ferrovue translates, written as one expression. */
|
|
363
|
+
export function bodyExpr(comp, fn) {
|
|
364
|
+
if (fn.body.type !== "BlockStatement")
|
|
365
|
+
return fn.body;
|
|
366
|
+
const done = desugared.get(fn);
|
|
367
|
+
if (done)
|
|
368
|
+
return done;
|
|
369
|
+
assignments(comp, fn.body.body);
|
|
370
|
+
const shadowed = new Map();
|
|
371
|
+
const renamed = new Renamer(comp).statements(fn.body.body, shadowed);
|
|
372
|
+
const out = new Desugar(comp).run(renamed, null, undefined, fn.body.body.at(-1) ?? fn);
|
|
373
|
+
desugared.set(fn, out);
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
/** `p` (a name, or an object or array pattern of names) bound to `value` around `body`. */
|
|
377
|
+
export function destructured(comp, p, value, body) {
|
|
378
|
+
return new Desugar(comp).pattern(p, value, body, p);
|
|
379
|
+
}
|
|
380
|
+
/** `fn` with an expression for its body, where it is an arrow or function expression with a block. */
|
|
381
|
+
export function expressionBodied(comp, fn) {
|
|
382
|
+
if ((fn?.type !== "ArrowFunctionExpression" && fn?.type !== "FunctionExpression") || fn.body.type !== "BlockStatement")
|
|
383
|
+
return fn;
|
|
384
|
+
return { ...fn, body: bodyExpr(comp, fn) };
|
|
385
|
+
}
|
|
386
|
+
function simple(v) {
|
|
387
|
+
if (v.iter !== undefined || v.held !== undefined || v.format !== undefined)
|
|
388
|
+
return false;
|
|
389
|
+
if (nothing(v.ty))
|
|
390
|
+
return true;
|
|
391
|
+
return /^(?:&\*?|\*)?[A-Za-z_]\w*(?:\.\w+)*$/.test(v.code) || /^"(?:[^"\\]|\\.)*"$/.test(v.code) || /^-?\d[\w.]*$/.test(v.code);
|
|
392
|
+
}
|
|
393
|
+
function reps(v) {
|
|
394
|
+
return [v.code, v.iter, v.held, v.f64, ...(v.format?.args ?? [])].filter((r) => r !== undefined);
|
|
395
|
+
}
|
|
396
|
+
function ownedCow(v) {
|
|
397
|
+
if (!isTemporary(v))
|
|
398
|
+
return `${COW}::Owned(${atom(v.code).replace(/^&\*?/, "")}.to_owned())`;
|
|
399
|
+
const cow = asCow(v);
|
|
400
|
+
return cow.startsWith(`${COW}::Owned(`) ? cow : `${COW}::Owned(${atom(cow)}.into_owned())`;
|
|
401
|
+
}
|
|
402
|
+
/** `const name = init` around `body`: the value `body` computes with `name` bound once, as a Rust
|
|
403
|
+
* `let` in a block that hands out nothing borrowed from it. */
|
|
404
|
+
export function letIn(s, n) {
|
|
405
|
+
return bound(s, n.name, expr(s, n.init), (inner) => expr(inner, n.body));
|
|
406
|
+
}
|
|
407
|
+
/** `v` bound once to `name` for `body`, which is given the scope that reads it. */
|
|
408
|
+
export function bound(s, local, v, then) {
|
|
409
|
+
if (simple(v))
|
|
410
|
+
return then({ ...s, locals: new Map(s.locals).set(local, v) });
|
|
411
|
+
const name = `fv_c${++ctx.narrowCount}`;
|
|
412
|
+
const { init, read, owned } = bindingOf(v);
|
|
413
|
+
const held = read(name);
|
|
414
|
+
const body = then({ ...s, locals: new Map(s.locals).set(local, held) });
|
|
415
|
+
if (reps(body).join("\0") === reps(held).join("\0"))
|
|
416
|
+
return v;
|
|
417
|
+
const uses = reps(body).some((r) => occurrences(r, name) > 0);
|
|
418
|
+
if (!uses && !panicky(init))
|
|
419
|
+
return body;
|
|
420
|
+
const lead = `let ${uses ? name : "_"} = ${init};`;
|
|
421
|
+
const block = (code) => `{ ${lead} ${bare(code)} }`;
|
|
422
|
+
const lone = body.lone ? { lone: true } : {};
|
|
423
|
+
switch (body.ty.k) {
|
|
424
|
+
case "int":
|
|
425
|
+
case "float":
|
|
426
|
+
case "bool":
|
|
427
|
+
case "undef":
|
|
428
|
+
case "null":
|
|
429
|
+
return { code: block(body.code), ty: body.ty, ...lone, ...(body.f64 !== undefined ? { f64: block(body.f64) } : {}) };
|
|
430
|
+
case "str":
|
|
431
|
+
if (!owned && !isTemporary(body))
|
|
432
|
+
return { code: block(body.code), ty: body.ty, ...lone };
|
|
433
|
+
return { code: `&*${block(owned ? ownedCow(body) : asCow(body))}`, ty: body.ty, ...lone };
|
|
434
|
+
case "list": {
|
|
435
|
+
if (!owned && body.iter === undefined)
|
|
436
|
+
return { code: block(body.code), ty: body.ty, ...(body.slice ? { slice: true } : {}), ...lone };
|
|
437
|
+
const all = block(collected(body));
|
|
438
|
+
return { code: all, ty: body.ty, iter: `${all}.into_iter()`, ...lone };
|
|
439
|
+
}
|
|
440
|
+
case "opt":
|
|
441
|
+
if (body.ty.of.k === "str" && (owned || body.held !== undefined)) {
|
|
442
|
+
const kept = block(owned ? `${atom(body.code)}.map(|v| ${COW}::Owned(v.to_owned()))` : body.held);
|
|
443
|
+
return { code: `${kept}.as_deref()`, ty: body.ty, held: kept, ...lone };
|
|
444
|
+
}
|
|
445
|
+
return { code: block(body.code), ty: body.ty, ...lone };
|
|
446
|
+
default: {
|
|
447
|
+
const ref = /^\w+(?:\.[\w#]+)+$/.test(body.code) ? `&${body.code}` : body.code;
|
|
448
|
+
return { code: `(${block(ref)})`, ty: body.ty, ...lone };
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
/** Whether a desugared `if` whose test is `a && b` or `a || b` should be split, so that a side
|
|
453
|
+
* that narrows a value narrows what follows it, as TypeScript narrows it. */
|
|
454
|
+
export function splits(s, test) {
|
|
455
|
+
if (test.type !== "LogicalExpression" || (test.operator !== "&&" && test.operator !== "||"))
|
|
456
|
+
return false;
|
|
457
|
+
const narrows = (side) => {
|
|
458
|
+
if (side.type === "LogicalExpression" && side.operator === test.operator)
|
|
459
|
+
return splits(s, side);
|
|
460
|
+
try {
|
|
461
|
+
return presence(s, side) !== null;
|
|
462
|
+
}
|
|
463
|
+
catch (e) {
|
|
464
|
+
if (e instanceof GenError)
|
|
465
|
+
return false;
|
|
466
|
+
throw e;
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
return narrows(test.left) || narrows(test.right);
|
|
470
|
+
}
|
|
471
|
+
/** `a && b ? x : y` as `a ? (b ? x : y) : y`, and `a || b ? x : y` as `a ? x : (b ? x : y)`. */
|
|
472
|
+
export function split(n) {
|
|
473
|
+
const { left, right, operator } = n.test;
|
|
474
|
+
const inner = { ...n, test: right };
|
|
475
|
+
return operator === "&&" ? { ...n, test: left, consequent: inner, alternate: n.alternate } : { ...n, test: left, consequent: n.consequent, alternate: inner };
|
|
476
|
+
}
|
package/dist/calls.d.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { type N, type Scope, type Val } from "./model.ts";
|
|
2
2
|
export declare function call(s: Scope, n: N): Val;
|
|
3
|
+
/** The globals of JavaScript a template may name, which the server translates only some of. */
|
|
4
|
+
export declare const BUILTINS: Set<string>;
|
|
5
|
+
/** Refuses `what`, a use of one of JavaScript's globals the server does not translate. */
|
|
6
|
+
export declare function builtin(s: Scope, what: string, n: N): never;
|
|
3
7
|
export declare function isObjectCall(n: N, method: string): boolean;
|
|
4
8
|
export declare function helperCall(s: Scope, name: string, args: N[], n: N): Val;
|