semantifold 0.1.1 → 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,175 @@
1
+ // @ts-check
2
+
3
+ import {emitExpression} from "./shared.js"
4
+ import {emitScalarType} from "./scalars.js"
5
+
6
+ /**
7
+ * Emits one deterministic standalone Python program.
8
+ * @param {import("../semantic/types.js").SemanticModule} module - Semantic module.
9
+ * @param {import("./writer.js").SourceWriter} writer - Source-aware writer.
10
+ * @returns {void}
11
+ */
12
+ export function generatePython(module, writer) {
13
+ module.functions.forEach((declaration, functionIndex) => {
14
+ if (functionIndex > 0) writer.synthetic("\n\n", "declaration separator", [declaration])
15
+
16
+ writer.mapped("def", {mappingKind: "anchor", node: declaration})
17
+ writer.synthetic(" ", "function spacing", [declaration])
18
+ writer.mapped(declaration.name, {mappingKind: "exact", node: declaration, role: "name"})
19
+ writer.mapped("(", {mappingKind: "anchor", node: declaration})
20
+ declaration.parameters.forEach((parameter, parameterIndex) => {
21
+ const parameterPath = `/functions/${functionIndex}/parameters/${parameterIndex}`
22
+
23
+ if (parameterIndex > 0) writer.synthetic(", ", "parameter separator", [declaration])
24
+ writer.mapped(parameter.name, {mappingKind: "exact", node: parameter, path: parameterPath, role: "name"})
25
+ writer.synthetic(": ", "Python annotation separator", [parameter], [parameterPath])
26
+ writer.mapped(emitScalarType("python", parameter.type), {
27
+ mappingKind: "exact",
28
+ node: parameter.type,
29
+ path: `${parameterPath}/type`,
30
+ role: "type"
31
+ })
32
+ })
33
+ writer.mapped(")", {mappingKind: "anchor", node: declaration})
34
+ writer.synthetic(" -> ", "Python return annotation separator", [declaration])
35
+ writer.mapped(emitScalarType("python", declaration.returnType), {
36
+ mappingKind: "exact",
37
+ node: declaration.returnType,
38
+ path: `/functions/${functionIndex}/returnType`,
39
+ role: "type"
40
+ })
41
+ writer.mapped(":", {mappingKind: "anchor", node: declaration})
42
+ writer.synthetic("\n", "line break", [declaration])
43
+ emitBlock(writer, declaration.body, " ", `/functions/${functionIndex}/body`)
44
+ })
45
+
46
+ writer.synthetic("\n\n", "entry-point separator", [module.entryPoint])
47
+ emitBlock(writer, module.entryPoint.body, "", "/entryPoint/body")
48
+ }
49
+
50
+ /**
51
+ * Emits one Python indentation block, using pass only for an empty semantic block.
52
+ * @param {import("./writer.js").SourceWriter} writer - Source-aware writer.
53
+ * @param {import("../semantic/types.js").Block} block - Semantic block.
54
+ * @param {string} indent - Leading indentation.
55
+ * @param {string} path - Exact block path.
56
+ * @returns {void}
57
+ */
58
+ function emitBlock(writer, block, indent, path) {
59
+ if (block.statements.length == 0) {
60
+ writer.synthetic(`${indent}pass\n`, "Python empty block", [block], [path])
61
+ return
62
+ }
63
+
64
+ block.statements.forEach((statement, index) => emitStatement(writer, statement, indent, `${path}/statements/${index}`))
65
+ }
66
+
67
+ /**
68
+ * Emits one supported Python statement.
69
+ * @param {import("./writer.js").SourceWriter} writer - Source-aware writer.
70
+ * @param {import("../semantic/types.js").Statement} statement - Semantic statement.
71
+ * @param {string} indent - Leading indentation.
72
+ * @param {string} path - Exact statement path.
73
+ * @returns {void}
74
+ */
75
+ function emitStatement(writer, statement, indent, path) {
76
+ if (statement.kind == "LocalDeclaration" || statement.kind == "AssignmentStatement") {
77
+ emitLocal(writer, statement, indent, path)
78
+ return
79
+ }
80
+ if (statement.kind == "IfStatement") {
81
+ emitIf(writer, statement, indent, path, "if")
82
+ return
83
+ }
84
+
85
+ writer.synthetic(indent, "indentation", [statement], [path])
86
+ if (statement.kind == "ReturnStatement") {
87
+ writer.mapped("return", {mappingKind: "anchor", node: statement, path})
88
+ writer.synthetic(" ", "return spacing", [statement], [path])
89
+ emitExpression(writer, statement.expression, `${path}/expression`, "python", identity)
90
+ writer.synthetic("\n", "line break", [statement], [path])
91
+ return
92
+ }
93
+
94
+ writer.mapped("print", {mappingKind: "anchor", node: statement, path})
95
+ writer.mapped("(", {mappingKind: "anchor", node: statement, path})
96
+ emitExpression(writer, statement.expression, `${path}/expression`, "python", identity)
97
+ writer.mapped(")", {mappingKind: "anchor", node: statement, path})
98
+ writer.synthetic("\n", "line break", [statement], [path])
99
+ }
100
+
101
+ /**
102
+ * Emits a Python if or elif and its optional alternative.
103
+ * @param {import("./writer.js").SourceWriter} writer - Source-aware writer.
104
+ * @param {import("../semantic/types.js").IfStatement} statement - Branch.
105
+ * @param {string} indent - Leading indentation.
106
+ * @param {string} path - Exact branch path.
107
+ * @param {"if" | "elif"} keyword - Branch keyword.
108
+ * @returns {void}
109
+ */
110
+ function emitIf(writer, statement, indent, path, keyword) {
111
+ writer.synthetic(indent, "indentation", [statement], [path])
112
+ writer.mapped(keyword, {mappingKind: "anchor", node: statement, path})
113
+ writer.synthetic(" ", "conditional spacing", [statement], [path])
114
+ emitExpression(writer, statement.condition, `${path}/condition`, "python", identity)
115
+ writer.mapped(":", {mappingKind: "anchor", node: statement.consequent, path: `${path}/consequent`})
116
+ writer.synthetic("\n", "line break", [statement], [path])
117
+ emitBlock(writer, statement.consequent, `${indent} `, `${path}/consequent`)
118
+
119
+ if (!statement.alternate) return
120
+ if (statement.alternate.statements.length == 1 && statement.alternate.statements[0].kind == "IfStatement") {
121
+ emitIf(writer, statement.alternate.statements[0], indent, `${path}/alternate/statements/0`, "elif")
122
+ return
123
+ }
124
+
125
+ writer.synthetic(indent, "indentation", [statement], [path])
126
+ writer.mapped("else", {mappingKind: "anchor", node: statement, path})
127
+ writer.mapped(":", {mappingKind: "anchor", node: statement.alternate, path: `${path}/alternate`})
128
+ writer.synthetic("\n", "line break", [statement], [path])
129
+ emitBlock(writer, statement.alternate, `${indent} `, `${path}/alternate`)
130
+ }
131
+
132
+ /**
133
+ * Emits one annotated local declaration or plain assignment.
134
+ * @param {import("./writer.js").SourceWriter} writer - Source-aware writer.
135
+ * @param {import("../semantic/types.js").LocalStatement} statement - Local statement.
136
+ * @param {string} indent - Leading indentation.
137
+ * @param {string} path - Exact statement path.
138
+ * @returns {void}
139
+ */
140
+ function emitLocal(writer, statement, indent, path) {
141
+ if (statement.kind == "LocalDeclaration" && !statement.mutable) {
142
+ writer.synthetic(`${indent}# @semantifold-immutable\n`, "Python immutable local carrier", [statement], [path])
143
+ }
144
+ writer.synthetic(indent, "indentation", [statement], [path])
145
+
146
+ if (statement.kind == "AssignmentStatement") {
147
+ writer.mapped(statement.target.name, {mappingKind: "exact", node: statement.target, path: `${path}/target`, role: "name"})
148
+ writer.synthetic(" ", "assignment spacing", [statement], [path])
149
+ writer.mapped("=", {mappingKind: "exact", node: statement, path, role: "operator"})
150
+ writer.synthetic(" ", "assignment spacing", [statement], [path])
151
+ emitExpression(writer, statement.expression, `${path}/expression`, "python", identity)
152
+ writer.synthetic("\n", "line break", [statement], [path])
153
+ return
154
+ }
155
+
156
+ writer.mapped(statement.name, {mappingKind: "exact", node: statement, path, role: "name"})
157
+ writer.synthetic(": ", "Python annotation separator", [statement], [path])
158
+ writer.mapped(emitScalarType("python", statement.type), {
159
+ mappingKind: "exact", node: statement.type, path: `${path}/type`, role: "type"
160
+ })
161
+ writer.synthetic(" ", "assignment spacing", [statement], [path])
162
+ writer.mapped("=", {mappingKind: "exact", node: statement, path, role: "operator"})
163
+ writer.synthetic(" ", "assignment spacing", [statement], [path])
164
+ emitExpression(writer, statement.initializer, `${path}/initializer`, "python", identity)
165
+ writer.synthetic("\n", "line break", [statement], [path])
166
+ }
167
+
168
+ /**
169
+ * Returns one unchanged Python identifier.
170
+ * @param {string} name - Semantic identifier.
171
+ * @returns {string} Unchanged Python identifier.
172
+ */
173
+ function identity(name) {
174
+ return name
175
+ }
@@ -5,6 +5,7 @@ const targetScalarTypes = Object.freeze({
5
5
  java: Object.freeze({boolean: "boolean", integer: "int", string: "String"}),
6
6
  javascript: Object.freeze({boolean: "boolean", integer: "number", string: "string"}),
7
7
  php: Object.freeze({boolean: "bool", integer: "int", string: "string"}),
8
+ python: Object.freeze({boolean: "bool", integer: "int", string: "str"}),
8
9
  ruby: Object.freeze({boolean: "bool", integer: "Integer", string: "String"}),
9
10
  typescript: Object.freeze({boolean: "boolean", integer: "number", string: "string"})
10
11
  })
@@ -7,12 +7,12 @@ import {emitStringLiteral} from "./scalars.js"
7
7
 
8
8
  /** @type {Readonly<Record<import("../semantic/types.js").SemanticUnaryOperation, string>>} */
9
9
  const unaryOperationSyntax = Object.freeze({BooleanNot: "!", IntegerNegate: "-"})
10
- /** @type {Readonly<Record<import("../semantic/types.js").SemanticBinaryOperation, Readonly<{default: string, php?: string, strict?: string}>>>} */
10
+ /** @type {Readonly<Record<import("../semantic/types.js").SemanticBinaryOperation, Readonly<{default: string, php?: string, python?: string, strict?: string}>>>} */
11
11
  const binaryOperationSyntax = Object.freeze({
12
- BooleanAnd: Object.freeze({default: "&&"}),
12
+ BooleanAnd: Object.freeze({default: "&&", python: "and"}),
13
13
  BooleanEqual: Object.freeze({default: "==", strict: "==="}),
14
14
  BooleanNotEqual: Object.freeze({default: "!=", strict: "!=="}),
15
- BooleanOr: Object.freeze({default: "||"}),
15
+ BooleanOr: Object.freeze({default: "||", python: "or"}),
16
16
  IntegerAdd: Object.freeze({default: "+"}),
17
17
  IntegerEqual: Object.freeze({default: "==", strict: "==="}),
18
18
  IntegerGreaterThan: Object.freeze({default: ">"}),
@@ -325,7 +325,9 @@ export function emitExpression(writer, expression, path, language, emitIdentifie
325
325
  return
326
326
  }
327
327
  if (expression.kind == "BooleanLiteral") {
328
- writer.mapped(expression.value ? "true" : "false", {mappingKind: "exact", node: expression, path, role: "literal"})
328
+ const spelling = language == "python" ? expression.value ? "True" : "False" : expression.value ? "true" : "false"
329
+
330
+ writer.mapped(spelling, {mappingKind: "exact", node: expression, path, role: "literal"})
329
331
  return
330
332
  }
331
333
  if (expression.kind == "StringLiteral") {
@@ -345,7 +347,7 @@ export function emitExpression(writer, expression, path, language, emitIdentifie
345
347
 
346
348
  if (expression.kind == "UnaryExpression") {
347
349
  writer.mapped("(", {mappingKind: "anchor", node: expression, path})
348
- writer.mapped(unaryOperationSyntax[expression.operation], {
350
+ writer.mapped(language == "python" && expression.operation == "BooleanNot" ? "not " : unaryOperationSyntax[expression.operation], {
349
351
  mappingKind: "exact", node: expression, path, role: "operator"
350
352
  })
351
353
  emitExpression(writer, expression.operand, `${path}/operand`, language, emitIdentifier)
@@ -450,6 +452,7 @@ function binaryOperationSpelling(operation, language) {
450
452
  const syntax = binaryOperationSyntax[operation]
451
453
 
452
454
  if (language == "php" && syntax.php) return syntax.php
455
+ if (language == "python" && syntax.python) return syntax.python
453
456
  if ((language == "php" || language == "javascript" || language == "typescript") && syntax.strict) return syntax.strict
454
457
 
455
458
  return syntax.default
@@ -0,0 +1,305 @@
1
+ // @ts-check
2
+
3
+ import {isDenseArray} from "./array.js"
4
+ import {isSafeArtifactPath} from "./artifact-path.js"
5
+ import {SemantifoldDiagnostic} from "./diagnostic.js"
6
+ import {isSourceRangeOrdered} from "./semantic/location.js"
7
+
8
+ /**
9
+ * Creates and validates an immutable byte-coordinate provenance map.
10
+ * @param {object} input - Mapping fields.
11
+ * @param {number} input.byteLength - Generated artifact byte length.
12
+ * @param {string} input.path - Generated artifact path.
13
+ * @param {import("./semantic/types.js").GeneratedByteRange[]} input.ranges - Ordered byte ranges.
14
+ * @returns {import("./semantic/types.js").SemantifoldByteMapping} Validated mapping.
15
+ */
16
+ export function createByteMapping(input) {
17
+ if (!isPlainObject(input)) invalidByteMapping("Byte mapping construction requires an object.")
18
+ const {byteLength, path, ranges} = input
19
+
20
+ return finalizeByteMapping({
21
+ coordinateSystem: "bytes",
22
+ generated: {byteLength, path},
23
+ ranges,
24
+ schema: "SemantifoldByteMapping",
25
+ version: 1
26
+ })
27
+ }
28
+
29
+ /**
30
+ * Parses serialized byte-coordinate provenance.
31
+ * @param {string} serialized - JSON mapping.
32
+ * @returns {import("./semantic/types.js").SemantifoldByteMapping} Validated mapping.
33
+ */
34
+ export function parseByteMapping(serialized) {
35
+ try {
36
+ return finalizeByteMapping(JSON.parse(serialized))
37
+ } catch (error) {
38
+ if (error instanceof SemantifoldDiagnostic && error.code == "INVALID_BYTE_MAPPING") throw error
39
+
40
+ return invalidByteMapping("Byte mapping must be valid JSON.", error)
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Serializes a validated mapping with stable object-key ordering.
46
+ * @param {import("./semantic/types.js").SemantifoldByteMapping} mapping - Byte mapping.
47
+ * @returns {string} Canonical JSON with trailing LF.
48
+ */
49
+ export function stringifyByteMapping(mapping) {
50
+ const validated = finalizeByteMapping(mapping)
51
+
52
+ return `${JSON.stringify(sortJson(validated), null, 2)}\n`
53
+ }
54
+
55
+ /**
56
+ * Validates an existing mapping and returns a detached immutable value.
57
+ * @param {unknown} candidate - Mapping candidate.
58
+ * @returns {import("./semantic/types.js").SemantifoldByteMapping} Validated mapping.
59
+ */
60
+ export function finalizeByteMapping(candidate) {
61
+ try {
62
+ if (!isPlainObject(candidate)) invalidByteMapping("Byte mapping must be a plain object.")
63
+ if (candidate.schema != "SemantifoldByteMapping" || candidate.version !== 1 || candidate.coordinateSystem != "bytes") {
64
+ invalidByteMapping("Byte mapping requires SemantifoldByteMapping version 1 with byte coordinates.")
65
+ }
66
+ if (!isPlainObject(candidate.generated)) {
67
+ invalidByteMapping("Byte mapping requires a safe generated artifact path.")
68
+ }
69
+ const generatedPath = candidate.generated.path
70
+
71
+ if (!isSafeArtifactPath(generatedPath)) {
72
+ invalidByteMapping("Byte mapping requires a safe generated artifact path.")
73
+ }
74
+ const byteLength = candidate.generated.byteLength
75
+
76
+ if (typeof byteLength != "number" || !Number.isSafeInteger(byteLength) || byteLength < 0) {
77
+ invalidByteMapping("Byte mapping requires a non-negative safe byte length.")
78
+ }
79
+ if (!isDenseArray(candidate.ranges)) invalidByteMapping("Byte mapping ranges must be an ordered dense array.")
80
+ let previousEnd = 0
81
+ /** @type {import("./semantic/types.js").GeneratedByteRange[]} */
82
+ const ranges = []
83
+
84
+ for (let index = 0; index < candidate.ranges.length; index += 1) {
85
+ const range = candidate.ranges[index]
86
+
87
+ if (!isPlainObject(range) || !isPlainObject(range.generated)) invalidByteMapping(`Byte range ${index} is malformed.`)
88
+ const end = range.generated.end
89
+ const start = range.generated.start
90
+
91
+ if (typeof start != "number" || typeof end != "number" || !Number.isSafeInteger(start) || !Number.isSafeInteger(end) ||
92
+ start < 0 || end <= start || end > byteLength) {
93
+ invalidByteMapping(`Byte range ${index} must be a non-empty in-bounds half-open integer range.`)
94
+ }
95
+ if (start < previousEnd) invalidByteMapping(`Byte range ${index} is out of order or overlaps its predecessor.`)
96
+ previousEnd = end
97
+ const origin = validateOrigin(range.origin, `Byte range ${index}`)
98
+ const result = {
99
+ generated: {end, start},
100
+ origin,
101
+ ...(optionalIdentity(range.nodeId, "nodeId", index)),
102
+ ...(optionalIdentity(range.symbolId, "symbolId", index)),
103
+ ...(optionalIdentity(range.role, "role", index))
104
+ }
105
+
106
+ ranges.push(result)
107
+ }
108
+ const mapping = {
109
+ coordinateSystem: /** @type {const} */ ("bytes"),
110
+ generated: {byteLength, path: generatedPath},
111
+ ranges,
112
+ schema: /** @type {const} */ ("SemantifoldByteMapping"),
113
+ version: /** @type {const} */ (1)
114
+ }
115
+
116
+ return deepFreeze(mapping)
117
+ } catch (error) {
118
+ if (error instanceof SemantifoldDiagnostic && error.code == "INVALID_BYTE_MAPPING") throw error
119
+
120
+ return invalidByteMapping("Byte mapping validation failed.", error)
121
+ }
122
+ }
123
+
124
+ /**
125
+ * Validates one closed provenance origin.
126
+ * @param {unknown} value - Origin candidate.
127
+ * @param {string} label - Diagnostic range label.
128
+ * @returns {import("./semantic/types.js").SemanticOrigin} Detached origin.
129
+ */
130
+ function validateOrigin(value, label) {
131
+ if (!isPlainObject(value)) invalidByteMapping(`${label} requires provenance.`)
132
+
133
+ if (value.kind == "synthetic") {
134
+ if (typeof value.reason != "string" || value.reason.length == 0 || !isDenseArray(value.relatedOrigins)) {
135
+ invalidByteMapping(`${label} synthetic provenance requires an explicit reason and related-origin array.`)
136
+ }
137
+ /** @type {import("./semantic/types.js").RelatedOrigin[]} */
138
+ const relatedOrigins = []
139
+
140
+ for (let index = 0; index < value.relatedOrigins.length; index += 1) {
141
+ relatedOrigins.push(validateRelatedOrigin(value.relatedOrigins[index], `${label} related origin ${index}`))
142
+ }
143
+
144
+ return {kind: "synthetic", reason: value.reason, relatedOrigins}
145
+ }
146
+ if (value.kind == "source") {
147
+ if (typeof value.sourceId != "string" || value.sourceId.length == 0) invalidByteMapping(`${label} source provenance requires a source ID.`)
148
+
149
+ return {kind: "source", location: validateLocation(value.location, label), sourceId: value.sourceId}
150
+ }
151
+ if (value.kind == "derived") {
152
+ if (!isDenseArray(value.origins) || value.origins.length == 0) invalidByteMapping(`${label} derived provenance requires origins.`)
153
+ /** @type {import("./semantic/types.js").RelatedOrigin[]} */
154
+ const origins = []
155
+
156
+ for (let index = 0; index < value.origins.length; index += 1) {
157
+ origins.push(validateRelatedOrigin(value.origins[index], `${label} origin ${index}`))
158
+ }
159
+
160
+ return {kind: "derived", origins}
161
+ }
162
+
163
+ return invalidByteMapping(`${label} has an unknown provenance kind.`)
164
+ }
165
+
166
+ /**
167
+ * Validates one related source origin.
168
+ * @param {unknown} value - Related-origin candidate.
169
+ * @param {string} label - Diagnostic range label.
170
+ * @returns {import("./semantic/types.js").RelatedOrigin} Detached related origin.
171
+ */
172
+ function validateRelatedOrigin(value, label) {
173
+ if (!isPlainObject(value) || typeof value.sourceId != "string" || value.sourceId.length == 0) {
174
+ invalidByteMapping(`${label} requires a source ID and location.`)
175
+ }
176
+ const result = {
177
+ location: validateLocation(value.location, label),
178
+ sourceId: value.sourceId,
179
+ ...optionalRelated(value.nodeId, "nodeId", label),
180
+ ...optionalRelated(value.symbolId, "symbolId", label),
181
+ ...optionalRelated(value.role, "role", label)
182
+ }
183
+
184
+ return result
185
+ }
186
+
187
+ /**
188
+ * Validates a UTF-16 original source location carried by byte provenance.
189
+ * @param {unknown} value - Location candidate.
190
+ * @param {string} label - Diagnostic range label.
191
+ * @returns {import("./semantic/types.js").SourceLocation} Detached source location.
192
+ */
193
+ function validateLocation(value, label) {
194
+ if (!isPlainObject(value) || typeof value.filename != "string" || value.filename.length == 0) {
195
+ invalidByteMapping(`${label} contains an invalid source location.`)
196
+ }
197
+ const start = snapshotPoint(value.start)
198
+ const end = snapshotPoint(value.end)
199
+
200
+ if (start == undefined || end == undefined || end.offset < start.offset || !isSourceRangeOrdered(start, end)) {
201
+ invalidByteMapping(`${label} contains an invalid source location.`)
202
+ }
203
+
204
+ return {
205
+ end,
206
+ filename: value.filename,
207
+ start
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Snapshots one valid UTF-16 source point without retaining undeclared fields.
213
+ * @param {unknown} value - Candidate point.
214
+ * @returns {import("./semantic/types.js").SourcePoint | undefined} Canonical point when valid.
215
+ */
216
+ function snapshotPoint(value) {
217
+ if (!isPlainObject(value)) return undefined
218
+ const line = value.line
219
+ const column = value.column
220
+ const offset = value.offset
221
+
222
+ if (typeof line != "number" || !Number.isSafeInteger(line) || line <= 0 ||
223
+ typeof column != "number" || !Number.isSafeInteger(column) || column <= 0 ||
224
+ typeof offset != "number" || !Number.isSafeInteger(offset) || offset < 0) return undefined
225
+
226
+ return {column, line, offset}
227
+ }
228
+
229
+ /**
230
+ * Validates an optional byte-range identity.
231
+ * @param {unknown} value - Candidate identity.
232
+ * @param {string} property - Property name.
233
+ * @param {number} index - Byte-range index.
234
+ * @returns {Record<string, string>} Present identity or an empty record.
235
+ */
236
+ function optionalIdentity(value, property, index) {
237
+ if (value === undefined) return {}
238
+ if (typeof value != "string" || value.length == 0) invalidByteMapping(`Byte range ${index} has an invalid ${property}.`)
239
+
240
+ return {[property]: value}
241
+ }
242
+
243
+ /**
244
+ * Validates an optional related-origin identity.
245
+ * @param {unknown} value - Candidate identity.
246
+ * @param {string} property - Property name.
247
+ * @param {string} label - Diagnostic range label.
248
+ * @returns {Record<string, string>} Present identity or an empty record.
249
+ */
250
+ function optionalRelated(value, property, label) {
251
+ if (value === undefined) return {}
252
+ if (typeof value != "string" || value.length == 0) invalidByteMapping(`${label} has an invalid ${property}.`)
253
+
254
+ return {[property]: value}
255
+ }
256
+
257
+ /**
258
+ * Throws a normalized byte-mapping diagnostic.
259
+ * @param {string} message - Failure detail.
260
+ * @param {unknown} [error] - Preserved validation cause.
261
+ * @returns {never} Always throws.
262
+ */
263
+ function invalidByteMapping(message, error) {
264
+ throw new SemantifoldDiagnostic({
265
+ cause: error instanceof Error ? error : undefined,
266
+ code: "INVALID_BYTE_MAPPING",
267
+ language: "binary",
268
+ message
269
+ })
270
+ }
271
+
272
+ /**
273
+ * Checks for an ordinary string-keyed object.
274
+ * @param {unknown} value - Candidate value.
275
+ * @returns {value is Record<string, unknown>} Whether the value is a plain object.
276
+ */
277
+ function isPlainObject(value) {
278
+ return typeof value == "object" && value != null && !Array.isArray(value) && Object.getPrototypeOf(value) == Object.prototype
279
+ }
280
+
281
+ /**
282
+ * Deeply freezes detached JSON mapping values.
283
+ * @template T
284
+ * @param {T} value - Detached value.
285
+ * @returns {T} Frozen value.
286
+ */
287
+ function deepFreeze(value) {
288
+ if (!value || typeof value != "object" || Object.isFrozen(value)) return value
289
+
290
+ for (const nested of Object.values(value)) deepFreeze(nested)
291
+
292
+ return Object.freeze(value)
293
+ }
294
+
295
+ /**
296
+ * Recursively sorts JSON object keys without reordering arrays.
297
+ * @param {unknown} value - JSON value.
298
+ * @returns {unknown} Key-sorted JSON value.
299
+ */
300
+ function sortJson(value) {
301
+ if (Array.isArray(value)) return value.map(sortJson)
302
+ if (!isPlainObject(value)) return value
303
+
304
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortJson(value[key])]))
305
+ }
package/src/diagnostic.js CHANGED
@@ -7,12 +7,20 @@ export class SemantifoldDiagnostic extends Error {
7
7
  * @param {object} options - Diagnostic fields.
8
8
  * @param {string} options.code - Stable diagnostic code.
9
9
  * @param {string} options.message - Human-readable message.
10
- * @param {import("./semantic/types.js").SemanticLanguage} options.language - Related language.
10
+ * @param {string} options.language - Related language, target, tool, or registry identity.
11
11
  * @param {import("./semantic/types.js").SourceLocation | undefined} [options.location] - Related location.
12
12
  * @param {import("./semantic/types.js").SourceLocation | undefined} [options.generatedLocation] - Pre-remapping generated location.
13
13
  * @param {Error | undefined} [options.cause] - Preserved parser error.
14
+ * @param {string | undefined} [options.command] - Canonical command when tool discovery is involved.
15
+ * @param {string | undefined} [options.executable] - Exact resolved executable when known.
16
+ * @param {number | undefined} [options.exitCode] - Process exit code when available.
17
+ * @param {string | undefined} [options.signal] - Process termination signal when available.
18
+ * @param {string | undefined} [options.stage] - Acceptance stage when execution is involved.
19
+ * @param {string | undefined} [options.stderr] - Captured standard error when available.
20
+ * @param {string | undefined} [options.stdout] - Captured standard output when available.
21
+ * @param {string | undefined} [options.version] - Captured executable version when known.
14
22
  */
15
- constructor({cause, code, generatedLocation, language, location, message}) {
23
+ constructor({cause, code, command, executable, exitCode, generatedLocation, language, location, message, signal, stage, stderr, stdout, version}) {
16
24
  const locationText = location ? ` at ${location.filename}:${location.start.line}:${location.start.column}` : ""
17
25
 
18
26
  super(`[${code}] ${language}${locationText}: ${message}`, cause ? {cause} : undefined)
@@ -22,6 +30,14 @@ export class SemantifoldDiagnostic extends Error {
22
30
  this.generatedLocation = generatedLocation
23
31
  this.language = language
24
32
  this.location = location
33
+ if (command != undefined) this.command = command
34
+ if (executable != undefined) this.executable = executable
35
+ if (exitCode != undefined) this.exitCode = exitCode
36
+ if (signal != undefined) this.signal = signal
37
+ if (stage != undefined) this.stage = stage
38
+ if (stderr != undefined) this.stderr = stderr
39
+ if (stdout != undefined) this.stdout = stdout
40
+ if (version != undefined) this.version = version
25
41
  }
26
42
  }
27
43
 
@@ -101,3 +117,19 @@ export function unsupportedCapability(language, capability, location) {
101
117
  message: `Backend cannot emit semantic capability '${capability}'.`
102
118
  })
103
119
  }
120
+
121
+ /**
122
+ * Throws when a registered identity lacks one requested role.
123
+ * @param {string} language - Registered language or target identity.
124
+ * @param {string} role - Requested registry role.
125
+ * @param {import("./semantic/types.js").SourceLocation | undefined} [location] - Related semantic location.
126
+ * @returns {never} Always throws.
127
+ */
128
+ export function unsupportedRole(language, role, location) {
129
+ throw new SemantifoldDiagnostic({
130
+ code: "UNSUPPORTED_ROLE",
131
+ language,
132
+ location,
133
+ message: `Registered language or target does not provide role '${role}'.`
134
+ })
135
+ }
@@ -1,12 +1,8 @@
1
1
  // @ts-check
2
2
 
3
- import {SemantifoldDiagnostic} from "../diagnostic.js"
3
+ import {languageRegistry} from "../language-registry.js"
4
4
  import {annotateParsedModule} from "../semantic/provenance.js"
5
5
  import {validateParsedModule} from "../semantic/validate.js"
6
- import {parseJava} from "./java.js"
7
- import {parseJavaScriptTypeScript} from "./javascript-typescript.js"
8
- import {parsePhp} from "./php.js"
9
- import {parseRuby} from "./ruby.js"
10
6
 
11
7
  /**
12
8
  * Parses supported source into a shared semantic module.
@@ -17,20 +13,9 @@ import {parseRuby} from "./ruby.js"
17
13
  * @returns {import("../semantic/types.js").SemanticModule} Semantic module.
18
14
  */
19
15
  export function parseSource({filename, language, source}) {
20
- let module
16
+ const frontend = /** @type {(input: {filename: string, source: string}) => import("../semantic/types.js").SemanticModule} */ (
17
+ languageRegistry.resolve(language, "frontend"))
18
+ const module = validateParsedModule(frontend({filename, source}), language)
21
19
 
22
- if (language == "php") module = validateParsedModule(parsePhp({filename, source}), language)
23
- else if (language == "ruby") module = validateParsedModule(parseRuby({filename, source}), language)
24
- if (language == "javascript" || language == "typescript") {
25
- module = validateParsedModule(parseJavaScriptTypeScript({filename, language, source}), language)
26
- }
27
- else if (language == "java") module = validateParsedModule(parseJava({filename, source}), language)
28
-
29
- if (module) return annotateParsedModule(module, {filename, language, source})
30
-
31
- throw new SemantifoldDiagnostic({
32
- code: "UNSUPPORTED_LANGUAGE",
33
- language,
34
- message: "No frontend adapter is registered for this language."
35
- })
20
+ return annotateParsedModule(module, {filename, language, source})
36
21
  }