semantifold 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +15 -0
- package/README.md +71 -0
- package/build/index.d.ts +54 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +45 -0
- package/build/src/backends/identifiers.d.ts +19 -0
- package/build/src/backends/identifiers.d.ts.map +1 -0
- package/build/src/backends/identifiers.js +93 -0
- package/build/src/backends/index.d.ts +35 -0
- package/build/src/backends/index.d.ts.map +1 -0
- package/build/src/backends/index.js +149 -0
- package/build/src/backends/java.d.ts +8 -0
- package/build/src/backends/java.d.ts.map +1 -0
- package/build/src/backends/java.js +172 -0
- package/build/src/backends/javascript.d.ts +8 -0
- package/build/src/backends/javascript.d.ts.map +1 -0
- package/build/src/backends/javascript.js +173 -0
- package/build/src/backends/php.d.ts +8 -0
- package/build/src/backends/php.d.ts.map +1 -0
- package/build/src/backends/php.js +175 -0
- package/build/src/backends/ruby.d.ts +8 -0
- package/build/src/backends/ruby.d.ts.map +1 -0
- package/build/src/backends/ruby.js +155 -0
- package/build/src/backends/scalars.d.ts +15 -0
- package/build/src/backends/scalars.d.ts.map +1 -0
- package/build/src/backends/scalars.js +138 -0
- package/build/src/backends/shared.d.ts +26 -0
- package/build/src/backends/shared.d.ts.map +1 -0
- package/build/src/backends/shared.js +424 -0
- package/build/src/backends/typescript.d.ts +8 -0
- package/build/src/backends/typescript.d.ts.map +1 -0
- package/build/src/backends/typescript.js +160 -0
- package/build/src/backends/writer.d.ts +87 -0
- package/build/src/backends/writer.d.ts.map +1 -0
- package/build/src/backends/writer.js +207 -0
- package/build/src/diagnostic.d.ts +67 -0
- package/build/src/diagnostic.d.ts.map +1 -0
- package/build/src/diagnostic.js +96 -0
- package/build/src/frontends/index.d.ts +14 -0
- package/build/src/frontends/index.d.ts.map +1 -0
- package/build/src/frontends/index.js +36 -0
- package/build/src/frontends/java.d.ts +12 -0
- package/build/src/frontends/java.d.ts.map +1 -0
- package/build/src/frontends/java.js +630 -0
- package/build/src/frontends/javascript-typescript.d.ts +19 -0
- package/build/src/frontends/javascript-typescript.d.ts.map +1 -0
- package/build/src/frontends/javascript-typescript.js +633 -0
- package/build/src/frontends/php.d.ts +12 -0
- package/build/src/frontends/php.d.ts.map +1 -0
- package/build/src/frontends/php.js +523 -0
- package/build/src/frontends/ruby.d.ts +12 -0
- package/build/src/frontends/ruby.d.ts.map +1 -0
- package/build/src/frontends/ruby.js +515 -0
- package/build/src/frontends/scalars.d.ts +19 -0
- package/build/src/frontends/scalars.d.ts.map +1 -0
- package/build/src/frontends/scalars.js +71 -0
- package/build/src/mapping.d.ts +160 -0
- package/build/src/mapping.d.ts.map +1 -0
- package/build/src/mapping.js +1120 -0
- package/build/src/semantic/location.d.ts +61 -0
- package/build/src/semantic/location.d.ts.map +1 -0
- package/build/src/semantic/location.js +142 -0
- package/build/src/semantic/operators.d.ts +16 -0
- package/build/src/semantic/operators.d.ts.map +1 -0
- package/build/src/semantic/operators.js +24 -0
- package/build/src/semantic/provenance.d.ts +74 -0
- package/build/src/semantic/provenance.d.ts.map +1 -0
- package/build/src/semantic/provenance.js +577 -0
- package/build/src/semantic/scalars.d.ts +19 -0
- package/build/src/semantic/scalars.d.ts.map +1 -0
- package/build/src/semantic/scalars.js +40 -0
- package/build/src/semantic/types.d.ts +1003 -0
- package/build/src/semantic/types.d.ts.map +1 -0
- package/build/src/semantic/types.js +279 -0
- package/build/src/semantic/validate.d.ts +60 -0
- package/build/src/semantic/validate.d.ts.map +1 -0
- package/build/src/semantic/validate.js +472 -0
- package/docs/architecture.md +39 -0
- package/docs/coding-standards.md +7 -0
- package/docs/goals.md +7 -0
- package/docs/language-support.md +69 -0
- package/docs/plans/2026-09-02-initial-toolchain.md +54 -0
- package/docs/source-maps.md +134 -0
- package/docs/testing.md +25 -0
- package/index.js +63 -0
- package/package.json +69 -0
- package/src/backends/identifiers.js +101 -0
- package/src/backends/index.js +159 -0
- package/src/backends/java.js +182 -0
- package/src/backends/javascript.js +184 -0
- package/src/backends/php.js +184 -0
- package/src/backends/ruby.js +163 -0
- package/src/backends/scalars.js +120 -0
- package/src/backends/shared.js +456 -0
- package/src/backends/typescript.js +168 -0
- package/src/backends/writer.js +224 -0
- package/src/diagnostic.js +103 -0
- package/src/frontends/index.js +36 -0
- package/src/frontends/java.js +735 -0
- package/src/frontends/javascript-typescript.js +718 -0
- package/src/frontends/php.js +593 -0
- package/src/frontends/ruby.js +622 -0
- package/src/frontends/scalars.js +77 -0
- package/src/mapping.js +1260 -0
- package/src/semantic/location.js +154 -0
- package/src/semantic/operators.js +28 -0
- package/src/semantic/provenance.js +629 -0
- package/src/semantic/scalars.js +44 -0
- package/src/semantic/types.js +319 -0
- package/src/semantic/validate.js +529 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import {createCoordinateIndex, indexedPointAt} from "./location.js"
|
|
4
|
+
|
|
5
|
+
/** @type {WeakMap<object, Readonly<Record<string, import("./types.js").SourceLocation>>>} */
|
|
6
|
+
const parserRanges = new WeakMap()
|
|
7
|
+
/** @type {WeakMap<import("./types.js").RegisteredSource, ReturnType<typeof createCoordinateIndex>>} */
|
|
8
|
+
const sourceCoordinateIndexes = new WeakMap()
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Records parser-provided semantic-token ranges without exposing parser trees.
|
|
12
|
+
* @param {object} node - Semantic node.
|
|
13
|
+
* @param {Readonly<Record<string, import("./types.js").SourceLocation>>} ranges - Exact role ranges.
|
|
14
|
+
* @returns {void}
|
|
15
|
+
*/
|
|
16
|
+
export function setParserRanges(node, ranges) {
|
|
17
|
+
parserRanges.set(node, ranges)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Records parser ranges and returns the same semantic node.
|
|
22
|
+
* @template {object} Node
|
|
23
|
+
* @param {Node} node - Semantic node.
|
|
24
|
+
* @param {Readonly<Record<string, import("./types.js").SourceLocation>>} ranges - Exact role ranges.
|
|
25
|
+
* @returns {Node} The same node.
|
|
26
|
+
*/
|
|
27
|
+
export function withParserRanges(node, ranges) {
|
|
28
|
+
setParserRanges(node, ranges)
|
|
29
|
+
|
|
30
|
+
return node
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Adds deterministic provenance to a validated frontend module.
|
|
35
|
+
* @param {import("./types.js").SemanticModule} module - Validated semantic module.
|
|
36
|
+
* @param {object} source - Parsed source.
|
|
37
|
+
* @param {string} source.filename - Source filename.
|
|
38
|
+
* @param {import("./types.js").SemanticLanguage} source.language - Source language.
|
|
39
|
+
* @param {string} source.source - Exact source content.
|
|
40
|
+
* @returns {import("./types.js").SemanticModule} The same semantic module.
|
|
41
|
+
*/
|
|
42
|
+
export function annotateParsedModule(module, {filename, language, source}) {
|
|
43
|
+
const sources = [{content: source, filename, id: "source:0", language}]
|
|
44
|
+
const entries = semanticEntries(module)
|
|
45
|
+
/** @type {Map<string, import("./types.js").SemanticNodeProvenance>} */
|
|
46
|
+
const recordsByPath = new Map()
|
|
47
|
+
/** @type {import("./types.js").SemanticNodeProvenance[]} */
|
|
48
|
+
const records = []
|
|
49
|
+
|
|
50
|
+
for (const [index, entry] of entries.entries()) {
|
|
51
|
+
const ranges = parserRanges.get(entry.node) ?? {}
|
|
52
|
+
const location = "location" in entry.node ? entry.node.location : ranges.type ?? entry.ownerLocation
|
|
53
|
+
const sourceProvenance = /** @type {import("./types.js").SemanticNodeSourceProvenance} */ ({
|
|
54
|
+
origin: {kind: "source", location, sourceId: "source:0"},
|
|
55
|
+
ranges,
|
|
56
|
+
schema: "SemantifoldNodeProvenance",
|
|
57
|
+
version: 1
|
|
58
|
+
})
|
|
59
|
+
const record = {
|
|
60
|
+
id: `node:${index}`,
|
|
61
|
+
kind: entry.node.kind,
|
|
62
|
+
origin: sourceProvenance.origin,
|
|
63
|
+
path: entry.path,
|
|
64
|
+
ranges: sourceProvenance.ranges
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
records.push(record)
|
|
68
|
+
recordsByPath.set(entry.path, record)
|
|
69
|
+
entry.node.sourceProvenance = sourceProvenance
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const symbols = resolveSymbols(module, recordsByPath)
|
|
73
|
+
|
|
74
|
+
module.provenance = {
|
|
75
|
+
coordinateSystem: "utf16",
|
|
76
|
+
nodes: records,
|
|
77
|
+
schema: "SemantifoldProvenance",
|
|
78
|
+
sources,
|
|
79
|
+
symbols,
|
|
80
|
+
version: 1
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return module
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Rebuilds canonical generation metadata without trusting caller-provided identities.
|
|
88
|
+
* @param {import("./types.js").SemanticModule} module - Semantic module.
|
|
89
|
+
* @param {{filename: string, content: string, language?: import("./types.js").SemanticLanguage}[]} [providedSources] - Additional source content.
|
|
90
|
+
* @returns {{provenance: import("./types.js").SemanticProvenance, recordFor: (node: import("./types.js").SemanticNode, path?: string) => import("./types.js").SemanticNodeProvenance}} Canonical index.
|
|
91
|
+
*/
|
|
92
|
+
export function createGenerationIndex(module, providedSources = []) {
|
|
93
|
+
const entries = semanticEntries(module)
|
|
94
|
+
const priorSources = usableSourceProvenance(module.provenance)
|
|
95
|
+
/** @type {import("./types.js").RegisteredSource[]} */
|
|
96
|
+
const sources = []
|
|
97
|
+
/** @type {Map<string, import("./types.js").RegisteredSource>} */
|
|
98
|
+
const priorSourcesById = new Map()
|
|
99
|
+
|
|
100
|
+
for (const source of providedSources) addSource(source.filename, source.content, source.language ?? null)
|
|
101
|
+
if (priorSources) {
|
|
102
|
+
for (const source of priorSources.sources) {
|
|
103
|
+
const canonical = addSource(source.filename, source.content, source.language)
|
|
104
|
+
|
|
105
|
+
priorSourcesById.set(source.id, canonical)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const entry of entries) {
|
|
109
|
+
const location = "location" in entry.node ? entry.node.location : entry.ownerLocation
|
|
110
|
+
|
|
111
|
+
if (!sources.some((source) => source.filename == location.filename)) addSource(location.filename, null, null)
|
|
112
|
+
}
|
|
113
|
+
const verifiableSources = sources.filter((source) => source.content != null)
|
|
114
|
+
|
|
115
|
+
/** @type {Map<object, import("./types.js").SemanticNodeProvenance[]>} */
|
|
116
|
+
const recordsByNode = new Map()
|
|
117
|
+
/** @type {Map<string, import("./types.js").SemanticNodeProvenance>} */
|
|
118
|
+
const recordsByPath = new Map()
|
|
119
|
+
/** @type {import("./types.js").SemanticNodeProvenance[]} */
|
|
120
|
+
const records = []
|
|
121
|
+
|
|
122
|
+
for (const [index, entry] of entries.entries()) {
|
|
123
|
+
const located = "location" in entry.node ? entry.node.location : entry.ownerLocation
|
|
124
|
+
const associated = usableNodeProvenance(entry.node.sourceProvenance)
|
|
125
|
+
const ranges = associated ? validRanges(associated.ranges, verifiableSources) : {}
|
|
126
|
+
const normalizedOrigin = associated
|
|
127
|
+
? normalizeOrigin(associated.origin, priorSourcesById, verifiableSources)
|
|
128
|
+
: undefined
|
|
129
|
+
const fallbackSource = sourceForLocation(located, sources)
|
|
130
|
+
const origin = normalizedOrigin ?? sourceOrigin(fallbackSource, located)
|
|
131
|
+
|
|
132
|
+
const record = {
|
|
133
|
+
id: `node:${index}`,
|
|
134
|
+
kind: entry.node.kind,
|
|
135
|
+
origin,
|
|
136
|
+
path: entry.path,
|
|
137
|
+
ranges
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
records.push(record)
|
|
141
|
+
recordsByPath.set(entry.path, record)
|
|
142
|
+
const occurrences = recordsByNode.get(entry.node) ?? []
|
|
143
|
+
|
|
144
|
+
occurrences.push(record)
|
|
145
|
+
recordsByNode.set(entry.node, occurrences)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const symbols = resolveSymbols(module, recordsByPath)
|
|
149
|
+
const provenance = /** @type {import("./types.js").SemanticProvenance} */ ({
|
|
150
|
+
coordinateSystem: "utf16",
|
|
151
|
+
nodes: records,
|
|
152
|
+
schema: "SemantifoldProvenance",
|
|
153
|
+
sources,
|
|
154
|
+
symbols,
|
|
155
|
+
version: 1
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
provenance,
|
|
160
|
+
recordFor(node, path) {
|
|
161
|
+
const occurrences = recordsByNode.get(node)
|
|
162
|
+
|
|
163
|
+
if (!occurrences) throw new RangeError("Semantic node is not part of this module.")
|
|
164
|
+
if (path !== undefined) {
|
|
165
|
+
const record = occurrences.find((candidate) => candidate.path == path)
|
|
166
|
+
|
|
167
|
+
if (!record) throw new RangeError(`Semantic node does not occur at '${path}'.`)
|
|
168
|
+
|
|
169
|
+
return record
|
|
170
|
+
}
|
|
171
|
+
if (occurrences.length != 1) {
|
|
172
|
+
throw new RangeError("Shared semantic node requires an occurrence path.")
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return occurrences[0]
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Registers one canonical source.
|
|
181
|
+
* @param {string} filename - Source filename.
|
|
182
|
+
* @param {string | null} content - Source content.
|
|
183
|
+
* @param {import("./types.js").SemanticLanguage | null} language - Source language.
|
|
184
|
+
* @returns {import("./types.js").RegisteredSource} Canonical source.
|
|
185
|
+
*/
|
|
186
|
+
function addSource(filename, content, language) {
|
|
187
|
+
if (typeof filename != "string" || content !== null && typeof content != "string" ||
|
|
188
|
+
language !== null && !["php", "ruby", "javascript", "typescript", "java"].includes(language)) {
|
|
189
|
+
throw new TypeError("Malformed semantic source registry entry.")
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const existing = sources.find((source) => source.filename == filename && source.content == content)
|
|
193
|
+
|
|
194
|
+
if (existing) return existing
|
|
195
|
+
|
|
196
|
+
const registered = {content, filename, id: `source:${sources.length}`, language}
|
|
197
|
+
|
|
198
|
+
sources.push(registered)
|
|
199
|
+
|
|
200
|
+
return registered
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Selects the safe primary original location for closed provenance.
|
|
206
|
+
* @param {import("./types.js").SemanticOrigin} origin - Closed provenance.
|
|
207
|
+
* @returns {import("./types.js").SourceLocation | undefined} Primary location.
|
|
208
|
+
*/
|
|
209
|
+
export function primaryLocation(origin) {
|
|
210
|
+
if (origin.kind == "source") return origin.location
|
|
211
|
+
if (origin.kind == "derived") return origin.origins[0]?.location
|
|
212
|
+
|
|
213
|
+
return origin.relatedOrigins[0]?.location
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Accepts source metadata only when it is attached to the semantic node it describes.
|
|
218
|
+
* Registry-local IDs and paths are intentionally ignored and rebuilt.
|
|
219
|
+
* @param {import("./types.js").SemanticNodeSourceProvenance | undefined} record - Candidate node association.
|
|
220
|
+
* @returns {import("./types.js").SemanticNodeSourceProvenance | undefined} Usable association.
|
|
221
|
+
*/
|
|
222
|
+
function usableNodeProvenance(record) {
|
|
223
|
+
if (!record || record.schema != "SemantifoldNodeProvenance" || record.version != 1 ||
|
|
224
|
+
!record.origin || typeof record.origin != "object" ||
|
|
225
|
+
!record.ranges || typeof record.ranges != "object" || Array.isArray(record.ranges)) return undefined
|
|
226
|
+
|
|
227
|
+
return record
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Retains a valid source registry independently from potentially stale node metadata.
|
|
232
|
+
* @param {import("./types.js").SemanticProvenance | undefined} provenance - Candidate provenance.
|
|
233
|
+
* @returns {import("./types.js").SemanticProvenance | undefined} Provenance with a usable source registry.
|
|
234
|
+
*/
|
|
235
|
+
function usableSourceProvenance(provenance) {
|
|
236
|
+
if (!provenance || provenance.schema != "SemantifoldProvenance" || provenance.version != 1 ||
|
|
237
|
+
provenance.coordinateSystem != "utf16" || !Array.isArray(provenance.sources)) return undefined
|
|
238
|
+
|
|
239
|
+
const sourceIds = new Set()
|
|
240
|
+
|
|
241
|
+
for (const source of provenance.sources) {
|
|
242
|
+
if (!source || typeof source.id != "string" || sourceIds.has(source.id) || typeof source.filename != "string" ||
|
|
243
|
+
source.filename.length == 0 ||
|
|
244
|
+
source.content !== null && typeof source.content != "string" || source.language !== null &&
|
|
245
|
+
!["php", "ruby", "javascript", "typescript", "java"].includes(source.language)) return undefined
|
|
246
|
+
sourceIds.add(source.id)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return provenance
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Retains only self-consistent source ranges.
|
|
254
|
+
* @param {Readonly<Record<string, import("./types.js").SourceLocation>>} ranges - Candidate ranges.
|
|
255
|
+
* @param {import("./types.js").RegisteredSource[]} sources - Canonical sources.
|
|
256
|
+
* @returns {Readonly<Record<string, import("./types.js").SourceLocation>>} Safe ranges.
|
|
257
|
+
*/
|
|
258
|
+
function validRanges(ranges, sources) {
|
|
259
|
+
if (!ranges || typeof ranges != "object" || Array.isArray(ranges)) return {}
|
|
260
|
+
|
|
261
|
+
/** @type {Record<string, import("./types.js").SourceLocation>} */
|
|
262
|
+
const valid = {}
|
|
263
|
+
|
|
264
|
+
for (const [role, location] of Object.entries(ranges)) {
|
|
265
|
+
if (validLocation(location, sources)) valid[role] = location
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return valid
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Normalizes a closed origin while remapping untrusted source identities.
|
|
273
|
+
* @param {import("./types.js").SemanticOrigin} origin - Candidate origin.
|
|
274
|
+
* @param {Map<string, import("./types.js").RegisteredSource>} priorSources - Prior-to-canonical source mapping.
|
|
275
|
+
* @param {import("./types.js").RegisteredSource[]} sources - Canonical sources.
|
|
276
|
+
* @returns {import("./types.js").SemanticOrigin | undefined} Safe origin.
|
|
277
|
+
*/
|
|
278
|
+
function normalizeOrigin(origin, priorSources, sources) {
|
|
279
|
+
if (!origin || typeof origin != "object") return undefined
|
|
280
|
+
|
|
281
|
+
if (origin.kind == "source" && validLocation(origin.location, sources)) {
|
|
282
|
+
const claimed = priorSources.get(origin.sourceId)
|
|
283
|
+
const source = claimed && sourceOwnsLocation(claimed, origin.location) ? claimed : sourceForLocation(origin.location, sources)
|
|
284
|
+
|
|
285
|
+
return sourceOrigin(source, origin.location)
|
|
286
|
+
}
|
|
287
|
+
if (origin.kind == "derived" && Array.isArray(origin.origins) && origin.origins.length > 0) {
|
|
288
|
+
const origins = origin.origins.map((related) => normalizeRelatedOrigin(related, priorSources, sources))
|
|
289
|
+
|
|
290
|
+
if (origins.every(Boolean)) return {kind: "derived", origins: /** @type {import("./types.js").RelatedOrigin[]} */ (origins)}
|
|
291
|
+
}
|
|
292
|
+
if (origin.kind == "synthetic" && typeof origin.reason == "string" && Array.isArray(origin.relatedOrigins)) {
|
|
293
|
+
const relatedOrigins = origin.relatedOrigins.map((related) => normalizeRelatedOrigin(related, priorSources, sources))
|
|
294
|
+
|
|
295
|
+
if (relatedOrigins.every(Boolean)) {
|
|
296
|
+
return {kind: "synthetic", reason: origin.reason, relatedOrigins: /** @type {import("./types.js").RelatedOrigin[]} */ (relatedOrigins)}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return undefined
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Normalizes one related origin.
|
|
305
|
+
* @param {import("./types.js").RelatedOrigin} related - Candidate related origin.
|
|
306
|
+
* @param {Map<string, import("./types.js").RegisteredSource>} priorSources - Prior sources.
|
|
307
|
+
* @param {import("./types.js").RegisteredSource[]} sources - Canonical sources.
|
|
308
|
+
* @returns {import("./types.js").RelatedOrigin | undefined} Safe related origin.
|
|
309
|
+
*/
|
|
310
|
+
function normalizeRelatedOrigin(related, priorSources, sources) {
|
|
311
|
+
if (!related || !validLocation(related.location, sources)) return undefined
|
|
312
|
+
|
|
313
|
+
const claimed = priorSources.get(related.sourceId)
|
|
314
|
+
const source = claimed && sourceOwnsLocation(claimed, related.location) ? claimed : sourceForLocation(related.location, sources)
|
|
315
|
+
const normalized = {location: related.location, sourceId: source.id}
|
|
316
|
+
|
|
317
|
+
if (typeof related.role == "string") Object.assign(normalized, {role: related.role})
|
|
318
|
+
|
|
319
|
+
return normalized
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Checks normalized coordinates against exact source content when available.
|
|
324
|
+
* @param {import("./types.js").SourceLocation} location - Candidate location.
|
|
325
|
+
* @param {import("./types.js").RegisteredSource[]} sources - Registered sources.
|
|
326
|
+
* @returns {boolean} Whether the location is safe.
|
|
327
|
+
*/
|
|
328
|
+
function validLocation(location, sources) {
|
|
329
|
+
if (!location || typeof location.filename != "string" || !location.start || !location.end ||
|
|
330
|
+
!Number.isInteger(location.start.offset) || !Number.isInteger(location.end.offset) || location.start.offset < 0 ||
|
|
331
|
+
location.end.offset < location.start.offset || !validPointShape(location.start) || !validPointShape(location.end)) return false
|
|
332
|
+
|
|
333
|
+
const candidates = sources.filter((source) => source.filename == location.filename)
|
|
334
|
+
|
|
335
|
+
return candidates.some((source) => sourceOwnsLocation(source, location))
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Checks that one identified registry source owns a normalized location.
|
|
340
|
+
* @param {import("./types.js").RegisteredSource} source - Identified source.
|
|
341
|
+
* @param {import("./types.js").SourceLocation} location - Candidate location.
|
|
342
|
+
* @returns {boolean} Whether the source owns the location.
|
|
343
|
+
*/
|
|
344
|
+
function sourceOwnsLocation(source, location) {
|
|
345
|
+
if (source.filename != location.filename) return false
|
|
346
|
+
if (source.content == null) return true
|
|
347
|
+
if (location.end.offset > source.content.length) return false
|
|
348
|
+
let coordinates = sourceCoordinateIndexes.get(source)
|
|
349
|
+
|
|
350
|
+
if (!coordinates) {
|
|
351
|
+
coordinates = createCoordinateIndex(source.content)
|
|
352
|
+
sourceCoordinateIndexes.set(source, coordinates)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return samePoint(indexedPointAt(coordinates, location.start.offset), location.start) &&
|
|
356
|
+
samePoint(indexedPointAt(coordinates, location.end.offset), location.end)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Checks the integer shape of one source point.
|
|
361
|
+
* @param {import("./types.js").SourcePoint} point - Source point.
|
|
362
|
+
* @returns {boolean} Whether it is structurally valid.
|
|
363
|
+
*/
|
|
364
|
+
function validPointShape(point) {
|
|
365
|
+
return Number.isInteger(point.line) && point.line >= 1 && Number.isInteger(point.column) && point.column >= 1
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Compares normalized source coordinates.
|
|
370
|
+
* @param {import("./types.js").SourcePoint} left - Left point.
|
|
371
|
+
* @param {import("./types.js").SourcePoint} right - Right point.
|
|
372
|
+
* @returns {boolean} Whether all coordinates match.
|
|
373
|
+
*/
|
|
374
|
+
function samePoint(left, right) {
|
|
375
|
+
return left.offset == right.offset && left.line == right.line && left.column == right.column
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Finds the deterministic source owning a location.
|
|
380
|
+
* @param {import("./types.js").SourceLocation} location - Source location.
|
|
381
|
+
* @param {import("./types.js").RegisteredSource[]} sources - Canonical sources.
|
|
382
|
+
* @returns {import("./types.js").RegisteredSource} Owning source.
|
|
383
|
+
*/
|
|
384
|
+
function sourceForLocation(location, sources) {
|
|
385
|
+
const source = sources.find((candidate) => sourceOwnsLocation(candidate, location))
|
|
386
|
+
|
|
387
|
+
if (!source) throw new RangeError(`No registered source owns ${location.filename}:${location.start.offset}.`)
|
|
388
|
+
|
|
389
|
+
return source
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Creates direct source provenance.
|
|
394
|
+
* @param {import("./types.js").RegisteredSource} source - Source.
|
|
395
|
+
* @param {import("./types.js").SourceLocation} location - Location.
|
|
396
|
+
* @returns {import("./types.js").SourceOrigin} Origin.
|
|
397
|
+
*/
|
|
398
|
+
function sourceOrigin(source, location) {
|
|
399
|
+
return {kind: "source", location, sourceId: source.id}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Finds one node provenance record by semantic object or node identity.
|
|
404
|
+
* @param {import("./types.js").SemanticModule} module - Semantic module.
|
|
405
|
+
* @param {import("./types.js").SemanticNode | string} node - Semantic object or node identity.
|
|
406
|
+
* @returns {import("./types.js").SemanticNodeProvenance} Provenance record.
|
|
407
|
+
*/
|
|
408
|
+
export function getNodeProvenance(module, node) {
|
|
409
|
+
requireProvenance(module)
|
|
410
|
+
const index = createGenerationIndex(module)
|
|
411
|
+
const record = typeof node == "string"
|
|
412
|
+
? index.provenance.nodes.find((candidate) => candidate.id == node)
|
|
413
|
+
: index.recordFor(node)
|
|
414
|
+
|
|
415
|
+
if (!record) throw new RangeError(`Unknown semantic node: ${node}`)
|
|
416
|
+
|
|
417
|
+
return record
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Finds one semantic symbol record.
|
|
422
|
+
* @param {import("./types.js").SemanticModule} module - Semantic module.
|
|
423
|
+
* @param {string} symbolId - Symbol identity.
|
|
424
|
+
* @returns {import("./types.js").SemanticSymbolProvenance} Symbol record.
|
|
425
|
+
*/
|
|
426
|
+
export function getSymbolProvenance(module, symbolId) {
|
|
427
|
+
requireProvenance(module)
|
|
428
|
+
const symbol = createGenerationIndex(module).provenance.symbols.find((candidate) => candidate.id == symbolId)
|
|
429
|
+
|
|
430
|
+
if (!symbol) throw new RangeError(`Unknown semantic symbol: ${symbolId}`)
|
|
431
|
+
|
|
432
|
+
return symbol
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Requires frontend provenance.
|
|
437
|
+
* @param {import("./types.js").SemanticModule} module - Semantic module.
|
|
438
|
+
* @returns {import("./types.js").SemanticProvenance} Provenance.
|
|
439
|
+
*/
|
|
440
|
+
function requireProvenance(module) {
|
|
441
|
+
if (!module.provenance) throw new TypeError("Semantic module does not contain source provenance.")
|
|
442
|
+
|
|
443
|
+
return module.provenance
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Traverses public semantic nodes in one stable order.
|
|
448
|
+
* @param {import("./types.js").SemanticModule} module - Semantic module.
|
|
449
|
+
* @returns {{node: import("./types.js").SemanticNode, ownerLocation: import("./types.js").SourceLocation, path: string}[]} Entries.
|
|
450
|
+
*/
|
|
451
|
+
export function semanticEntries(module) {
|
|
452
|
+
/** @type {{node: import("./types.js").SemanticNode, ownerLocation: import("./types.js").SourceLocation, path: string}[]} */
|
|
453
|
+
const entries = []
|
|
454
|
+
|
|
455
|
+
visit(module, "", module.location)
|
|
456
|
+
|
|
457
|
+
return entries
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Visits one semantic node and its children.
|
|
461
|
+
* @param {import("./types.js").SemanticNode} node - Current node.
|
|
462
|
+
* @param {string} path - Current JSON Pointer.
|
|
463
|
+
* @param {import("./types.js").SourceLocation} ownerLocation - Nearest located owner.
|
|
464
|
+
* @returns {void}
|
|
465
|
+
*/
|
|
466
|
+
function visit(node, path, ownerLocation) {
|
|
467
|
+
const location = "location" in node ? node.location : ownerLocation
|
|
468
|
+
|
|
469
|
+
entries.push({node, ownerLocation: location, path})
|
|
470
|
+
|
|
471
|
+
if (node.kind == "Module") {
|
|
472
|
+
node.functions.forEach((child, index) => visit(child, `/functions/${index}`, location))
|
|
473
|
+
visit(node.entryPoint, "/entryPoint", location)
|
|
474
|
+
} else if (node.kind == "FunctionDeclaration") {
|
|
475
|
+
node.parameters.forEach((child, index) => visit(child, `${path}/parameters/${index}`, location))
|
|
476
|
+
visit(node.returnType, `${path}/returnType`, location)
|
|
477
|
+
visit(node.body, `${path}/body`, location)
|
|
478
|
+
} else if (node.kind == "Block") {
|
|
479
|
+
node.statements.forEach((child, index) => visit(child, `${path}/statements/${index}`, location))
|
|
480
|
+
} else if (node.kind == "Parameter") {
|
|
481
|
+
visit(node.type, `${path}/type`, location)
|
|
482
|
+
} else if (node.kind == "LocalDeclaration") {
|
|
483
|
+
visit(node.type, `${path}/type`, location)
|
|
484
|
+
visit(node.initializer, `${path}/initializer`, location)
|
|
485
|
+
} else if (node.kind == "AssignmentStatement") {
|
|
486
|
+
visit(node.target, `${path}/target`, location)
|
|
487
|
+
visit(node.expression, `${path}/expression`, location)
|
|
488
|
+
} else if (node.kind == "ReturnStatement" || node.kind == "PrintStatement") {
|
|
489
|
+
visit(node.expression, `${path}/expression`, location)
|
|
490
|
+
} else if (node.kind == "IfStatement") {
|
|
491
|
+
visit(node.condition, `${path}/condition`, location)
|
|
492
|
+
visit(node.consequent, `${path}/consequent`, location)
|
|
493
|
+
if (node.alternate) visit(node.alternate, `${path}/alternate`, location)
|
|
494
|
+
} else if (node.kind == "EntryPoint") {
|
|
495
|
+
visit(node.body, `${path}/body`, location)
|
|
496
|
+
} else if (node.kind == "UnaryExpression") {
|
|
497
|
+
visit(node.operand, `${path}/operand`, location)
|
|
498
|
+
} else if (node.kind == "BinaryExpression") {
|
|
499
|
+
visit(node.left, `${path}/left`, location)
|
|
500
|
+
visit(node.right, `${path}/right`, location)
|
|
501
|
+
} else if (node.kind == "CallExpression") {
|
|
502
|
+
node.arguments.forEach((child, index) => visit(child, `${path}/arguments/${index}`, location))
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Resolves declarations and references after semantic validation.
|
|
509
|
+
* @param {import("./types.js").SemanticModule} module - Validated module.
|
|
510
|
+
* @param {Map<string, import("./types.js").SemanticNodeProvenance>} records - Node records by occurrence path.
|
|
511
|
+
* @returns {import("./types.js").SemanticSymbolProvenance[]} Symbols.
|
|
512
|
+
*/
|
|
513
|
+
function resolveSymbols(module, records) {
|
|
514
|
+
/** @type {import("./types.js").SemanticSymbolProvenance[]} */
|
|
515
|
+
const symbols = []
|
|
516
|
+
/** @type {Map<string, string>} */
|
|
517
|
+
const functions = new Map()
|
|
518
|
+
|
|
519
|
+
for (const [index, declaration] of module.functions.entries()) {
|
|
520
|
+
functions.set(declaration.name, declare(declaration, declaration.name, "function", `/functions/${index}`))
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
for (const [index, declaration] of module.functions.entries()) {
|
|
524
|
+
const declarationPath = `/functions/${index}`
|
|
525
|
+
const scope = new Map()
|
|
526
|
+
|
|
527
|
+
for (const [parameterIndex, parameter] of declaration.parameters.entries()) {
|
|
528
|
+
scope.set(parameter.name, declare(parameter, parameter.name, "parameter", `${declarationPath}/parameters/${parameterIndex}`))
|
|
529
|
+
}
|
|
530
|
+
visitBlock(declaration.body, scope, `${declarationPath}/body`)
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
visitBlock(module.entryPoint.body, new Map(), "/entryPoint/body")
|
|
534
|
+
|
|
535
|
+
return symbols
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Declares one canonical symbol.
|
|
539
|
+
* @param {import("./types.js").FunctionDeclaration | import("./types.js").Parameter | import("./types.js").LocalDeclaration} node - Declaration.
|
|
540
|
+
* @param {string} name - Symbol name.
|
|
541
|
+
* @param {import("./types.js").SemanticSymbolKind} kind - Symbol kind.
|
|
542
|
+
* @param {string} path - Declaration occurrence path.
|
|
543
|
+
* @returns {string} Symbol identity.
|
|
544
|
+
*/
|
|
545
|
+
function declare(node, name, kind, path) {
|
|
546
|
+
const record = records.get(path)
|
|
547
|
+
|
|
548
|
+
if (!record) throw new Error(`Missing provenance for ${node.kind}.`)
|
|
549
|
+
|
|
550
|
+
const location = record.ranges.name ?? node.location
|
|
551
|
+
const id = `symbol:${symbols.length}`
|
|
552
|
+
|
|
553
|
+
record.symbolId = id
|
|
554
|
+
symbols.push({declarationNodeId: record.id, id, kind, location, name, references: []})
|
|
555
|
+
|
|
556
|
+
return id
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Resolves references in one lexical block.
|
|
561
|
+
* @param {import("./types.js").Block} block - Semantic block.
|
|
562
|
+
* @param {Map<string, string>} parent - Visible bindings.
|
|
563
|
+
* @param {string} path - Statement sequence path.
|
|
564
|
+
* @returns {void}
|
|
565
|
+
*/
|
|
566
|
+
function visitBlock(block, parent, path) {
|
|
567
|
+
const scope = new Map(parent)
|
|
568
|
+
|
|
569
|
+
for (const [index, statement] of block.statements.entries()) {
|
|
570
|
+
const statementPath = `${path}/statements/${index}`
|
|
571
|
+
|
|
572
|
+
if (statement.kind == "LocalDeclaration") {
|
|
573
|
+
visitExpression(statement.initializer, scope, `${statementPath}/initializer`)
|
|
574
|
+
scope.set(statement.name, declare(statement, statement.name, "local", statementPath))
|
|
575
|
+
} else if (statement.kind == "AssignmentStatement") {
|
|
576
|
+
reference(statement.target, scope.get(statement.target.name), "write", `${statementPath}/target`)
|
|
577
|
+
visitExpression(statement.expression, scope, `${statementPath}/expression`)
|
|
578
|
+
} else if (statement.kind == "ReturnStatement" || statement.kind == "PrintStatement") {
|
|
579
|
+
visitExpression(statement.expression, scope, `${statementPath}/expression`)
|
|
580
|
+
} else if (statement.kind == "IfStatement") {
|
|
581
|
+
visitExpression(statement.condition, scope, `${statementPath}/condition`)
|
|
582
|
+
visitBlock(statement.consequent, scope, `${statementPath}/consequent`)
|
|
583
|
+
if (statement.alternate) visitBlock(statement.alternate, scope, `${statementPath}/alternate`)
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Resolves references in one expression.
|
|
590
|
+
* @param {import("./types.js").Expression} expression - Expression.
|
|
591
|
+
* @param {Map<string, string>} scope - Visible bindings.
|
|
592
|
+
* @param {string} path - Expression occurrence path.
|
|
593
|
+
* @returns {void}
|
|
594
|
+
*/
|
|
595
|
+
function visitExpression(expression, scope, path) {
|
|
596
|
+
if (expression.kind == "IdentifierExpression") reference(expression, scope.get(expression.name), "read", path)
|
|
597
|
+
else if (expression.kind == "CallExpression") {
|
|
598
|
+
reference(expression, functions.get(expression.callee), "call", path)
|
|
599
|
+
for (const [index, argument] of expression.arguments.entries()) visitExpression(argument, scope, `${path}/arguments/${index}`)
|
|
600
|
+
} else if (expression.kind == "UnaryExpression") {
|
|
601
|
+
visitExpression(expression.operand, scope, `${path}/operand`)
|
|
602
|
+
} else if (expression.kind == "BinaryExpression") {
|
|
603
|
+
visitExpression(expression.left, scope, `${path}/left`)
|
|
604
|
+
visitExpression(expression.right, scope, `${path}/right`)
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Attaches one resolved symbol reference.
|
|
610
|
+
* @param {import("./types.js").IdentifierExpression | import("./types.js").CallExpression} node - Reference node.
|
|
611
|
+
* @param {string | undefined} symbolId - Resolved symbol.
|
|
612
|
+
* @param {"read" | "write" | "call"} role - Reference role.
|
|
613
|
+
* @param {string} path - Reference occurrence path.
|
|
614
|
+
* @returns {void}
|
|
615
|
+
*/
|
|
616
|
+
function reference(node, symbolId, role, path) {
|
|
617
|
+
if (!symbolId) return
|
|
618
|
+
|
|
619
|
+
const record = records.get(path)
|
|
620
|
+
const symbol = symbols.find((candidate) => candidate.id == symbolId)
|
|
621
|
+
|
|
622
|
+
if (!record || !symbol) throw new Error(`Missing provenance while resolving ${node.kind}.`)
|
|
623
|
+
|
|
624
|
+
const location = record.ranges[role == "call" ? "callee" : "name"] ?? node.location
|
|
625
|
+
|
|
626
|
+
record.symbolId = symbolId
|
|
627
|
+
symbol.references.push({location, nodeId: record.id, role})
|
|
628
|
+
}
|
|
629
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
/** @type {readonly import("./types.js").SemanticTypeName[]} */
|
|
4
|
+
const scalarTypeNames = Object.freeze(["integer", "boolean", "string"])
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns the canonical semantic scalar type reference.
|
|
8
|
+
* @param {import("./types.js").SemanticTypeName} name - Semantic scalar name.
|
|
9
|
+
* @returns {import("./types.js").TypeReference} Scalar type reference.
|
|
10
|
+
*/
|
|
11
|
+
export function scalarType(name) {
|
|
12
|
+
return {kind: /** @type {const} */ ("TypeReference"), name}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Checks an opaque scalar type name without coercion.
|
|
17
|
+
* @param {string} name - Candidate semantic type name.
|
|
18
|
+
* @returns {name is import("./types.js").SemanticTypeName} Whether the name is supported.
|
|
19
|
+
*/
|
|
20
|
+
export function isScalarTypeName(name) {
|
|
21
|
+
return scalarTypeNames.some((candidate) => candidate == name)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Checks that a JavaScript string contains only complete Unicode scalar values.
|
|
26
|
+
* @param {string} value - Candidate Unicode string.
|
|
27
|
+
* @returns {boolean} Whether the string excludes lone UTF-16 surrogates.
|
|
28
|
+
*/
|
|
29
|
+
export function hasOnlyUnicodeScalars(value) {
|
|
30
|
+
for (let index = 0; index < value.length; index++) {
|
|
31
|
+
const codeUnit = value.charCodeAt(index)
|
|
32
|
+
|
|
33
|
+
if (codeUnit >= 0xD800 && codeUnit <= 0xDBFF) {
|
|
34
|
+
const trailing = value.charCodeAt(index + 1)
|
|
35
|
+
|
|
36
|
+
if (!Number.isInteger(trailing) || trailing < 0xDC00 || trailing > 0xDFFF) return false
|
|
37
|
+
index++
|
|
38
|
+
} else if (codeUnit >= 0xDC00 && codeUnit <= 0xDFFF) {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return true
|
|
44
|
+
}
|