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.
Files changed (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +71 -0
  3. package/build/index.d.ts +54 -0
  4. package/build/index.d.ts.map +1 -0
  5. package/build/index.js +45 -0
  6. package/build/src/backends/identifiers.d.ts +19 -0
  7. package/build/src/backends/identifiers.d.ts.map +1 -0
  8. package/build/src/backends/identifiers.js +93 -0
  9. package/build/src/backends/index.d.ts +35 -0
  10. package/build/src/backends/index.d.ts.map +1 -0
  11. package/build/src/backends/index.js +149 -0
  12. package/build/src/backends/java.d.ts +8 -0
  13. package/build/src/backends/java.d.ts.map +1 -0
  14. package/build/src/backends/java.js +172 -0
  15. package/build/src/backends/javascript.d.ts +8 -0
  16. package/build/src/backends/javascript.d.ts.map +1 -0
  17. package/build/src/backends/javascript.js +173 -0
  18. package/build/src/backends/php.d.ts +8 -0
  19. package/build/src/backends/php.d.ts.map +1 -0
  20. package/build/src/backends/php.js +175 -0
  21. package/build/src/backends/ruby.d.ts +8 -0
  22. package/build/src/backends/ruby.d.ts.map +1 -0
  23. package/build/src/backends/ruby.js +155 -0
  24. package/build/src/backends/scalars.d.ts +15 -0
  25. package/build/src/backends/scalars.d.ts.map +1 -0
  26. package/build/src/backends/scalars.js +138 -0
  27. package/build/src/backends/shared.d.ts +26 -0
  28. package/build/src/backends/shared.d.ts.map +1 -0
  29. package/build/src/backends/shared.js +424 -0
  30. package/build/src/backends/typescript.d.ts +8 -0
  31. package/build/src/backends/typescript.d.ts.map +1 -0
  32. package/build/src/backends/typescript.js +160 -0
  33. package/build/src/backends/writer.d.ts +87 -0
  34. package/build/src/backends/writer.d.ts.map +1 -0
  35. package/build/src/backends/writer.js +207 -0
  36. package/build/src/diagnostic.d.ts +67 -0
  37. package/build/src/diagnostic.d.ts.map +1 -0
  38. package/build/src/diagnostic.js +96 -0
  39. package/build/src/frontends/index.d.ts +14 -0
  40. package/build/src/frontends/index.d.ts.map +1 -0
  41. package/build/src/frontends/index.js +36 -0
  42. package/build/src/frontends/java.d.ts +12 -0
  43. package/build/src/frontends/java.d.ts.map +1 -0
  44. package/build/src/frontends/java.js +630 -0
  45. package/build/src/frontends/javascript-typescript.d.ts +19 -0
  46. package/build/src/frontends/javascript-typescript.d.ts.map +1 -0
  47. package/build/src/frontends/javascript-typescript.js +633 -0
  48. package/build/src/frontends/php.d.ts +12 -0
  49. package/build/src/frontends/php.d.ts.map +1 -0
  50. package/build/src/frontends/php.js +523 -0
  51. package/build/src/frontends/ruby.d.ts +12 -0
  52. package/build/src/frontends/ruby.d.ts.map +1 -0
  53. package/build/src/frontends/ruby.js +515 -0
  54. package/build/src/frontends/scalars.d.ts +19 -0
  55. package/build/src/frontends/scalars.d.ts.map +1 -0
  56. package/build/src/frontends/scalars.js +71 -0
  57. package/build/src/mapping.d.ts +160 -0
  58. package/build/src/mapping.d.ts.map +1 -0
  59. package/build/src/mapping.js +1120 -0
  60. package/build/src/semantic/location.d.ts +61 -0
  61. package/build/src/semantic/location.d.ts.map +1 -0
  62. package/build/src/semantic/location.js +142 -0
  63. package/build/src/semantic/operators.d.ts +16 -0
  64. package/build/src/semantic/operators.d.ts.map +1 -0
  65. package/build/src/semantic/operators.js +24 -0
  66. package/build/src/semantic/provenance.d.ts +74 -0
  67. package/build/src/semantic/provenance.d.ts.map +1 -0
  68. package/build/src/semantic/provenance.js +577 -0
  69. package/build/src/semantic/scalars.d.ts +19 -0
  70. package/build/src/semantic/scalars.d.ts.map +1 -0
  71. package/build/src/semantic/scalars.js +40 -0
  72. package/build/src/semantic/types.d.ts +1003 -0
  73. package/build/src/semantic/types.d.ts.map +1 -0
  74. package/build/src/semantic/types.js +279 -0
  75. package/build/src/semantic/validate.d.ts +60 -0
  76. package/build/src/semantic/validate.d.ts.map +1 -0
  77. package/build/src/semantic/validate.js +472 -0
  78. package/docs/architecture.md +39 -0
  79. package/docs/coding-standards.md +7 -0
  80. package/docs/goals.md +7 -0
  81. package/docs/language-support.md +69 -0
  82. package/docs/plans/2026-09-02-initial-toolchain.md +54 -0
  83. package/docs/source-maps.md +134 -0
  84. package/docs/testing.md +25 -0
  85. package/index.js +63 -0
  86. package/package.json +69 -0
  87. package/src/backends/identifiers.js +101 -0
  88. package/src/backends/index.js +159 -0
  89. package/src/backends/java.js +182 -0
  90. package/src/backends/javascript.js +184 -0
  91. package/src/backends/php.js +184 -0
  92. package/src/backends/ruby.js +163 -0
  93. package/src/backends/scalars.js +120 -0
  94. package/src/backends/shared.js +456 -0
  95. package/src/backends/typescript.js +168 -0
  96. package/src/backends/writer.js +224 -0
  97. package/src/diagnostic.js +103 -0
  98. package/src/frontends/index.js +36 -0
  99. package/src/frontends/java.js +735 -0
  100. package/src/frontends/javascript-typescript.js +718 -0
  101. package/src/frontends/php.js +593 -0
  102. package/src/frontends/ruby.js +622 -0
  103. package/src/frontends/scalars.js +77 -0
  104. package/src/mapping.js +1260 -0
  105. package/src/semantic/location.js +154 -0
  106. package/src/semantic/operators.js +28 -0
  107. package/src/semantic/provenance.js +629 -0
  108. package/src/semantic/scalars.js +44 -0
  109. package/src/semantic/types.js +319 -0
  110. package/src/semantic/validate.js +529 -0
  111. 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
+ }