@formaui-net/nexo 0.0.0 → 1.0.0-preview.1
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/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +148 -6
- package/package.json +23 -3
- package/src/common/ArrayUtils.mjs +16 -0
- package/src/common/ObjectUtils.mjs +33 -0
- package/src/language/DefinitionLocations.mjs +41 -0
- package/src/language/DefinitionValidator.mjs +1265 -0
- package/src/language/DefinitionWriter.mjs +475 -0
- package/src/language/Dictionary.mjs +52 -0
- package/src/language/Evaluator.mjs +3480 -0
- package/src/language/ImportedVocabulary.mjs +137 -0
- package/src/language/Imports.mjs +141 -0
- package/src/language/Index.mjs +29 -0
- package/src/language/LanguageResolver.mjs +4068 -0
- package/src/language/Limits.mjs +78 -0
- package/src/language/LineStructure.mjs +385 -0
- package/src/language/Lowering.mjs +453 -0
- package/src/language/MemberSelector.mjs +89 -0
- package/src/language/Names.mjs +65 -0
- package/src/language/NexoError.mjs +145 -0
- package/src/language/Pair.mjs +21 -0
- package/src/language/ParameterBindings.mjs +361 -0
- package/src/language/ParsedTexts.mjs +62 -0
- package/src/language/Parser.mjs +1817 -0
- package/src/language/Range.mjs +51 -0
- package/src/language/RecentlyUsed.mjs +50 -0
- package/src/language/ResolvedDefinition.mjs +219 -0
- package/src/language/ResultPath.mjs +101 -0
- package/src/language/ScopeLocals.mjs +102 -0
- package/src/language/StructureTypes.mjs +48 -0
- package/src/language/SyntaxTree.mjs +203 -0
- package/src/language/TextLocations.mjs +185 -0
- package/src/language/TextPositions.mjs +23 -0
- package/src/language/Tokenizer.mjs +917 -0
- package/src/language/Type.mjs +401 -0
- package/src/language/TypeChecker.mjs +449 -0
- package/src/language/ValueCapture.mjs +315 -0
- package/src/language/Versions.mjs +14 -0
- package/src/language/vocabulary/Declarations.mjs +64 -0
- package/src/language/vocabulary/ItemPositions.mjs +48 -0
- package/src/language/vocabulary/ListOrdering.mjs +145 -0
- package/src/language/vocabulary/MemberFailure.mjs +21 -0
- package/src/language/vocabulary/NexoModule.mjs +1119 -0
- package/src/language/vocabulary/NexoRegistry.mjs +1540 -0
- package/src/language/vocabulary/TypeNameScope.mjs +63 -0
- package/src/language/vocabulary/Vocabulary.mjs +640 -0
- package/src/nexo.d.ts +743 -0
- package/src/nexo.js +338 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NexoError — a Nexo failure, the code of its condition and its location
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { TextPositions } from './TextPositions.mjs';
|
|
6
|
+
|
|
7
|
+
/** The first code of each kind's range of Nexo's own codes, which runs a thousand codes. */
|
|
8
|
+
const CODE_RANGE_STARTS = { syntax: 1000, structure: 2000, validation: 3000, module: 4000, evaluation: 5000 };
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} Location
|
|
12
|
+
* @property {string} [pointer] The failing node's JSON Pointer into the definition.
|
|
13
|
+
* @property {number} [offset] The failing text's offset into the source text.
|
|
14
|
+
* @property {number} [line] The failing text's line, from 1.
|
|
15
|
+
* @property {number} [column] The failing text's column, from 1.
|
|
16
|
+
* @property {string} [binding] The failing binding's name.
|
|
17
|
+
* @property {string} [module] The failing module's name.
|
|
18
|
+
* @property {string} [declaration] The failing declaration's name.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export class NexoError extends Error {
|
|
22
|
+
/** The code of the condition that failed: `NEXO` and four digits for Nexo's own, a host's own prefix for a host's. */
|
|
23
|
+
code;
|
|
24
|
+
|
|
25
|
+
/** @type {'syntax' | 'structure' | 'validation' | 'module' | 'evaluation'} */
|
|
26
|
+
kind;
|
|
27
|
+
|
|
28
|
+
/** @type {Location} */
|
|
29
|
+
location;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @param {'syntax' | 'structure' | 'validation' | 'module' | 'evaluation'} kind The stage the request failed in.
|
|
33
|
+
* @param {string} code The code of the condition that failed.
|
|
34
|
+
* @param {string} message What is wrong.
|
|
35
|
+
* @param {Location} location Where it is wrong.
|
|
36
|
+
* @param {{ cause?: unknown }} [options] What caused it.
|
|
37
|
+
*/
|
|
38
|
+
constructor(kind, code, message, location, options = undefined) {
|
|
39
|
+
NexoError.#requireCode(kind, code);
|
|
40
|
+
const place = NexoError.#describe(location);
|
|
41
|
+
super(place === '' ? message : `${message} (${place})`, options);
|
|
42
|
+
this.name = 'NexoError';
|
|
43
|
+
this.code = code;
|
|
44
|
+
this.kind = kind;
|
|
45
|
+
this.location = location;
|
|
46
|
+
// The stack is written as the error is made, and so is its cause's where the cause is an error, so the engine lets go of the frames each was raised in, the evaluator among their receivers, rather than holding
|
|
47
|
+
// them until a stack is first read; writing a stack changes nothing it reads.
|
|
48
|
+
void this.stack;
|
|
49
|
+
|
|
50
|
+
if (options?.cause instanceof Error) {
|
|
51
|
+
void options.cause.stack;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** A definition, or a binding, that does not follow the structure, failing at the location. */
|
|
56
|
+
static structure(code, message, location) {
|
|
57
|
+
return new NexoError('structure', code, message, location);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** A request that does not follow the vocabulary, failing at the location. */
|
|
61
|
+
static validation(code, message, location) {
|
|
62
|
+
return new NexoError('validation', code, message, location);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A request that fails as it is carried out, at the location of the part that failed. */
|
|
66
|
+
static evaluation(code, message, location, options) {
|
|
67
|
+
return new NexoError('evaluation', code, message, location, options);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A module whose declarations do not follow the declaration contract, failing at the location. */
|
|
71
|
+
static module(code, message, location) {
|
|
72
|
+
return new NexoError('module', code, message, location);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Refuses a code that is not one, or one of Nexo's own outside its kind's range, which is a mistake in the code raising it. */
|
|
76
|
+
static #requireCode(kind, code) {
|
|
77
|
+
if (typeof code !== 'string' || code === '') {
|
|
78
|
+
throw new TypeError(`A ${kind} error is raised with the code of its condition, and is given ${String(code)}`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const own = /^NEXO(\d{4})$/.exec(code);
|
|
82
|
+
if (code.startsWith('NEXO') && (own === null || Math.floor(Number(own[1]) / 1000) * 1000 !== CODE_RANGE_STARTS[kind])) {
|
|
83
|
+
throw new TypeError(`The code ${code} is outside the range of Nexo's ${kind} codes, ${CODE_RANGE_STARTS[kind]} to ${CODE_RANGE_STARTS[kind] + 999}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static #describe(location) {
|
|
88
|
+
const parts = [];
|
|
89
|
+
|
|
90
|
+
if (location.line !== undefined) {
|
|
91
|
+
parts.push(`line ${location.line}, column ${location.column}`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (location.pointer !== undefined) {
|
|
95
|
+
parts.push(`at ${location.pointer === '' ? 'the definition' : location.pointer}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (location.binding !== undefined) {
|
|
99
|
+
parts.push(`in the binding '${location.binding}'`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (location.module !== undefined) {
|
|
103
|
+
parts.push(`in the module '${location.module}'`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (location.type !== undefined) {
|
|
107
|
+
parts.push(`in the type '${location.type}'`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (location.declaration !== undefined) {
|
|
111
|
+
parts.push(`in the declaration '${location.declaration}'`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (location.declarationKey !== undefined) {
|
|
115
|
+
parts.push(`at '${location.declarationKey}'`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return parts.join(', ');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** The errors Nexo's own code raised, each made by NexoErrors; one a host builds with the class's factories is not among them. @type {WeakSet<NexoError>} */
|
|
123
|
+
const raisedErrors = new WeakSet();
|
|
124
|
+
|
|
125
|
+
/** The error, marked as one Nexo raised. */
|
|
126
|
+
const raise = error => {
|
|
127
|
+
raisedErrors.add(error);
|
|
128
|
+
|
|
129
|
+
return error;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The factories Nexo's own code raises its errors with, each marking the error as one Nexo raised, and `isRaisedByNexo`, whether that marker is on an error, which a failure passing through a host's code keeps as it
|
|
134
|
+
* is: a NexoError a host builds with the exported class carries none, so it stays apart from Nexo's own. A syntax error stands in text, which Nexo alone reads, so it is made here, at the text's offset, and
|
|
135
|
+
* the class declares no factory of it.
|
|
136
|
+
* The package exports the class alone, whose factories a host builds its own errors with, so a host's error is never taken for Nexo's.
|
|
137
|
+
*/
|
|
138
|
+
export const NexoErrors = Object.freeze({
|
|
139
|
+
syntax: (code, message, text, offset) => raise(new NexoError('syntax', code, message, TextPositions.locate(text, offset))),
|
|
140
|
+
structure: (code, message, location) => raise(NexoError.structure(code, message, location)),
|
|
141
|
+
validation: (code, message, location) => raise(NexoError.validation(code, message, location)),
|
|
142
|
+
evaluation: (code, message, location, options) => raise(NexoError.evaluation(code, message, location, options)),
|
|
143
|
+
module: (code, message, location) => raise(NexoError.module(code, message, location)),
|
|
144
|
+
isRaisedByNexo: error => raisedErrors.has(error),
|
|
145
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pair — a Nexo pair, immutable data of a key and its value, as a dictionary's entries give them
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class Pair {
|
|
6
|
+
/** The pair's key. */
|
|
7
|
+
key;
|
|
8
|
+
|
|
9
|
+
/** The pair's value. */
|
|
10
|
+
value;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {unknown} key
|
|
14
|
+
* @param {unknown} value
|
|
15
|
+
*/
|
|
16
|
+
constructor(key, value) {
|
|
17
|
+
this.key = key;
|
|
18
|
+
this.value = value;
|
|
19
|
+
Object.freeze(this);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ParameterBindings — the values an expression's parameters are bound to, by name, against the registry that recognizes their references
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { ArrayUtils } from '../common/ArrayUtils.mjs';
|
|
6
|
+
import { ObjectUtils } from '../common/ObjectUtils.mjs';
|
|
7
|
+
import { Dictionary } from './Dictionary.mjs';
|
|
8
|
+
import { Limits } from './Limits.mjs';
|
|
9
|
+
import { NexoErrors } from './NexoError.mjs';
|
|
10
|
+
import { Names } from './Names.mjs';
|
|
11
|
+
import { Pair } from './Pair.mjs';
|
|
12
|
+
import { Type } from './Type.mjs';
|
|
13
|
+
import { ValueCapture } from './ValueCapture.mjs';
|
|
14
|
+
|
|
15
|
+
export class ParameterBindings {
|
|
16
|
+
/** @type {import('./vocabulary/NexoRegistry.mjs').NexoRegistry} */
|
|
17
|
+
#registry;
|
|
18
|
+
|
|
19
|
+
/** @type {Map<string, unknown>} */
|
|
20
|
+
#values = new Map();
|
|
21
|
+
|
|
22
|
+
/** @type {Map<string, Type>} */
|
|
23
|
+
#types = new Map();
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Binds each value to its name, asking the registry's modules which values are references before copying anything: a reference is kept as it is, by its identity, a reference nested in data among them, and the rest of the data
|
|
27
|
+
* is copied and frozen. A top-level binding's declared type, a notation the registry resolves, is its type in place of recognition: a reference type's value checked by the type's membership, and any other
|
|
28
|
+
* type's value captured as that type.
|
|
29
|
+
* @param {import('./vocabulary/NexoRegistry.mjs').NexoRegistry} registry The registry the bindings belong to.
|
|
30
|
+
* @param {Record<string, unknown>} parameters Each parameter's name and the value it is bound to.
|
|
31
|
+
* @param {{ types?: Record<string, string> }} [options] The type declared for a top-level binding, by its name, as a notation.
|
|
32
|
+
*/
|
|
33
|
+
constructor(registry, parameters = {}, options = {}) {
|
|
34
|
+
this.#registry = registry;
|
|
35
|
+
|
|
36
|
+
if (!ObjectUtils.isPlainObject(parameters)) {
|
|
37
|
+
throw NexoErrors.structure('NEXO2020', 'Bindings are a plain object of names and the values bound to them', {});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const declaredTypeNames = options?.types ?? {};
|
|
41
|
+
|
|
42
|
+
if (!ObjectUtils.isPlainObject(options ?? {}) || !ObjectUtils.isPlainObject(declaredTypeNames)) {
|
|
43
|
+
throw NexoErrors.structure('NEXO2020', "A binding's options are a plain object, and the types it declares a plain object of top-level bindings' names and the types declared for them", {});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const name of Object.keys(declaredTypeNames)) {
|
|
47
|
+
if (!Object.hasOwn(parameters, name)) {
|
|
48
|
+
throw NexoErrors.structure('NEXO2039', `A type is declared for a top-level binding alone, and no binding is named '${name}'`, { binding: name });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const [name, value] of Object.entries(parameters)) {
|
|
53
|
+
// A binding a reserved word names is read as `~` and its name, which written alone keeps its own meaning.
|
|
54
|
+
if (!Names.isIdentifier(name)) {
|
|
55
|
+
throw NexoErrors.structure('NEXO2033', 'A binding is named by a letter followed by letters and digits', { binding: name });
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const bound = Object.hasOwn(declaredTypeNames, name) ? this.#bindDeclared(name, value, declaredTypeNames[name]) : this.#bind(name, value, new Set());
|
|
59
|
+
|
|
60
|
+
this.#types.set(name, bound.type);
|
|
61
|
+
this.#values.set(name, bound.value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The registry the bindings belong to, which recognized their references. */
|
|
66
|
+
get registry() {
|
|
67
|
+
return this.#registry;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Whether a value is bound to the name. */
|
|
71
|
+
has(name) {
|
|
72
|
+
return this.#values.has(name);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Reads the value bound to the name: its data a frozen copy of the data given, and each reference the reference itself. */
|
|
76
|
+
readValue(name) {
|
|
77
|
+
return this.#values.get(name);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The type of the value bound to the name, which its host declared or the value reveals. */
|
|
81
|
+
getType(name) {
|
|
82
|
+
return this.#types.get(name);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* What resolving a definition against these bindings depends on: each name with its type, written by its qualified identity, and its value where that is a string, a Boolean or null, which can decide a
|
|
87
|
+
* resolution, since a bound string can select what a member reads, and a Boolean and null decide by their truthiness. A number is always truthy, never null and never a string, so none decides one, and no number
|
|
88
|
+
* is in the key. Two bindings with one key resolve a definition alike, so they can share its resolution.
|
|
89
|
+
*/
|
|
90
|
+
get resolutionKey() {
|
|
91
|
+
return [...this.#types.keys()].sort().map(name => {
|
|
92
|
+
const value = this.#values.get(name);
|
|
93
|
+
const decides = value === null || typeof value === 'string' || typeof value === 'boolean';
|
|
94
|
+
|
|
95
|
+
return `${name}:${this.#types.get(name).toString(true)}${decides ? `=${JSON.stringify(value)}` : ''}`;
|
|
96
|
+
}).join(';');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* A top-level binding of a declared type, the notation read in the binding scope, its names qualified or not. A reference type keeps its contract: the value belongs to it, checked by its membership, and no `typeOf`
|
|
101
|
+
* is asked of it. Any other type captures the value as a value its host supplies is captured: a structure as its data shape alone, a property left out null where its type is nullable, a reference of another type
|
|
102
|
+
* null where a nullable reference type is declared, and data the type doesn't admit refused (NEXO2045), naming the part; the capture freezes the data it makes, as bound data is, its references kept as they are.
|
|
103
|
+
*/
|
|
104
|
+
#bindDeclared(name, value, typeName) {
|
|
105
|
+
const fail = (code, message) => {
|
|
106
|
+
throw NexoErrors.structure(code, `The type declared for '${name}': ${message}`, { binding: name });
|
|
107
|
+
};
|
|
108
|
+
const type = typeof typeName === 'string' ? this.#registry.resolveBindingType(typeName, fail) : fail('NEXO2044', `${String(typeName)} is no type's notation, which is a string`);
|
|
109
|
+
|
|
110
|
+
if (this.#registry.isReferenceType(type)) {
|
|
111
|
+
if (value === null && type.isNullable) {
|
|
112
|
+
return { type, value };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (value === null || typeof value !== 'object') {
|
|
116
|
+
throw NexoErrors.structure('NEXO2038', `A type is declared for a reference, and '${name}' is bound to data`, { binding: name });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!this.#registry.belongsTo(value, type, `the binding '${name}'`)) {
|
|
120
|
+
throw NexoErrors.structure('NEXO2036', `The value bound to '${name}' doesn't belong to its declared type, ${type}`, { binding: name });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return { type, value };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!this.#holdsBoundValues(type, new Set())) {
|
|
127
|
+
fail('NEXO2044', `${typeName} is a type no binding holds, or holds one in its data, as Any, Never, an Index and a Range are`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const captured = new ValueCapture(this.#registry).capture(value, type, (path, part, partType, reason) => {
|
|
131
|
+
if (reason === 'cycle') {
|
|
132
|
+
throw NexoErrors.structure('NEXO2022', `A binding holds a cycle, a list or an object that reaches itself, which data never does${path === '' ? '' : `, at '${path}'`}`, { binding: name });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (reason === 'depth') {
|
|
136
|
+
throw NexoErrors.structure('NEXO2047', `The data bound to '${name}' nests deeper than ${Limits.dataNesting} levels, the most data nests${path === '' ? '' : `, at '${path}'`}`, { binding: name });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (reason === 'duplicate') {
|
|
140
|
+
throw NexoErrors.structure('NEXO2045', `The value bound to '${name}' is declared ${type}, and holds a key twice, at '${path}', which a dictionary never does`, { binding: name });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const given = path === '' ? `it is given ${ParameterBindings.#describe(part)}` : part === undefined ? `it is given a value without '${path}'` : `its '${path}' is ${ParameterBindings.#describe(part)}`;
|
|
144
|
+
|
|
145
|
+
throw NexoErrors.structure('NEXO2045', `The value bound to '${name}' is declared ${type}, and ${given}, where ${partType} is declared`, { binding: name });
|
|
146
|
+
}, `the binding '${name}'`);
|
|
147
|
+
|
|
148
|
+
// The capture freezes each part of the data it makes, never a reference it holds, which stays the host's own.
|
|
149
|
+
return { type, value: captured };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Whether values of the type are ones a binding holds, at any depth, its type arguments, its object fields and a structure's data shape among them: never `Any`, `Never` or a position, which no host gives a
|
|
154
|
+
* script, a structure already being walked counting as holding none, since its properties are walked where it is first met.
|
|
155
|
+
*/
|
|
156
|
+
#holdsBoundValues(type, walking) {
|
|
157
|
+
switch (type.kind) {
|
|
158
|
+
case 'any':
|
|
159
|
+
case 'never':
|
|
160
|
+
return false;
|
|
161
|
+
case 'object':
|
|
162
|
+
return [...type.fieldTypes.values()].every(fieldType => this.#holdsBoundValues(fieldType, walking));
|
|
163
|
+
case 'named': {
|
|
164
|
+
if (type.moduleName === null && ['Index', 'Range'].includes(type.name)) {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (type.typeArguments.length > 0) {
|
|
169
|
+
return type.typeArguments.every(typeArgument => this.#holdsBoundValues(typeArgument, walking));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const shape = this.#registry.getDataShape(type);
|
|
173
|
+
|
|
174
|
+
if (shape === null || walking.has(type.identity)) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
walking.add(type.identity);
|
|
179
|
+
|
|
180
|
+
return [...shape.values()].every(propertyType => this.#holdsBoundValues(propertyType, walking));
|
|
181
|
+
}
|
|
182
|
+
default:
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** What a value is, for a message: its text where it is data, and what kind of value it is otherwise. */
|
|
188
|
+
static #describe(value) {
|
|
189
|
+
if (value === undefined) {
|
|
190
|
+
return 'nothing';
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// JSON writes a number that isn't finite as null, which would name another value.
|
|
194
|
+
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
195
|
+
return String(value);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
return JSON.stringify(value) ?? String(value);
|
|
200
|
+
} catch {
|
|
201
|
+
return Array.isArray(value) ? 'a list' : 'an object';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The type and the bound form of a value, at any depth of a binding: a reference the registry's modules recognize, the one type beneath every other where several do, kept as it is; null, a string, a finite number and a Boolean as
|
|
207
|
+
* they are; a list typed by the type its items share, a set as the list of its items in its order, a map and a dictionary as a dictionary of the key and value types its entries share, a pair by its key's and value's
|
|
208
|
+
* types, and an object by its fields' types, each copied and frozen. It types and copies in one walk, offering each object it reaches to the registry's modules before copying it, so every module's `typeOf` is asked
|
|
209
|
+
* of it and a reference nested in data is kept as it is. A value no module recognizes that is no data (NEXO2034), a list of items of no shared type (NEXO2021), a dictionary key of no key type (NEXO2041), keys or
|
|
210
|
+
* values of no shared type (NEXO2042), and data that reaches itself (NEXO2022) are refused.
|
|
211
|
+
*/
|
|
212
|
+
#bind(name, value, ancestors) {
|
|
213
|
+
if (value === null) {
|
|
214
|
+
return { type: Type.null, value };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (typeof value === 'string' || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value))) {
|
|
218
|
+
return { type: Type.createPrimitiveType(value), value };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (typeof value === 'object') {
|
|
222
|
+
const recognized = this.#registry.recognize(value, `the binding '${name}'`);
|
|
223
|
+
|
|
224
|
+
if (recognized.length > 0) {
|
|
225
|
+
return { type: this.#mostSpecificOf(name, recognized), value };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (ancestors.has(value)) {
|
|
229
|
+
throw NexoErrors.structure('NEXO2022', 'A binding holds a cycle, a list or an object that reaches itself, which data never does', { binding: name });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (ancestors.size >= Limits.dataNesting) {
|
|
233
|
+
throw NexoErrors.structure('NEXO2047', `The data bound to '${name}' nests deeper than ${Limits.dataNesting} levels, the most data nests`, { binding: name });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (Array.isArray(value) && ArrayUtils.findMissingIndex(value) === -1) {
|
|
237
|
+
return this.#bindList(name, value, ancestors);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// A set is a list of its items in its order, its uniqueness making no later projection one, and a map a dictionary, each captured as it stands and never a live view.
|
|
241
|
+
if (value instanceof Set) {
|
|
242
|
+
return this.#bindList(name, [...value], ancestors, value);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (value instanceof Map || value instanceof Dictionary) {
|
|
246
|
+
return this.#bindDictionary(name, value instanceof Map ? [...value] : value.entries.map(entry => [entry.key, entry.value]), ancestors, value);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (value instanceof Pair) {
|
|
250
|
+
return this.#bindPair(name, value, ancestors);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (ObjectUtils.isPlainObject(value)) {
|
|
254
|
+
return this.#bindObject(name, value, ancestors);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
throw NexoErrors.structure('NEXO2034', 'A binding is a reference a registered module recognizes, or data: a number, a string, a Boolean, null, a list holding a value at every index, or an object', { binding: name });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** A list, typed by the type its items share, the nearest supertype references of different types share, made nullable where one of them is null, and the empty list's being never; copied and frozen. Its type merges as each item is bound, so binding stops at the first item whose type shares none with those before it. */
|
|
262
|
+
#bindList(name, list, ancestors, container = list) {
|
|
263
|
+
ancestors.add(container);
|
|
264
|
+
|
|
265
|
+
let itemType = Type.never;
|
|
266
|
+
const items = [];
|
|
267
|
+
|
|
268
|
+
for (const item of list) {
|
|
269
|
+
const bound = this.#bind(name, item, ancestors);
|
|
270
|
+
|
|
271
|
+
itemType = this.#registry.typeChecker.merge(itemType, bound.type);
|
|
272
|
+
|
|
273
|
+
if (itemType === null) {
|
|
274
|
+
throw NexoErrors.structure('NEXO2021', 'A list whose items have different types has no type, since Nexo has no union types and a list holds items of one type', { binding: name });
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
items.push(bound.value);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
ancestors.delete(container);
|
|
281
|
+
|
|
282
|
+
return { type: Type.createListType(itemType), value: Object.freeze(items) };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* A dictionary of its entries, in their order, of the key type its keys share and the value type its values share, each merging as a list's items' types do, the empty one's both never: each key a number, a string,
|
|
287
|
+
* a Boolean, a reference or null, which Nexo's key equality compares exactly, and each key and value bound in turn. A JavaScript map's keys are unique by that equality, so no capture holds a key twice.
|
|
288
|
+
*/
|
|
289
|
+
#bindDictionary(name, entries, ancestors, container) {
|
|
290
|
+
ancestors.add(container);
|
|
291
|
+
|
|
292
|
+
let keyType = Type.never;
|
|
293
|
+
let valueType = Type.never;
|
|
294
|
+
const bound = [];
|
|
295
|
+
|
|
296
|
+
for (const [key, value] of entries) {
|
|
297
|
+
const boundKey = this.#bind(name, key, ancestors);
|
|
298
|
+
|
|
299
|
+
if (!this.#registry.isKeyType(boundKey.type)) {
|
|
300
|
+
throw NexoErrors.structure('NEXO2041', `A dictionary's key is a number, a string, a Boolean, a reference or null, which Nexo's key equality compares exactly, and one bound to '${name}' is of ${boundKey.type}`, { binding: name });
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const boundValue = this.#bind(name, value, ancestors);
|
|
304
|
+
|
|
305
|
+
keyType = this.#registry.typeChecker.merge(keyType, boundKey.type);
|
|
306
|
+
valueType = valueType === null ? null : this.#registry.typeChecker.merge(valueType, boundValue.type);
|
|
307
|
+
|
|
308
|
+
if (keyType === null || valueType === null) {
|
|
309
|
+
throw NexoErrors.structure('NEXO2042', `A dictionary whose ${keyType === null ? 'keys' : 'values'} have different types has no type, since Nexo has no union types and a dictionary holds keys of one type and values of one type`, { binding: name });
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
bound.push([boundKey.value, boundValue.value]);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
ancestors.delete(container);
|
|
316
|
+
|
|
317
|
+
return { type: Type.createDictionaryType(keyType, valueType), value: new Dictionary(bound) };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** A pair, of its key's type and its value's, each bound in turn. */
|
|
321
|
+
#bindPair(name, pair, ancestors) {
|
|
322
|
+
ancestors.add(pair);
|
|
323
|
+
|
|
324
|
+
const key = this.#bind(name, pair.key, ancestors);
|
|
325
|
+
const value = this.#bind(name, pair.value, ancestors);
|
|
326
|
+
|
|
327
|
+
ancestors.delete(pair);
|
|
328
|
+
|
|
329
|
+
return { type: Type.createPairType(key.type, value.type), value: new Pair(key.value, value.value) };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** An object, of the object type of its fields' types, copied and frozen, every own field kept, `__proto__` among them, as an ordinary field. */
|
|
333
|
+
#bindObject(name, object, ancestors) {
|
|
334
|
+
ancestors.add(object);
|
|
335
|
+
|
|
336
|
+
const fieldTypes = new Map();
|
|
337
|
+
const copy = {};
|
|
338
|
+
|
|
339
|
+
for (const [fieldName, field] of Object.entries(object)) {
|
|
340
|
+
const bound = this.#bind(name, field, ancestors);
|
|
341
|
+
|
|
342
|
+
fieldTypes.set(fieldName, bound.type);
|
|
343
|
+
ObjectUtils.defineDataProperty(copy, fieldName, bound.value);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
ancestors.delete(object);
|
|
347
|
+
|
|
348
|
+
return { type: Type.createObjectType(fieldTypes), value: Object.freeze(copy) };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** The one recognized type beneath every other, as a light that two modules recognize as a `Device` and a `Light` is a `Light`; types of which none is beneath the others are refused as ambiguous. */
|
|
352
|
+
#mostSpecificOf(name, types) {
|
|
353
|
+
const specific = types.find(type => types.every(other => this.#registry.typeChecker.isAssignable(type, other)));
|
|
354
|
+
|
|
355
|
+
if (specific === undefined) {
|
|
356
|
+
throw NexoErrors.structure('NEXO2035', `The value bound to '${name}' is recognized as ${types.map(type => type.toString(true)).join(' and ')}, of which none is beneath the others`, { binding: name });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return specific;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ParsedTexts — the definitions of the source texts parsed most recently, each with the imports a host supplied to it
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { DefinitionValidator } from './DefinitionValidator.mjs';
|
|
6
|
+
import { Lowering } from './Lowering.mjs';
|
|
7
|
+
import { Parser } from './Parser.mjs';
|
|
8
|
+
import { TextLocations } from './TextLocations.mjs';
|
|
9
|
+
|
|
10
|
+
/** The most texts whose definitions are kept. */
|
|
11
|
+
const CAPACITY = 256;
|
|
12
|
+
|
|
13
|
+
export class ParsedTexts {
|
|
14
|
+
/** Each kept text's definition, by the imports supplied to it and the text, the least recently parsed first. @type {Map<string, object>} */
|
|
15
|
+
static #definitionCache = new Map();
|
|
16
|
+
|
|
17
|
+
/** @type {WeakMap<object, TextLocations>} */
|
|
18
|
+
static #locations = new WeakMap();
|
|
19
|
+
|
|
20
|
+
/** Where each part of the definition came from: in its text, for one parsed from text, or in the definition its caller supplied, where validation moved a part; null otherwise. */
|
|
21
|
+
static locationsOf(definition) {
|
|
22
|
+
return ParsedTexts.#locations.get(definition) ?? DefinitionValidator.locationsOf(definition);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The text the definition was parsed from, or null for a definition not parsed from text. */
|
|
26
|
+
static textOf(definition) {
|
|
27
|
+
return ParsedTexts.#locations.get(definition)?.text ?? null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* The validated, frozen definition the text parses into with the imports a host supplies, kept for the next parse of the same text with the same imports. A definition depends on its text and those imports alone, so a
|
|
32
|
+
* kept one stays valid for good; the 256 asked for most recently are kept, a kept one asked for again counting as the latest, the least recently asked for released as another is kept, and its locations are released
|
|
33
|
+
* with it. A text that fails to parse is kept by none, so parsing it again refuses it again.
|
|
34
|
+
* @param {string} text
|
|
35
|
+
* @param {readonly object[]} [suppliedImports] The imports a host supplies, in a definition's shape, each once and in order of the local name it gives, checked already.
|
|
36
|
+
*/
|
|
37
|
+
static parse(text, suppliedImports = []) {
|
|
38
|
+
const key = `${JSON.stringify(suppliedImports)}|${text}`;
|
|
39
|
+
const kept = ParsedTexts.#definitionCache.get(key);
|
|
40
|
+
|
|
41
|
+
if (kept) {
|
|
42
|
+
ParsedTexts.#definitionCache.delete(key);
|
|
43
|
+
ParsedTexts.#definitionCache.set(key, kept);
|
|
44
|
+
|
|
45
|
+
return kept;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// The tree is lowered and let go: a kept definition holds its text, read again for its locations where one is asked for, never its tokens or their trivia.
|
|
49
|
+
const { definition, scriptParametersWrittenAlone } = Lowering.lower(new Parser(text, suppliedImports).parse(), suppliedImports);
|
|
50
|
+
const locations = new TextLocations(text, suppliedImports);
|
|
51
|
+
const validated = new DefinitionValidator(locations, scriptParametersWrittenAlone).validate(definition);
|
|
52
|
+
|
|
53
|
+
ParsedTexts.#locations.set(validated, locations);
|
|
54
|
+
ParsedTexts.#definitionCache.set(key, validated);
|
|
55
|
+
|
|
56
|
+
if (ParsedTexts.#definitionCache.size > CAPACITY) {
|
|
57
|
+
ParsedTexts.#definitionCache.delete(ParsedTexts.#definitionCache.keys().next().value);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return validated;
|
|
61
|
+
}
|
|
62
|
+
}
|