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/narrowing.js CHANGED
@@ -1,9 +1,9 @@
1
- import { absence, BOOL, fail, nothing } from "./model.js";
1
+ import { absence, BOOL, fail, nothing, rustStr, STR } from "./model.js";
2
2
  import { ctx } from "./context.js";
3
3
  import { claim } from "./plugin.js";
4
4
  import { atom, bare, binary, condition, ifElse, logical, not, occurrences, receiver } from "./parens.js";
5
5
  import { isNumber, negatedOrder } from "./numbers.js";
6
- import { expr } from "./expr.js";
6
+ import { describeTy, expr } from "./expr.js";
7
7
  export function known(v) {
8
8
  if (v.konst !== undefined)
9
9
  return v.konst;
@@ -34,8 +34,10 @@ export function truthy(v) {
34
34
  case "undef":
35
35
  case "null":
36
36
  return "false";
37
- case "opt":
38
- return `${atom(v.code)}.is_some_and(|v| ${truthy({ code: "v", ty: v.ty.of })})`;
37
+ case "opt": {
38
+ const each = truthy({ code: "v", ty: v.ty.of });
39
+ return each === "true" ? `${atom(v.code)}.is_some()` : `${atom(v.code)}.is_some_and(|v| ${each})`;
40
+ }
39
41
  default:
40
42
  return claim((p) => p.values?.truthy?.(v)) ?? "true";
41
43
  }
@@ -52,6 +54,8 @@ export function pathOf(n) {
52
54
  return `${base}.${n.property.name}`;
53
55
  if (n.computed && n.property.type === "StringLiteral")
54
56
  return `${base}.${n.property.value}`;
57
+ if (n.computed && n.property.type === "NumericLiteral")
58
+ return `${base}[${n.property.value}]`;
55
59
  return null;
56
60
  }
57
61
  function nothingLiteral(n) {
@@ -83,6 +87,72 @@ export function checkNullTest(s, t, v) {
83
87
  const what = none === "null" ? "is `T | null`, never `undefined`" : "is optional, which is `undefined` when absent, never `null`";
84
88
  fail(s.comp, "FV0627", `\`${op}= ${t.literal}\` of a value that ${what}: compare with \`${other}\``, t.target);
85
89
  }
90
+ export function typeofCompare(n) {
91
+ if (n.type !== "BinaryExpression" || !["===", "!==", "==", "!="].includes(n.operator))
92
+ return null;
93
+ const isTypeof = (m) => m.type === "UnaryExpression" && m.operator === "typeof";
94
+ const [t, lit] = isTypeof(n.left) && n.right.type === "StringLiteral" ? [n.left, n.right] : isTypeof(n.right) && n.left.type === "StringLiteral" ? [n.right, n.left] : [null, null];
95
+ return t ? { target: t.argument, literal: lit.value, equal: n.operator.startsWith("=") } : null;
96
+ }
97
+ function typeName(s, ty, n) {
98
+ switch (ty.k) {
99
+ case "str":
100
+ case "html":
101
+ return "string";
102
+ case "int":
103
+ case "float":
104
+ return "number";
105
+ case "bool":
106
+ return "boolean";
107
+ case "undef":
108
+ return "undefined";
109
+ case "null":
110
+ case "list":
111
+ case "record":
112
+ case "struct":
113
+ case "child":
114
+ case "date":
115
+ return "object";
116
+ default:
117
+ return fail(s.comp, "FV0722", `\`typeof\` of ${describeTy(ty)}, whose JavaScript type the compiler does not know`, n);
118
+ }
119
+ }
120
+ /** What `typeof v` gives: `present` for a value that is there, and `absent` for an optional one
121
+ * that is not (`null` when `v` cannot be absent, `either` when it may be `null` or `undefined`). */
122
+ function typeNames(s, v, n) {
123
+ if (v.ty.k !== "opt")
124
+ return { present: typeName(s, v.ty, n), absent: null };
125
+ const absent = v.ty.none === "null" ? "object" : v.ty.none === "either" ? "either" : "undefined";
126
+ return { present: typeName(s, v.ty.of, n), absent };
127
+ }
128
+ function eitherRefused(s, literal, n) {
129
+ return fail(s.comp, "FV0722", `\`typeof\` compared with "${literal}" of a value that may be \`null\` (an "object") or \`undefined\`, which its Rust \`Option\` cannot tell apart: test it with \`== null\``, n);
130
+ }
131
+ export function typeofVal(s, n) {
132
+ const v = expr(s, n.argument);
133
+ const { present, absent } = typeNames(s, v, n.argument);
134
+ if (absent === null || absent === present)
135
+ return { code: rustStr(present), ty: STR };
136
+ if (absent === "either")
137
+ return eitherRefused(s, present, n);
138
+ return { code: `if ${atom(v.code)}.is_some() { ${rustStr(present)} } else { ${rustStr(absent)} }`, ty: STR };
139
+ }
140
+ export function typeofTest(s, t, n) {
141
+ const v = expr(s, t.target);
142
+ const { present, absent } = typeNames(s, v, t.target);
143
+ const konst = (is) => ({ code: String(is === t.equal), ty: BOOL, konst: is === t.equal });
144
+ if (absent === null)
145
+ return konst(present === t.literal);
146
+ if (absent === "either" && (t.literal === "undefined" || t.literal === "object"))
147
+ return eitherRefused(s, t.literal, n);
148
+ if (present === t.literal && absent === t.literal)
149
+ return konst(true);
150
+ if (present === t.literal)
151
+ return boolOf(t.equal ? `${atom(v.code)}.is_some()` : `${atom(v.code)}.is_none()`);
152
+ if (absent === t.literal)
153
+ return boolOf(t.equal ? `${atom(v.code)}.is_none()` : `${atom(v.code)}.is_some()`);
154
+ return konst(false);
155
+ }
86
156
  export function presence(s, n) {
87
157
  for (const p of ctx.plugins) {
88
158
  const own = p.presence?.(s, n);
@@ -97,12 +167,19 @@ export function presence(s, n) {
97
167
  negated = true;
98
168
  }
99
169
  else if (n.type === "BinaryExpression") {
170
+ const tested = typeofCompare(n);
100
171
  const test = nullTest(n);
101
- if (!test)
172
+ if (tested) {
173
+ target = tested.target;
174
+ byTruth = false;
175
+ }
176
+ else if (!test)
102
177
  return null;
103
- target = test.target;
104
- negated = test.is;
105
- byTruth = false;
178
+ else {
179
+ target = test.target;
180
+ negated = test.is;
181
+ byTruth = false;
182
+ }
106
183
  }
107
184
  const path = pathOf(target);
108
185
  if (path === null)
@@ -110,7 +187,19 @@ export function presence(s, n) {
110
187
  const v = expr(s, target);
111
188
  if (v.ty.k !== "opt")
112
189
  return null;
113
- if (n.type === "BinaryExpression")
190
+ const tested = typeofCompare(n);
191
+ if (tested) {
192
+ const { present, absent } = typeNames(s, v, target);
193
+ if (absent === "either" || present === absent)
194
+ return null;
195
+ if (tested.literal === present)
196
+ negated = !tested.equal;
197
+ else if (tested.literal === absent)
198
+ negated = tested.equal;
199
+ else
200
+ return null;
201
+ }
202
+ else if (n.type === "BinaryExpression")
114
203
  checkNullTest(s, nullTest(n), v);
115
204
  const of = v.ty.of;
116
205
  const scalar = of.k === "str" || of.k === "int" || of.k === "float" || of.k === "bool";
package/dist/numbers.d.ts CHANGED
@@ -6,3 +6,7 @@ export declare function numberVal(x: number, int: boolean): Val;
6
6
  export declare function arithmetic(a: Val, op: string, b: Val, int: boolean): Val;
7
7
  export declare function compare(a: Val, op: string, b: Val): Val;
8
8
  export declare function negatedOrder(s: Scope, n: N): Val | null;
9
+ /** `a ** b` where every engine gives the same answer: an integer to a whole power from 0 whose
10
+ * result is an integer within ±2⁵³, worked out exactly; `null` for any other. */
11
+ export declare function exactPower(a: number, b: number): number | null;
12
+ export declare function power(s: Scope, base: N, exponent: N, n: N, what: string): Val;
package/dist/numbers.js CHANGED
@@ -1,5 +1,5 @@
1
- import { BOOL, FLOAT, INT } from "./model.js";
2
- import { AS, binary, FLIPPED, operand, UNARY } from "./parens.js";
1
+ import { BOOL, fail, FLOAT, INT } from "./model.js";
2
+ import { AS, atom, binary, FLIPPED, operand, UNARY } from "./parens.js";
3
3
  import { expr } from "./expr.js";
4
4
  export function isNumber(ty) {
5
5
  return ty.k === "int" || ty.k === "float";
@@ -20,7 +20,31 @@ function floatLiteral(x) {
20
20
  const spelled = String(Math.abs(x)).replace("e+", "e");
21
21
  return `${x < 0 || Object.is(x, -0) ? "-" : ""}${/[.e]/.test(spelled) ? spelled : `${spelled}.0`}f64`;
22
22
  }
23
+ const RUST_CONSTANTS = new Map(Object.entries({
24
+ E: "2.71828182845904523536028747135266250",
25
+ FRAC_1_PI: "0.318309886183790671537767526745028724",
26
+ FRAC_1_SQRT_2: "0.707106781186547524400844362104849039",
27
+ FRAC_2_PI: "0.636619772367581343075535053490057448",
28
+ FRAC_2_SQRT_PI: "1.12837916709551257389615890312154517",
29
+ FRAC_PI_2: "1.57079632679489661923132169163975144",
30
+ FRAC_PI_3: "1.04719755119659774615421446109316763",
31
+ FRAC_PI_4: "0.785398163397448309615660845819875721",
32
+ FRAC_PI_6: "0.52359877559829887307710723054658381",
33
+ FRAC_PI_8: "0.39269908169872415480783042290993786",
34
+ LN_10: "2.30258509299404568401799145468436421",
35
+ LN_2: "0.693147180559945309417232121458176568",
36
+ LOG10_2: "0.301029995663981195213738894724493027",
37
+ LOG10_E: "0.434294481903251827651128918916605082",
38
+ LOG2_10: "3.32192809488736234787031942948939018",
39
+ LOG2_E: "1.44269504088896340735992468100189214",
40
+ PI: "3.14159265358979323846264338327950288",
41
+ SQRT_2: "1.41421356237309504880168872420969808",
42
+ TAU: "6.28318530717958647692528676655900577",
43
+ }).map(([name, digits]) => [Number(digits), `std::f64::consts::${name}`]));
23
44
  export function numberVal(x, int) {
45
+ const constant = int ? undefined : RUST_CONSTANTS.get(Math.abs(x));
46
+ if (constant !== undefined)
47
+ return { code: x < 0 ? `-${constant}` : constant, ty: FLOAT, num: x };
24
48
  if (!int)
25
49
  return { code: floatLiteral(x), ty: FLOAT, num: x };
26
50
  if (!Number.isSafeInteger(x))
@@ -63,6 +87,13 @@ export function compare(a, op, b) {
63
87
  const konst = fold(a.num, op, b.num);
64
88
  return { code: String(konst), ty: BOOL, konst };
65
89
  }
90
+ if (asF64(a) === asF64(b)) {
91
+ if (op === "<" || op === ">")
92
+ return { code: "false", ty: BOOL, konst: false };
93
+ if (a.ty.k === "int")
94
+ return { code: "true", ty: BOOL, konst: true };
95
+ return { code: `!${atom(asF64(a))}.is_nan()`, ty: BOOL };
96
+ }
66
97
  return { code: binary(asF64(a), op, asF64(b)), ty: BOOL };
67
98
  }
68
99
  export function negatedOrder(s, n) {
@@ -72,3 +103,31 @@ export function negatedOrder(s, n) {
72
103
  const b = expr(s, n.right);
73
104
  return a.ty.k === "int" && b.ty.k === "int" ? compare(a, FLIPPED[n.operator], b) : null;
74
105
  }
106
+ /** `a ** b` where every engine gives the same answer: an integer to a whole power from 0 whose
107
+ * result is an integer within ±2⁵³, worked out exactly; `null` for any other. */
108
+ export function exactPower(a, b) {
109
+ if (!Number.isInteger(a) || !Number.isInteger(b) || b < 0 || Math.abs(a) > 2 ** 53)
110
+ return null;
111
+ if (b === 0)
112
+ return 1;
113
+ if (Object.is(a, -0))
114
+ return b % 2 === 1 ? -0 : 0;
115
+ if (Math.abs(a) > 1 && b > 53)
116
+ return null;
117
+ const r = BigInt(a) ** BigInt(b);
118
+ const limit = 2n ** 53n;
119
+ return r > limit || r < -limit ? null : Number(r);
120
+ }
121
+ export function power(s, base, exponent, n, what) {
122
+ const [a, b] = [expr(s, base), expr(s, exponent)];
123
+ if (!isNumber(a.ty) || !isNumber(b.ty))
124
+ return fail(s.comp, "FV0720", `${what} of numbers that are present`, n);
125
+ const engines = "Node 22 writes `10 ** -20` as `1.0000000000000001e-20`, and engines round powers beyond 2⁵³ in their own ways";
126
+ if (a.num === undefined || b.num === undefined) {
127
+ return fail(s.comp, "FV0720", `${what} of a value known only when rendering: ferrovue works ${what} out when compiling, for an integer to a whole power from 0 whose result is within ±2⁵³, since ${engines}: compute it in Rust and pass it as a prop`, n);
128
+ }
129
+ const r = exactPower(a.num, b.num);
130
+ if (r === null)
131
+ return fail(s.comp, "FV0720", `${what} of ${String(a.num)} and ${String(b.num)}, which is not an integer to a whole power from 0 with a result within ±2⁵³: ${engines}`, n);
132
+ return numberVal(r, true);
133
+ }
package/dist/parens.js CHANGED
@@ -178,7 +178,14 @@ export function strArg(code) {
178
178
  const branches = ifElse(code);
179
179
  if (branches)
180
180
  return `if ${branches.test} { ${strArg(branches.yes)} } else { ${strArg(branches.no)} }`;
181
+ if (/^\(\*\w+\)$/.test(code))
182
+ return bare(code);
181
183
  const m = /^&\*(\**)(.*)$/s.exec(code);
184
+ if (m && !m[1]) {
185
+ const lazy = /^\(\*(\w+)\)$/.exec(m[2])?.[1];
186
+ if (lazy !== undefined)
187
+ return `&${lazy}`;
188
+ }
182
189
  if (!m || (binding(m[2]) < ATOM && !enclosed(m[2])))
183
190
  return code;
184
191
  return m[1] ? m[2] : `&${m[2]}`;
package/dist/plugin.d.ts CHANGED
@@ -34,6 +34,9 @@ export interface ValueHooks {
34
34
  } | undefined;
35
35
  nullish?(s: Scope, a: Val, b: Val, n: N): Val | undefined;
36
36
  equals?(a: Val, b: Val): string | undefined;
37
+ order?(a: Val, op: string, b: Val): Val | undefined;
38
+ refuseEquality?(s: Scope, a: Val, b: Val, n: N): void;
39
+ json?(v: Val): string | undefined;
37
40
  isArray?(v: Val): Val | undefined;
38
41
  }
39
42
  export interface ReadComponent {
@@ -75,6 +78,8 @@ export interface Plugin<Run = unknown, Local = unknown> {
75
78
  };
76
79
  global?(s: Scope, name: string, n: N): Val | null;
77
80
  call?(s: Scope, n: N): Val | null;
81
+ construct?(s: Scope, n: N): Val | null;
82
+ method?(s: Scope, target: Val, method: string, args: N[], n: N): Val | null;
78
83
  member?(s: Scope, base: Val, prop: string, n: N, computed: boolean): Val | null;
79
84
  equality?(s: Scope, n: N): Val | null;
80
85
  presence?(s: Scope, n: N): Presence | null | undefined;
@@ -0,0 +1,25 @@
1
+ import { type N, type Scope } from "../model.ts";
2
+ import { type Plugin } from "../plugin.ts";
3
+ declare module "../model.ts" {
4
+ interface PluginTys {
5
+ date: {
6
+ k: "date";
7
+ };
8
+ }
9
+ }
10
+ /** The time value `Date.parse(s)` gives a string every JavaScript engine reads alike: one in
11
+ * ECMAScript's date time string format, a date alone or a date and time with a `Z` or an offset, its
12
+ * fields within their ranges. `undefined` for any other string, which an engine may read by rules of
13
+ * its own, or in the time zone of the machine running it, and for a date and time whose fields lie
14
+ * beyond ±8.64 × 10¹⁵ ms read as UTC while its offset brings it within, which Safari reads as an
15
+ * invalid date and the other engines as a valid one. */
16
+ export declare function portableTime(s: string): number | undefined;
17
+ /** Whether `Date.UTC(...parts)` gives what every JavaScript engine gives. V8 reads a year beyond
18
+ * ±1,000,000, a month beyond ±10,000,000, and a year before −399,999 once the month is carried into
19
+ * it by rules of its own, which Firefox and Safari do not share, so those are portable only where
20
+ * the date is invalid in every engine; and a part worth more than 2⁵³ ms that the others bring back
21
+ * within the range of dates is rounded by Node otherwise than by the browsers. */
22
+ export declare function portableUtc(parts: readonly number[]): boolean;
23
+ /** The refusal of anything that formats by locale, which `Intl` decides from data each engine ships. */
24
+ export declare function localeRefusal(s: Scope, what: string, n: N): never;
25
+ export declare const dates: Plugin;
@@ -0,0 +1,224 @@
1
+ import { fail, FLOAT, nothing, STR } from "../model.js";
2
+ import {} from "../plugin.js";
3
+ import { bare, strArg } from "../parens.js";
4
+ import { asF64, compare, numberVal } from "../numbers.js";
5
+ import { describeTy, expr } from "../expr.js";
6
+ const GUIDE = "see the `dates_and_prices` guide";
7
+ const PORTABLE = /^([+-]\d{6}|\d{4})(?:-(\d{2})(?:-(\d{2}))?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{3})?)?(Z|[+-](\d{2}):(\d{2}))?)?$/;
8
+ function daysInMonth(year, month) {
9
+ if (month === 2)
10
+ return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
11
+ return [4, 6, 9, 11].includes(month) ? 30 : 31;
12
+ }
13
+ /** The time value `Date.parse(s)` gives a string every JavaScript engine reads alike: one in
14
+ * ECMAScript's date time string format, a date alone or a date and time with a `Z` or an offset, its
15
+ * fields within their ranges. `undefined` for any other string, which an engine may read by rules of
16
+ * its own, or in the time zone of the machine running it, and for a date and time whose fields lie
17
+ * beyond ±8.64 × 10¹⁵ ms read as UTC while its offset brings it within, which Safari reads as an
18
+ * invalid date and the other engines as a valid one. */
19
+ export function portableTime(s) {
20
+ const m = PORTABLE.exec(s);
21
+ if (!m || m[1] === "-000000" || (m[4] !== undefined && m[7] === undefined))
22
+ return undefined;
23
+ const [year, month, day, hours, minutes, seconds, , zoneHours, zoneMinutes] = m.slice(1).map((x) => (x === undefined ? undefined : Number(x)));
24
+ const within = (x, low, high) => x === undefined || (x >= low && x <= high);
25
+ const fields = [within(month, 1, 12), within(day, 1, daysInMonth(year, month ?? 1)), within(hours, 0, 23), within(minutes, 0, 59), within(seconds, 0, 59), within(zoneHours, 0, 23), within(zoneMinutes, 0, 59)];
26
+ if (!fields.every(Boolean))
27
+ return undefined;
28
+ const t = Date.parse(s);
29
+ const zone = m[7];
30
+ if (zone !== undefined && zone !== "Z" && Number.isNaN(Date.parse(s.slice(0, -zone.length) + "Z")) && !Number.isNaN(t))
31
+ return undefined;
32
+ return t;
33
+ }
34
+ const COMMON_DAYS = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
35
+ function v8Day(year, month, date) {
36
+ const before = (y) => 365 * (y + 399_999) + Math.trunc((y + 399_999) / 4) - Math.trunc((y + 399_999) / 100) + Math.trunc((y + 399_999) / 400);
37
+ let y = Math.trunc(year) + Math.trunc(Math.trunc(month) / 12);
38
+ let m = Math.trunc(month) % 12;
39
+ if (m < 0)
40
+ [m, y] = [m + 12, y - 1];
41
+ const leap = y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0);
42
+ return before(y) - before(1970) + COMMON_DAYS[m] + (leap && m >= 2 ? 1 : 0) - 1 + Math.trunc(date);
43
+ }
44
+ /** Whether `Date.UTC(...parts)` gives what every JavaScript engine gives. V8 reads a year beyond
45
+ * ±1,000,000, a month beyond ±10,000,000, and a year before −399,999 once the month is carried into
46
+ * it by rules of its own, which Firefox and Safari do not share, so those are portable only where
47
+ * the date is invalid in every engine; and a part worth more than 2⁵³ ms that the others bring back
48
+ * within the range of dates is rounded by Node otherwise than by the browsers. */
49
+ export function portableUtc(parts) {
50
+ const [year = NaN, month = 0, date = 1, hours = 0, minutes = 0, seconds = 0, ms = 0] = parts;
51
+ if (![year, month, date, hours, minutes, seconds, ms].every(Number.isFinite))
52
+ return true;
53
+ const int = Math.trunc;
54
+ const terms = [int(hours) * 3_600_000, int(minutes) * 60_000, int(seconds) * 1000];
55
+ const time = terms[0] + terms[1] + terms[2] + int(ms);
56
+ if (!Number.isFinite(time))
57
+ return true;
58
+ const y = int(year) >= 0 && int(year) <= 99 ? 1900 + int(year) : year;
59
+ const carried = int(y) + Math.floor(int(month) / 12);
60
+ if (!(Math.abs(y) <= 1e6 && Math.abs(month) <= 1e7 && carried >= -399_999)) {
61
+ return Math.abs(carried) >= 399_999 && Math.abs(int(date)) <= 1e7 && Math.abs(time) <= 1e15;
62
+ }
63
+ const days = v8Day(y, month, date) * 86_400_000;
64
+ const t = days + time;
65
+ const largest = Math.max(Math.abs(days), ...terms.map(Math.abs));
66
+ return !(Number.isFinite(t) && largest > 2 ** 53 && Math.abs(t) <= 8.64e15 + largest * 2 ** -50);
67
+ }
68
+ const UTC_DEFAULTS = ["0.0f64", "1.0f64", "0.0f64", "0.0f64", "0.0f64", "0.0f64"];
69
+ function fromParts(s, n) {
70
+ const args = n.arguments;
71
+ if (args.length < 1 || args.length > 7)
72
+ return fail(s.comp, "FV1807", `\`Date.UTC()\` takes from one to seven numbers: a year, a month from 0 and the day, hours, minutes, seconds and milliseconds that follow (${GUIDE})`, n);
73
+ const vals = args.map((a) => {
74
+ const v = expr(s, a);
75
+ if (v.ty.k !== "int" && v.ty.k !== "float")
76
+ fail(s.comp, "FV1807", `\`Date.UTC()\` takes numbers that are present: this is ${describeTy(v.ty)}`, a);
77
+ return v;
78
+ });
79
+ if (vals.every((v) => v.num !== undefined)) {
80
+ const parts = vals.map((v) => v.num);
81
+ if (!portableUtc(parts)) {
82
+ return fail(s.comp, "FV1808", `\`Date.UTC(${parts.join(", ")})\` is a date engines compute differently: V8 reads a year beyond ±1,000,000, a month beyond ±10,000,000 and a year before −399,999 by rules of its own, and Node rounds a part beyond 2⁵³ ms otherwise than the browsers (${GUIDE})`, n);
83
+ }
84
+ return numberVal(Date.UTC(...parts), false);
85
+ }
86
+ const code = [...vals.map((v) => bare(asF64(v))), ...UTC_DEFAULTS.slice(vals.length - 1)].join(", ");
87
+ return { code: `fv::js_date_utc(${code})`, ty: FLOAT };
88
+ }
89
+ const LOCAL_GETTERS = new Map([
90
+ ["getFullYear", "getUTCFullYear"],
91
+ ["getYear", "getUTCFullYear"],
92
+ ["getMonth", "getUTCMonth"],
93
+ ["getDate", "getUTCDate"],
94
+ ["getDay", "getUTCDay"],
95
+ ["getHours", "getUTCHours"],
96
+ ["getMinutes", "getUTCMinutes"],
97
+ ["getSeconds", "getUTCSeconds"],
98
+ ["getMilliseconds", "getUTCMilliseconds"],
99
+ ]);
100
+ const UTC_GETTERS = new Map([
101
+ ["getUTCFullYear", "full_year"],
102
+ ["getUTCMonth", "month"],
103
+ ["getUTCDate", "date"],
104
+ ["getUTCDay", "day"],
105
+ ["getUTCHours", "hours"],
106
+ ["getUTCMinutes", "minutes"],
107
+ ["getUTCSeconds", "seconds"],
108
+ ["getUTCMilliseconds", "milliseconds"],
109
+ ]);
110
+ const LOCALE_METHODS = new Set(["toLocaleString", "toLocaleDateString", "toLocaleTimeString", "localeCompare"]);
111
+ const DATE = { k: "date" };
112
+ function global(s, n, name) {
113
+ return n?.type === "Identifier" && n.name === name && !s.locals.has(name) && !s.setup.has(name);
114
+ }
115
+ function rootOf(n) {
116
+ while (n?.type === "MemberExpression" || n?.type === "CallExpression" || n?.type === "NewExpression")
117
+ n = n.type === "MemberExpression" ? n.object : n.callee;
118
+ return n;
119
+ }
120
+ /** The refusal of anything that formats by locale, which `Intl` decides from data each engine ships. */
121
+ export function localeRefusal(s, what, n) {
122
+ return fail(s.comp, "FV1805", `${what} formats by locale with \`Intl\`, whose output differs between Node, Chromium, Firefox and Safari: format the value in Rust and pass a string, or translate it with \`$t\` (${GUIDE})`, n);
123
+ }
124
+ function parsed(s, arg, what) {
125
+ if (arg.type === "StringLiteral") {
126
+ const t = portableTime(arg.value);
127
+ if (t === undefined) {
128
+ fail(s.comp, "FV1803", `${what} of ${JSON.stringify(arg.value)}, which is not a date alone nor a date and time with a \`Z\` or an offset in ECMAScript's date time string format, so each browser may read it differently (${GUIDE})`, arg);
129
+ }
130
+ return numberVal(t, false);
131
+ }
132
+ const v = expr(s, arg);
133
+ if (v.ty.k !== "str")
134
+ return fail(s.comp, "FV1802", `${what} takes a string that is present`, arg);
135
+ return { code: `fv::js_date_parse(${strArg(v.code)})`, ty: FLOAT };
136
+ }
137
+ function constructed(s, n) {
138
+ const args = n.arguments;
139
+ if (args.length === 0)
140
+ return fail(s.comp, "FV1801", `\`new Date()\` is the time it runs, which differs between the server's render and the browser's hydration: pass the time as a prop (${GUIDE})`, n);
141
+ if (args.length > 1) {
142
+ return fail(s.comp, "FV1802", `\`new Date(year, month, …)\` reads its parts in the time zone of the machine running it: pass an ISO string with a \`Z\` or an offset, or a number of milliseconds (${GUIDE})`, n);
143
+ }
144
+ if (args[0].type === "StringLiteral")
145
+ return { code: parsed(s, args[0], "`new Date()`").code, ty: DATE };
146
+ const v = expr(s, args[0]);
147
+ if (v.ty.k === "date")
148
+ return v;
149
+ if (v.ty.k === "str")
150
+ return { code: `fv::js_date_parse(${strArg(v.code)})`, ty: DATE };
151
+ if (v.ty.k === "int" || v.ty.k === "float")
152
+ return { code: `fv::js_time_clip(${bare(asF64(v))})`, ty: DATE };
153
+ return fail(s.comp, "FV1802", `\`new Date()\` takes one string or one number that is present (${GUIDE})`, args[0]);
154
+ }
155
+ export const dates = {
156
+ name: "dates",
157
+ construct(s, n) {
158
+ if (global(s, rootOf(n.callee), "Intl"))
159
+ return localeRefusal(s, "`Intl`", n);
160
+ return global(s, n.callee, "Date") ? constructed(s, n) : null;
161
+ },
162
+ call(s, n) {
163
+ const callee = n.callee;
164
+ if (callee.type !== "MemberExpression" || callee.computed)
165
+ return null;
166
+ const method = callee.property.name;
167
+ const object = callee.object;
168
+ if (object.type === "Identifier" && object.name === "_ctx" && (method === "$d" || method === "$n"))
169
+ return localeRefusal(s, `vue-i18n's \`${method}\``, n);
170
+ const own = object.type === "Identifier" && (object.name === "_ctx" || object.name === "$setup");
171
+ if (!own && LOCALE_METHODS.has(method))
172
+ return localeRefusal(s, `\`.${method}()\``, n);
173
+ if (!global(s, object, "Date"))
174
+ return null;
175
+ if (method === "now")
176
+ return fail(s.comp, "FV1801", `\`Date.now()\` is the time it runs, which differs between the server's render and the browser's hydration: pass the time as a prop (${GUIDE})`, n);
177
+ if (method === "parse" && n.arguments.length === 1)
178
+ return parsed(s, n.arguments[0], "`Date.parse()`");
179
+ if (method === "UTC")
180
+ return fromParts(s, n);
181
+ return fail(s.comp, "FV1804", `\`Date.${method}()\` is not supported: build a date with \`new Date()\` of an ISO string or a number of milliseconds, or with \`Date.UTC()\` of its parts (${GUIDE})`, n);
182
+ },
183
+ method(s, target, method, args, n) {
184
+ if (target.ty.k !== "date")
185
+ return null;
186
+ const t = bare(target.code);
187
+ const plain = args.length === 0;
188
+ if (plain && method === "toISOString")
189
+ return { code: `&*fv::js_to_iso_string(${t})`, ty: STR };
190
+ if (plain && method === "toUTCString")
191
+ return { code: `&*fv::js_to_utc_string(${t})`, ty: STR };
192
+ if (plain && method === "toJSON")
193
+ return { code: `fv::js_to_json(${t}).as_deref()`, ty: { k: "opt", of: STR, none: "null" }, held: `fv::js_to_json(${t}).map(std::borrow::Cow::<str>::Owned)` };
194
+ if (plain && (method === "getTime" || method === "valueOf"))
195
+ return { code: target.code, ty: FLOAT };
196
+ const utc = UTC_GETTERS.get(method);
197
+ if (plain && utc !== undefined)
198
+ return { code: `fv::js_utc_${utc}(${t})`, ty: FLOAT };
199
+ if (LOCAL_GETTERS.has(method) || method === "getTimezoneOffset") {
200
+ const instead = LOCAL_GETTERS.get(method);
201
+ return fail(s.comp, "FV1804", `\`.${method}()\` reads the date in the time zone of the machine running it, which differs between the server and the reader's browser${instead ? `: use \`.${instead}()\`` : ""} (${GUIDE})`, n);
202
+ }
203
+ return fail(s.comp, "FV1804", `\`.${method}(${plain ? "" : "…"})\` on a \`Date\` is not supported: a date is read with \`.toISOString()\`, \`.toUTCString()\`, \`.toJSON()\`, \`.getTime()\` and the \`.getUTC…()\` methods (${GUIDE})`, n);
204
+ },
205
+ values: {
206
+ order(a, op, b) {
207
+ if (a.ty.k !== "date" || b.ty.k !== "date")
208
+ return undefined;
209
+ return compare({ code: a.code, ty: FLOAT }, op, { code: b.code, ty: FLOAT });
210
+ },
211
+ refuseEquality(s, a, b, n) {
212
+ const dated = (ty) => ty.k === "date" || (ty.k === "opt" && ty.of.k === "date");
213
+ if (!(dated(a.ty) || dated(b.ty)) || nothing(a.ty) || nothing(b.ty))
214
+ return;
215
+ const why = dated(a.ty) && dated(b.ty)
216
+ ? "is true only when the two are one object, so two dates of the same time are unequal"
217
+ : "compares the date as an object, or as its `.toString()` in the time zone of the machine running it";
218
+ fail(s.comp, "FV1809", `\`${n.operator}\` between ${describeTy(a.ty)} and ${describeTy(b.ty)} ${why}: compare time values, \`a.getTime() === b.getTime()\` (${GUIDE})`, n);
219
+ },
220
+ json: (v) => (v.ty.k === "date" ? `fv::js_json_date(${bare(v.code)})` : undefined),
221
+ describe: (ty) => (ty.k === "date" ? "a `Date`" : undefined),
222
+ unbindable: (ty) => ty.k === "date" ? { what: "a `Date`", fix: "bind `.toISOString()`, or format it in Rust and pass a string" } : undefined,
223
+ },
224
+ };
@@ -67,7 +67,7 @@ function messageSource(file, key, ast) {
67
67
  }
68
68
  };
69
69
  const items = (c) => c.items ?? (c.static !== undefined ? [{ type: 3, value: c.static }] : []);
70
- return `Message { cases: &[${cases.map((c) => `&[${items(c).map(part).join(", ")}]`).join(", ")}] }`;
70
+ return `Message::new(&[${cases.map((c) => `&[${items(c).map(part).join(", ")}]`).join(", ")}])`;
71
71
  }
72
72
  function byteOrder(a, b) {
73
73
  return Buffer.compare(Buffer.from(a), Buffer.from(b));
@@ -76,13 +76,12 @@ function i18nSource(setup) {
76
76
  const locales = setup.locales
77
77
  .map((l) => {
78
78
  const keys = [...l.messages.keys()].toSorted(byteOrder);
79
- const entries = keys.map((k) => ` (${rustStr(k)}, ${messageSource(l.file, k, l.messages.get(k))}),`).join("\n");
80
- return ` Locale {
81
- name: ${rustStr(l.name)},
82
- messages: &[
79
+ if (keys.length === 0)
80
+ return ` Locale::new(${rustStr(l.name)}, &[]),`;
81
+ const entries = keys.map((k) => ` (${rustStr(k)}, ${messageSource(l.file, k, l.messages.get(k))}),`).join("\n");
82
+ return ` Locale::new(${rustStr(l.name)}, &[
83
83
  ${entries}
84
- ],
85
- },`;
84
+ ]),`;
86
85
  })
87
86
  .join("\n");
88
87
  return `${header(setup.dir.replace(/\/?$/, "/"), "the locale files")}
@@ -130,9 +129,9 @@ function translate(s, args, n) {
130
129
  const key = expr(s, args[0]);
131
130
  if (key.ty.k !== "str")
132
131
  fail(s.comp, "FV1409", "the key given to `t()` is a string", args[0]);
133
- let named = "&[]";
134
- let list = "&[]";
135
- let plural = "None";
132
+ let named = "";
133
+ let list = "";
134
+ let plural = "";
136
135
  for (const a of args.slice(1)) {
137
136
  if (a.type === "ObjectExpression") {
138
137
  const pairs = a.properties.map((p) => {
@@ -140,10 +139,10 @@ function translate(s, args, n) {
140
139
  fail(s.comp, "FV1410", "named values hold plain keys", p);
141
140
  return `(${rustStr(String(p.key.name ?? p.key.value))}, ${i18nValue(s, p.value)})`;
142
141
  });
143
- named = `&[${pairs.join(", ")}]`;
142
+ named = `.with_named(&[${pairs.join(", ")}])`;
144
143
  }
145
144
  else if (a.type === "ArrayExpression") {
146
- list = `&[${a.elements.map((el) => i18nValue(s, el)).join(", ")}]`;
145
+ list = `.with_list(&[${a.elements.map((el) => i18nValue(s, el)).join(", ")}])`;
147
146
  }
148
147
  else {
149
148
  const v = expr(s, a);
@@ -151,10 +150,10 @@ function translate(s, args, n) {
151
150
  fail(s.comp, "FV1411", "a default message given to `t()` is not supported: add the message to the locale files", a);
152
151
  if (v.ty.k !== "int")
153
152
  fail(s.comp, "FV1412", "the plural number given to `t()` is an integer", a);
154
- plural = `Some(${bare(v.code)})`;
153
+ plural = `.with_plural(${bare(v.code)})`;
155
154
  }
156
155
  }
157
- return { code: `&*fv_i18n.t(${strArg(key.code)}, &fv::i18n::Args { named: ${named}, list: ${list}, plural: ${plural} })`, ty: STR };
156
+ return { code: `&*fv_i18n.t(${strArg(key.code)}, &fv::i18n::Args::new()${named}${list}${plural})`, ty: STR };
158
157
  }
159
158
  export const i18n = {
160
159
  name: "i18n",
@@ -193,6 +192,9 @@ export const i18n = {
193
192
  s.setup.set(p.value.name, { code: "fv_i18n.locale()", ty: STR });
194
193
  s.refs.add(p.value.name);
195
194
  }
195
+ else if (key === "d" || key === "n") {
196
+ s.clientOnly.set(p.value.name, `\`${key}\` from \`useI18n()\` formats by locale with \`Intl\`, whose output differs between Node and each browser (see the \`dates_and_prices\` guide)`);
197
+ }
196
198
  else
197
199
  s.clientOnly.set(p.value.name, `\`${key}\` from \`useI18n()\` does not run on the server`);
198
200
  }
@@ -10,4 +10,5 @@ import { twinsPlugin } from "./twins.js";
10
10
  import { provideInject } from "./provide.js";
11
11
  import { head } from "./head.js";
12
12
  import { cssModules } from "./css-modules.js";
13
- export const PLUGINS = [router, piniaStores, sharedTypes, i18n, teleport, scoped, clientOnly, twinsPlugin, provideInject, head, cssModules];
13
+ import { dates } from "./dates.js";
14
+ export const PLUGINS = [router, piniaStores, sharedTypes, i18n, teleport, scoped, clientOnly, twinsPlugin, provideInject, head, cssModules, dates];
@@ -109,9 +109,10 @@ function routeDefs(routes, depth) {
109
109
  const pad = " ".repeat(depth);
110
110
  return routes
111
111
  .map((r) => {
112
- const name = r.name === undefined ? "None" : `Some(${rustStr(r.name)})`;
113
- const children = r.children?.length ? `&[\n${routeDefs(r.children, depth + 1)}\n${pad}]` : "&[]";
114
- return `${pad}ferrovue::RouteDef { path: ${rustStr(r.path)}, name: ${name}, view: ${r.view !== false}, children: ${children} },`;
112
+ const name = r.name === undefined ? "" : `.with_name(${rustStr(r.name)})`;
113
+ const view = r.view === false ? ".with_view(false)" : "";
114
+ const children = r.children?.length ? `.with_children(&[\n${routeDefs(r.children, depth + 1)}\n${pad}])` : "";
115
+ return `${pad}ferrovue::RouteDef::new(${rustStr(r.path)})${name}${view}${children},`;
115
116
  })
116
117
  .join("\n");
117
118
  }
@@ -206,7 +207,7 @@ export const router = {
206
207
  return null;
207
208
  const v = expr(s, tested.target);
208
209
  if (v.ty.k !== "query")
209
- fail(s.comp, "FV1234", '`typeof` tests a query value only, as `typeof route.query.q === "string"`', n);
210
+ return null;
210
211
  const one = `${atom(v.code)}.attr_value().is_some()`;
211
212
  return boolOf(tested.negated ? not(one) : one);
212
213
  },
@@ -216,7 +217,9 @@ export const router = {
216
217
  return undefined;
217
218
  const path = pathOf(t.target);
218
219
  const v = expr(s, t.target);
219
- if (path === null || v.ty.k !== "query")
220
+ if (v.ty.k !== "query")
221
+ return undefined;
222
+ if (path === null)
220
223
  return null;
221
224
  const option = `${atom(v.code)}.attr_value()`;
222
225
  return { path, of: STR, negated: t.negated, truthy: false, option, present: `${option}.is_some()`, pattern: (name) => `let Some(${name}) = ${option}` };