@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,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Range — a half-open interval of a list's positions: its start and its end, each a number counted from the start, an index counted from the end, or null where the range leaves it out
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Index } from './Index.mjs';
|
|
6
|
+
|
|
7
|
+
export class Range {
|
|
8
|
+
/** @type {number | Index | null} */
|
|
9
|
+
#start;
|
|
10
|
+
|
|
11
|
+
/** @type {number | Index | null} */
|
|
12
|
+
#end;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {number | Index | null} start Its start, a nonnegative safe integer or an index, null where it is left out, the list's start.
|
|
16
|
+
* @param {number | Index | null} end Its end, a nonnegative safe integer or an index, null where it is left out, the list's end.
|
|
17
|
+
*/
|
|
18
|
+
constructor(start, end) {
|
|
19
|
+
this.#start = start;
|
|
20
|
+
this.#end = end;
|
|
21
|
+
Object.freeze(this);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Its start, null where it is left out. */
|
|
25
|
+
get start() {
|
|
26
|
+
return this.#start;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Its end, null where it is left out. */
|
|
30
|
+
get end() {
|
|
31
|
+
return this.#end;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The offsets from the start its boundaries stand at within a list of the length, applied to that one length: a start left out 0, an end left out the length, an index the length less its offset. */
|
|
35
|
+
resolveIn(length) {
|
|
36
|
+
return { start: Range.#offsetOf(this.#start, 0, length), end: Range.#offsetOf(this.#end, length, length) };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The range as a script writes it. */
|
|
40
|
+
toString() {
|
|
41
|
+
return `${this.#start ?? ''}..${this.#end ?? ''}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
static #offsetOf(boundary, absent, length) {
|
|
45
|
+
if (boundary === null) {
|
|
46
|
+
return absent;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return boundary instanceof Index ? boundary.resolveIn(length) : boundary;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RecentlyUsed — the entries kept for the keys used most recently, as many as its capacity, which a key enters while there is room and otherwise at its second miss among the misses it remembers, so a stream of keys
|
|
3
|
+
* met once never displaces the keys met again, and the entry used least recently leaves to make room
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export class RecentlyUsed {
|
|
7
|
+
#capacity;
|
|
8
|
+
|
|
9
|
+
/** The entries, the most recently used last. @type {Map<string, unknown>} */
|
|
10
|
+
#entries = new Map();
|
|
11
|
+
|
|
12
|
+
/** The keys that missed while the entries were full, the most recent last, as many as twice the capacity, each entering at its next miss. @type {Set<string>} */
|
|
13
|
+
#missed = new Set();
|
|
14
|
+
|
|
15
|
+
/** @param {number} capacity The most entries it keeps. */
|
|
16
|
+
constructor(capacity) {
|
|
17
|
+
this.#capacity = capacity;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The entry of the key, which becomes the most recently used, or undefined where none is kept. */
|
|
21
|
+
get(key) {
|
|
22
|
+
const value = this.#entries.get(key);
|
|
23
|
+
|
|
24
|
+
if (value !== undefined) {
|
|
25
|
+
this.#entries.delete(key);
|
|
26
|
+
this.#entries.set(key, value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Offers the entry of a key that missed: it is kept while there is room or at the key's second miss, letting the entry used least recently go, and only remembered otherwise. */
|
|
33
|
+
offer(key, value) {
|
|
34
|
+
if (this.#entries.size < this.#capacity || this.#missed.delete(key)) {
|
|
35
|
+
this.#entries.set(key, value);
|
|
36
|
+
|
|
37
|
+
if (this.#entries.size > this.#capacity) {
|
|
38
|
+
this.#entries.delete(this.#entries.keys().next().value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.#missed.add(key);
|
|
45
|
+
|
|
46
|
+
if (this.#missed.size > this.#capacity * 2) {
|
|
47
|
+
this.#missed.delete(this.#missed.values().next().value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResolvedDefinition — a definition resolved against the vocabulary: the host call it ends in, the type of its result, and what each of its nodes resolved to
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** The character that begins each segment of a JSON Pointer. */
|
|
6
|
+
const SLASH = '/'.charCodeAt(0);
|
|
7
|
+
|
|
8
|
+
export class ResolvedDefinition {
|
|
9
|
+
#definition;
|
|
10
|
+
#type;
|
|
11
|
+
#hostCall;
|
|
12
|
+
#resolutions;
|
|
13
|
+
#usedDeclarations;
|
|
14
|
+
#deferredAccesses;
|
|
15
|
+
|
|
16
|
+
/** The JSON Pointer each node of the definition's expression stands at, by the node, a call's copy of a function's body among them, as resolution recorded it reaching the node. @type {WeakMap<object, string>} */
|
|
17
|
+
#pointers;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {object} definition The request's definition, whose every node is an object of its own.
|
|
21
|
+
* @param {import('./Type.mjs').Type | null} type The type of its result, null for a host call that supplies nothing.
|
|
22
|
+
* @param {{ node: object, pointer: string, name: string, declaration: object | null, candidates: readonly object[] } | null} hostCall The host call the script ends in, null for one that gives a value.
|
|
23
|
+
* @param {Map<string, object>} resolutions What each member node, by its JSON Pointer, resolved to.
|
|
24
|
+
* @param {WeakMap<object, string>} pointers The JSON Pointer each node resolution reached stands at, by the node.
|
|
25
|
+
* @param {object[]} usedDeclarations The declarations the request uses, each with the pointer that used it, a call's copy of a function's body among them, and its qualified name, the name a message gives it, qualified
|
|
26
|
+
* by the type its declaration is for.
|
|
27
|
+
* @param {{ name: string | null, pointer: string, candidates: object[] }[]} deferredAccesses The accesses whose member is selected during evaluation, each with the name it accesses, null for an unnamed indexer,
|
|
28
|
+
* and the declarations its selection could choose, each a use at its pointer.
|
|
29
|
+
* @param {{ node: object, pointer: string, bodyPointer: string }[]} functionBodyCopies The copy of a function's body each call resolved, with the pointer it was resolved under and the pointer of the body it copies.
|
|
30
|
+
* @param {boolean} mayRecognize Whether evaluating it may ask a module's `typeOf` of a value, in any branch, reached or not; true where left out, so a resolution that doesn't say is made stale by every module
|
|
31
|
+
* declaring a `typeOf`, never kept current past one.
|
|
32
|
+
*/
|
|
33
|
+
constructor(definition, type, hostCall, resolutions, pointers, usedDeclarations, deferredAccesses, functionBodyCopies = [], mayRecognize = true) {
|
|
34
|
+
this.#mayRecognize = mayRecognize;
|
|
35
|
+
this.#definition = definition;
|
|
36
|
+
this.#type = type;
|
|
37
|
+
this.#hostCall = hostCall;
|
|
38
|
+
this.#resolutions = resolutions;
|
|
39
|
+
this.#pointers = pointers;
|
|
40
|
+
this.#functionBodyCopies = functionBodyCopies;
|
|
41
|
+
this.#usedDeclarations = Object.freeze(this.#definitionUsesOf(usedDeclarations));
|
|
42
|
+
this.#deferredAccesses = Object.freeze(this.#definitionAccessesOf(deferredAccesses));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @type {{ node: object, pointer: string, bodyPointer: string }[]} */
|
|
46
|
+
#functionBodyCopies;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The uses, each named by the pointer into the definition it stands at, as a hook's request names its call, a body copy's pointer taken back to the body it copies; a declaration is kept once at each pointer, since
|
|
50
|
+
* every call of one function resolves its body's uses again.
|
|
51
|
+
*/
|
|
52
|
+
#definitionUsesOf(usedDeclarations) {
|
|
53
|
+
const kept = [];
|
|
54
|
+
// The declarations kept at each pointer, so a use is looked for among those at its own pointer alone.
|
|
55
|
+
const keptAt = new Map();
|
|
56
|
+
|
|
57
|
+
for (const use of usedDeclarations) {
|
|
58
|
+
const pointer = this.resolveDefinitionPointer(use.pointer);
|
|
59
|
+
let declarations = keptAt.get(pointer);
|
|
60
|
+
|
|
61
|
+
if (declarations === undefined) {
|
|
62
|
+
declarations = new Set();
|
|
63
|
+
keptAt.set(pointer, declarations);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!declarations.has(use.declaration)) {
|
|
67
|
+
declarations.add(use.declaration);
|
|
68
|
+
kept.push({ ...use, pointer });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return kept;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** The deferred accesses, each named by the pointer into the definition it stands at, as a use is, and kept once at each pointer, since every call of one function resolves its body's accesses again. */
|
|
76
|
+
#definitionAccessesOf(deferredAccesses) {
|
|
77
|
+
const kept = new Map();
|
|
78
|
+
|
|
79
|
+
for (const access of deferredAccesses) {
|
|
80
|
+
const pointer = this.resolveDefinitionPointer(access.pointer);
|
|
81
|
+
|
|
82
|
+
if (!kept.has(pointer)) {
|
|
83
|
+
kept.set(pointer, { ...access, pointer, candidates: access.candidates.map(candidate => ({ ...candidate, pointer })) });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return [...kept.values()];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The pointer into the definition each pointer resolved to, by the pointer: valid for good, since the body copies never change, and released with this resolved definition. An evaluation with hooks resolves the pointer
|
|
92
|
+
* of every invocation it reports, which would scan the body copies each time.
|
|
93
|
+
* @type {Map<string, string>}
|
|
94
|
+
*/
|
|
95
|
+
#definitionPointers = new Map();
|
|
96
|
+
|
|
97
|
+
/** Resolves the pointer into the definition a pointer names, a pointer into a call's copy of a function's body naming the part of the body it copies, and any other pointer itself. */
|
|
98
|
+
resolveDefinitionPointer(pointer) {
|
|
99
|
+
let definitionPointer = this.#definitionPointers.get(pointer);
|
|
100
|
+
|
|
101
|
+
if (definitionPointer === undefined) {
|
|
102
|
+
definitionPointer = ResolvedDefinition.resolveDefinitionPointer(pointer, this.#functionBodyCopies);
|
|
103
|
+
this.#definitionPointers.set(pointer, definitionPointer);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return definitionPointer;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The pointer into the definition a pointer names among the function body copies, each call's copy of a function's body taken back to the body it copies, a copy within a copy as many times as it is one. */
|
|
110
|
+
static resolveDefinitionPointer(pointer, functionBodyCopies) {
|
|
111
|
+
let definitionPointer = pointer;
|
|
112
|
+
|
|
113
|
+
for (let bodyCopy = ResolvedDefinition.#bodyCopyOf(definitionPointer, functionBodyCopies); bodyCopy !== undefined; bodyCopy = ResolvedDefinition.#bodyCopyOf(definitionPointer, functionBodyCopies)) {
|
|
114
|
+
definitionPointer = `${bodyCopy.bodyPointer}${definitionPointer.slice(bodyCopy.pointer.length)}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return definitionPointer;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The body copy whose pointer the pointer is or lies beneath, the innermost one, which is resolved after the one it lies in. */
|
|
121
|
+
static #bodyCopyOf(pointer, functionBodyCopies) {
|
|
122
|
+
for (let index = functionBodyCopies.length - 1; index >= 0; index--) {
|
|
123
|
+
const bodyCopy = functionBodyCopies[index];
|
|
124
|
+
const { length } = bodyCopy.pointer;
|
|
125
|
+
|
|
126
|
+
// The pointer is the copy's, or one beneath it, whose next segment begins with a slash.
|
|
127
|
+
if (pointer.startsWith(bodyCopy.pointer) && (pointer.length === length || pointer.charCodeAt(length) === SLASH)) {
|
|
128
|
+
return bodyCopy;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
#mayRecognize;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Whether evaluating it may recognize a value, asking each module's `typeOf`, in any branch, reached or not: where a declaration it uses gives the script a value of `Any`, a deferred access among them, or it ends
|
|
139
|
+
* in a host call whose supplied type holds a nullable reference type. A registration changes a resolution through recognition alone, so one that may not stays current whatever is registered after it.
|
|
140
|
+
*/
|
|
141
|
+
get mayRecognize() {
|
|
142
|
+
return this.#mayRecognize;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The request's definition. */
|
|
146
|
+
get definition() {
|
|
147
|
+
return this.#definition;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** The type of the request's result, null for a host call that supplies nothing. */
|
|
151
|
+
get type() {
|
|
152
|
+
return this.#type;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* The host call the script ends in: its node, where it stands, its name, its declaration, null on a receiver of unknown type, and its candidates, the declaration alone or, on a receiver of unknown type, every one
|
|
157
|
+
* that takes the call as it is written; null for a script that gives a value.
|
|
158
|
+
*/
|
|
159
|
+
get hostCall() {
|
|
160
|
+
return this.#hostCall;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** The declarations the request uses, with where each is used in the definition and the name a message gives it. */
|
|
164
|
+
get usedDeclarations() {
|
|
165
|
+
return this.#usedDeclarations;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The accesses whose member is selected during evaluation, by its value's actual type, each with where it stands in the definition, the name it accesses and the declarations its selection could choose. */
|
|
169
|
+
get deferredAccesses() {
|
|
170
|
+
return this.#deferredAccesses;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Every declaration evaluation could select: the used declarations, each deferred access's candidates at its pointer, every accessible declaration of its name and form, and each conversion of a value of Any's,
|
|
175
|
+
* every conversion to its type the imports make available, which its value's actual type selects one of or none of, so they are an overestimate wherever either remains.
|
|
176
|
+
*/
|
|
177
|
+
get candidateDeclarations() {
|
|
178
|
+
return this.#definitionUsesOf([...this.#usedDeclarations, ...this.#deferredAccesses.flatMap(access => access.candidates), ...this.#deferredConversionUses()]);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Whether evaluation selects a declaration of its own, at a deferred access or a conversion of a value of Any, so the candidate declarations overestimate what it uses. */
|
|
182
|
+
get selectsAsEvaluated() {
|
|
183
|
+
return this.#deferredAccesses.length > 0 || this.#deferredConversionUses().length > 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** The candidates of each conversion of a value of Any, at its pointer. */
|
|
187
|
+
#deferredConversionUses() {
|
|
188
|
+
return [...this.#resolutions.values()].flatMap(resolution => (resolution.kind === 'conversion' && resolution.candidateUses !== undefined ? resolution.candidateUses : []));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The copy of a function's body each call resolved, with the pointer it was resolved under and the pointer of the body it copies.
|
|
193
|
+
* @internal
|
|
194
|
+
*/
|
|
195
|
+
get functionBodyCopies() {
|
|
196
|
+
return this.#functionBodyCopies;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** What the member node at the pointer resolved to. */
|
|
200
|
+
getResolution(pointer) {
|
|
201
|
+
return this.#resolutions.get(pointer);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* What the node of the definition resolved to.
|
|
206
|
+
* @internal
|
|
207
|
+
*/
|
|
208
|
+
resolutionOf(node) {
|
|
209
|
+
return this.#resolutions.get(this.#pointers.get(node));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The JSON Pointer of the node of the definition.
|
|
214
|
+
* @internal
|
|
215
|
+
*/
|
|
216
|
+
pointerOf(node) {
|
|
217
|
+
return this.#pointers.get(node);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResultPath — a path into an expression's result, of field names and list indices: its shape, the type it reaches and the value it reads
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { NexoErrors } from './NexoError.mjs';
|
|
6
|
+
import { Names } from './Names.mjs';
|
|
7
|
+
import { Type } from './Type.mjs';
|
|
8
|
+
|
|
9
|
+
export class ResultPath {
|
|
10
|
+
/** Validates a path's shape before anything reads it: a list, each of its items a name or a list index, a whole number that is not negative, and any other is refused. */
|
|
11
|
+
static validateShape(path) {
|
|
12
|
+
if (!Array.isArray(path)) {
|
|
13
|
+
throw NexoErrors.structure('NEXO2646', `A path is a list of the names of fields and of list indices, and ${JSON.stringify(path) ?? String(path)} is none`, {});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
for (const item of path) {
|
|
17
|
+
const isIndex = Number.isInteger(item) && item >= 0;
|
|
18
|
+
|
|
19
|
+
// A field a result holds is named by any word, a reserved word among them.
|
|
20
|
+
if (!isIndex && !Names.isIdentifier(item)) {
|
|
21
|
+
throw NexoErrors.structure('NEXO2646', `A path holds the names of fields and list indices that are whole numbers and not negative, and ${JSON.stringify(item) ?? String(item)} is neither`, {});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The type the path reaches in the result's type, nullable where a step it passes is; a type that says nothing of its value takes any path, which is checked once its type is known. The path's shape is its
|
|
28
|
+
* caller's to validate first.
|
|
29
|
+
* @param {Type} resultType The result's type.
|
|
30
|
+
* @param {(string | number)[]} path
|
|
31
|
+
* @param {import('./vocabulary/NexoRegistry.mjs').NexoRegistry} registry Where a structured type's fields are declared.
|
|
32
|
+
* @param {() => object} locationOf Where a path that does not fit the result is refused, found only when one is.
|
|
33
|
+
*/
|
|
34
|
+
static typeAt(resultType, path, registry, locationOf) {
|
|
35
|
+
let type = resultType;
|
|
36
|
+
let isNullable = false;
|
|
37
|
+
|
|
38
|
+
for (const item of path) {
|
|
39
|
+
if (type.kind === 'null' || type.kind === 'never') {
|
|
40
|
+
return Type.null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
isNullable ||= type.isNullable;
|
|
44
|
+
|
|
45
|
+
if (typeof item === 'number') {
|
|
46
|
+
if (!type.isList) {
|
|
47
|
+
throw NexoErrors.validation('NEXO3601', `The path indexes ${type.toNonNullable()}, which is no list`, locationOf());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// An index out of range gives null.
|
|
51
|
+
type = type.itemType;
|
|
52
|
+
isNullable = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A dictionary exports its entries, and a pair its key and its value, so a path steps through them as through the result's exported form.
|
|
57
|
+
if (type.isDictionary && item === 'entries') {
|
|
58
|
+
const [keyType, valueType] = type.typeArguments;
|
|
59
|
+
|
|
60
|
+
type = Type.createListType(Type.createObjectType(new Map([['key', keyType], ['value', valueType]])));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (type.isPair && (item === 'key' || item === 'value')) {
|
|
65
|
+
type = type.typeArguments[item === 'key' ? 0 : 1];
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// An object type's object field, or a property of a structured type's data, each type's lookup its own.
|
|
70
|
+
const nextType = type.kind === 'object' ? type.getObjectFieldType(item) : (registry.getDataShape(type)?.get(item) ?? null);
|
|
71
|
+
|
|
72
|
+
if (nextType === null) {
|
|
73
|
+
throw NexoErrors.validation('NEXO3602', `The path names '${item}', which ${type.toNonNullable()} does not hold`, locationOf());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type = nextType;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return isNullable ? type.toNullable() : type;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The value the path reaches in the result: a name steps to an object field the value holds, and an index to a list's item; null where a step it passes is null or holds nothing at the step, an object field it
|
|
84
|
+
* lacks, an index out of range, and a name of a list or an index of an object among them. The path's shape is its caller's to validate first.
|
|
85
|
+
*/
|
|
86
|
+
static valueAt(value, path) {
|
|
87
|
+
let current = value;
|
|
88
|
+
|
|
89
|
+
for (const item of path) {
|
|
90
|
+
const holds = typeof item === 'number' ? Array.isArray(current) && item < current.length : current !== null && typeof current === 'object' && !Array.isArray(current) && Object.hasOwn(current, item);
|
|
91
|
+
|
|
92
|
+
if (!holds) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
current = current[item] ?? null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return current;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScopeLocals — the names in scope where resolution reaches a node, each with what it holds: a local, an object field, a conditional binding, a function parameter or an option
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/** How many names a scope adds over the map beneath it before it is flattened into one. */
|
|
6
|
+
const MOST_LAYERS = 16;
|
|
7
|
+
|
|
8
|
+
export class ScopeLocals {
|
|
9
|
+
/** The names this scope adds to, the one beneath it; null for a scope flattened into its map. @type {ScopeLocals | null} */
|
|
10
|
+
#enclosing;
|
|
11
|
+
|
|
12
|
+
/** The name this scope adds. @type {string | null} */
|
|
13
|
+
#name;
|
|
14
|
+
|
|
15
|
+
/** What the name it adds holds. */
|
|
16
|
+
#entry;
|
|
17
|
+
|
|
18
|
+
/** Every name in scope, for a scope flattened into one map; null for one that adds a name. @type {Map<string, unknown> | null} */
|
|
19
|
+
#names;
|
|
20
|
+
|
|
21
|
+
/** How many names this scope and those beneath it add over the map beneath them. */
|
|
22
|
+
#layers;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A scope is made empty, or by adding a name to another; neither is made directly.
|
|
26
|
+
* @param {ScopeLocals | null} enclosing The scope the name is added to, null for one flattened into its map.
|
|
27
|
+
* @param {string | null} name The name it adds.
|
|
28
|
+
* @param {unknown} entry What the name holds.
|
|
29
|
+
* @param {Map<string, unknown> | null} names Every name in scope, for a scope flattened into one map.
|
|
30
|
+
* @param {number} layers How many names it and those beneath it add over the map beneath them.
|
|
31
|
+
*/
|
|
32
|
+
constructor(enclosing, name, entry, names, layers) {
|
|
33
|
+
this.#enclosing = enclosing;
|
|
34
|
+
this.#name = name;
|
|
35
|
+
this.#entry = entry;
|
|
36
|
+
this.#names = names;
|
|
37
|
+
this.#layers = layers;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The scope with no name in it. */
|
|
41
|
+
static empty = new ScopeLocals(null, null, undefined, new Map(), 0);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* The scope with the name added, holding the entry, and hiding any name of its spelling this one has; this scope is left as it is, so each scope keeps the names in scope where it was made. A scope adds its name over
|
|
45
|
+
* this one rather than copying every name, so adding the fields of an object one by one takes as long as the fields do, not as their square; every sixteenth added flattens them into one map, so a lookup reads
|
|
46
|
+
* sixteen names at most.
|
|
47
|
+
*/
|
|
48
|
+
with(name, entry) {
|
|
49
|
+
if (this.#layers < MOST_LAYERS) {
|
|
50
|
+
return new ScopeLocals(this, name, entry, null, this.#layers + 1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const names = new Map();
|
|
54
|
+
|
|
55
|
+
this.#copyInto(names);
|
|
56
|
+
names.set(name, entry);
|
|
57
|
+
|
|
58
|
+
return new ScopeLocals(null, null, undefined, names, 0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** What the name holds in this scope, the one added last of its spelling; undefined where none is in scope. */
|
|
62
|
+
get(name) {
|
|
63
|
+
let scope = this;
|
|
64
|
+
|
|
65
|
+
while (scope.#names === null) {
|
|
66
|
+
if (scope.#name === name) {
|
|
67
|
+
return scope.#entry;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
scope = scope.#enclosing;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return scope.#names.get(name);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Whether a name of the spelling is in scope. */
|
|
77
|
+
has(name) {
|
|
78
|
+
let scope = this;
|
|
79
|
+
|
|
80
|
+
while (scope.#names === null) {
|
|
81
|
+
if (scope.#name === name) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
scope = scope.#enclosing;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return scope.#names.has(name);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Copies every name in scope into the map, each added later replacing one of its spelling added before it. */
|
|
92
|
+
#copyInto(names) {
|
|
93
|
+
if (this.#names !== null) {
|
|
94
|
+
this.#names.forEach((entry, name) => names.set(name, entry));
|
|
95
|
+
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.#enclosing.#copyInto(names);
|
|
100
|
+
names.set(this.#name, this.#entry);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StructureTypes — the structured type each structured value Nexo made is of, held by the value itself: a member's answer it copied, an object expression it constructed, and a value a host supplies or binds that it
|
|
3
|
+
* captured, so a value of Any keeps the identity its declaration gave it
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** A base whose constructor answers the value it is given, so a private field its subclass declares is added to that value. */
|
|
7
|
+
class Stamp {
|
|
8
|
+
constructor(value) {
|
|
9
|
+
return value;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export class StructureTypes extends Stamp {
|
|
14
|
+
/**
|
|
15
|
+
* The registry whose declaration typed the structured value, held by the value as a private field, which no reflection reads and no copy carries, beside its type. A record lives as long as its value, is replaced
|
|
16
|
+
* where another declaration gives that value as another type, and is read only against the registry that wrote it, so a value never carries a type into a registry that doesn't declare it. A weak map of every
|
|
17
|
+
* value, which each record was written to before, grew with every binding until a collection, and writing it measured half of binding.
|
|
18
|
+
* @type {object}
|
|
19
|
+
*/
|
|
20
|
+
#registry;
|
|
21
|
+
|
|
22
|
+
/** The structured type the registry gave the value. @type {import('./Type.mjs').Type} */
|
|
23
|
+
#type;
|
|
24
|
+
|
|
25
|
+
constructor(value, registry, type) {
|
|
26
|
+
super(value);
|
|
27
|
+
this.#registry = registry;
|
|
28
|
+
this.#type = type;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Records the structured value as of the type, a non-null structured type the registry declares; a value recorded already as the same type by the same registry keeps its record. The value is one Nexo made,
|
|
33
|
+
* recorded before it is frozen, never a host's own.
|
|
34
|
+
*/
|
|
35
|
+
static recordType(value, registry, type) {
|
|
36
|
+
if (!(#registry in value)) {
|
|
37
|
+
new StructureTypes(value, registry, type);
|
|
38
|
+
} else {
|
|
39
|
+
value.#registry = registry;
|
|
40
|
+
value.#type = type;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The structured type the value was recorded as, where the registry given recorded it, and undefined otherwise. */
|
|
45
|
+
static getType(value, registry) {
|
|
46
|
+
return #registry in value && value.#registry === registry ? value.#type : undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|