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.
- package/README.md +13 -6
- package/build/index.d.ts +32 -3
- package/build/index.d.ts.map +1 -1
- package/build/index.js +99 -4
- package/build/src/acceptance.d.ts +22 -0
- package/build/src/acceptance.d.ts.map +1 -0
- package/build/src/acceptance.js +339 -0
- package/build/src/array.d.ts +7 -0
- package/build/src/array.d.ts.map +1 -0
- package/build/src/array.js +18 -0
- package/build/src/artifact-path.d.ts +13 -0
- package/build/src/artifact-path.d.ts.map +1 -0
- package/build/src/artifact-path.js +30 -0
- package/build/src/artifacts.d.ts +7 -0
- package/build/src/artifacts.d.ts.map +1 -0
- package/build/src/artifacts.js +389 -0
- package/build/src/backends/identifiers.d.ts.map +1 -1
- package/build/src/backends/identifiers.js +10 -2
- package/build/src/backends/index.d.ts.map +1 -1
- package/build/src/backends/index.js +19 -37
- package/build/src/backends/python.d.ts +8 -0
- package/build/src/backends/python.d.ts.map +1 -0
- package/build/src/backends/python.js +162 -0
- package/build/src/backends/scalars.d.ts.map +1 -1
- package/build/src/backends/scalars.js +2 -1
- package/build/src/backends/shared.d.ts.map +1 -1
- package/build/src/backends/shared.js +9 -6
- package/build/src/binary-mapping.d.ts +32 -0
- package/build/src/binary-mapping.d.ts.map +1 -0
- package/build/src/binary-mapping.js +285 -0
- package/build/src/diagnostic.d.ts +36 -4
- package/build/src/diagnostic.d.ts.map +1 -1
- package/build/src/diagnostic.js +42 -3
- package/build/src/frontends/index.d.ts.map +1 -1
- package/build/src/frontends/index.js +5 -23
- package/build/src/frontends/python.d.ts +14 -0
- package/build/src/frontends/python.d.ts.map +1 -0
- package/build/src/frontends/python.js +615 -0
- package/build/src/frontends/scalars.d.ts.map +1 -1
- package/build/src/frontends/scalars.js +6 -1
- package/build/src/language-registry.d.ts +113 -0
- package/build/src/language-registry.d.ts.map +1 -0
- package/build/src/language-registry.js +372 -0
- package/build/src/mapping.js +3 -2
- package/build/src/semantic/location.d.ts +7 -0
- package/build/src/semantic/location.d.ts.map +1 -1
- package/build/src/semantic/location.js +10 -1
- package/build/src/semantic/provenance.js +3 -3
- package/build/src/semantic/types.d.ts +399 -2
- package/build/src/semantic/types.d.ts.map +1 -1
- package/build/src/semantic/types.js +110 -2
- package/build/src/subprocess.d.ts +29 -0
- package/build/src/subprocess.d.ts.map +1 -0
- package/build/src/subprocess.js +207 -0
- package/build/src/timeout.d.ts +8 -0
- package/build/src/timeout.d.ts.map +1 -0
- package/build/src/timeout.js +11 -0
- package/build/src/toolchains.d.ts +83 -0
- package/build/src/toolchains.d.ts.map +1 -0
- package/build/src/toolchains.js +413 -0
- package/docs/architecture.md +7 -3
- package/docs/language-support.md +25 -4
- package/docs/parser-qualification.md +70 -0
- package/docs/source-maps.md +31 -4
- package/docs/testing.md +21 -2
- package/index.js +109 -4
- package/package.json +4 -2
- package/src/acceptance.js +359 -0
- package/src/array.js +19 -0
- package/src/artifact-path.js +34 -0
- package/src/artifacts.js +420 -0
- package/src/backends/identifiers.js +9 -1
- package/src/backends/index.js +16 -32
- package/src/backends/python.js +175 -0
- package/src/backends/scalars.js +1 -0
- package/src/backends/shared.js +8 -5
- package/src/binary-mapping.js +305 -0
- package/src/diagnostic.js +34 -2
- package/src/frontends/index.js +5 -20
- package/src/frontends/python.js +689 -0
- package/src/frontends/scalars.js +5 -0
- package/src/language-registry.js +403 -0
- package/src/mapping.js +2 -1
- package/src/semantic/location.js +10 -0
- package/src/semantic/provenance.js +2 -2
- package/src/semantic/types.js +124 -1
- package/src/subprocess.js +203 -0
- package/src/timeout.js +12 -0
- package/src/toolchains.js +444 -0
- 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
|
+
}
|
package/src/backends/scalars.js
CHANGED
|
@@ -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
|
})
|
package/src/backends/shared.js
CHANGED
|
@@ -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
|
-
|
|
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 {
|
|
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
|
+
}
|
package/src/frontends/index.js
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
// @ts-check
|
|
2
2
|
|
|
3
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|