soml-lang 0.0.1 → 0.0.2

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/source/shared.js DELETED
@@ -1,166 +0,0 @@
1
- /*
2
- The deepest nesting either direction accepts. A recursive-descent parser would otherwise end in an engine stack overflow.
3
- */
4
- export const MAX_DEPTH = 1000;
5
-
6
- export const INT64_MIN = -(2n ** 63n);
7
- export const INT64_MAX = (2n ** 63n) - 1n;
8
-
9
- /*
10
- The instant range is checked in UTC, so that every instant has a canonical form that can be read back.
11
- */
12
- export const MIN_INSTANT = Temporal.Instant.from('0001-01-01T00:00:00Z').epochNanoseconds;
13
- export const MAX_INSTANT = Temporal.Instant.from('9999-12-31T23:59:59.999999999Z').epochNanoseconds;
14
-
15
- /*
16
- The duration units in their required order, with their length in nanoseconds.
17
- */
18
- export const DURATION_UNITS = new Map([
19
- ['h', 3_600_000_000_000n],
20
- ['m', 60_000_000_000n],
21
- ['s', 1_000_000_000n],
22
- ['ms', 1_000_000n],
23
- ['us', 1000n],
24
- ['ns', 1n],
25
- ]);
26
-
27
- /**
28
- Remove the trailing zeros of a run of digits. A loop rather than a regular expression, which would be quadratic on a long run of zeros followed by another digit.
29
- */
30
- export function trimTrailingZeros(digits) {
31
- let end = digits.length;
32
-
33
- while (end > 0 && digits.charCodeAt(end - 1) === 0x30) {
34
- end--;
35
- }
36
-
37
- return digits.slice(0, end);
38
- }
39
-
40
- /**
41
- A duration as a `Temporal.Duration` in hours and smaller units, which hold any int64 count of nanoseconds exactly.
42
- */
43
- export function createDuration(nanoseconds) {
44
- const sign = nanoseconds < 0n ? -1n : 1n;
45
- let rest = nanoseconds * sign;
46
- const parts = [];
47
-
48
- for (const size of DURATION_UNITS.values()) {
49
- parts.push(Number(sign * (rest / size)));
50
- rest %= size;
51
- }
52
-
53
- return new Temporal.Duration(0, 0, 0, 0, ...parts);
54
- }
55
-
56
- export function validateIntegersOption(integers) {
57
- if (integers !== 'bigint' && integers !== 'number') {
58
- throw new TypeError(`The \`integers\` option must be 'bigint' or 'number', got ${typeof integers === 'string' ? `'${abbreviate(integers)}'` : typeof integers}`);
59
- }
60
- }
61
-
62
- /*
63
- Names for the characters that are invisible or easy to mistake, so an error can say what it found.
64
- */
65
- const CHARACTER_NAMES = new Map([
66
- [0x06_1C, 'ARABIC LETTER MARK'],
67
- [0x85, 'NEXT LINE'],
68
- [0xA0, 'NO-BREAK SPACE'],
69
- [0xAD, 'SOFT HYPHEN'],
70
- [0x16_80, 'OGHAM SPACE MARK'],
71
- [0x20_00, 'EN QUAD'],
72
- [0x20_01, 'EM QUAD'],
73
- [0x20_02, 'EN SPACE'],
74
- [0x20_03, 'EM SPACE'],
75
- [0x20_04, 'THREE-PER-EM SPACE'],
76
- [0x20_05, 'FOUR-PER-EM SPACE'],
77
- [0x20_06, 'SIX-PER-EM SPACE'],
78
- [0x20_07, 'FIGURE SPACE'],
79
- [0x20_08, 'PUNCTUATION SPACE'],
80
- [0x20_09, 'THIN SPACE'],
81
- [0x20_0A, 'HAIR SPACE'],
82
- [0x20_0B, 'ZERO WIDTH SPACE'],
83
- [0x20_0C, 'ZERO WIDTH NON-JOINER'],
84
- [0x20_0D, 'ZERO WIDTH JOINER'],
85
- [0x20_0E, 'LEFT-TO-RIGHT MARK'],
86
- [0x20_0F, 'RIGHT-TO-LEFT MARK'],
87
- [0x20_28, 'LINE SEPARATOR'],
88
- [0x20_29, 'PARAGRAPH SEPARATOR'],
89
- [0x20_2A, 'LEFT-TO-RIGHT EMBEDDING'],
90
- [0x20_2B, 'RIGHT-TO-LEFT EMBEDDING'],
91
- [0x20_2C, 'POP DIRECTIONAL FORMATTING'],
92
- [0x20_2D, 'LEFT-TO-RIGHT OVERRIDE'],
93
- [0x20_2E, 'RIGHT-TO-LEFT OVERRIDE'],
94
- [0x20_2F, 'NARROW NO-BREAK SPACE'],
95
- [0x20_5F, 'MEDIUM MATHEMATICAL SPACE'],
96
- [0x20_60, 'WORD JOINER'],
97
- [0x20_66, 'LEFT-TO-RIGHT ISOLATE'],
98
- [0x20_67, 'RIGHT-TO-LEFT ISOLATE'],
99
- [0x20_68, 'FIRST STRONG ISOLATE'],
100
- [0x20_69, 'POP DIRECTIONAL ISOLATE'],
101
- [0x30_00, 'IDEOGRAPHIC SPACE'],
102
- [0xFE_FF, 'ZERO WIDTH NO-BREAK SPACE'],
103
- ]);
104
-
105
- /*
106
- Letters, digits, `_`, and `-`, the characters of a bare key, as a lookup table by code unit.
107
- */
108
- export const bareKeyCharacters = new Uint8Array(128);
109
-
110
- for (const character of 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-') {
111
- bareKeyCharacters[character.codePointAt(0)] = 1;
112
- }
113
-
114
- /**
115
- Whether a key can be written without quotes: one or more ASCII letters, digits, `_`, or `-`.
116
- */
117
- export function isBareKey(key) {
118
- if (key === '') {
119
- return false;
120
- }
121
-
122
- for (let index = 0; index < key.length; index++) {
123
- const code = key.charCodeAt(index);
124
-
125
- if (code >= 128 || bareKeyCharacters[code] !== 1) {
126
- return false;
127
- }
128
- }
129
-
130
- return true;
131
- }
132
-
133
- /**
134
- Shorten text quoted in an error message, so a huge token does not make a huge message. The cut never splits a surrogate pair.
135
- */
136
- export function abbreviate(text, maximumLength = 40) {
137
- if (text.length <= maximumLength) {
138
- return text;
139
- }
140
-
141
- const lastCode = text.charCodeAt(maximumLength - 1);
142
- const end = lastCode >= 0xD8_00 && lastCode <= 0xDB_FF ? maximumLength - 1 : maximumLength;
143
- return `${text.slice(0, end)}…`;
144
- }
145
-
146
- export function formatCodePoint(codePoint) {
147
- return `U+${codePoint.toString(16).toUpperCase().padStart(4, '0')}`;
148
- }
149
-
150
- /**
151
- Describe a character for an error message, such as `"$"` or `U+00A0 (NO-BREAK SPACE)`.
152
- @param codePoint
153
- */
154
- export function describeCharacter(codePoint) {
155
- const name = CHARACTER_NAMES.get(codePoint);
156
-
157
- if (name !== undefined) {
158
- const isWhitespace = /^[\p{White_Space}\u{200B}\u{FEFF}]$/v.test(String.fromCodePoint(codePoint));
159
- return `${formatCodePoint(codePoint)} (${name}${isWhitespace ? '; only space, tab, and line feed are whitespace' : ''})`;
160
- }
161
-
162
- const character = String.fromCodePoint(codePoint);
163
-
164
- // Control, format, private-use, unassigned, surrogate, and separator characters are invisible or ambiguous, so they are shown by code point.
165
- return codePoint !== 0x20 && /^[\p{Other}\p{Separator}]$/v.test(character) ? formatCodePoint(codePoint) : `"${character}"`;
166
- }
@@ -1,439 +0,0 @@
1
- import {
2
- MAX_DEPTH,
3
- INT64_MIN,
4
- INT64_MAX,
5
- MIN_INSTANT,
6
- MAX_INSTANT,
7
- DURATION_UNITS,
8
- trimTrailingZeros,
9
- validateIntegersOption,
10
- isBareKey,
11
- abbreviate,
12
- } from './shared.js';
13
-
14
- const instantNanoseconds = Object.getOwnPropertyDescriptor(Temporal.Instant.prototype, 'epochNanoseconds').get;
15
- const dateTime = Date.prototype.getTime;
16
- const durationField = name => Object.getOwnPropertyDescriptor(Temporal.Duration.prototype, name).get;
17
- const durationCalendarFields = ['years', 'months', 'weeks', 'days'].map(name => durationField(name));
18
- // In the order of `DURATION_UNITS`.
19
- const durationTimeFields = ['hours', 'minutes', 'seconds', 'milliseconds', 'microseconds', 'nanoseconds'].map(name => durationField(name));
20
- const durationSizes = DURATION_UNITS.values().toArray();
21
- const durationToString = Temporal.Duration.prototype.toString;
22
-
23
- const HIGH_CODE_UNIT = /[\u{D800}-\u{10FFFF}]/v;
24
-
25
- // eslint-disable-next-line no-control-regex, regexp/no-control-character -- Control characters are exactly what must be found.
26
- const NEEDS_ESCAPED_STRING = /[\u{0}-\u{1F}'\u{7F}]/v;
27
-
28
- // eslint-disable-next-line no-control-regex, regexp/no-control-character -- As above, plus a lone surrogate, which the `v` flag matches only when it is unpaired.
29
- const NEEDS_ATTENTION = /[\u{0}-\u{1F}'\u{7F}\u{D800}-\u{DFFF}]/v;
30
-
31
- // eslint-disable-next-line no-control-regex, regexp/no-control-character -- Control characters are exactly what must be escaped.
32
- const ESCAPED_CHARACTER = /[\u{0}-\u{1F}"\\\u{7F}]/gv;
33
-
34
- const ESCAPES = {
35
- '\\': '\\\\',
36
- '"': String.raw`\"`,
37
- '\n': String.raw`\n`,
38
- '\t': String.raw`\t`,
39
- };
40
-
41
- /**
42
- Serialize a value to canonical form.
43
-
44
- @param {object} value
45
- @param {{integers?: 'bigint' | 'number'}} [options]
46
- */
47
- export function stringify(value, options = {}) {
48
- const {integers = 'bigint'} = options;
49
- validateIntegersOption(integers);
50
-
51
- if (!Array.isArray(value) && !isPlainObject(value)) {
52
- throw new TypeError(`The top-level value must be an object or an array, because a document is a collection. Got ${describeType(value)}`);
53
- }
54
-
55
- return new Writer(integers).writeDocument(value);
56
- }
57
-
58
- /*
59
- Appends to one array of parts that is joined once at the end, so the time is linear in the output whatever the nesting. A scalar and its line are one part.
60
- */
61
- class Writer {
62
- #integers;
63
- #ancestors = new Set();
64
- #parts = [];
65
-
66
- constructor(integers) {
67
- this.#integers = integers;
68
- }
69
-
70
- writeDocument(value) {
71
- if (Array.isArray(value)) {
72
- this.#writeContainer(value, 1, '');
73
- this.#parts.push('\n');
74
- return this.#parts.join('');
75
- }
76
-
77
- this.#enter(value, 1);
78
- const members = sortedMembers(value);
79
-
80
- for (const [key, member] of members) {
81
- this.#writeItem(`${formatKey(key)}: `, member, 2, '', '\n', key);
82
- }
83
-
84
- this.#ancestors.delete(value);
85
-
86
- // A brace-less object needs at least one entry, so the empty object keeps its braces.
87
- return members.length === 0 ? '{}\n' : this.#parts.join('');
88
- }
89
-
90
- /*
91
- Writes `prefix`, the value, and `suffix`. A container's own depth is `depth`, and its lines are indented by `indent`.
92
- */
93
- #writeItem(prefix, value, depth, indent, suffix, key) {
94
- const scalar = this.#formatScalar(value, key);
95
-
96
- if (scalar === undefined) {
97
- this.#parts.push(prefix);
98
- this.#writeContainer(value, depth, indent);
99
- this.#parts.push(suffix);
100
- } else {
101
- this.#parts.push(`${prefix}${scalar}${suffix}`);
102
- }
103
- }
104
-
105
- /*
106
- Formats a scalar, or returns `undefined` for an array or a plain object.
107
- */
108
- #formatScalar(value, key) {
109
- switch (typeof value) {
110
- case 'string': {
111
- return formatString(value, 'string');
112
- }
113
-
114
- case 'bigint': {
115
- return formatInteger(value);
116
- }
117
-
118
- case 'number': {
119
- return this.#integers === 'number' && Number.isSafeInteger(value) ? formatInteger(BigInt(value)) : formatFloat(value);
120
- }
121
-
122
- case 'boolean': {
123
- return value ? 'true' : 'false';
124
- }
125
-
126
- case 'object': {
127
- break;
128
- }
129
-
130
- default: {
131
- throw new TypeError(`Cannot serialize ${describeType(value)}${key === undefined ? '' : ` at key "${abbreviate(key)}"`}`);
132
- }
133
- }
134
-
135
- if (value === null) {
136
- return 'null';
137
- }
138
-
139
- if (Array.isArray(value) || isPlainObject(value)) {
140
- return undefined;
141
- }
142
-
143
- // A brand check rather than `instanceof`, so that an instant or a date from another realm, such as a `vm` context, is accepted too. The tag is only a cheap filter, since `Symbol.toStringTag` can fake it. The getters read internal slots, so they throw for anything else.
144
- const brand = Object.prototype.toString.call(value);
145
-
146
- if (brand === '[object Temporal.Instant]') {
147
- return formatInstant(readBranded(instantNanoseconds, value, 'Temporal.Instant'));
148
- }
149
-
150
- if (brand === '[object Temporal.Duration]') {
151
- return formatDuration(value);
152
- }
153
-
154
- if (brand === '[object Date]') {
155
- const milliseconds = readBranded(dateTime, value, 'Date');
156
-
157
- if (Number.isNaN(milliseconds)) {
158
- throw new TypeError('Cannot serialize an invalid Date');
159
- }
160
-
161
- return formatInstant(BigInt(milliseconds) * 1_000_000n);
162
- }
163
-
164
- throw new TypeError(`Cannot serialize ${describeType(value)}. Only plain objects, arrays, and the scalar types can be serialized`);
165
- }
166
-
167
- #enter(value, depth) {
168
- if (depth > MAX_DEPTH) {
169
- throw new RangeError(`Cannot serialize a value nested more than ${MAX_DEPTH} levels deep`);
170
- }
171
-
172
- if (this.#ancestors.has(value)) {
173
- throw new TypeError('Cannot serialize a circular structure');
174
- }
175
-
176
- this.#ancestors.add(value);
177
- }
178
-
179
- #writeContainer(value, depth, indent) {
180
- this.#enter(value, depth);
181
- const parts = this.#parts;
182
- const innerIndent = `${indent}\t`;
183
-
184
- if (Array.isArray(value)) {
185
- if (value.length === 0) {
186
- parts.push('[]');
187
- } else {
188
- parts.push('[\n');
189
-
190
- for (const [index, item] of value.entries()) {
191
- // A hole reads as `undefined`, and both would otherwise have to be guessed into something.
192
- if (item === undefined) {
193
- throw new TypeError(`Cannot serialize undefined in an array, at index ${index}`);
194
- }
195
-
196
- this.#writeItem(innerIndent, item, depth + 1, innerIndent, ',\n');
197
- }
198
-
199
- parts.push(`${indent}]`);
200
- }
201
- } else {
202
- const members = sortedMembers(value);
203
-
204
- if (members.length === 0) {
205
- parts.push('{}');
206
- } else {
207
- parts.push('{\n');
208
-
209
- for (const [key, member] of members) {
210
- this.#writeItem(`${innerIndent}${formatKey(key)}: `, member, depth + 1, innerIndent, ',\n', key);
211
- }
212
-
213
- parts.push(`${indent}}`);
214
- }
215
- }
216
-
217
- this.#ancestors.delete(value);
218
- }
219
- }
220
-
221
- /*
222
- The members to write, sorted by key, with each value read once. A member whose value is `undefined` is left out, as `JSON.stringify` does.
223
- */
224
- function sortedMembers(object) {
225
- const keys = Object.keys(object);
226
-
227
- // The default sort compares UTF-16 code units, which matches code point order unless a key has a code unit from U+D800 up.
228
- if (keys.some(key => HIGH_CODE_UNIT.test(key))) {
229
- keys.sort(compareCodePoints);
230
- } else {
231
- keys.sort(); // eslint-disable-line unicorn/require-array-sort-compare -- The native string order is the point, and it is much faster than a comparator.
232
- }
233
-
234
- const members = [];
235
-
236
- for (const key of keys) {
237
- const value = object[key];
238
-
239
- if (value !== undefined) {
240
- members.push([key, value]);
241
- }
242
- }
243
-
244
- return members;
245
- }
246
-
247
- function isPlainObject(value) {
248
- if (typeof value !== 'object' || value === null) {
249
- return false;
250
- }
251
-
252
- const prototype = Object.getPrototypeOf(value);
253
- return prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null;
254
- }
255
-
256
- function describeType(value) {
257
- if (value === null) {
258
- return 'null';
259
- }
260
-
261
- if (typeof value === 'number' && Number.isNaN(value)) {
262
- return 'NaN, which is not representable';
263
- }
264
-
265
- if (typeof value === 'function') {
266
- return 'a function';
267
- }
268
-
269
- if (typeof value === 'object') {
270
- const name = value.constructor?.name;
271
- return Array.isArray(value) ? 'an array' : (name ? `an instance of ${name}` : 'an object');
272
- }
273
-
274
- if (value === undefined) {
275
- return 'undefined';
276
- }
277
-
278
- return `a ${typeof value}`;
279
- }
280
-
281
- /*
282
- Members are sorted by the Unicode scalar values of their keys. `Array#sort` compares UTF-16 code units, which orders U+E000 to U+FFFF after every astral character, so the code units are compared with surrogates moved above that range.
283
- */
284
- function compareCodePoints(left, right) {
285
- const length = Math.min(left.length, right.length);
286
-
287
- for (let index = 0; index < length; index++) {
288
- let leftCode = left.charCodeAt(index);
289
- let rightCode = right.charCodeAt(index);
290
-
291
- if (leftCode !== rightCode) {
292
- if (leftCode >= 0xD8_00 && rightCode >= 0xD8_00) {
293
- leftCode = toCodePointOrder(leftCode);
294
- rightCode = toCodePointOrder(rightCode);
295
- }
296
-
297
- return leftCode - rightCode;
298
- }
299
- }
300
-
301
- return left.length - right.length;
302
- }
303
-
304
- /*
305
- Moves U+E000 to U+FFFF down by 0x800 and the surrogates up by 0x2000, so the surrogates sort last.
306
- */
307
- function toCodePointOrder(code) {
308
- if (code >= 0xE0_00) {
309
- return code - 0x8_00;
310
- }
311
-
312
- return code >= 0xD8_00 ? code + 0x20_00 : code;
313
- }
314
-
315
- function formatKey(key) {
316
- if (isBareKey(key)) {
317
- return key;
318
- }
319
-
320
- return formatString(key, 'key');
321
- }
322
-
323
- function formatString(string, kind) {
324
- // One scan settles the common case: no quote, no control character, and no lone surrogate.
325
- if (!NEEDS_ATTENTION.test(string)) {
326
- return `'${string}'`;
327
- }
328
-
329
- if (!string.isWellFormed()) {
330
- throw new TypeError(`Cannot serialize a ${kind} containing a lone surrogate, which is not a Unicode scalar value`);
331
- }
332
-
333
- if (string.includes('\r')) {
334
- throw new TypeError(`Cannot serialize a ${kind} containing a carriage return (U+000D), which is not representable`);
335
- }
336
-
337
- return NEEDS_ESCAPED_STRING.test(string) ? `"${string.replaceAll(ESCAPED_CHARACTER, character => ESCAPES[character] ?? String.raw`\u{${character.codePointAt(0).toString(16)}}`)}"` : `'${string}'`;
338
- }
339
-
340
- function formatInteger(value) {
341
- if (value < INT64_MIN || value > INT64_MAX) {
342
- throw new RangeError(`Cannot serialize the integer ${value}, because it is outside the 64-bit range`);
343
- }
344
-
345
- return String(value);
346
- }
347
-
348
- /*
349
- The shortest decimal that reads back as the same binary64 value, laid out as ECMAScript `Number::toString` does, which is also RFC 8785's choice. A fractional part is added when there is neither one nor an exponent, so that a float never reads back as an int.
350
- */
351
- function formatFloat(value) {
352
- if (Number.isNaN(value)) {
353
- throw new TypeError('Cannot serialize NaN, which is not representable. Use null for a missing value');
354
- }
355
-
356
- if (value === Infinity) {
357
- return 'infinity';
358
- }
359
-
360
- if (value === Number.NEGATIVE_INFINITY) {
361
- return '-infinity';
362
- }
363
-
364
- // Covers -0 too, since zero has one value whatever its sign.
365
- if (value === 0) {
366
- return '0.0';
367
- }
368
-
369
- const text = String(value);
370
-
371
- if (text.includes('e')) {
372
- return text.replace('e+', 'e');
373
- }
374
-
375
- return text.includes('.') ? text : `${text}.0`;
376
- }
377
-
378
- function readBranded(getter, value, name) {
379
- try {
380
- return getter.call(value);
381
- } catch {
382
- throw new TypeError(`Cannot serialize an object that claims to be a ${name} but is not one`);
383
- }
384
- }
385
-
386
- /*
387
- The total is computed from the fields as BigInts, because `Temporal.Duration#total()` returns a float, which cannot hold every int64 count of nanoseconds.
388
- */
389
- function formatDuration(duration) {
390
- if (durationCalendarFields.some(getter => readBranded(getter, duration, 'Temporal.Duration') !== 0)) {
391
- throw new TypeError(`Cannot serialize the duration ${durationToString.call(duration)}, because years, months, weeks, and days are not a fixed length. Use hours or smaller units`);
392
- }
393
-
394
- let nanoseconds = 0n;
395
-
396
- for (const [index, getter] of durationTimeFields.entries()) {
397
- nanoseconds += BigInt(getter.call(duration)) * durationSizes[index];
398
- }
399
-
400
- if (nanoseconds < INT64_MIN || nanoseconds > INT64_MAX) {
401
- throw new RangeError(`Cannot serialize the duration ${durationToString.call(duration)}, because it is outside the 64-bit range of nanoseconds`);
402
- }
403
-
404
- if (nanoseconds === 0n) {
405
- return '0s';
406
- }
407
-
408
- const magnitude = nanoseconds < 0n ? -nanoseconds : nanoseconds;
409
- const hours = magnitude / 3_600_000_000_000n;
410
- const minutes = (magnitude / 60_000_000_000n) % 60n;
411
- const seconds = (magnitude / 1_000_000_000n) % 60n;
412
- const fraction = magnitude % 1_000_000_000n;
413
- let text = nanoseconds < 0n ? '-' : '';
414
-
415
- if (hours > 0n) {
416
- text += `${hours}h`;
417
- }
418
-
419
- if (minutes > 0n) {
420
- text += `${minutes}m`;
421
- }
422
-
423
- if (seconds > 0n || fraction > 0n) {
424
- // The fraction is written like an instant's: nine digits with the trailing zeros removed.
425
- text += fraction === 0n ? `${seconds}s` : `${seconds}.${trimTrailingZeros(String(fraction).padStart(9, '0'))}s`;
426
- }
427
-
428
- return text;
429
- }
430
-
431
- function formatInstant(nanoseconds) {
432
- const instant = new Temporal.Instant(nanoseconds);
433
-
434
- if (nanoseconds < MIN_INSTANT || nanoseconds > MAX_INSTANT) {
435
- throw new RangeError(`Cannot serialize the instant ${instant}, because it is outside the years 0001 to 9999`);
436
- }
437
-
438
- return instant.toString();
439
- }