semantifold 0.1.0 → 0.2.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 (90) hide show
  1. package/README.md +13 -6
  2. package/build/index.d.ts +32 -3
  3. package/build/index.d.ts.map +1 -1
  4. package/build/index.js +99 -4
  5. package/build/src/acceptance.d.ts +22 -0
  6. package/build/src/acceptance.d.ts.map +1 -0
  7. package/build/src/acceptance.js +339 -0
  8. package/build/src/array.d.ts +7 -0
  9. package/build/src/array.d.ts.map +1 -0
  10. package/build/src/array.js +18 -0
  11. package/build/src/artifact-path.d.ts +13 -0
  12. package/build/src/artifact-path.d.ts.map +1 -0
  13. package/build/src/artifact-path.js +30 -0
  14. package/build/src/artifacts.d.ts +7 -0
  15. package/build/src/artifacts.d.ts.map +1 -0
  16. package/build/src/artifacts.js +389 -0
  17. package/build/src/backends/identifiers.d.ts.map +1 -1
  18. package/build/src/backends/identifiers.js +10 -2
  19. package/build/src/backends/index.d.ts.map +1 -1
  20. package/build/src/backends/index.js +19 -37
  21. package/build/src/backends/python.d.ts +8 -0
  22. package/build/src/backends/python.d.ts.map +1 -0
  23. package/build/src/backends/python.js +162 -0
  24. package/build/src/backends/scalars.d.ts.map +1 -1
  25. package/build/src/backends/scalars.js +2 -1
  26. package/build/src/backends/shared.d.ts.map +1 -1
  27. package/build/src/backends/shared.js +9 -6
  28. package/build/src/binary-mapping.d.ts +32 -0
  29. package/build/src/binary-mapping.d.ts.map +1 -0
  30. package/build/src/binary-mapping.js +285 -0
  31. package/build/src/diagnostic.d.ts +36 -4
  32. package/build/src/diagnostic.d.ts.map +1 -1
  33. package/build/src/diagnostic.js +42 -3
  34. package/build/src/frontends/index.d.ts.map +1 -1
  35. package/build/src/frontends/index.js +5 -23
  36. package/build/src/frontends/python.d.ts +14 -0
  37. package/build/src/frontends/python.d.ts.map +1 -0
  38. package/build/src/frontends/python.js +615 -0
  39. package/build/src/frontends/scalars.d.ts.map +1 -1
  40. package/build/src/frontends/scalars.js +6 -1
  41. package/build/src/language-registry.d.ts +113 -0
  42. package/build/src/language-registry.d.ts.map +1 -0
  43. package/build/src/language-registry.js +372 -0
  44. package/build/src/mapping.js +3 -2
  45. package/build/src/semantic/location.d.ts +7 -0
  46. package/build/src/semantic/location.d.ts.map +1 -1
  47. package/build/src/semantic/location.js +10 -1
  48. package/build/src/semantic/provenance.js +3 -3
  49. package/build/src/semantic/types.d.ts +399 -2
  50. package/build/src/semantic/types.d.ts.map +1 -1
  51. package/build/src/semantic/types.js +110 -2
  52. package/build/src/subprocess.d.ts +29 -0
  53. package/build/src/subprocess.d.ts.map +1 -0
  54. package/build/src/subprocess.js +207 -0
  55. package/build/src/timeout.d.ts +8 -0
  56. package/build/src/timeout.d.ts.map +1 -0
  57. package/build/src/timeout.js +11 -0
  58. package/build/src/toolchains.d.ts +83 -0
  59. package/build/src/toolchains.d.ts.map +1 -0
  60. package/build/src/toolchains.js +413 -0
  61. package/docs/architecture.md +7 -3
  62. package/docs/language-support.md +25 -4
  63. package/docs/parser-qualification.md +70 -0
  64. package/docs/source-maps.md +31 -4
  65. package/docs/testing.md +21 -2
  66. package/index.js +109 -4
  67. package/package.json +4 -2
  68. package/src/acceptance.js +359 -0
  69. package/src/array.js +19 -0
  70. package/src/artifact-path.js +34 -0
  71. package/src/artifacts.js +420 -0
  72. package/src/backends/identifiers.js +9 -1
  73. package/src/backends/index.js +16 -32
  74. package/src/backends/python.js +175 -0
  75. package/src/backends/scalars.js +1 -0
  76. package/src/backends/shared.js +8 -5
  77. package/src/binary-mapping.js +305 -0
  78. package/src/diagnostic.js +34 -2
  79. package/src/frontends/index.js +5 -20
  80. package/src/frontends/python.js +689 -0
  81. package/src/frontends/scalars.js +5 -0
  82. package/src/language-registry.js +403 -0
  83. package/src/mapping.js +2 -1
  84. package/src/semantic/location.js +10 -0
  85. package/src/semantic/provenance.js +2 -2
  86. package/src/semantic/types.js +124 -1
  87. package/src/subprocess.js +203 -0
  88. package/src/timeout.js +12 -0
  89. package/src/toolchains.js +444 -0
  90. package/tsconfig.json +1 -0
@@ -0,0 +1,420 @@
1
+ // @ts-check
2
+
3
+ import {decodedMappings, encodedMappings, presortedDecodedMap, TraceMap} from "@jridgewell/trace-mapping"
4
+ import {isDenseArray} from "./array.js"
5
+ import {isSafeArtifactPath, isValidFilenameMetadata} from "./artifact-path.js"
6
+ import {finalizeByteMapping} from "./binary-mapping.js"
7
+ import {SemantifoldDiagnostic} from "./diagnostic.js"
8
+ import {finalizeMapping, toSourceMapV3} from "./mapping.js"
9
+ import {isSourceRangeOrdered} from "./semantic/location.js"
10
+ import {hasOnlyUnicodeScalars} from "./semantic/scalars.js"
11
+
12
+ const artifactRoles = new Set(["entry", "source", "manifest", "support", "mapping", "resource", "loader"])
13
+ const mediaTypePattern = /^[A-Za-z0-9!#$&^_.+-]+\/[A-Za-z0-9!#$&^_.+-]+(?:;[\u0020-\u007e]+)?$/u
14
+
15
+ /**
16
+ * Transactionally validates and constructs one deterministic generated artifact set.
17
+ * @param {unknown} candidate - Artifact-set fields.
18
+ * @returns {import("./semantic/types.js").GeneratedArtifactSet} Detached immutable set.
19
+ */
20
+ export function createGeneratedArtifactSet(candidate) {
21
+ try {
22
+ if (!isPlainObject(candidate) || typeof candidate.target != "string" || candidate.target.length == 0 ||
23
+ !/^[a-z][a-z0-9-]*$/u.test(candidate.target)) {
24
+ invalidArtifactSet("Artifact set requires a stable lowercase target ID.", targetOf(candidate))
25
+ }
26
+ if (!isDenseArray(candidate.artifacts) || candidate.artifacts.length == 0) {
27
+ invalidArtifactSet("Artifact set requires a non-empty ordered artifact array.", candidate.target)
28
+ }
29
+ const target = candidate.target
30
+
31
+ const paths = new Set()
32
+ const directoryPaths = new Set()
33
+ /** @type {string[]} */
34
+ const entryPaths = []
35
+ /** @type {import("./semantic/types.js").GeneratedSetArtifact[]} */
36
+ const artifacts = []
37
+
38
+ for (let index = 0; index < candidate.artifacts.length; index += 1) {
39
+ const artifact = candidate.artifacts[index]
40
+
41
+ if (!isPlainObject(artifact)) invalidArtifactSet(`Artifact ${index} must be a plain object.`, target)
42
+ const artifactPath = artifact.path
43
+ const mediaType = artifact.mediaType
44
+ const role = artifact.role
45
+ const contentKind = artifact.contentKind
46
+
47
+ if (!isSafeArtifactPath(artifactPath)) invalidArtifactSet(`Artifact ${index} has an unsafe path.`, target)
48
+ if (paths.has(artifactPath)) invalidArtifactSet(`Duplicate artifact path '${artifactPath}'.`, target)
49
+ if (directoryPaths.has(artifactPath)) {
50
+ invalidArtifactSet(`Artifact path '${artifactPath}' collides with a required directory.`, target)
51
+ }
52
+ const parentPaths = artifactDirectoryPaths(artifactPath)
53
+
54
+ for (const parentPath of parentPaths) {
55
+ if (paths.has(parentPath)) {
56
+ invalidArtifactSet(`Artifact path '${artifactPath}' descends from artifact file '${parentPath}'.`, target)
57
+ }
58
+ }
59
+ paths.add(artifactPath)
60
+ for (const parentPath of parentPaths) directoryPaths.add(parentPath)
61
+ if (typeof mediaType != "string" || !mediaTypePattern.test(mediaType) || /[\r\n]/u.test(mediaType)) {
62
+ invalidArtifactSet(`Artifact '${artifactPath}' has an invalid media type.`, target)
63
+ }
64
+ if (typeof role != "string" || !artifactRoles.has(role)) invalidArtifactSet(`Artifact '${artifactPath}' has an invalid role.`, target)
65
+ if (artifact.ownership != "generated") invalidArtifactSet(`Artifact '${artifactPath}' must have generated ownership.`, target)
66
+ if (role == "entry") {
67
+ if (entryPaths.length > 0) invalidArtifactSet("Artifact set must declare exactly one entry artifact.", target)
68
+ entryPaths.push(artifactPath)
69
+ }
70
+
71
+ const contentValue = artifact.content
72
+ /** @type {string | Uint8Array} */
73
+ let content
74
+
75
+ if (contentKind == "text") {
76
+ if (typeof contentValue != "string" || contentValue.length == 0 || !hasOnlyUnicodeScalars(contentValue)) {
77
+ invalidArtifactSet(`Text artifact '${artifactPath}' requires non-empty string content.`, target)
78
+ }
79
+ content = contentValue
80
+ } else if (contentKind == "binary") {
81
+ if (!(contentValue instanceof Uint8Array) || contentValue.byteLength == 0) {
82
+ invalidArtifactSet(`Binary artifact '${artifactPath}' requires non-empty bytes.`, target)
83
+ }
84
+ content = new Uint8Array(contentValue)
85
+ } else invalidArtifactSet(`Artifact '${artifactPath}' has an invalid content kind.`, target)
86
+
87
+ const provenance = validateArtifactProvenance(artifact.provenance, contentKind, artifactPath, content, target)
88
+
89
+ artifacts.push(/** @type {import("./semantic/types.js").GeneratedSetArtifact} */ (freezeArtifact({
90
+ content,
91
+ contentKind,
92
+ mediaType,
93
+ ownership: /** @type {const} */ ("generated"),
94
+ path: artifactPath,
95
+ provenance,
96
+ role
97
+ })))
98
+ }
99
+
100
+ if (entryPaths.length != 1) invalidArtifactSet("Artifact set must declare exactly one entry artifact.", candidate.target)
101
+
102
+ return Object.freeze({
103
+ artifacts: Object.freeze(artifacts),
104
+ entry: entryPaths[0],
105
+ schema: /** @type {const} */ ("GeneratedArtifactSet"),
106
+ target,
107
+ version: /** @type {const} */ (1)
108
+ })
109
+ } catch (error) {
110
+ if (error instanceof SemantifoldDiagnostic && error.code == "INVALID_ARTIFACT_SET") throw error
111
+
112
+ return invalidArtifactSet("Artifact-set validation failed.", targetOf(candidate), error)
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Validates artifact-level text, byte, or synthetic provenance.
118
+ * @param {unknown} value - Provenance candidate.
119
+ * @param {unknown} contentKind - Validated artifact content kind.
120
+ * @param {string} artifactPath - Validated artifact path.
121
+ * @param {string | Uint8Array} content - Detached artifact content.
122
+ * @param {string} target - Artifact target.
123
+ * @returns {import("./semantic/types.js").ArtifactProvenance} Validated provenance.
124
+ */
125
+ function validateArtifactProvenance(value, contentKind, artifactPath, content, target) {
126
+ if (!isPlainObject(value)) invalidArtifactSet(`Artifact '${artifactPath}' requires provenance.`, target)
127
+
128
+ if (value.kind == "synthetic") {
129
+ if (typeof value.reason != "string" || value.reason.length == 0 || !isDenseArray(value.relatedOrigins)) {
130
+ invalidArtifactSet(`Synthetic artifact '${artifactPath}' requires an explicit reason and related origins.`, target)
131
+ }
132
+ if (contentKind == "binary") {
133
+ invalidArtifactSet(`Binary artifact '${artifactPath}' requires byte-range provenance.`, target)
134
+ }
135
+ /** @type {import("./semantic/types.js").RelatedOrigin[]} */
136
+ const relatedOrigins = []
137
+
138
+ for (let index = 0; index < value.relatedOrigins.length; index += 1) {
139
+ relatedOrigins.push(validateRelatedOrigin(value.relatedOrigins[index], artifactPath, index, target))
140
+ }
141
+
142
+ return Object.freeze({kind: /** @type {const} */ ("synthetic"), reason: value.reason, relatedOrigins: Object.freeze(relatedOrigins)})
143
+ }
144
+
145
+ if (value.kind == "text") {
146
+ const mappingCandidate = value.mapping
147
+
148
+ if (contentKind != "text" || !isPlainObject(mappingCandidate) || mappingCandidate.schema != "SemantifoldMapping" ||
149
+ !isPlainObject(mappingCandidate.generated) || mappingCandidate.generated.filename != artifactPath ||
150
+ mappingCandidate.generated.content != content || !isPlainObject(value.sourceMap)) {
151
+ invalidArtifactSet(`Text artifact '${artifactPath}' has mismatched rich or Source Map provenance.`, target)
152
+ }
153
+ if (value.sourceMapFilename !== undefined && !isValidFilenameMetadata(value.sourceMapFilename)) {
154
+ invalidArtifactSet(`Text artifact '${artifactPath}' has invalid Source Map filename metadata.`, target)
155
+ }
156
+ const mapping = finalizeMapping(mappingCandidate)
157
+ const sourceMap = validateSourceMap(value.sourceMap, artifactPath, target)
158
+ const projectedSourceMap = toSourceMapV3(mapping)
159
+
160
+ if (!sameJsonValue(sourceMap, projectedSourceMap)) {
161
+ invalidArtifactSet(`Text artifact '${artifactPath}' has Source Map provenance that contradicts its rich mapping.`, target)
162
+ }
163
+
164
+ return Object.freeze({
165
+ kind: /** @type {const} */ ("text"),
166
+ mapping,
167
+ sourceMap,
168
+ sourceMapFilename: value.sourceMapFilename
169
+ })
170
+ }
171
+
172
+ if (value.kind == "bytes") {
173
+ if (contentKind != "binary") invalidArtifactSet(`Byte provenance requires binary content for '${artifactPath}'.`, target)
174
+ const mapping = finalizeByteMapping(value.mapping)
175
+
176
+ if (mapping.generated.path != artifactPath || mapping.generated.byteLength != /** @type {Uint8Array} */ (content).byteLength) {
177
+ invalidArtifactSet(`Byte provenance does not match artifact '${artifactPath}'.`, target)
178
+ }
179
+
180
+ return Object.freeze({kind: /** @type {const} */ ("bytes"), mapping})
181
+ }
182
+
183
+ return invalidArtifactSet(`Artifact '${artifactPath}' has an unknown provenance kind.`, target)
184
+ }
185
+
186
+ /**
187
+ * Freezes one validated artifact record.
188
+ * @param {Record<string, unknown>} artifact - Validated fields.
189
+ * @returns {Readonly<Record<string, unknown>>} Frozen record.
190
+ */
191
+ function freezeArtifact(artifact) {
192
+ if (artifact.contentKind == "binary" && artifact.content instanceof Uint8Array) {
193
+ const content = artifact.content
194
+
195
+ Object.defineProperty(artifact, "content", {
196
+ configurable: false,
197
+ enumerable: true,
198
+ get() {
199
+ return new Uint8Array(content)
200
+ }
201
+ })
202
+ }
203
+
204
+ return Object.freeze(artifact)
205
+ }
206
+
207
+ /**
208
+ * Lists every directory a safe relative POSIX artifact path requires.
209
+ * @param {string} artifactPath - Validated artifact path.
210
+ * @returns {string[]} Ordered parent directory paths.
211
+ */
212
+ function artifactDirectoryPaths(artifactPath) {
213
+ const parts = artifactPath.split("/")
214
+ /** @type {string[]} */
215
+ const directories = []
216
+
217
+ for (let index = 1; index < parts.length; index += 1) directories.push(parts.slice(0, index).join("/"))
218
+
219
+ return directories
220
+ }
221
+
222
+ /**
223
+ * Throws a normalized artifact-set diagnostic.
224
+ * @param {string} message - Failure detail.
225
+ * @param {string} target - Artifact target.
226
+ * @param {unknown} [error] - Preserved validation cause.
227
+ * @returns {never} Always throws.
228
+ */
229
+ function invalidArtifactSet(message, target, error) {
230
+ throw new SemantifoldDiagnostic({
231
+ cause: error instanceof Error ? error : undefined,
232
+ code: "INVALID_ARTIFACT_SET",
233
+ language: target,
234
+ message
235
+ })
236
+ }
237
+
238
+ /**
239
+ * Derives a safe diagnostic identity from an unknown request.
240
+ * @param {unknown} value - Candidate set.
241
+ * @returns {string} Diagnostic identity.
242
+ */
243
+ function targetOf(value) {
244
+ return isPlainObject(value) && typeof value.target == "string" && value.target.length > 0 ? value.target : "artifact"
245
+ }
246
+
247
+ /**
248
+ * Checks for an ordinary string-keyed object.
249
+ * @param {unknown} value - Candidate value.
250
+ * @returns {value is Record<string, unknown>} Whether the value is a plain object.
251
+ */
252
+ function isPlainObject(value) {
253
+ return typeof value == "object" && value != null && !Array.isArray(value) && Object.getPrototypeOf(value) == Object.prototype
254
+ }
255
+
256
+ /**
257
+ * Validates an artifact-level related origin without interpreting source offsets as generated bytes.
258
+ * @param {unknown} value - Related-origin candidate.
259
+ * @param {string} artifactPath - Owning artifact path.
260
+ * @param {number} index - Origin index.
261
+ * @param {string} target - Artifact target.
262
+ * @returns {import("./semantic/types.js").RelatedOrigin} Detached related origin.
263
+ */
264
+ function validateRelatedOrigin(value, artifactPath, index, target) {
265
+ if (!isPlainObject(value) || typeof value.sourceId != "string" || value.sourceId.length == 0 ||
266
+ !isPlainObject(value.location) || typeof value.location.filename != "string" || value.location.filename.length == 0 ||
267
+ !validSourcePoint(value.location.start) || !validSourcePoint(value.location.end) ||
268
+ value.location.end.offset < value.location.start.offset || !isSourceRangeOrdered(value.location.start, value.location.end)) {
269
+ invalidArtifactSet(`Artifact '${artifactPath}' has malformed related origin ${index}.`, target)
270
+ }
271
+
272
+ return deepFreeze({
273
+ location: {
274
+ end: {...value.location.end},
275
+ filename: value.location.filename,
276
+ start: {...value.location.start}
277
+ },
278
+ sourceId: value.sourceId,
279
+ ...(optionalString(value.nodeId, "nodeId", artifactPath, index, target)),
280
+ ...(optionalString(value.symbolId, "symbolId", artifactPath, index, target)),
281
+ ...(optionalString(value.role, "role", artifactPath, index, target))
282
+ })
283
+ }
284
+
285
+ /**
286
+ * Validates one optional related-origin string field.
287
+ * @param {unknown} value - Candidate value.
288
+ * @param {string} property - Property name.
289
+ * @param {string} artifactPath - Owning artifact path.
290
+ * @param {number} index - Origin index.
291
+ * @param {string} target - Artifact target.
292
+ * @returns {Record<string, string>} Present validated property or an empty record.
293
+ */
294
+ function optionalString(value, property, artifactPath, index, target) {
295
+ if (value === undefined) return {}
296
+ if (typeof value != "string" || value.length == 0) {
297
+ invalidArtifactSet(`Artifact '${artifactPath}' has invalid ${property} in related origin ${index}.`, target)
298
+ }
299
+
300
+ return {[property]: value}
301
+ }
302
+
303
+ /**
304
+ * Checks one UTF-16 source point.
305
+ * @param {unknown} value - Candidate point.
306
+ * @returns {value is import("./semantic/types.js").SourcePoint} Whether the point is structurally valid.
307
+ */
308
+ function validSourcePoint(value) {
309
+ return isPlainObject(value) && typeof value.line == "number" && Number.isSafeInteger(value.line) && value.line > 0 &&
310
+ typeof value.column == "number" && Number.isSafeInteger(value.column) && value.column > 0 &&
311
+ typeof value.offset == "number" && Number.isSafeInteger(value.offset) && value.offset >= 0
312
+ }
313
+
314
+ /**
315
+ * Validates and detaches the Source Map v3 projection carried by a text artifact.
316
+ * @param {unknown} value - Source Map candidate.
317
+ * @param {string} artifactPath - Owning artifact path.
318
+ * @param {string} target - Artifact target.
319
+ * @returns {import("@jridgewell/gen-mapping").EncodedSourceMap} Detached Source Map.
320
+ */
321
+ function validateSourceMap(value, artifactPath, target) {
322
+ if (!isPlainObject(value) || value.version != 3 || value.file != artifactPath || typeof value.mappings != "string" ||
323
+ !isDenseArray(value.names) || !value.names.every((name) => typeof name == "string") ||
324
+ !isDenseArray(value.sources) || !value.sources.every((source) => typeof source == "string") ||
325
+ (value.sourcesContent !== undefined && (!isDenseArray(value.sourcesContent) ||
326
+ !value.sourcesContent.every((source) => source == null || typeof source == "string")))) {
327
+ invalidArtifactSet(`Text artifact '${artifactPath}' has malformed Source Map v3 provenance.`, target)
328
+ }
329
+ validateEncodedSourceMapMappings(
330
+ /** @type {import("@jridgewell/trace-mapping").EncodedSourceMap} */ (/** @type {unknown} */ (value)),
331
+ artifactPath,
332
+ target
333
+ )
334
+
335
+ const detached = /** @type {unknown} */ (structuredClone(value))
336
+
337
+ return deepFreeze(/** @type {import("@jridgewell/gen-mapping").EncodedSourceMap} */ (detached))
338
+ }
339
+
340
+ /**
341
+ * Decodes and validates the Source Map payload with the installed mapping implementation.
342
+ * @param {import("@jridgewell/trace-mapping").EncodedSourceMap} sourceMap - Structurally validated map.
343
+ * @param {string} artifactPath - Owning artifact path.
344
+ * @param {string} target - Artifact target.
345
+ * @returns {void}
346
+ */
347
+ function validateEncodedSourceMapMappings(sourceMap, artifactPath, target) {
348
+ /** @type {Readonly<import("@jridgewell/trace-mapping").SourceMapSegment[][]>} */
349
+ let decoded
350
+ let canonicalMappings
351
+
352
+ try {
353
+ decoded = decodedMappings(new TraceMap(sourceMap))
354
+ const decodedMap = /** @type {import("@jridgewell/trace-mapping").DecodedSourceMap} */ ({
355
+ ...sourceMap,
356
+ mappings: structuredClone(decoded)
357
+ })
358
+
359
+ canonicalMappings = encodedMappings(presortedDecodedMap(decodedMap))
360
+ } catch (error) {
361
+ invalidArtifactSet(`Text artifact '${artifactPath}' has invalid encoded Source Map mappings.`, target, error)
362
+ }
363
+
364
+ if (canonicalMappings != sourceMap.mappings ||
365
+ !validDecodedSourceMapMappings(decoded, sourceMap.sources.length, sourceMap.names.length)) {
366
+ invalidArtifactSet(`Text artifact '${artifactPath}' has invalid encoded Source Map mappings.`, target)
367
+ }
368
+ }
369
+
370
+ /**
371
+ * Validates decoded Source Map segment shape and referenced indices.
372
+ * @param {Readonly<import("@jridgewell/trace-mapping").SourceMapSegment[][]>} lines - Decoded mapping lines.
373
+ * @param {number} sourceCount - Declared source count.
374
+ * @param {number} nameCount - Declared name count.
375
+ * @returns {boolean} Whether every segment is semantically valid.
376
+ */
377
+ function validDecodedSourceMapMappings(lines, sourceCount, nameCount) {
378
+ return lines.every((line) => line.every((segment) => {
379
+ if (segment.length != 1 && segment.length != 4 && segment.length != 5) return false
380
+ if (!segment.every((field) => Number.isSafeInteger(field) && field >= 0)) return false
381
+ if (segment.length == 1) return true
382
+ if (segment[1] >= sourceCount) return false
383
+
384
+ return segment.length == 4 || segment[4] < nameCount
385
+ }))
386
+ }
387
+
388
+ /**
389
+ * Compares detached JSON values without depending on caller object-key order.
390
+ * @param {unknown} left - First JSON value.
391
+ * @param {unknown} right - Second JSON value.
392
+ * @returns {boolean} Whether both values are structurally identical.
393
+ */
394
+ function sameJsonValue(left, right) {
395
+ if (left === right) return true
396
+ if (Array.isArray(left) || Array.isArray(right)) {
397
+ return Array.isArray(left) && Array.isArray(right) && left.length == right.length &&
398
+ left.every((entry, index) => sameJsonValue(entry, right[index]))
399
+ }
400
+ if (!isPlainObject(left) || !isPlainObject(right)) return false
401
+ const leftKeys = Object.keys(left)
402
+ const rightKeys = Object.keys(right)
403
+
404
+ return leftKeys.length == rightKeys.length &&
405
+ leftKeys.every((key) => Object.hasOwn(right, key) && sameJsonValue(left[key], right[key]))
406
+ }
407
+
408
+ /**
409
+ * Deeply freezes a detached JSON value.
410
+ * @template T
411
+ * @param {T} value - Detached value.
412
+ * @returns {T} Frozen value.
413
+ */
414
+ function deepFreeze(value) {
415
+ if (!value || typeof value != "object" || Object.isFrozen(value)) return value
416
+
417
+ for (const nested of Object.values(value)) deepFreeze(nested)
418
+
419
+ return Object.freeze(value)
420
+ }
@@ -7,6 +7,7 @@ const identifierPatterns = {
7
7
  java: /^[A-Za-z_$][A-Za-z0-9_$]*$/u,
8
8
  javascript: /^[A-Za-z_$][A-Za-z0-9_$]*$/u,
9
9
  php: /^[A-Za-z_][A-Za-z0-9_]*$/u,
10
+ python: /^(?:_|\p{XID_Start})(?:_|\p{XID_Continue})*$/u,
10
11
  ruby: /^[a-z_][A-Za-z0-9_]*$/u,
11
12
  typescript: /^[A-Za-z_$][A-Za-z0-9_$]*$/u
12
13
  }
@@ -38,6 +39,12 @@ const reservedWords = {
38
39
  "require_once", "return", "self", "static", "switch", "throw", "trait", "true", "try", "unset", "use", "var",
39
40
  "void", "while", "xor", "yield"
40
41
  ]),
42
+ python: new Set([
43
+ "False", "None", "True", "and", "as", "assert", "async", "await", "bool", "break", "case", "class",
44
+ "continue", "def", "del", "elif", "else", "except", "exec", "finally", "for", "from", "global", "if",
45
+ "import", "in", "int", "is", "lambda", "match", "nonlocal", "not", "or", "pass", "print",
46
+ "raise", "return", "str", "try", "type", "while", "with", "yield", "_", "__debug__"
47
+ ]),
41
48
  ruby: new Set([
42
49
  "BEGIN", "END", "__ENCODING__", "__END__", "__FILE__", "__LINE__", "alias", "and", "begin", "break", "case",
43
50
  "class", "def", "defined?", "do", "else", "elsif", "end", "ensure", "false", "for", "if", "in", "module",
@@ -73,7 +80,8 @@ export function validateTargetIdentifier(language, name, role, location) {
73
80
 
74
81
  const reservedName = language == "php" ? name.toLowerCase() : name
75
82
 
76
- if (!identifierPatterns[language].test(name) || reservedWords[language].has(reservedName)) {
83
+ if (!identifierPatterns[language].test(name) || reservedWords[language].has(reservedName) ||
84
+ language == "python" && name.normalize("NFKC") != name) {
77
85
  unsupportedCapability(language, `${role} identifier '${name}'`, location)
78
86
  }
79
87
  }
@@ -1,24 +1,11 @@
1
1
  // @ts-check
2
2
 
3
- import {SemantifoldDiagnostic, unsupportedCapability} from "../diagnostic.js"
4
- import {generateJava} from "./java.js"
5
- import {generateJavaScript} from "./javascript.js"
6
- import {generatePhp} from "./php.js"
7
- import {generateRuby} from "./ruby.js"
3
+ import {unsupportedCapability, unsupportedRole} from "../diagnostic.js"
4
+ import {isValidFilenameMetadata} from "../artifact-path.js"
5
+ import {languageRegistry} from "../language-registry.js"
8
6
  import {validateBackendModule} from "./shared.js"
9
- import {generateTypeScript} from "./typescript.js"
10
7
  import {SourceWriter} from "./writer.js"
11
8
  import {finalizeMapping, toSourceMapV3} from "../mapping.js"
12
-
13
- /** @type {Readonly<Record<import("../semantic/types.js").SemanticLanguage, string>>} */
14
- const defaultFilenames = Object.freeze({
15
- java: "Main.java",
16
- javascript: "program.js",
17
- php: "program.php",
18
- ruby: "program.rb",
19
- typescript: "program.ts"
20
- })
21
- const lineTerminatorPattern = /[\n\r\u2028\u2029]/u
22
9
  const syntheticArtifactBase = Object.freeze(["__semantifold_artifacts__"])
23
10
 
24
11
  /**
@@ -43,18 +30,27 @@ export function generateSource({language, module}) {
43
30
  * @param {{filename: string, content: string, language?: import("../semantic/types.js").SemanticLanguage}[]} [input.sources] - Source content for legacy or assembled modules.
44
31
  * @returns {import("../semantic/types.js").GeneratedArtifact} Generated artifact.
45
32
  */
46
- export function generateArtifactSource({language, filename = defaultFilenames[language], mapDirective = "none", module, sourceMapFilename = `${filename}.map`, sources}) {
33
+ export function generateArtifactSource({language, filename, mapDirective = "none", module, sourceMapFilename, sources}) {
34
+ const backend = languageRegistry.resolve(language, "textBackend", module?.location)
35
+ const record = languageRegistry.record(language)
36
+
37
+ if (record.artifactMultiplicity != "single") unsupportedRole(language, "single-text backend", module?.location)
38
+ if (filename === undefined) filename = record.defaultFilename
39
+ if (sourceMapFilename === undefined) sourceMapFilename = `${filename}.map`
47
40
  validateBackendModule(module, language)
48
41
 
49
- if (typeof filename != "string" || filename.length == 0 || lineTerminatorPattern.test(filename)) {
42
+ if (!isValidFilenameMetadata(filename)) {
50
43
  throw new TypeError("Generated filename must be a non-empty single-line string.")
51
44
  }
52
- if (typeof sourceMapFilename != "string" || sourceMapFilename.length == 0 || lineTerminatorPattern.test(sourceMapFilename)) {
45
+ if (!isValidFilenameMetadata(sourceMapFilename)) {
53
46
  throw new TypeError("Source map filename must be a non-empty single-line string.")
54
47
  }
55
48
  if (language == "java" && filename.split(/[\\/]/u).at(-1) != "Main.java") {
56
49
  unsupportedCapability(language, "artifact filename basename other than Main.java", module.location)
57
50
  }
51
+ if (language == "python" && filename.split(/[\\/]/u).at(-1) != "program.py") {
52
+ unsupportedCapability(language, "artifact filename basename other than program.py", module.location)
53
+ }
58
54
  if (!["none", "external", "inline"].includes(mapDirective)) throw new TypeError(`Unsupported map directive: ${mapDirective}`)
59
55
  if (mapDirective != "none" && language != "javascript" && language != "typescript") {
60
56
  throw new TypeError("Source map directives are supported only for JavaScript and TypeScript outputs.")
@@ -62,19 +58,7 @@ export function generateArtifactSource({language, filename = defaultFilenames[la
62
58
 
63
59
  const writer = new SourceWriter({filename, language, module, sources})
64
60
 
65
- if (language == "php") generatePhp(module, writer)
66
- else if (language == "ruby") generateRuby(module, writer)
67
- else if (language == "javascript") generateJavaScript(module, writer)
68
- else if (language == "typescript") generateTypeScript(module, writer)
69
- else if (language == "java") generateJava(module, writer)
70
- else {
71
- throw new SemantifoldDiagnostic({
72
- code: "UNSUPPORTED_LANGUAGE",
73
- language,
74
- location: module.location,
75
- message: "No source backend is registered for this language."
76
- })
77
- }
61
+ backend(module, writer)
78
62
 
79
63
  const sourceMap = toSourceMapV3(writer.finish())
80
64