ucn 5.5.1 → 5.5.3
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/.claude/skills/ucn/SKILL.md +35 -9
- package/.claude/skills/ucn/references/commands.md +40 -18
- package/.claude/skills/ucn/references/resolution-rules.md +126 -20
- package/.claude/skills/ucn/references/trust-contract.md +5 -3
- package/README.md +32 -21
- package/cli/index.js +2 -1
- package/core/accessors.js +341 -54
- package/core/account.js +74 -20
- package/core/analysis.js +257 -125
- package/core/ast-analysis.js +12 -3
- package/core/bridge.js +15 -65
- package/core/brief.js +8 -4
- package/core/build-worker.js +2 -1
- package/core/cache.js +137 -6
- package/core/call-name-cache.js +134 -0
- package/core/callable-scope.js +21 -0
- package/core/callers.js +4726 -940
- package/core/check.js +22 -5
- package/core/class-identity.js +399 -12
- package/core/client-receivers.js +189 -0
- package/core/constructor-calls.js +831 -0
- package/core/constructor-hierarchy.js +147 -0
- package/core/constructor-protocol.js +196 -0
- package/core/contract-membership.js +346 -140
- package/core/cpp-conversion-constraints.js +123 -0
- package/core/cpp-conversion-syntax.js +216 -0
- package/core/cpp-conversions.js +414 -0
- package/core/cpp-scope.js +245 -44
- package/core/deadcode.js +61 -16
- package/core/decorator-binding.js +137 -3
- package/core/enum-reference-syntax.js +134 -0
- package/core/execute.js +17 -1
- package/core/external-macros.js +42 -10
- package/core/go-packages.js +171 -0
- package/core/graph-build.js +2 -0
- package/core/graph.js +75 -59
- package/core/ignore-rules.js +2 -1
- package/core/imports.js +145 -19
- package/core/index-ir.js +41 -4
- package/core/inprocess-clients.js +41 -15
- package/core/ir.js +27 -6
- package/core/macro-expansion.js +38 -2
- package/core/out-of-class-members.js +182 -0
- package/core/output/analysis-ext.js +4 -2
- package/core/output/analysis.js +89 -20
- package/core/output/graph.js +12 -3
- package/core/output/lines.js +4 -3
- package/core/output/public.js +2 -0
- package/core/output/reporting.js +12 -1
- package/core/output/shared.js +2 -0
- package/core/output/tracing.js +28 -22
- package/core/output-budget.js +2 -2
- package/core/parallel-build.js +52 -1
- package/core/project.js +217 -112
- package/core/provenance-constructor-base.js +161 -0
- package/core/provenance-facts.js +257 -27
- package/core/provenance-overload.js +458 -54
- package/core/provenance-type.js +487 -0
- package/core/provenance.js +234 -15
- package/core/public-command.js +17 -2
- package/core/query-bodies.js +159 -0
- package/core/reflection.js +270 -2
- package/core/rename-token-syntax.js +111 -0
- package/core/reporting.js +13 -7
- package/core/request-helpers.js +63 -7
- package/core/rust-macro-expansion.js +151 -19
- package/core/rust-modules.js +1 -1
- package/core/rust-names.js +607 -0
- package/core/search.js +70 -17
- package/core/shared.js +98 -1
- package/core/source-memo.js +201 -0
- package/core/text-usage-syntax.js +38 -0
- package/core/tracing.js +7 -1
- package/core/type-denotation.js +124 -37
- package/core/type-references.js +615 -246
- package/core/value-references.js +2777 -0
- package/core/verify.js +1097 -281
- package/languages/adapter.js +28 -4
- package/languages/c-family.js +1118 -164
- package/languages/constructor-bases.js +105 -0
- package/languages/cpp-platform.js +26 -0
- package/languages/cpp-site-syntax.js +83 -0
- package/languages/csharp-scope.js +439 -0
- package/languages/csharp.js +428 -68
- package/languages/go.js +385 -207
- package/languages/html.js +4 -2
- package/languages/index.js +397 -2
- package/languages/java-platform.js +226 -0
- package/languages/java-unicode.js +269 -0
- package/languages/java.js +461 -117
- package/languages/javascript.js +418 -66
- package/languages/lexical-scope.js +190 -23
- package/languages/python.js +404 -54
- package/languages/rust.js +510 -82
- package/languages/utils.js +67 -8
- package/mcp/server.js +8 -2
- package/package.json +3 -3
|
@@ -1,66 +1,291 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { declarationIdentity, sameDeclaration } = require('./provenance');
|
|
3
|
+
const { declarationIdentity, identityKey, sameDeclaration } = require('./provenance');
|
|
4
|
+
const { langTraits } = require('../languages');
|
|
5
|
+
const { UcnError, isInternalError } = require('./errors');
|
|
6
|
+
|
|
7
|
+
// Modules read lazily (load cycles), each resolved once: a require() per
|
|
8
|
+
// call in hot paths was measurable (fix #436).
|
|
9
|
+
let lazyCallersModuleCache = null;
|
|
10
|
+
const lazyCallersModule = () => lazyCallersModuleCache || (lazyCallersModuleCache = require('./callers'));
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A witness that cannot prove its answer (fix #421): a fact its view did
|
|
14
|
+
* not record, or a filesystem read that failed when it was captured. A
|
|
15
|
+
* capture or a replay that meets one proves nothing; any other exception is
|
|
16
|
+
* a defect and propagates.
|
|
17
|
+
*/
|
|
18
|
+
class ProvenanceRefusal extends UcnError {
|
|
19
|
+
constructor(message, code) {
|
|
20
|
+
super(message);
|
|
21
|
+
this.name = 'UcnError';
|
|
22
|
+
this.provenanceRefusal = true;
|
|
23
|
+
if (code !== undefined) this.code = code;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Null for an expected refusal (a missing witness fact, an unreadable
|
|
29
|
+
* source); rethrows an internal error so it reaches the surface as one
|
|
30
|
+
* instead of becoming a review row (fix #421).
|
|
31
|
+
*/
|
|
32
|
+
function refusedProof(error, refusal = null) {
|
|
33
|
+
if (error?.provenanceRefusal || !isInternalError(error)) return refusal;
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const OVERLOAD_FILE_FIELDS = ['language', 'relativePath', 'importBindings',
|
|
38
|
+
'importDetails', 'symbols', 'moduleResolved', 'cppUsings', 'macroScopeMarkers'];
|
|
39
|
+
|
|
40
|
+
// Work counters of witness construction (fix #418): a scaling test asserts
|
|
41
|
+
// that encoded and decoded facts grow with the sites a query visits, never
|
|
42
|
+
// with the project each witness could copy.
|
|
43
|
+
const WORK = { encodedNodes: 0, decodedNodes: 0, captures: 0, replays: 0 };
|
|
44
|
+
function provenanceWork() { return { ...WORK }; }
|
|
4
45
|
|
|
5
46
|
// Materialize the declaration/ancestry reads used by overload selection.
|
|
6
47
|
// Rechecking a witness runs the same selector over these data, with no live
|
|
7
48
|
// index, filesystem, or name-ranking fallback. An unrecorded read abstains.
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
if (value
|
|
14
|
-
|
|
49
|
+
// `frozen` (inside an operation) records immutable facts; `frozen.plain`
|
|
50
|
+
// turns false when the value holds a Map or Set, which cannot be frozen.
|
|
51
|
+
function encode(value, frozen = null) {
|
|
52
|
+
WORK.encodedNodes++;
|
|
53
|
+
const seal = out => frozen ? Object.freeze(out) : out;
|
|
54
|
+
if (value === undefined) return seal({ $undefined: true });
|
|
55
|
+
if (value instanceof Set) {
|
|
56
|
+
if (frozen) frozen.plain = false;
|
|
57
|
+
return seal({ $set: seal([...value].map(item => encode(item, frozen))) });
|
|
58
|
+
}
|
|
59
|
+
if (value instanceof Map) {
|
|
60
|
+
if (frozen) frozen.plain = false;
|
|
61
|
+
return seal({ $map: seal([...value].map(([k, v]) => seal([encode(k, frozen), encode(v, frozen)]))) });
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(value)) return seal(value.map(item => encode(item, frozen)));
|
|
64
|
+
if (value && typeof value === 'object') {
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const key of Object.keys(value)) {
|
|
67
|
+
const item = value[key];
|
|
68
|
+
if (item === undefined || typeof item === 'function') continue;
|
|
69
|
+
const encoded = encode(item, frozen);
|
|
70
|
+
if (key === '__proto__') Object.defineProperty(out, key, {
|
|
71
|
+
value: encoded, enumerable: true, writable: true, configurable: true,
|
|
72
|
+
});
|
|
73
|
+
else out[key] = encoded;
|
|
74
|
+
}
|
|
75
|
+
return seal(out);
|
|
76
|
+
}
|
|
15
77
|
return value;
|
|
16
78
|
}
|
|
17
|
-
function decode(value) {
|
|
79
|
+
function decode(value, freeze = false) {
|
|
80
|
+
WORK.decodedNodes++;
|
|
81
|
+
const seal = out => freeze ? Object.freeze(out) : out;
|
|
18
82
|
if (value?.$undefined) return undefined;
|
|
19
|
-
if (value?.$set) return new Set(value.$set.map(decode));
|
|
83
|
+
if (value?.$set) return new Set(value.$set.map(item => decode(item)));
|
|
20
84
|
if (value?.$map) return new Map(value.$map.map(([k, v]) => [decode(k), decode(v)]));
|
|
21
|
-
if (Array.isArray(value)) return value.map(decode);
|
|
22
|
-
if (value && typeof value === 'object')
|
|
85
|
+
if (Array.isArray(value)) return seal(value.map(item => decode(item, freeze)));
|
|
86
|
+
if (value && typeof value === 'object') {
|
|
87
|
+
const out = {};
|
|
88
|
+
for (const key of Object.keys(value)) {
|
|
89
|
+
const decoded = decode(value[key], freeze);
|
|
90
|
+
if (key === '__proto__') Object.defineProperty(out, key, {
|
|
91
|
+
value: decoded, enumerable: true, writable: true, configurable: true,
|
|
92
|
+
});
|
|
93
|
+
else out[key] = decoded;
|
|
94
|
+
}
|
|
95
|
+
return seal(out);
|
|
96
|
+
}
|
|
23
97
|
return value;
|
|
24
98
|
}
|
|
25
99
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
100
|
+
// Recorded facts are immutable. Within one operation each live index value
|
|
101
|
+
// is encoded once for every witness that reads it, and a plain recorded
|
|
102
|
+
// value has one frozen decoded twin that every view reads (fix #418: a copy
|
|
103
|
+
// of a whole symbol table per site and per replay made constructor and
|
|
104
|
+
// overload queries quadratic). Writing to a shared fact throws rather than
|
|
105
|
+
// leaking into another view; a Map or Set fact is decoded per view.
|
|
106
|
+
const PLAIN_FACTS = new WeakSet();
|
|
107
|
+
const DECODED_FACTS = new WeakMap();
|
|
108
|
+
|
|
109
|
+
function encodeFact(value, memo) {
|
|
110
|
+
if (!memo || !value || typeof value !== 'object') return encode(value, memo && {});
|
|
111
|
+
let encoded = memo.get(value);
|
|
112
|
+
if (encoded === undefined) {
|
|
113
|
+
const frozen = { plain: true };
|
|
114
|
+
encoded = encode(value, frozen);
|
|
115
|
+
if (frozen.plain) PLAIN_FACTS.add(encoded);
|
|
116
|
+
memo.set(value, encoded);
|
|
117
|
+
}
|
|
118
|
+
return encoded;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function decodeFact(value) {
|
|
122
|
+
if (!value || typeof value !== 'object' || !PLAIN_FACTS.has(value)) return decode(value);
|
|
123
|
+
let decoded = DECODED_FACTS.get(value);
|
|
124
|
+
if (decoded === undefined) {
|
|
125
|
+
decoded = decode(value, true);
|
|
126
|
+
DECODED_FACTS.set(value, decoded);
|
|
127
|
+
}
|
|
128
|
+
return decoded;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Shared computations of one operation by key and view kind (fix #418).
|
|
132
|
+
const FIELD_SET_TAGS = new WeakMap();
|
|
133
|
+
let fieldSetTags = 0;
|
|
134
|
+
const fieldSetTag = fields => {
|
|
135
|
+
if (!fields) return 'all';
|
|
136
|
+
if (!FIELD_SET_TAGS.has(fields)) FIELD_SET_TAGS.set(fields, `fields${++fieldSetTags}`);
|
|
137
|
+
return FIELD_SET_TAGS.get(fields);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
function factIndex(reads, live, fileFields) {
|
|
141
|
+
if (live) WORK.captures++;
|
|
142
|
+
else WORK.replays++;
|
|
143
|
+
// Derived identities must be rebuilt from captured declarations. A live
|
|
144
|
+
// WeakMap is neither a raw input nor a replayable serialization.
|
|
145
|
+
const local = new Map([['_classIdentity', null], ['_typeDenotation', null], ['_cppScope', null]]);
|
|
146
|
+
// Inside an operation of the live index, facts are immutable and an
|
|
147
|
+
// index value is encoded once for every witness of the operation.
|
|
148
|
+
const encoded = live && !live.isProvenanceFactIndex && typeof live._opMemo === 'function'
|
|
149
|
+
? live._opMemo('provenanceEncodedFacts', () => new WeakMap()) : null;
|
|
150
|
+
let missingFacts = false;
|
|
151
|
+
// `indexed`: the value is an index structure (a file field, a symbol
|
|
152
|
+
// list, a graph entry), the same object for every reader.
|
|
153
|
+
const read = (key, compute, indexed = false) => {
|
|
154
|
+
if (live && !Object.hasOwn(reads, key)) {
|
|
155
|
+
reads[key] = indexed ? encodeFact(compute(), encoded) : encode(compute(), encoded && {});
|
|
156
|
+
}
|
|
157
|
+
if (!Object.hasOwn(reads, key)) {
|
|
158
|
+
missingFacts = true;
|
|
159
|
+
throw new ProvenanceRefusal(`Missing provenance fact: ${key}`);
|
|
160
|
+
}
|
|
161
|
+
return decodeFact(reads[key]);
|
|
32
162
|
};
|
|
33
163
|
// A map-like read answers with one object per key, as the live index's
|
|
34
164
|
// Map.get does (fix #388: decoding the same file entry again for every
|
|
35
165
|
// read dominated warm `find` on Java projects).
|
|
36
166
|
const mapReads = new Map();
|
|
37
|
-
const readMapValue = (key, compute) => {
|
|
167
|
+
const readMapValue = (key, compute, indexed = false) => {
|
|
38
168
|
if (mapReads.has(key)) return mapReads.get(key);
|
|
39
|
-
const value = read(key, compute);
|
|
169
|
+
const value = read(key, compute, indexed);
|
|
40
170
|
mapReads.set(key, value);
|
|
41
171
|
return value;
|
|
42
172
|
};
|
|
43
|
-
|
|
173
|
+
const readFile = key => {
|
|
174
|
+
const base = `files.get:${JSON.stringify(key)}`;
|
|
175
|
+
if (mapReads.has(base)) return mapReads.get(base);
|
|
176
|
+
if (!read(`${base}.$present`, () => !!live.files.get(key))) {
|
|
177
|
+
mapReads.set(base, undefined);
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
// A language or package read must not copy every declaration in the
|
|
181
|
+
// file. Each observed field is an independent raw input; replaying
|
|
182
|
+
// an unobserved field still refuses for missing facts.
|
|
183
|
+
const value = {};
|
|
184
|
+
for (const field of fileFields) Object.defineProperty(value, field, {
|
|
185
|
+
enumerable: true, configurable: true,
|
|
186
|
+
get: () => readMapValue(`${base}.${JSON.stringify(field)}`,
|
|
187
|
+
() => {
|
|
188
|
+
const entry = live.files.get(key);
|
|
189
|
+
if (fileFields === OVERLOAD_FILE_FIELDS && field === 'symbols' &&
|
|
190
|
+
!langTraits(entry.language)?.overloadFileSymbols) return undefined;
|
|
191
|
+
return entry[field];
|
|
192
|
+
}, true),
|
|
193
|
+
set: next => {
|
|
194
|
+
Object.defineProperty(value, field, {
|
|
195
|
+
value: next, writable: true, enumerable: true, configurable: true,
|
|
196
|
+
});
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
mapReads.set(base, value);
|
|
200
|
+
return value;
|
|
201
|
+
};
|
|
202
|
+
const view = new Proxy({}, {
|
|
44
203
|
get(_, property) {
|
|
45
204
|
// Callers skip their per-operation memos on this facade: every
|
|
46
205
|
// read must be recorded for the replay (fix #396).
|
|
47
206
|
if (property === 'isProvenanceFactIndex') return true;
|
|
207
|
+
if (property === 'hasMissingProvenanceFacts') return missingFacts;
|
|
208
|
+
// A hint never changes an answer, only how it is reached: a
|
|
209
|
+
// capture may consult the live index for it, a replay goes without.
|
|
210
|
+
if (property === 'unrecordedHint') {
|
|
211
|
+
return (compute) => live && !live.isProvenanceFactIndex ? compute(live) : undefined;
|
|
212
|
+
}
|
|
213
|
+
// A pure computation over the view, shared by every witness of
|
|
214
|
+
// the operation: a capture runs it once on a view of its own and
|
|
215
|
+
// records that view's reads; a replay recomputes it.
|
|
216
|
+
if (property === 'sharedRead') {
|
|
217
|
+
return (key, compute) => {
|
|
218
|
+
const memo = live && !live.isProvenanceFactIndex && typeof live._opMemo === 'function'
|
|
219
|
+
? live._opMemo('provenanceSharedReads', () => new Map()) : null;
|
|
220
|
+
if (!memo) return compute(view);
|
|
221
|
+
const memoKey = `${fieldSetTag(fileFields)}\0${key}`;
|
|
222
|
+
let saved = memo.get(memoKey);
|
|
223
|
+
if (!saved) {
|
|
224
|
+
const childReads = {};
|
|
225
|
+
const value = compute(factIndex(childReads, live, fileFields));
|
|
226
|
+
saved = { value, reads: childReads };
|
|
227
|
+
memo.set(memoKey, saved);
|
|
228
|
+
}
|
|
229
|
+
for (const name in saved.reads) {
|
|
230
|
+
if (!Object.hasOwn(reads, name)) reads[name] = saved.reads[name];
|
|
231
|
+
}
|
|
232
|
+
return saved.value;
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (property === 'projectFact') {
|
|
236
|
+
return (key, compute) => readMapValue(`fact:${key}`, () => projectFact(live, key, compute), true);
|
|
237
|
+
}
|
|
238
|
+
if (property === 'projectFileSymbols') {
|
|
239
|
+
return (file, key, compute) => readMapValue(`fact:${key}`, () => {
|
|
240
|
+
const entry = live.files.get(file);
|
|
241
|
+
// Keep exactly the file fields this view exposes. In
|
|
242
|
+
// particular, overload views omit declarations for
|
|
243
|
+
// languages whose selector does not consume them.
|
|
244
|
+
const exposed = fileFields ? fileFields.includes('symbols') &&
|
|
245
|
+
(fileFields !== OVERLOAD_FILE_FIELDS || langTraits(entry?.language)?.overloadFileSymbols)
|
|
246
|
+
: langTraits(entry?.language)?.overloadFileSymbols;
|
|
247
|
+
return compute(exposed ? entry?.symbols : undefined, live);
|
|
248
|
+
}, true);
|
|
249
|
+
}
|
|
250
|
+
if (property === 'provenanceFs') return Object.fromEntries(
|
|
251
|
+
['existsSync', 'readdirSync', 'readFileSync'].map(method => [method, (...args) => {
|
|
252
|
+
const result = read(`fs.${method}:${JSON.stringify(args)}`, () => {
|
|
253
|
+
try { return { ok: true, value: require('fs')[method](...args) }; }
|
|
254
|
+
catch (error) { return { ok: false, code: error.code || null }; }
|
|
255
|
+
});
|
|
256
|
+
if (!result.ok) throw new ProvenanceRefusal('Captured filesystem read failed', result.code);
|
|
257
|
+
return result.value;
|
|
258
|
+
}]));
|
|
48
259
|
if (local.has(property)) return local.get(property);
|
|
49
260
|
if (String(property).includes('Cache')) {
|
|
50
261
|
const cache = new Map(); local.set(property, cache); return cache;
|
|
51
262
|
}
|
|
52
263
|
if (['symbols', 'files', 'importGraph', 'extendsGraph', 'extendedByGraph'].includes(property)) {
|
|
53
|
-
|
|
264
|
+
// One map facade per view, as on the live index. Reusing
|
|
265
|
+
// the facade does not skip its methods' recorded reads.
|
|
266
|
+
const map = {
|
|
54
267
|
get(key) {
|
|
268
|
+
if (property === 'files' && fileFields) return readFile(key);
|
|
55
269
|
return readMapValue(`${property}.get:${JSON.stringify(key)}`, () => {
|
|
56
270
|
const value = live[property]?.get(key);
|
|
57
271
|
if (property !== 'files' || !value) return value;
|
|
58
272
|
return { language: value.language, relativePath: value.relativePath,
|
|
59
|
-
importBindings: value.importBindings,
|
|
60
|
-
|
|
273
|
+
importBindings: value.importBindings,
|
|
274
|
+
...(value.importDetails !== undefined && { importDetails: value.importDetails }),
|
|
275
|
+
...(langTraits(value.language)?.overloadFileSymbols && value.symbols !== undefined && { symbols: value.symbols }),
|
|
276
|
+
moduleResolved: value.moduleResolved };
|
|
277
|
+
}, property !== 'files');
|
|
61
278
|
},
|
|
62
279
|
has(key) { return !!this.get(key); },
|
|
280
|
+
keys() {
|
|
281
|
+
return read(`${property}.keys`, () => keyList(live, live[property]), true)[Symbol.iterator]();
|
|
282
|
+
},
|
|
283
|
+
*values() { for (const key of this.keys()) yield this.get(key); },
|
|
284
|
+
*entries() { for (const key of this.keys()) yield [key, this.get(key)]; },
|
|
285
|
+
[Symbol.iterator]() { return this.entries(); },
|
|
63
286
|
};
|
|
287
|
+
local.set(property, map);
|
|
288
|
+
return map;
|
|
64
289
|
}
|
|
65
290
|
const method = live ? typeof live[property] === 'function'
|
|
66
291
|
: reads[`method:${String(property)}`] === true;
|
|
@@ -73,65 +298,243 @@ function factIndex(reads, live) {
|
|
|
73
298
|
},
|
|
74
299
|
set(_, property, value) { local.set(property, value); return true; },
|
|
75
300
|
});
|
|
301
|
+
return view;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// One key list per index map and operation: the shared encoding of a key
|
|
305
|
+
// read needs the same object for every witness.
|
|
306
|
+
function keyList(live, map) {
|
|
307
|
+
const lists = !live.isProvenanceFactIndex && typeof live._opMemo === 'function'
|
|
308
|
+
? live._opMemo('provenanceKeyLists', () => new WeakMap()) : null;
|
|
309
|
+
let keys = lists?.get(map);
|
|
310
|
+
if (!keys) {
|
|
311
|
+
keys = [...map.keys()];
|
|
312
|
+
lists?.set(map, keys);
|
|
313
|
+
}
|
|
314
|
+
return keys;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* A whole-program projection (every namespace, every alias spelling) that a
|
|
319
|
+
* witness consults. The witness records the projection's answer as one
|
|
320
|
+
* fact, never every file it scans: a per-site copy of the project made each
|
|
321
|
+
* constructor and overload query quadratic in project size (fix #418).
|
|
322
|
+
* `compute(index)` runs on the live index, under its own memo.
|
|
323
|
+
*/
|
|
324
|
+
function projectFact(index, key, compute) {
|
|
325
|
+
return index?.isProvenanceFactIndex ? index.projectFact(key, compute) : compute(index);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function fileSymbolFact(index, file, key, compute) {
|
|
329
|
+
return index.isProvenanceFactIndex ? index.projectFileSymbols(file, key, compute)
|
|
330
|
+
: compute(index.files.get(file)?.symbols, index);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Raw declarations whose line ranges contain the queried declaration.
|
|
334
|
+
* Scope proofs need these candidates, not every unrelated member in the
|
|
335
|
+
* file. The innermost choice is still derived from the recorded ranges.
|
|
336
|
+
*/
|
|
337
|
+
function containingDeclarations(index, definition) {
|
|
338
|
+
const { file, startLine, endLine } = definition;
|
|
339
|
+
if (!index.isProvenanceFactIndex || !Number.isInteger(startLine) || !Number.isInteger(endLine)) {
|
|
340
|
+
return index.files.get(file)?.symbols;
|
|
341
|
+
}
|
|
342
|
+
const key = JSON.stringify(['containing-declarations', file, startLine, endLine]);
|
|
343
|
+
return fileSymbolFact(index, file, key, (symbols, live) => {
|
|
344
|
+
if (!Array.isArray(symbols)) return symbols;
|
|
345
|
+
const memo = live._opMemo?.('containingDeclarations', () => new Map());
|
|
346
|
+
if (memo?.has(key)) return memo.get(key);
|
|
347
|
+
const candidates = require('./shared').symbolsSpanning(symbols, startLine)
|
|
348
|
+
.filter(symbol => !(symbol.endLine < endLine));
|
|
349
|
+
memo?.set(key, candidates);
|
|
350
|
+
return candidates;
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* `compute(index)` for a pure answer (plain data) of the index, once per
|
|
356
|
+
* operation and key; on a witness view, its reads join the witness.
|
|
357
|
+
*/
|
|
358
|
+
function sharedRead(index, key, compute) {
|
|
359
|
+
if (index?.isProvenanceFactIndex) return index.sharedRead(key, compute);
|
|
360
|
+
const memo = index?._opMemo?.('provenanceSharedReads:live', () => new Map());
|
|
361
|
+
if (!memo) return compute(index);
|
|
362
|
+
if (!memo.has(key)) memo.set(key, compute(index));
|
|
363
|
+
return memo.get(key);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Is `member` in a whole-program set (`computeSet(liveIndex)`, memoized by
|
|
368
|
+
* the caller)? A witness records the answer for that member only.
|
|
369
|
+
*/
|
|
370
|
+
function projectHas(index, setKey, member, computeSet) {
|
|
371
|
+
return projectFact(index, `${setKey}\0${member}`, live => computeSet(live).has(member));
|
|
76
372
|
}
|
|
77
373
|
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
374
|
+
// A witness captured inside an operation holds only frozen facts. Its
|
|
375
|
+
// replays are pure functions of those facts and of the replay's other
|
|
376
|
+
// inputs, so each distinct replay runs once (fix #418).
|
|
377
|
+
const SEALED_FACTS = new WeakSet();
|
|
378
|
+
const REPLAYS = new WeakMap();
|
|
379
|
+
|
|
380
|
+
function sealFacts(reads) {
|
|
381
|
+
for (const key in reads) {
|
|
382
|
+
const value = reads[key];
|
|
383
|
+
if (value && typeof value === 'object' && !Object.isFrozen(value)) return reads;
|
|
384
|
+
}
|
|
385
|
+
SEALED_FACTS.add(Object.freeze(reads));
|
|
386
|
+
return reads;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** `compute()` once per sealed fact set and key; it returns plain data. */
|
|
390
|
+
function replayed(reads, key, compute) {
|
|
391
|
+
if (!SEALED_FACTS.has(reads)) return compute();
|
|
392
|
+
let memo = REPLAYS.get(reads);
|
|
393
|
+
if (!memo) REPLAYS.set(reads, memo = new Map());
|
|
394
|
+
if (!memo.has(key)) memo.set(key, compute());
|
|
395
|
+
return memo.get(key);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* A capture runs its selector over exactly the facts it records: every read
|
|
400
|
+
* answers with the recorded value, and the facts it records are the reads
|
|
401
|
+
* it made. Its answer is therefore the first replay of the sealed witness
|
|
402
|
+
* (fix #418); a deserialized or changed copy is replayed in full.
|
|
403
|
+
*/
|
|
404
|
+
function seedReplay(reads, key, value) {
|
|
405
|
+
if (!SEALED_FACTS.has(reads)) return;
|
|
406
|
+
let memo = REPLAYS.get(reads);
|
|
407
|
+
if (!memo) REPLAYS.set(reads, memo = new Map());
|
|
408
|
+
if (!memo.has(key)) memo.set(key, value);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Reuse the encoding while its signature fields are unchanged. A check
|
|
412
|
+
// against a base revision temporarily restores those fields on the indexed
|
|
413
|
+
// declaration; that view needs its own witness, even for the same object.
|
|
414
|
+
// The encoding is immutable: every witness of the candidate shares it.
|
|
81
415
|
const ENCODED_DEFINITIONS = new WeakMap();
|
|
82
416
|
function encodeDefinition(definition) {
|
|
83
417
|
if (!definition || typeof definition !== 'object') return encode(definition);
|
|
84
|
-
let
|
|
85
|
-
if (!
|
|
86
|
-
|
|
87
|
-
|
|
418
|
+
let saved = ENCODED_DEFINITIONS.get(definition);
|
|
419
|
+
if (!saved || saved.params !== definition.params || saved.paramsStructured !== definition.paramsStructured ||
|
|
420
|
+
saved.generics !== definition.generics) {
|
|
421
|
+
const frozen = { plain: true };
|
|
422
|
+
saved = { value: encode(definition, frozen), params: definition.params,
|
|
423
|
+
paramsStructured: definition.paramsStructured, generics: definition.generics };
|
|
424
|
+
// A plain recorded declaration has one frozen decoded twin every
|
|
425
|
+
// capture selects over (fix #428).
|
|
426
|
+
if (frozen.plain) PLAIN_FACTS.add(saved.value);
|
|
427
|
+
ENCODED_DEFINITIONS.set(definition, saved);
|
|
88
428
|
}
|
|
89
|
-
return
|
|
429
|
+
return saved.value;
|
|
90
430
|
}
|
|
91
431
|
|
|
92
432
|
function captureOverload(index, call, candidates, language, select) {
|
|
93
433
|
const reads = {};
|
|
94
434
|
try {
|
|
95
|
-
|
|
96
|
-
|
|
435
|
+
// The selector reads the recorded call and candidates, as a replay
|
|
436
|
+
// does, so the capture's answer is the witness's first replay.
|
|
437
|
+
const recordedCall = encode(call, {});
|
|
438
|
+
const recordedCandidates = Object.freeze(candidates.map(encodeDefinition));
|
|
439
|
+
const view = factIndex(reads, index, OVERLOAD_FILE_FIELDS);
|
|
440
|
+
const selectable = recordedCandidates.map(decodeFact);
|
|
441
|
+
const result = select(view, decode(recordedCall), selectable, language);
|
|
442
|
+
const witness = { call: recordedCall, candidates: recordedCandidates, language,
|
|
443
|
+
reads: sealFacts(reads), fileFields: true,
|
|
97
444
|
selected: declarationIdentity(result.match),
|
|
98
445
|
outcome: result.match ? 'selected' : result.ambiguous ? 'ambiguous' : 'no-fit' };
|
|
99
|
-
|
|
446
|
+
if (SEALED_FACTS.has(witness.reads)) {
|
|
447
|
+
OVERLOAD_REPLAYS.set(witness, { reads: witness.reads, call: recordedCall,
|
|
448
|
+
candidates: recordedCandidates, language, fileFields: true, replay: {
|
|
449
|
+
candidates: selectable.map(declarationIdentity), call: decode(recordedCall),
|
|
450
|
+
result: { missing: view.hasMissingProvenanceFacts, ambiguous: result.ambiguous === true,
|
|
451
|
+
match: result.match ? declarationIdentity(result.match) : null } } });
|
|
452
|
+
}
|
|
453
|
+
return witness;
|
|
454
|
+
} catch (error) { return refusedProof(error); }
|
|
100
455
|
}
|
|
101
456
|
|
|
102
|
-
|
|
103
|
-
|
|
457
|
+
// The selector's answer over a witness's recorded facts. A frozen witness
|
|
458
|
+
// is replayed once; a changed or deserialized copy is replayed again.
|
|
459
|
+
const OVERLOAD_REPLAYS = new WeakMap();
|
|
460
|
+
function overloadReplay(witness) {
|
|
461
|
+
const reusable = SEALED_FACTS.has(witness.reads) && Object.isFrozen(witness.call) &&
|
|
462
|
+
Object.isFrozen(witness.candidates);
|
|
463
|
+
const saved = reusable ? OVERLOAD_REPLAYS.get(witness) : null;
|
|
464
|
+
if (saved && saved.reads === witness.reads && saved.call === witness.call &&
|
|
465
|
+
saved.candidates === witness.candidates && saved.language === witness.language &&
|
|
466
|
+
saved.fileFields === witness.fileFields) return saved.replay;
|
|
104
467
|
const candidates = decode(witness.candidates);
|
|
105
|
-
|
|
106
|
-
!members.some(member => sameDeclaration(member, declarationIdentity(candidate))))) return false;
|
|
468
|
+
const replay = { candidates: candidates.map(declarationIdentity), call: decode(witness.call), result: null };
|
|
107
469
|
try {
|
|
108
470
|
// Lazy access avoids an initialization cycle with confidence.js.
|
|
109
471
|
// The selector only sees the data-only replay facade above.
|
|
110
|
-
const select =
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
} catch {
|
|
472
|
+
const select = lazyCallersModule().selectProvenanceOverload;
|
|
473
|
+
const index = factIndex(witness.reads, undefined, witness.fileFields ? OVERLOAD_FILE_FIELDS : undefined);
|
|
474
|
+
const result = select(index, decode(witness.call), candidates, witness.language);
|
|
475
|
+
replay.result = { missing: index.hasMissingProvenanceFacts, ambiguous: result.ambiguous === true,
|
|
476
|
+
match: result.match ? declarationIdentity(result.match) : null };
|
|
477
|
+
} catch (error) { refusedProof(error); /* An unreplayable witness proves nothing. */ }
|
|
478
|
+
if (reusable) OVERLOAD_REPLAYS.set(witness, { reads: witness.reads, call: witness.call,
|
|
479
|
+
candidates: witness.candidates, language: witness.language, fileFields: witness.fileFields, replay });
|
|
480
|
+
return replay;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function validateOverload(witness, members, selected, invalidCall = false, ambiguous = false, site = null) {
|
|
484
|
+
if (!witness || !Array.isArray(witness.candidates)) return false;
|
|
485
|
+
let replay;
|
|
486
|
+
try { replay = overloadReplay(witness); } catch (error) { return refusedProof(error, false); }
|
|
487
|
+
// Members usually arrive in the replay's own order: an aligned pass
|
|
488
|
+
// decides those in one comparison each (fix #428).
|
|
489
|
+
if (replay.candidates.length !== members.length || (!replay.candidates.every((candidate, i) =>
|
|
490
|
+
sameDeclaration(members[i], candidate)) && replay.candidates.some(candidate =>
|
|
491
|
+
!members.some(member => sameDeclaration(member, candidate))))) return false;
|
|
492
|
+
const { call, result } = replay;
|
|
493
|
+
if (site && call?.argumentFile !== undefined && call.argumentFile !== site.file) return false;
|
|
494
|
+
if (site && call?.argumentFile !== undefined && call.callSite &&
|
|
495
|
+
(call.line !== site.line || call.callSite.line !== site.line ||
|
|
496
|
+
call.callSite.start !== site.start || call.callSite.column !== site.column)) return false;
|
|
497
|
+
if (!result || result.missing) return false;
|
|
498
|
+
if (ambiguous) return !result.match && result.ambiguous;
|
|
499
|
+
return invalidCall ? !result.match && !result.ambiguous :
|
|
500
|
+
!!result.match && sameDeclaration(result.match, selected);
|
|
116
501
|
}
|
|
117
502
|
|
|
118
503
|
// Reconstruct inherited overload slots from the actual declarations, with
|
|
119
504
|
// the nearest declaration of an identical signature hiding the ancestor.
|
|
505
|
+
const SLOT_SIGNATURES = new WeakMap();
|
|
506
|
+
const STEP_MEMBER_CHECKS = new WeakMap();
|
|
120
507
|
function overloadMemberGroup(steps) {
|
|
121
|
-
const normalize =
|
|
508
|
+
const normalize = lazyCallersModule().provenanceParameterIdentity;
|
|
122
509
|
const seen = new Set();
|
|
123
510
|
const group = [];
|
|
124
511
|
for (const step of steps) {
|
|
125
512
|
if (!Array.isArray(step.memberDefinitions) ||
|
|
126
|
-
step.memberDefinitions.length !== step.members.length
|
|
127
|
-
|
|
128
|
-
|
|
513
|
+
step.memberDefinitions.length !== step.members.length) return null;
|
|
514
|
+
// Every definition is one of the step's members: one key set per
|
|
515
|
+
// pair of lists, never a pairwise comparison per witness (fix #428).
|
|
516
|
+
let byDefinitions = STEP_MEMBER_CHECKS.get(step.members);
|
|
517
|
+
if (!byDefinitions) STEP_MEMBER_CHECKS.set(step.members, byDefinitions = new WeakMap());
|
|
518
|
+
let consistent = byDefinitions.get(step.memberDefinitions);
|
|
519
|
+
if (consistent === undefined) {
|
|
520
|
+
const memberKeys = new Set(step.members.map(member => identityKey(member)));
|
|
521
|
+
consistent = !memberKeys.has(null) &&
|
|
522
|
+
step.memberDefinitions.every(d => memberKeys.has(identityKey(declarationIdentity(d))));
|
|
523
|
+
byDefinitions.set(step.memberDefinitions, consistent);
|
|
524
|
+
}
|
|
525
|
+
if (!consistent) return null;
|
|
129
526
|
const sameDepth = [];
|
|
130
527
|
for (const definition of step.memberDefinitions) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
:
|
|
528
|
+
// One signature per declaration and parameter list (fix #428).
|
|
529
|
+
let saved = SLOT_SIGNATURES.get(definition);
|
|
530
|
+
if (!saved || saved.params !== definition.paramsStructured) {
|
|
531
|
+
saved = { params: definition.paramsStructured, signature: Array.isArray(definition.paramsStructured)
|
|
532
|
+
? definition.paramsStructured.filter(p => !p?.extensionReceiver)
|
|
533
|
+
.map(p => `${normalize(p?.type || '')}:${p?.rest ? 'rest' : 'fixed'}`).join(',')
|
|
534
|
+
: `${definition.file}:${definition.startLine}` };
|
|
535
|
+
SLOT_SIGNATURES.set(definition, saved);
|
|
536
|
+
}
|
|
537
|
+
const signature = saved.signature;
|
|
135
538
|
if (!seen.has(signature)) {
|
|
136
539
|
group.push(definition);
|
|
137
540
|
sameDepth.push(signature);
|
|
@@ -142,4 +545,5 @@ function overloadMemberGroup(steps) {
|
|
|
142
545
|
return group;
|
|
143
546
|
}
|
|
144
547
|
|
|
145
|
-
module.exports = { captureOverload, validateOverload, overloadMemberGroup, factIndex
|
|
548
|
+
module.exports = { captureOverload, validateOverload, overloadMemberGroup, factIndex, projectFact, projectHas, sharedRead, containingDeclarations, fileSymbolFact,
|
|
549
|
+
sealFacts, replayed, seedReplay, provenanceWork, ProvenanceRefusal, refusedProof };
|